diff --git a/apps/maestro/package.json b/apps/maestro/package.json index 3514e9f1b..77700e8c9 100644 --- a/apps/maestro/package.json +++ b/apps/maestro/package.json @@ -41,6 +41,7 @@ "@axelarjs/ui": "workspace:*", "@axelarjs/utils": "workspace:*", "@hookform/resolvers": "^3.3.4", + "@neondatabase/serverless": "^0.9.0", "@sentry/nextjs": "^7.108.0", "@tanstack/react-query": "^5.28.6", "@trpc/client": "11.0.0-next.320", @@ -48,9 +49,8 @@ "@trpc/react-query": "11.0.0-next.320", "@trpc/server": "11.0.0-next.320", "@vercel/kv": "^1.0.1", - "@vercel/postgres": "^0.7.2", "@web3modal/wagmi": "^4.1.1", - "drizzle-orm": "^0.29.5", + "drizzle-orm": "^0.30.4", "lucide-react": "^0.265.0", "next": "^14.1.4", "nextjs-cors": "^2.2.0", diff --git a/apps/maestro/src/lib/drizzle/client.ts b/apps/maestro/src/lib/drizzle/client.ts index 2cbd389a3..b322e8a72 100644 --- a/apps/maestro/src/lib/drizzle/client.ts +++ b/apps/maestro/src/lib/drizzle/client.ts @@ -1,10 +1,25 @@ -import { sql } from "@vercel/postgres"; -import { drizzle } from "drizzle-orm/vercel-postgres"; +import { neon } from "@neondatabase/serverless"; +import type { BatchItem as _BatchItem } from "drizzle-orm/batch"; +import { drizzle } from "drizzle-orm/neon-http"; import * as schema from "./schema"; +const sql = neon(process.env.POSTGRES_URL!); + const dbClient = drizzle(sql, { schema }); export type DBClient = typeof dbClient; export default dbClient; + +export type BatchItem = _BatchItem<"pg">; +export type BatchItems = [BatchItem, ...BatchItem[]]; + +/** + * Type-guard to check if drizzle batch is valid. + * + * @param batch - The batch to check + * @returns true if the batch is valid + */ +export const isValidBatch = (batch: BatchItem[]): batch is BatchItems => + batch.length > 1; diff --git a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts index 4030d2965..afab4dd03 100644 --- a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts +++ b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts @@ -97,69 +97,72 @@ export default class MaestroPostgresClient { async recordRemoteInterchainTokenDeployments( values: NewRemoteInterchainTokenInput[] ) { - await this.db.transaction(async (tx) => { - const existingTokens = await tx.query.remoteInterchainTokens.findMany({ - where: (table, { eq }) => eq(table.tokenId, values[0].tokenId), - }); - - const updateValues = existingTokens - .map( - (t) => - [ - t, - sanitizeObject( - values.find( - (v) => - v.axelarChainId === t.axelarChainId && - v.tokenId === t.tokenId - ) ?? {} - ), - ] as const - ) - .filter(([, v]) => Boolean(v)); - - const insertValues = values.filter((v) => { - const id = `${v.axelarChainId}:${v.tokenAddress}`; - return !existingTokens.some((t) => t.id === id); - }); - - if (updateValues.length > 0) { - for (const [existingToken, updateValue] of updateValues) { - await tx - .update(remoteInterchainTokens) - .set({ - id: updateValue.id ?? existingToken.id, - tokenManagerAddress: - updateValue.tokenManagerAddress ?? - existingToken.tokenManagerAddress, - deploymentMessageId: - updateValue.deploymentMessageId ?? - existingToken.deploymentMessageId, - deploymentStatus: - updateValue.deploymentStatus ?? existingToken.deploymentStatus, - tokenManagerType: - updateValue.tokenManagerType ?? existingToken.tokenManagerType, - tokenAddress: - updateValue.tokenAddress ?? existingToken.tokenAddress, - updatedAt: new Date(), - }) - .where(eq(remoteInterchainTokens.id, existingToken.id)); - } - } - - if (!insertValues.length) { - return; - } - - await tx.insert(remoteInterchainTokens).values( - insertValues.map((v) => ({ - ...v, - id: `${v.axelarChainId}:${v.tokenAddress}`, - createdAt: new Date(), - updatedAt: new Date(), - })) - ); + const existingTokens = await this.db.query.remoteInterchainTokens.findMany({ + where: (table, { eq }) => eq(table.tokenId, values[0].tokenId), }); + + const updateValues = existingTokens + .map( + (t) => + [ + t, + sanitizeObject( + values.find( + (v) => + v.axelarChainId === t.axelarChainId && v.tokenId === t.tokenId + ) ?? {} + ), + ] as const + ) + .filter(([, v]) => Boolean(v)); + + const insertValues = values.filter((v) => { + const id = `${v.axelarChainId}:${v.tokenAddress}`; + return !existingTokens.some((t) => t.id === id); + }); + + if (updateValues.length > 0) { + const [head, ...tail] = updateValues.map(([existingToken, updateValue]) => + this.db + .update(remoteInterchainTokens) + .set({ + id: updateValue.id ?? existingToken.id, + tokenManagerAddress: + updateValue.tokenManagerAddress ?? + existingToken.tokenManagerAddress, + deploymentMessageId: + updateValue.deploymentMessageId ?? + existingToken.deploymentMessageId, + deploymentStatus: + updateValue.deploymentStatus ?? existingToken.deploymentStatus, + tokenManagerType: + updateValue.tokenManagerType ?? existingToken.tokenManagerType, + tokenAddress: + updateValue.tokenAddress ?? existingToken.tokenAddress, + updatedAt: new Date(), + }) + .where(eq(remoteInterchainTokens.id, existingToken.id)) + ); + + // only batch if there are more than one update operation, + // otherise run the single update + const update = tail.length ? this.db.batch([head, ...tail]) : head; + + await update; + } + + if (!insertValues.length) { + return; + } + + await this.db.insert(remoteInterchainTokens).values( + insertValues.map((v) => ({ + ...v, + id: `${v.axelarChainId}:${v.tokenAddress}`, + createdAt: new Date(), + updatedAt: new Date(), + })) + ); } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 306bf12a0..7f959f547 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.51.1) + '@neondatabase/serverless': + specifier: ^0.9.0 + version: 0.9.0 '@sentry/nextjs': specifier: ^7.108.0 version: 7.108.0(next@14.1.4)(react@18.2.0) @@ -115,15 +118,12 @@ importers: '@vercel/kv': specifier: ^1.0.1 version: 1.0.1 - '@vercel/postgres': - specifier: ^0.7.2 - version: 0.7.2 '@web3modal/wagmi': specifier: ^4.1.1 version: 4.1.1(@types/react@18.2.21)(@wagmi/connectors@4.1.18)(@wagmi/core@2.6.9)(@web3modal/wallet@4.1.1)(typescript@5.4.3)(viem@2.8.18) drizzle-orm: - specifier: ^0.29.5 - version: 0.29.5(@types/react@18.2.21)(@vercel/postgres@0.7.2)(pg@8.11.3)(react@18.2.0) + specifier: ^0.30.4 + version: 0.30.4(@neondatabase/serverless@0.9.0)(@types/react@18.2.21)(pg@8.11.3)(react@18.2.0) lucide-react: specifier: ^0.265.0 version: 0.265.0(react@18.2.0) @@ -265,7 +265,7 @@ importers: version: 0.20.14 drizzle-zod: specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.29.5)(zod@3.22.4) + version: 0.5.1(drizzle-orm@0.30.4)(zod@3.22.4) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -5061,8 +5061,8 @@ packages: tar-fs: 2.1.1 dev: true - /@neondatabase/serverless@0.7.2: - resolution: {integrity: sha512-wU3WA2uTyNO7wjPs3Mg0G01jztAxUxzd9/mskMmtPwPTjf7JKWi9AW5/puOGXLxmZ9PVgRFeBVRVYq5nBPhsCg==} + /@neondatabase/serverless@0.9.0: + resolution: {integrity: sha512-mmJnUAzlzvxNSZuuhI6kgJjH+JgFdBMYUWxihtq/nj0Tjt+Y5UU3W+SvRFoucnd5NObYkuLYQzk+zV5DGFKGJg==} dependencies: '@types/pg': 8.6.6 @@ -9147,15 +9147,6 @@ packages: '@upstash/redis': 1.25.1 dev: false - /@vercel/postgres@0.7.2: - resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==} - engines: {node: '>=14.6'} - dependencies: - '@neondatabase/serverless': 0.7.2 - bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) - /@vitejs/plugin-react@4.2.1(vite@5.2.6): resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -10728,6 +10719,7 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.1 + dev: false /bundle-require@4.0.2(esbuild@0.19.12): resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} @@ -12110,13 +12102,14 @@ packages: - supports-color dev: true - /drizzle-orm@0.29.5(@types/react@18.2.21)(@vercel/postgres@0.7.2)(pg@8.11.3)(react@18.2.0): - resolution: {integrity: sha512-jS3+uyzTz4P0Y2CICx8FmRQ1eplURPaIMWDn/yq6k4ShRFj9V7vlJk67lSf2kyYPzQ60GkkNGXcJcwrxZ6QCRw==} + /drizzle-orm@0.30.4(@neondatabase/serverless@0.9.0)(@types/react@18.2.21)(pg@8.11.3)(react@18.2.0): + resolution: {integrity: sha512-kWoSMGbrOFkmkAweLTFtHJMpN+nwhx89q0mLELqT2aEU+1szNV8jrnBmJwZ0WGNp7J7yQn/ezEtxBI/qzTSElQ==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=3' '@libsql/client': '*' '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1' '@types/better-sqlite3': '*' @@ -12124,6 +12117,7 @@ packages: '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '*' + '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' expo-sqlite: '>=13.2.0' @@ -12144,6 +12138,8 @@ packages: optional: true '@neondatabase/serverless': optional: true + '@op-engineering/op-sqlite': + optional: true '@opentelemetry/api': optional: true '@planetscale/database': @@ -12158,6 +12154,8 @@ packages: optional: true '@vercel/postgres': optional: true + '@xata.io/client': + optional: true better-sqlite3: optional: true bun-types: @@ -12181,18 +12179,18 @@ packages: sqlite3: optional: true dependencies: + '@neondatabase/serverless': 0.9.0 '@types/react': 18.2.21 - '@vercel/postgres': 0.7.2 pg: 8.11.3 react: 18.2.0 - /drizzle-zod@0.5.1(drizzle-orm@0.29.5)(zod@3.22.4): + /drizzle-zod@0.5.1(drizzle-orm@0.30.4)(zod@3.22.4): resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==} peerDependencies: drizzle-orm: '>=0.23.13' zod: '*' dependencies: - drizzle-orm: 0.29.5(@types/react@18.2.21)(@vercel/postgres@0.7.2)(pg@8.11.3)(react@18.2.0) + drizzle-orm: 0.30.4(@neondatabase/serverless@0.9.0)(@types/react@18.2.21)(pg@8.11.3)(react@18.2.0) zod: 3.22.4 dev: true @@ -16794,6 +16792,7 @@ packages: /node-gyp-build@4.6.1: resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} hasBin: true + dev: false /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -20960,6 +20959,7 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.1 + dev: false /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -21672,21 +21672,6 @@ packages: utf-8-validate: optional: true - /ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dependencies: - bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - /ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'}