diff --git a/.github/workflows/vercel-preview.yaml b/.github/workflows/vercel-preview.yaml deleted file mode 100644 index eea3d583..00000000 --- a/.github/workflows/vercel-preview.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Vercel Preview Deployment - -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - -on: - push: - branches-ignore: - - "main" - - "renovate/**" - -jobs: - Deploy-Preview: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - - - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/vercel-production.yaml b/.github/workflows/vercel-production.yaml deleted file mode 100644 index 5ce8253c..00000000 --- a/.github/workflows/vercel-production.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Vercel Production Deployment - -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - -on: - push: - branches: - - main - -jobs: - Deploy-Production: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build Project Artifacts - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - - - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/apps/www/env.ts b/apps/www/env.ts index af880684..9917feb4 100644 --- a/apps/www/env.ts +++ b/apps/www/env.ts @@ -13,9 +13,9 @@ export const env = createEnv({ * This way you can ensure the app isn't built with invalid env vars. */ server: { - DATABASE_HOST: z.string().min(1), - DATABASE_USERNAME: z.string().min(1), - DATABASE_PASSWORD: z.string().min(1), + // DATABASE_HOST: z.string().min(1), + // DATABASE_USERNAME: z.string().min(1), + // DATABASE_PASSWORD: z.string().min(1), STRIPE_API_KEY: z.string().min(1).optional(), STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(), }, diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index 991ec9b5..de2a977b 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -3,21 +3,17 @@ import type { Config } from "drizzle-kit"; dotenv.config({ path: "../../.env.local" }); -const uri = [ - "mysql://", - process.env.DATABASE_USERNAME, - ":", - process.env.DATABASE_PASSWORD, - "@", - process.env.DATABASE_HOST, - ":3306/", - process.env.DATABASE_NAME, - '?ssl={"rejectUnauthorized":true}', -].join(""); +const uri = process.env.DATABASE_URL || ""; + +if (!uri) { + throw new Error("DATABASE_URL is not set in the environment variables."); +} export default { schema: "./src/schema", - driver: "mysql2", - dbCredentials: { uri }, + driver: "pg", + dbCredentials: { + connectionString: uri, + }, tablesFilter: ["projectx_*"], } satisfies Config; diff --git a/packages/db/package.json b/packages/db/package.json index f9e3d1e8..b9897a4d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -19,12 +19,13 @@ "clean": "git clean -xdf .turbo node_modules", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint . --no-error-on-unmatched-pattern", - "push": "drizzle-kit push:mysql", + "push": "drizzle-kit push:pg", "studio": "drizzle-kit studio", "seed": "tsx ./src/seed.ts", "typecheck": "tsc --noEmit" }, "dependencies": { + "@neondatabase/serverless": "^0.9.1", "@planetscale/database": "^1.16.0", "drizzle-orm": "^0.29.3", "iso-country-currency": "^0.7.2" diff --git a/packages/db/src/data/mock.ts b/packages/db/src/data/mock.ts index 1e8c4efd..9e631c1e 100644 --- a/packages/db/src/data/mock.ts +++ b/packages/db/src/data/mock.ts @@ -15,22 +15,25 @@ const connectorsData: CanonicalConnector[] = []; const integrationsData: CanonicalIntegration[] = []; const resourcesData: CanonicalResource[] = []; +// Helper function to convert numeric IDs to string format for varchar fields +const formatId = (id: number) => id.toString(); + // seed connectors connectorsConfigData.push({ - id: BigInt(1), + id: formatId(1), env: "SANDBOX", secret: { clientId: process.env.PLAID_CLIENT_ID, clientSecret: process.env.PLAID_CLIENT_SECRET, }, orgId: "org_", - connectorId: BigInt(1), + connectorId: formatId(1), createdAt: new Date(), updatedAt: new Date(), }); connectorsData.push({ - id: BigInt(1), + id: formatId(1), name: "plaid", logoUrl: "https://pbs.twimg.com/profile_images/1415067514460000256/1iPIdd20_400x400.png", @@ -41,20 +44,20 @@ connectorsData.push({ }); connectorsConfigData.push({ - id: BigInt(2), + id: formatId(2), env: "SANDBOX", secret: { secretId: process.env.GOCARDLESS_SECRET_ID, secretKey: process.env.GOCARDLESS_SECRET_KEY, }, orgId: "org_", - connectorId: BigInt(2), + connectorId: formatId(2), createdAt: new Date(), updatedAt: new Date(), }); connectorsData.push({ - id: BigInt(2), + id: formatId(2), name: "gocardless", logoUrl: "https://asset.brandfetch.io/idNfPDHpG3/idamTYtkQh.png", status: ConnectorStatus.ACTIVE, @@ -64,15 +67,15 @@ connectorsData.push({ }); integrationsData.push({ - id: BigInt(1), + id: formatId(1), name: "Sandbox", - connectorId: BigInt(2), + connectorId: formatId(2), }); resourcesData.push({ - id: BigInt(1), + id: formatId(1), originalId: "28ce28fb-877b-4e5c-882a-6066f3f5f728", - integrationId: BigInt(1), + integrationId: formatId(1), userId: "user_", createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/db/src/enum.ts b/packages/db/src/enum.ts index 24f078cb..19dfb866 100644 --- a/packages/db/src/enum.ts +++ b/packages/db/src/enum.ts @@ -19,60 +19,6 @@ export const ResourceStatus = { export type ResourceStatus = (typeof ResourceStatus)[keyof typeof ResourceStatus]; -// Plaid Enums - -export const Products = { - Assets: "assets", - Auth: "auth", - Balance: "balance", - // Products.Identity, "identity", - // Products.Investments, "investments", - // Products.InvestmentsAuth, "investments_auth", - // Products.Liabilities, "liabilities", - // Products.PaymentInitiation, "payment_initiation", - // Products.IdentityVerification, - Transactions: "transactions", - // Products.CreditDetails, - // Products.Income, - // Products.IncomeVerification, - // Products.DepositSwitch, - // Products.StandingOrders, - // Products.Transfer, - // Products.Employment, - RecurringTransactions: "recurring_transactions", - // Products.Signal, - // Products.Statements, -} as const; -export type Products = (typeof Products)[keyof typeof Products]; - -export const TransactionCode = { - Adjustment: "adjustment", - Atm: "atm", - BankCharge: "bank charge", - BillPayment: "bill payment", - Cash: "cash", - Cashback: "cashback", - Cheque: "cheque", - DirectDebit: "direct debit", - Interest: "interest", - Null: "null", - Purchase: "purchase", - StandingOrder: "standing order", - Transfer: "transfer", -} as const; -export type TransactionCode = - (typeof TransactionCode)[keyof typeof TransactionCode]; - -export const MerchantConfidenceLevel = { - VeryHigh: "VERY_HIGH", - High: "HIGH", - Medium: "MEDIUM", - Low: "LOW", - Unknown: "UNKNOWN", -} as const; -export type MerchantConfidenceLevel = - (typeof MerchantConfidenceLevel)[keyof typeof MerchantConfidenceLevel]; - export const PrimaryCategory = { INCOME: "INCOME", TRANSFER_IN: "TRANSFER_IN", diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 50c3afe7..b53f536c 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,5 +1,5 @@ -import { Client } from "@planetscale/database"; -import { drizzle } from "drizzle-orm/planetscale-serverless"; +import { neon } from "@neondatabase/serverless"; +import { drizzle } from "drizzle-orm/neon-http"; import { customAlphabet } from "nanoid"; import * as asset from "./schema/asset"; @@ -30,20 +30,12 @@ export type CanonicalAccount = typeof schema.account.$inferInsert; export type CanonicalBalance = typeof schema.balance.$inferInsert; export type CanonicalTransaction = typeof schema.transaction.$inferInsert; -export { mySqlTable as tableCreator } from "./schema/_table"; +export { pgTable as tableCreator } from "./schema/_table"; export * from "./enum"; export * from "drizzle-orm"; -export const db = drizzle( - new Client({ - host: process.env.DATABASE_HOST, - username: process.env.DATABASE_USERNAME, - password: process.env.DATABASE_PASSWORD, - }).connection(), - { schema }, -); +export const db = drizzle(neon(process.env.DATABASE_URL || "")); // Use custom alphabet without special chars for less chaotic, copy-able URLs -// Will not collide for a long long time: https://zelark.github.io/nano-id-cc/ export const genId = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 16); diff --git a/packages/db/src/schema/_table.ts b/packages/db/src/schema/_table.ts index 27b99b26..5a17f630 100644 --- a/packages/db/src/schema/_table.ts +++ b/packages/db/src/schema/_table.ts @@ -1,4 +1,4 @@ -import { mysqlTableCreator } from "drizzle-orm/mysql-core"; +import { pgTableCreator } from "drizzle-orm/pg-core"; /** * This is an example of how to use the multi-project schema feature of Drizzle ORM. @@ -6,4 +6,4 @@ import { mysqlTableCreator } from "drizzle-orm/mysql-core"; * * @see https://orm.drizzle.team/docs/goodies#multi-project-schema */ -export const mySqlTable = mysqlTableCreator((name) => `projectx_${name}`); +export const pgTable = pgTableCreator((name) => `badget_${name}`); diff --git a/packages/db/src/schema/asset.ts b/packages/db/src/schema/asset.ts index 63681fa0..95eb5c5b 100644 --- a/packages/db/src/schema/asset.ts +++ b/packages/db/src/schema/asset.ts @@ -1,53 +1,55 @@ import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - json, - mysqlEnum, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; +import { index, json, pgEnum, timestamp, varchar } from "drizzle-orm/pg-core"; import { AssetType } from "../enum"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; -export const asset = mySqlTable( +export const assetTypeEnum = pgEnum("account_type", [ + AssetType.STOCKS, + AssetType.CRYPTO, + AssetType.BONDS, + AssetType.ETF, + AssetType.OPTIONS, + AssetType.FUTURES, + AssetType.REAL_ESTATE, + AssetType.COMMODITIES, +]); + +export const asset = pgTable( "asset", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + userId: varchar("user_id", { length: 36 }), name: varchar("name", { length: 255 }).notNull(), - assetType: mysqlEnum("asset_type", [ - AssetType.STOCKS, - AssetType.CRYPTO, - AssetType.BONDS, - AssetType.ETF, - AssetType.OPTIONS, - AssetType.FUTURES, - AssetType.REAL_ESTATE, - AssetType.COMMODITIES, - ]).notNull(), + assetType: assetTypeEnum("type").notNull(), originalPayload: json("original_payload"), }, (table) => { return { - userIdIdx: index("user_id_idx").on(table.userId), + userIdIdx: index().on(table.userId), }; }, ); -export const assetRealEstate = mySqlTable("asset_real_estate", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), - assetId: bigint("asset_id", { mode: "bigint" }).notNull(), +export const assetRealEstate = pgTable("asset_real_estate", { + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + + assetId: varchar("asset_id", { length: 30 }).notNull(), + address: varchar("address", { length: 255 }).notNull(), city: varchar("city", { length: 255 }).notNull(), state: varchar("state", { length: 255 }).notNull(), @@ -55,6 +57,10 @@ export const assetRealEstate = mySqlTable("asset_real_estate", { purchaseDate: timestamp("purchase_date").notNull(), }); +/** + * 👇 This code block will tell Drizzle that asset is related to: + * - asset <-> real_estate -> 1-to-1 + */ export const assetRealEstateRelation = relations( assetRealEstate, ({ one }) => ({ diff --git a/packages/db/src/schema/country.ts b/packages/db/src/schema/country.ts index 7ad2a76b..0aa5d5dc 100644 --- a/packages/db/src/schema/country.ts +++ b/packages/db/src/schema/country.ts @@ -1,14 +1,16 @@ import { sql } from "drizzle-orm"; -import { bigint, boolean, timestamp, varchar } from "drizzle-orm/mysql-core"; +import { boolean, serial, timestamp, varchar } from "drizzle-orm/pg-core"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; -export const country = mySqlTable("country", { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), +export const country = pgTable("country", { + id: serial("id").primaryKey(), createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), iso: varchar("iso", { length: 2 }).unique(), name: varchar("name", { length: 63 }).notNull(), diff --git a/packages/db/src/schema/currency.ts b/packages/db/src/schema/currency.ts index 27006ae9..7ba0c691 100644 --- a/packages/db/src/schema/currency.ts +++ b/packages/db/src/schema/currency.ts @@ -1,16 +1,18 @@ import { sql } from "drizzle-orm"; -import { bigint, int, timestamp, varchar } from "drizzle-orm/mysql-core"; +import { integer, serial, timestamp, varchar } from "drizzle-orm/pg-core"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; -export const currency = mySqlTable("currency", { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), +export const currency = pgTable("currency", { + id: serial("id").primaryKey(), createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), iso: varchar("iso", { length: 3 }).unique().notNull(), symbol: varchar("symbol", { length: 5 }).notNull(), - numericCode: int("numeric_code"), + numericCode: integer("numeric_code"), }); diff --git a/packages/db/src/schema/customer.ts b/packages/db/src/schema/customer.ts index edeaabc3..c6ab19c3 100644 --- a/packages/db/src/schema/customer.ts +++ b/packages/db/src/schema/customer.ts @@ -1,44 +1,40 @@ import { sql } from "drizzle-orm"; -import { - bigint, - index, - mysqlEnum, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; +import { index, pgEnum, serial, timestamp, varchar } from "drizzle-orm/pg-core"; import { SubscriptionPlan } from "../enum"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; -export const customer = mySqlTable( +export const planEnum = pgEnum("plan", [ + SubscriptionPlan.FREE, + SubscriptionPlan.STANDARD, + SubscriptionPlan.PRO, +]); + +export const customer = pgTable( "customer", { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), stripeId: varchar("stripe_id", { length: 36 }).notNull().unique(), subscriptionId: varchar("subscription_id", { length: 36 }), - clerkUserId: varchar("clerk_user_id", { length: 36 }).notNull(), - clerkOrganizationId: varchar("clerk_organization_id", { length: 36 }), + userId: varchar("user_id", { length: 36 }).notNull(), + orgId: varchar("org_id", { length: 36 }), name: varchar("name", { length: 256 }), - plan: mysqlEnum("plan", [ - SubscriptionPlan.FREE, - SubscriptionPlan.STANDARD, - SubscriptionPlan.PRO, - ]), + plan: planEnum("plan"), paidUntil: timestamp("paid_until"), endsAt: timestamp("ends_at"), }, (table) => { return { - clerkUserIdIdx: index("clerk_user_id_idx").on(table.clerkUserId), - clerkOrganizationIdIDX: index("clerk_organization_id_idx").on( - table.clerkOrganizationId, - ), + userIdIdx: index().on(table.userId), + orgIdIdx: index().on(table.orgId), }; }, ); diff --git a/packages/db/src/schema/openbanking.ts b/packages/db/src/schema/openbanking.ts index a6e461c7..37956b52 100644 --- a/packages/db/src/schema/openbanking.ts +++ b/packages/db/src/schema/openbanking.ts @@ -1,58 +1,61 @@ import { relations, sql } from "drizzle-orm"; import { - bigint, - float, + decimal, index, json, - mysqlEnum, + pgEnum, timestamp, uniqueIndex, varchar, -} from "drizzle-orm/mysql-core"; +} from "drizzle-orm/pg-core"; import { AccountType, BalanceType } from "../enum"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; import { asset } from "./asset"; import { currency } from "./currency"; import { resource } from "./provider"; -export const account = mySqlTable( +export const accountTypeEnum = pgEnum("account_type", [ + AccountType.BANK, + AccountType.CRYPTO, + AccountType.INVESTMENT, +]); + +export const account = pgTable( "account", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), - resourceId: bigint("resource_id", { mode: "bigint" }), + resourceId: varchar("resource_id", { length: 30 }), originalId: varchar("original_id", { length: 36 }), orgId: varchar("org_id", { length: 36 }), userId: varchar("user_id", { length: 36 }), name: varchar("name", { length: 255 }).notNull(), - accountType: mysqlEnum("account_type", [ - AccountType.BANK, - AccountType.CRYPTO, - AccountType.INVESTMENT, - ]) + accountType: accountTypeEnum("account_type") .notNull() .default(AccountType.BANK), originalPayload: json("original_payload"), }, (table) => { return { - orgIdIdx: index("org_id_idx").on(table.orgId), - userIdIdx: index("user_id_idx").on(table.userId), + orgIdIdx: index().on(table.orgId), + userIdIdx: index().on(table.userId), }; }, ); /** * 👇 This code block will tell Drizzle that account is related to: - * - account <-> balance -> 1-to-N - * - account <-> transaction -> 1-to-N - * - account <-> resource -> 1-to-1 + * - account <-> balance -> 1-to-N + * - account <-> transaction -> 1-to-N + * - account <-> resource -> 1-to-1 */ export const accountRelations = relations(account, ({ many, one }) => ({ balances: many(balance), @@ -63,40 +66,44 @@ export const accountRelations = relations(account, ({ many, one }) => ({ }), })); -export const balance = mySqlTable( +export const balanceTypeEnum = pgEnum("type", [ + BalanceType.AVAILABLE, + BalanceType.BOOKED, + BalanceType.EXPECTED, +]); + +export const balance = pgTable( "balance", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), - accountId: bigint("account_id", { mode: "bigint" }), - assetId: bigint("asset_id", { mode: "bigint" }), + accountId: varchar("account_id", { length: 30 }), + assetId: varchar("asset_id", { length: 30 }), currencyIso: varchar("currency_iso", { length: 3 }).notNull(), - amount: float("amount").notNull(), + amount: decimal("amount").notNull(), date: timestamp("date").notNull(), - type: mysqlEnum("type", [ - BalanceType.AVAILABLE, - BalanceType.BOOKED, - BalanceType.EXPECTED, - ]).notNull(), + type: balanceTypeEnum("type").notNull(), originalPayload: json("original_payload"), }, (table) => { return { - accountIdIdx: index("account_id_idx").on(table.accountId), + accountIdIdx: index().on(table.accountId), }; }, ); /** * 👇 This code block will tell Drizzle that balance is related to: - * - balance <-> account -> 1-to-1 - * - balance <-> asset -> 1-to-1 - * - balance <-> currency -> 1-to-1 + * - balance <-> account -> 1-to-1 + * - balance <-> asset -> 1-to-1 + * - balance <-> currency -> 1-to-1 */ export const balanceRelations = relations(balance, ({ one }) => ({ account: one(account, { @@ -113,12 +120,14 @@ export const balanceRelations = relations(balance, ({ one }) => ({ }), })); -export const category = mySqlTable("category", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), +export const category = pgTable("category", { + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), name: varchar("name", { length: 63 }).notNull(), icon: varchar("icon", { length: 63 }).notNull(), @@ -132,30 +141,32 @@ export const categoryRelations = relations(category, ({ many }) => ({ transactions: many(transaction), })); -export const transaction = mySqlTable( +export const transaction = pgTable( "transaction", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), - accountId: bigint("account_id", { mode: "bigint" }), - assetId: bigint("asset_id", { mode: "bigint" }), - categoryId: bigint("category_id", { mode: "bigint" }), + accountId: varchar("account_id", { length: 30 }), + assetId: varchar("asset_id", { length: 30 }), + categoryId: varchar("category_id", { length: 30 }), currencyIso: varchar("currency_iso", { length: 3 }).notNull(), originalId: varchar("original_id", { length: 36 }), - amount: float("amount").notNull(), + amount: decimal("amount").notNull(), date: timestamp("date").notNull(), description: varchar("description", { length: 255 }).notNull(), originalPayload: json("original_payload"), }, (table) => { return { - accountIdIdx: index("account_id_idx").on(table.accountId), - originalIdUnqIdx: uniqueIndex("original_id_unq_idx").on(table.originalId), + accountIdIdx: index().on(table.accountId), + originalIdUnqIdx: uniqueIndex().on(table.originalId), }; }, ); @@ -163,7 +174,7 @@ export const transaction = mySqlTable( /** * 👇 This code block will tell Drizzle that transaction is related to: * - transaction <-> account -> 1-to-1 - * - transaction <-> asset -> 1-to-1 + * - transaction <-> asset -> 1-to-1 * - transaction <-> category -> 1-to-1 * - transaction <-> currency -> 1-to-1 */ diff --git a/packages/db/src/schema/provider.ts b/packages/db/src/schema/provider.ts index fd3f8381..696d1103 100644 --- a/packages/db/src/schema/provider.ts +++ b/packages/db/src/schema/provider.ts @@ -1,73 +1,78 @@ import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - json, - mysqlEnum, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; +import { index, json, pgEnum, timestamp, varchar } from "drizzle-orm/pg-core"; import { ConnectorEnv, ConnectorStatus, ConnectorType } from "../enum"; -import { mySqlTable } from "./_table"; +import { pgTable } from "./_table"; import { country } from "./country"; import { account } from "./openbanking"; -export const connectorConfig = mySqlTable( +export const connectorEnvEnum = pgEnum("env", [ + ConnectorEnv.DEVELOPMENT, + ConnectorEnv.SANDBOX, + ConnectorEnv.PRODUCTION, +]); + +export const connectorConfig = pgTable( "connectorConfig", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), orgId: varchar("org_id", { length: 36 }).notNull(), secret: json("secret"), - env: mysqlEnum("env", [ - ConnectorEnv.DEVELOPMENT, - ConnectorEnv.SANDBOX, - ConnectorEnv.PRODUCTION, - ]).notNull(), + env: connectorEnvEnum("env").notNull(), - connectorId: bigint("connector_id", { mode: "bigint" }), + connectorId: varchar("connector_id", { length: 30 }), }, (table) => { return { - orgIdIdx: index("org_id_idx").on(table.orgId), + orgIdIdx: index().on(table.orgId), }; }, ); -export const connector = mySqlTable("connector", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), +export const connectorStatusEnum = pgEnum("status", [ + ConnectorStatus.ACTIVE, + ConnectorStatus.BETA, + ConnectorStatus.DEV, + ConnectorStatus.INACTIVE, +]); + +export const connectorTypeEnum = pgEnum("type", [ + ConnectorType.DIRECT, + ConnectorType.AGGREGATED, +]); + +export const connector = pgTable("connector", { + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), name: varchar("name", { length: 36 }).notNull(), // TODO: maybe use enum ? logoUrl: varchar("logo_url", { length: 255 }), - status: mysqlEnum("status", [ - ConnectorStatus.ACTIVE, - ConnectorStatus.BETA, - ConnectorStatus.DEV, - ConnectorStatus.INACTIVE, - ]).notNull(), - type: mysqlEnum("type", [ - ConnectorType.DIRECT, - ConnectorType.AGGREGATED, - ]).notNull(), + status: connectorStatusEnum("status").notNull(), + type: connectorTypeEnum("type").notNull(), }); -export const integration = mySqlTable( +export const integration = pgTable( "integration", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), name: varchar("name", { length: 255 }).notNull(), logoUrl: varchar("logo_url", { length: 255 }), @@ -75,40 +80,42 @@ export const integration = mySqlTable( length: 127, }).unique(), - connectorId: bigint("connector_id", { mode: "bigint" }).notNull(), + connectorId: varchar("connector_id", { length: 30 }).notNull(), }, (table) => { return { - connectorIdIdx: index("connector_id_idx").on(table.connectorId), + connectorIdIdx: index().on(table.connectorId), }; }, ); -export const resource = mySqlTable( +export const resource = pgTable( "resource", { - id: bigint("id", { mode: "bigint" }).primaryKey().autoincrement(), + id: varchar("id", { length: 30 }).primaryKey(), // prefix_ + nanoid (16) createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), // TODO: add fields - integrationId: bigint("integration_id", { mode: "bigint" }).notNull(), + integrationId: varchar("integration_id", { length: 30 }).notNull(), originalId: varchar("original_id", { length: 36 }).notNull(), userId: varchar("user_id", { length: 36 }).notNull(), }, (table) => { return { - integrationIdIdx: index("integration_id_idx").on(table.integrationId), + integrationIdIdx: index().on(table.integrationId), }; }, ); /** * 👇 This code block will tell Drizzle that connector is related to: - * - connector <-> connectorConfig -> 1-to-N - * - connector <-> integration -> 1-to-N + * - connector <-> connectorConfig -> 1-to-N + * - connector <-> integration -> 1-to-N */ export const connectorsRelations = relations(connector, ({ many }) => ({ connectorConfigs: many(connectorConfig), diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index fa83acde..4bfa26a0 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1,6 +1,6 @@ -import { Client } from "@planetscale/database"; +import { neon } from "@neondatabase/serverless"; import * as dotenv from "dotenv"; -import { drizzle } from "drizzle-orm/planetscale-serverless"; +import { drizzle } from "drizzle-orm/neon-http"; import { connectorConfigs, @@ -13,52 +13,35 @@ import { schema, sql } from "./index"; dotenv.config({ path: "../../.env.local" }); -if (!("DATABASE_HOST" in process.env)) - throw new Error("DATABASE_HOST not found on .env.local"); -if (!("DATABASE_USERNAME" in process.env)) - throw new Error("DATABASE_USERNAME not found on .env.local"); -if (!("DATABASE_PASSWORD" in process.env)) - throw new Error("DATABASE_PASSWORD not found on .env.local"); +const uri = process.env.DATABASE_URL || ""; +if (!uri) { + throw new Error("DATABASE_URL is not set in the environment variables."); +} +//TODO: Make the script working. const main = async () => { - const client = new Client({ - host: process.env.DATABASE_HOST, - username: process.env.DATABASE_USERNAME, - password: process.env.DATABASE_PASSWORD, - }).connection(); + const client = neon(uri); const db = drizzle(client); console.log("Seed start"); - await db - .insert(schema.country) - .values(countries) - .onDuplicateKeyUpdate({ set: { iso: sql`iso`, name: sql`name` } }); - await db - .insert(schema.currency) - .values(currencies) - .onDuplicateKeyUpdate({ - set: { - iso: sql`iso`, - symbol: sql`symbol`, - numericCode: sql`numeric_code`, - }, - }); - await db - .insert(schema.connectorConfig) - .values(connectorConfigs) - .onDuplicateKeyUpdate({ set: { id: sql`id` } }); - await db - .insert(schema.connector) - .values(connectors) - .onDuplicateKeyUpdate({ set: { id: sql`id` } }); - await db - .insert(schema.integration) - .values(integrations) - .onDuplicateKeyUpdate({ set: { id: sql`id` } }); - await db - .insert(schema.resource) - .values(resources) - .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + await db.insert(schema.country).values(countries); + // .onDuplicateKeyUpdate({ set: { iso: sql`iso`, name: sql`name` } }); + await db.insert(schema.currency).values(currencies); + // .onDuplicateKeyUpdate({ + // set: { + // iso: sql`iso`, + // symbol: sql`symbol`, + // numericCode: sql`numeric_code`, + // }, + // }); + await db.insert(schema.connectorConfig).values(connectorConfigs); + // .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + await db.insert(schema.connector).values(connectors); + // .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + await db.insert(schema.integration).values(integrations); + // .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + await db.insert(schema.resource).values(resources); + // .onDuplicateKeyUpdate({ set: { id: sql`id` } }); console.log("Seed done"); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eff91f08..3ef3bf0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,12 +406,15 @@ importers: packages/db: dependencies: + '@neondatabase/serverless': + specifier: ^0.9.1 + version: 0.9.1 '@planetscale/database': specifier: ^1.16.0 version: 1.16.0 drizzle-orm: specifier: ^0.29.3 - version: 0.29.3(@planetscale/database@1.16.0) + version: 0.29.3(@neondatabase/serverless@0.9.1)(@planetscale/database@1.16.0) iso-country-currency: specifier: ^0.7.2 version: 0.7.2 @@ -1967,6 +1970,12 @@ packages: react: 18.2.0 dev: false + /@neondatabase/serverless@0.9.1: + resolution: {integrity: sha512-Xi+tVIXuaeB24BHzhr0W/4vcbb9WwIaB6yK0RsMIteLtzNB86+am6EDFovd3rYCYM1ea7rWcwte2dLOrzW7eqA==} + dependencies: + '@types/pg': 8.6.6 + dev: false + /@next/env@14.0.5-canary.46: resolution: {integrity: sha512-dvNzrArTfe3VY1VIscpb3E2e7SZ1qwFe82WGzpOVbxilT3JcsnVGYF/uq8Jj1qKWPI5C/aePNXwA97JRNAXpRQ==} dev: false @@ -4515,6 +4524,14 @@ packages: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: false + /@types/pg@8.6.6: + resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + dependencies: + '@types/node': 20.11.6 + pg-protocol: 1.6.1 + pg-types: 2.2.0 + dev: false + /@types/prismjs@1.26.3: resolution: {integrity: sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==} dev: false @@ -6022,7 +6039,7 @@ packages: - supports-color dev: true - /drizzle-orm@0.29.3(@planetscale/database@1.16.0): + /drizzle-orm@0.29.3(@neondatabase/serverless@0.9.1)(@planetscale/database@1.16.0): resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' @@ -6093,6 +6110,7 @@ packages: sqlite3: optional: true dependencies: + '@neondatabase/serverless': 0.9.1 '@planetscale/database': 1.16.0 dev: false @@ -6102,7 +6120,7 @@ packages: drizzle-orm: '>=0.23.13' zod: '*' dependencies: - drizzle-orm: 0.29.3(@planetscale/database@1.16.0) + drizzle-orm: 0.29.3(@neondatabase/serverless@0.9.1)(@planetscale/database@1.16.0) zod: 3.22.4 dev: false @@ -9678,6 +9696,26 @@ packages: is-reference: 3.0.2 dev: false + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -9791,6 +9829,28 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + /postmark@3.11.0: resolution: {integrity: sha512-asguBQ9M/8ueQMJ1D45iPF+3+T641q8rAU8m8cQSfhDWePw4TVYql9wszjAwSCE93dUonyrF08D8Kvg6USBoFA==} dependencies: @@ -12005,6 +12065,11 @@ packages: engines: {node: '>=0.4.0'} dev: false + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'}