From 3b1a9eb9a54a9f5f9cb25ab725ee3c101cbc8899 Mon Sep 17 00:00:00 2001 From: Alan Soares Date: Tue, 26 Mar 2024 11:23:06 +1300 Subject: [PATCH 1/4] fix: use for-of loop for bulk update inside pg txn --- .../db/postgres/MaestroPostgresClient.ts | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts index 8a155afae..611732e4e 100644 --- a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts +++ b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts @@ -107,7 +107,13 @@ export default class MaestroPostgresClient { (t) => [ t, - sanitizeObject(values.find((v) => v.tokenId === t.tokenId) ?? {}), + sanitizeObject( + values.find( + (v) => + v.axelarChainId === t.axelarChainId && + v.tokenId === t.tokenId + ) ?? {} + ), ] as const ) .filter(([, v]) => Boolean(v)); @@ -118,31 +124,35 @@ export default class MaestroPostgresClient { }); if (updateValues.length > 0) { - await Promise.all( - updateValues.map(([existingToken, updateValue]) => - 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)) - ) - ); + 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( + and( + eq(remoteInterchainTokens.tokenId, updateValue.tokenId), + eq( + remoteInterchainTokens.axelarChainId, + updateValue.axelarChainId + ) + ) + ); + } } if (!insertValues.length) { From 7aa5efbb960bfe89c4ec2b77ed6b7228c40fc38f Mon Sep 17 00:00:00 2001 From: Alan Soares Date: Tue, 26 Mar 2024 11:23:52 +1300 Subject: [PATCH 2/4] fix(ui): fix Button component loading state forwarding --- .../compounds/MultiStepForm/MultiStepForm.tsx | 2 +- packages/ui/package.json | 2 +- .../src/components/Button/Button.stories.tsx | 2 +- packages/ui/src/components/Button/Button.tsx | 26 +++++++++++++++---- pnpm-lock.yaml | 8 +++--- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/maestro/src/ui/compounds/MultiStepForm/MultiStepForm.tsx b/apps/maestro/src/ui/compounds/MultiStepForm/MultiStepForm.tsx index 4a886dcc4..2fa4b4dae 100644 --- a/apps/maestro/src/ui/compounds/MultiStepForm/MultiStepForm.tsx +++ b/apps/maestro/src/ui/compounds/MultiStepForm/MultiStepForm.tsx @@ -46,7 +46,7 @@ ModalFormInput.defaultProps = { export const NextButton: FC = ({ children, ...props }) => ( ); diff --git a/packages/ui/package.json b/packages/ui/package.json index 969385002..e901d2a17 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -82,7 +82,7 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "react-jazzicon": "^1.0.4", - "styled-cva": "^0.2.1", + "styled-cva": "^0.2.2", "tailwind-merge": "^1.14.0" }, "devDependencies": { diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 758cc0751..96eeb3002 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -24,7 +24,7 @@ export const Default = Template.bind({}); export const Loading = Template.bind({}); Loading.args = { - loading: true, + $loading: true, }; export const Disabled = Template.bind({}); diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 7c8292a62..3f7e5d406 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -62,14 +62,21 @@ type VProps = VariantProps; type ButtonElement = Omit; export interface ButtonProps extends ButtonElement, VProps { - loading?: boolean; + /** + * @deprecated Use $loading instead @see $loading + */ + loading?: never; disabled?: boolean; } type LinkElement = JSX.IntrinsicElements["a"]; export interface LinkButtonProps extends LinkElement, VProps { - loading?: boolean; + /** + * @deprecated Use $loading instead @see $loading + */ + loading?: never; + disabled?: boolean; } const getSegmentedProps = ( @@ -104,8 +111,6 @@ const getSegmentedProps = ( ] as const; }; -// write tsdoc for this component - /** * A button component * @@ -128,7 +133,7 @@ export const Button = forwardRef( {...componentProps} ref={ref} > - {props.loading && ( + {props.$loading && ( )} {props.children} @@ -139,6 +144,17 @@ export const Button = forwardRef( Button.displayName = "Button"; +/** + * A link component that looks like a button + * + * @param {LinkButtonProps} props + * @returns {JSX.Element} + * + * @example + * + * Hello World + * + */ export const LinkButton = forwardRef( (props, ref) => { const [classes, componentProps] = getSegmentedProps(props); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9352a3e6c..306bf12a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -729,8 +729,8 @@ importers: specifier: ^1.0.4 version: 1.0.4(react-dom@18.2.0)(react@18.2.0) styled-cva: - specifier: ^0.2.1 - version: 0.2.1(typescript@5.4.3) + specifier: ^0.2.2 + version: 0.2.2(typescript@5.4.3) tailwind-merge: specifier: ^1.14.0 version: 1.14.0 @@ -19764,8 +19764,8 @@ packages: inline-style-parser: 0.1.1 dev: false - /styled-cva@0.2.1(typescript@5.4.3): - resolution: {integrity: sha512-RM/U/5ex54En5g+qFQkdAb51WgSAv4OqGU+gwkgXX1c+Oabu7MibcmPwNTthRAnxJIH1Vret6oRpLioVPQ//rA==} + /styled-cva@0.2.2(typescript@5.4.3): + resolution: {integrity: sha512-Azr+P07BlPRl2Wan9wfSoDcVHOksa6TKIN+rW4SVpJUsWhwTe3Q6Z/gUw6etsgY+yJsvVWoxUqNsjaz49OsIrw==} peerDependencies: typescript: '>=5.0.0' dependencies: From cd16aaaa47e44bdc749d3482c5222d643af1a945 Mon Sep 17 00:00:00 2001 From: Alan Soares Date: Tue, 26 Mar 2024 11:27:14 +1300 Subject: [PATCH 3/4] chore: revert where logic for bulk update --- .../src/services/db/postgres/MaestroPostgresClient.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts index 611732e4e..4030d2965 100644 --- a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts +++ b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts @@ -143,15 +143,7 @@ export default class MaestroPostgresClient { updateValue.tokenAddress ?? existingToken.tokenAddress, updatedAt: new Date(), }) - .where( - and( - eq(remoteInterchainTokens.tokenId, updateValue.tokenId), - eq( - remoteInterchainTokens.axelarChainId, - updateValue.axelarChainId - ) - ) - ); + .where(eq(remoteInterchainTokens.id, existingToken.id)); } } From b2917e21c1d903e9af3c3b310f3ebe0c00ac3d13 Mon Sep 17 00:00:00 2001 From: Alan Soares Date: Tue, 26 Mar 2024 13:58:58 +1300 Subject: [PATCH 4/4] refactor: migrate from @vercel/postgres to neon-http to support batch --- apps/maestro/package.json | 4 +- apps/maestro/src/lib/drizzle/client.ts | 19 +++- .../db/postgres/MaestroPostgresClient.ts | 89 ++++++++++--------- pnpm-lock.yaml | 59 +++++------- 4 files changed, 88 insertions(+), 83 deletions(-) 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..10ad085c6 100644 --- a/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts +++ b/apps/maestro/src/services/db/postgres/MaestroPostgresClient.ts @@ -2,7 +2,7 @@ import { and, eq, inArray } from "drizzle-orm"; import { type Address } from "viem"; import { z } from "zod"; -import type { DBClient } from "~/lib/drizzle/client"; +import type { BatchItems, DBClient } from "~/lib/drizzle/client"; import { AuditLogEvent, AuditLogEventKind, @@ -97,35 +97,34 @@ 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 existingTokens = await this.db.query.remoteInterchainTokens.findMany({ + where: (table, { eq }) => eq(table.tokenId, values[0].tokenId), + }); - const insertValues = values.filter((v) => { - const id = `${v.axelarChainId}:${v.tokenAddress}`; - return !existingTokens.some((t) => t.id === id); - }); + 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 + if (updateValues.length > 0) { + const [first, ...rest] = updateValues.map( + ([existingToken, updateValue]) => + this.db .update(remoteInterchainTokens) .set({ id: updateValue.id ?? existingToken.id, @@ -143,23 +142,29 @@ export default class MaestroPostgresClient { updateValue.tokenAddress ?? existingToken.tokenAddress, updatedAt: new Date(), }) - .where(eq(remoteInterchainTokens.id, existingToken.id)); - } - } + .where(eq(remoteInterchainTokens.id, existingToken.id)) + ); - if (!insertValues.length) { - return; + // only batch if there are more than one update + if (!rest.length) { + await first; + } else { + await this.db.batch([first, ...rest] as BatchItems); } + } - await tx.insert(remoteInterchainTokens).values( - insertValues.map((v) => ({ - ...v, - id: `${v.axelarChainId}:${v.tokenAddress}`, - createdAt: new Date(), - updatedAt: new Date(), - })) - ); - }); + 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'}