From aac23596ec357baf6a41a55c842153cc3258d615 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:20:05 +0100 Subject: [PATCH] refactor: shared internal postgres package around slonik (#7887) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: n1ru4l <14338007+n1ru4l@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Laurin Quast --- integration-tests/package.json | 2 +- integration-tests/testkit/auth.ts | 4 +- integration-tests/testkit/oidc-integration.ts | 6 +- integration-tests/testkit/seed.ts | 45 +- .../tests/api/app-deployments.spec.ts | 23 +- .../tests/api/schema/cleanup-tracker.spec.ts | 23 +- .../tests/api/schema/publish.spec.ts | 33 +- .../tests/apollo-router/apollo-router.test.ts | 3 +- .../tests/migrations/retention.spec.ts | 1 - packages/internal/postgres/README.md | 3 + packages/internal/postgres/package.json | 16 + .../postgres}/src/connection-string.ts | 7 +- packages/internal/postgres/src/index.ts | 18 + .../postgres/src/postgres-database-pool.ts | 246 + packages/internal/postgres/src/psql.ts | 3 + packages/internal/postgres/src/utils.ts | 5 + packages/migrations/package.json | 5 +- .../actions/2021-03-05T19-06-23.initial.ts | 2 +- .../src/actions/2021-03-08T11-02-26.urls.ts | 2 +- .../src/actions/2021-03-09T10-30-35.roles.ts | 2 +- .../actions/2021-03-09T14-02-34.activities.ts | 2 +- .../2021-03-15T19-32-01.commit-project-id.ts | 2 +- .../src/actions/2021-04-20T11-30-30.tokens.ts | 2 +- .../2021-04-30T07-01-57.token-per-target.ts | 2 +- .../actions/2021-04-30T11-47-26.validation.ts | 2 +- ...021-04-30T18-30-00.persisted-operations.ts | 2 +- .../2021-05-07T07-28-07.token-last-used-at.ts | 2 +- .../2021-06-11T10-46-24.slack-integration.ts | 2 +- .../src/actions/2021-06-11T15-38-28.alerts.ts | 2 +- .../src/actions/2021-08-18T13-20-45.urls.ts | 2 +- .../2021-08-27T14-19-48.non-unique-emails.ts | 2 +- .../2021.09.17T14.45.36.token-deleted.ts | 2 +- .../2021.10.07T12.11.13.access-scopes.ts | 2 +- .../2021.11.22T11.23.44.base-schema.ts | 2 +- ...021.12.20T14.05.30.commits-with-targets.ts | 2 +- .../2022.01.21T12.34.46.validation-targets.ts | 2 +- .../2022.03.28T10.31.26.github-integration.ts | 2 +- .../2022.04.15T14.24.17.hash-tokens.ts | 2 +- .../2022.05.03T15.58.13.org_rate_limits.ts | 2 +- .../2022.05.04T11.01.22.billing_plans.ts | 4 +- .../2022.05.05T08.05.35.commits-metadata.ts | 2 +- ...2.07.07T12.15.10.no-schema-pushes-limit.ts | 2 +- .../2022.07.11T10.09.41.get-started-wizard.ts | 2 +- ...11T20.09.37.migrate-pro-hobby-retention.ts | 2 +- ...0.44.target-validation-client-exclusion.ts | 2 +- ...8.25T09.59.16.multiple-invitation-codes.ts | 2 +- .../2022.08.26T06.23.24.add-supertokens-id.ts | 2 +- .../2022.09.14T16.09.43.external-projects.ts | 2 +- .../2022.10.20T08.00.46.oidc-integrations.ts | 2 +- ....07T09.30.47.user-table-varchar-to-text.ts | 2 +- ...22.12.03T09.12.28.organization-transfer.ts | 2 +- .../2022.12.20T09.20.36.oidc-columns.ts | 2 +- .../2023.01.04T17.00.23.hobby-7-by-default.ts | 2 +- .../actions/2023.01.12T17.00.23.cdn-tokens.ts | 2 +- ...46.28.import-legacy-s3-keys-to-database.ts | 14 +- .../2023.01.18T11.03.41.registry-v2.ts | 2 +- ...2023.02.22T09.27.02.delete-personal-org.ts | 2 +- .../2023.03.14T12.14.23.schema-policy.ts | 2 +- .../2023.03.29T11.42.44.feature-flags.ts | 2 +- ...023.04.03T12.51.36.schema-versions-meta.ts | 2 +- ...45.clean-invalid-schema-version-changes.ts | 6 +- ....06.store-supergraph-on-schema-versions.ts | 2 +- .../2023.06.01T09.07.53.create_collections.ts | 2 +- .../2023.06.06T11.26.04.schema-checks.ts | 2 +- ...T11.26.04.schema-checks-manual-approval.ts | 2 +- .../2023.07.27T11.44.36.graphql-endpoint.ts | 2 +- ...08.01T11.44.36.schema-checks-expires-at.ts | 2 +- .../2023.09.01T09.54.00.zendesk-support.ts | 2 +- ...15.23.00.github-check-with-project-name.ts | 2 +- .../2023.09.28T14.14.14.native-fed-v2.ts | 3 +- ...1.44.36.schema-checks-github-repository.ts | 2 +- ...26T12.44.36.schema-checks-filters-index.ts | 8 +- ...0.30T00-00-00.drop-persisted-operations.ts | 2 +- ...2023.11.02T14.41.41.schema-checks-dedup.ts | 14 +- ...23.11.09T00.00.00.schema-check-approval.ts | 2 +- ...1.20T10-00-00.organization-member-roles.ts | 18 +- ...0.schema-version-diff-schema-version-id.ts | 8 +- .../actions/2024.01.26T00.00.00.contracts.ts | 2 +- ...01.schema-check-pagination-index-update.ts | 14 +- ...ma-check-store-breaking-change-metadata.ts | 2 +- ...4.04.09T10-10-00.check-approval-comment.ts | 2 +- .../2024.06.11T10-10-00.ms-teams-webhook.ts | 2 +- .../2024.07.16T13-44-00.oidc-only-access.ts | 2 +- .../2024.07.17T00-00-00.app-deployments.ts | 2 +- ...4.07.23T09.36.00.schema-cleanup-tracker.ts | 95 +- ...24.12.23T00-00-00.improve-version-index.ts | 6 +- ....12.24T00-00-00.improve-version-index-2.ts | 4 +- ...12.27T00.00.00.create-preflight-scripts.ts | 2 +- ...01.02T00-00-00.cascade-deletion-indices.ts | 38 +- ....01.02T00-00-00.legacy-user-org-cleanup.ts | 12 +- ...025.01.09T00-00-00.legacy-member-scopes.ts | 16 +- ...00.00.00.breaking-changes-request-count.ts | 2 +- .../2025.01.13T10-08-00.default-role.ts | 6 +- .../2025.01.17T10-08-00.drop-activities.ts | 2 +- ...T00-00-00.legacy-registry-model-removal.ts | 2 +- ...-00-00.granular-member-role-permissions.ts | 2 +- ...02.14T00-00-00.schema-versions-metadata.ts | 2 +- ....20T00-00-00.organization-access-tokens.ts | 2 +- ...-00.schema-versions-metadata-attributes.ts | 2 +- .../2025.03.20T00-00-00.dangerous-breaking.ts | 2 +- ....14T00-00-00.cascade-deletion-indices-2.ts | 42 +- ...00.contracts-foreign-key-constraint-fix.ts | 4 +- .../2025.05.15T00-00-00.redundant-indices.ts | 10 +- ...00-00-01.organization-member-pagination.ts | 14 +- .../2025.05.28T00-00-00.schema-log-by-ids.ts | 4 +- .../2025.08.30T00-00-00.schema-proposals.ts | 6 +- ...6T00-00-00.schema-log-by-commit-ordered.ts | 6 +- ...25.10.17T00-00-00.project-access-tokens.ts | 8 +- ...00-00-00.granular-oidc-role-permissions.ts | 2 +- .../2025.11.25T00-00-00.members-search.ts | 6 +- ....12.12T00-00-00.workflows-deduplication.ts | 2 +- .../2025.12.17T00-00-00.custom-oidc-scopes.ts | 4 +- ....12.30T00-00-00.schema-proposals-part-2.ts | 4 +- .../2025.12.5T00-00-00.schema-check-url.ts | 2 +- ...2026.01.09T00-00-00.email-verifications.ts | 2 +- ...get-validation-app-deployment-exclusion.ts | 2 +- ...01.25T00-00-00.checks-proposals-changes.ts | 4 +- ...1.27T00-00-00.app-deployment-protection.ts | 2 +- .../2026.01.30T00-00-00.account-linking.ts | 6 +- ....01.30T10-00-00.oidc-require-invitation.ts | 4 +- .../2026.02.06T00-00-00.zendesk-unique.ts | 2 +- .../2026.02.07T00-00-00.saved-filters.ts | 2 +- ...2.18T00-00-00.ensure-supertokens-tables.ts | 8 +- ....02.19T00-00-00.saved-filter-permission.ts | 2 +- ...026.02.24T00-00-00.proposal-composition.ts | 4 +- ...02.25T00-00-00.oidc-integration-domains.ts | 2 +- ....03.25T00-00-00.access-token-expiration.ts | 2 +- packages/migrations/src/index.ts | 6 +- packages/migrations/src/pg-migrator.ts | 26 +- packages/migrations/src/run-pg-migrations.ts | 4 +- packages/migrations/src/scripts/006-cloud.ts | 20 +- packages/migrations/src/scripts/008-cloud.ts | 16 +- .../migrations/src/scripts/environment.ts | 3 +- .../src/scripts/update-retention.ts | 10 +- ...02.22T09.27.02.delete-personal-org.test.ts | 45 +- ....00.github-check-with-project-name.test.ts | 6 +- ...10-00-00.organization-member-roles.test.ts | 73 +- ...1.26T00.00.01.schema-check-purging.test.ts | 473 +- ...23T09.36.00.schema-cleanup-tracker.test.ts | 47 +- ...1.09T00-00-00.legacy-member-scopes.test.ts | 91 +- packages/migrations/test/utils/testkit.ts | 39 +- packages/migrations/tsconfig.json | 29 +- packages/services/api/package.json | 2 +- packages/services/api/src/create.ts | 4 +- .../providers/app-deployments.ts | 97 +- .../auth/providers/email-verification.ts | 31 +- .../auth/providers/supertokens-store.ts | 62 +- .../providers/preflight-script.provider.ts | 11 +- .../providers/oidc-integration.store.ts | 21 +- .../organization-access-tokens-cache.ts | 5 +- .../providers/organization-access-tokens.ts | 51 +- .../providers/organization-member-roles.ts | 37 +- .../providers/organization-members.ts | 47 +- .../providers/resource-selector.ts | 19 +- .../providers/schema-proposal-storage.ts | 51 +- .../proposals/resolvers/SchemaProposal.ts | 6 +- .../providers/saved-filters-storage.ts | 33 +- .../src/modules/schema/providers/contracts.ts | 87 +- .../modules/schema/providers/schema-helper.ts | 1 - .../schema/providers/schema-manager.ts | 2 +- .../src/modules/shared/providers/pg-pool.ts | 4 - .../src/modules/shared/providers/storage.ts | 4 +- .../target/providers/targets-by-id-cache.ts | 5 +- .../target/providers/targets-by-slug-cache.ts | 5 +- packages/services/api/src/shared/entities.ts | 53 +- packages/services/commerce/package.json | 1 + packages/services/commerce/src/index.ts | 15 +- packages/services/server/package.json | 1 + packages/services/server/src/index.ts | 5 +- packages/services/service-common/package.json | 1 - .../services/service-common/src/tracing.ts | 11 +- packages/services/storage/package.json | 6 +- packages/services/storage/src/db/index.ts | 2 - packages/services/storage/src/db/pool.ts | 50 - packages/services/storage/src/db/utils.ts | 23 - packages/services/storage/src/index.ts | 3979 ++++++++--------- .../storage/src/schema-change-model.ts | 2 +- packages/services/storage/src/shared.ts | 8 - packages/services/storage/src/tokens.ts | 104 +- packages/services/tokens/package.json | 1 + .../services/tokens/src/multi-tier-storage.ts | 5 +- packages/services/usage/package.json | 1 + packages/services/usage/src/authn.ts | 4 +- packages/services/usage/src/index.ts | 13 +- packages/services/workflows/package.json | 2 +- packages/services/workflows/src/context.ts | 4 +- .../services/workflows/src/environment.ts | 6 +- packages/services/workflows/src/index.ts | 8 +- .../src/lib/expired-schema-checks.ts | 51 +- .../workflows/src/lib/schema/provider.ts | 55 +- .../src/tasks/purge-expired-dedupe-keys.ts | 4 +- pnpm-lock.yaml | 79 +- pnpm-workspace.yaml | 1 + tsconfig.json | 1 + 194 files changed, 3635 insertions(+), 3333 deletions(-) create mode 100644 packages/internal/postgres/README.md create mode 100644 packages/internal/postgres/package.json rename packages/{migrations => internal/postgres}/src/connection-string.ts (76%) create mode 100644 packages/internal/postgres/src/index.ts create mode 100644 packages/internal/postgres/src/postgres-database-pool.ts create mode 100644 packages/internal/postgres/src/psql.ts create mode 100644 packages/internal/postgres/src/utils.ts delete mode 100644 packages/services/api/src/modules/shared/providers/pg-pool.ts delete mode 100644 packages/services/storage/src/db/pool.ts delete mode 100644 packages/services/storage/src/db/utils.ts delete mode 100644 packages/services/storage/src/shared.ts diff --git a/integration-tests/package.json b/integration-tests/package.json index e6ece78a7..bcb1c4d98 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -20,6 +20,7 @@ "@graphql-hive/core": "workspace:*", "@graphql-typed-document-node/core": "3.2.0", "@hive/commerce": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/schema": "workspace:*", "@hive/server": "workspace:*", "@hive/service-common": "workspace:*", @@ -42,7 +43,6 @@ "human-id": "4.1.1", "ioredis": "5.8.2", "set-cookie-parser": "2.7.1", - "slonik": "30.4.4", "strip-ansi": "7.1.2", "tslib": "2.8.1", "vitest": "4.0.9", diff --git a/integration-tests/testkit/auth.ts b/integration-tests/testkit/auth.ts index cb81fdd28..53ca55884 100644 --- a/integration-tests/testkit/auth.ts +++ b/integration-tests/testkit/auth.ts @@ -1,10 +1,10 @@ -import { DatabasePool } from 'slonik'; import { AccessTokenKeyContainer, hashPassword, } from '@hive/api/modules/auth/lib/supertokens-at-home/crypto'; import { SuperTokensStore } from '@hive/api/modules/auth/providers/supertokens-store'; import { NoopLogger } from '@hive/api/modules/shared/providers/logger'; +import { PostgresDatabasePool } from '@hive/postgres'; import type { InternalApi } from '@hive/server'; import { createNewSession } from '@hive/server/supertokens-at-home/shared'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; @@ -76,7 +76,7 @@ const tokenResponsePromise: { } = {}; export async function authenticate( - pool: DatabasePool, + pool: PostgresDatabasePool, email: string, oidcIntegrationId?: string, ): Promise<{ access_token: string; refresh_token: string; supertokensUserId: string }> { diff --git a/integration-tests/testkit/oidc-integration.ts b/integration-tests/testkit/oidc-integration.ts index 99db94859..d77e35336 100644 --- a/integration-tests/testkit/oidc-integration.ts +++ b/integration-tests/testkit/oidc-integration.ts @@ -1,9 +1,9 @@ import type { AddressInfo } from 'node:net'; import humanId from 'human-id'; import setCookie from 'set-cookie-parser'; -import { sql, type DatabasePool } from 'slonik'; import z from 'zod'; import formDataPlugin from '@fastify/formbody'; +import { psql, type PostgresDatabasePool } from '@hive/postgres'; import { createServer, type FastifyReply, type FastifyRequest } from '@hive/service-common'; import { graphql } from './gql'; import { execute } from './graphql'; @@ -157,7 +157,7 @@ const VerifyEmailMutation = graphql(` export async function createOIDCIntegration(args: { organizationId: string; accessToken: string; - getPool: () => Promise; + getPool: () => Promise; }) { const { accessToken: authToken, getPool } = args; const result = await execute({ @@ -192,7 +192,7 @@ export async function createOIDCIntegration(args: { }) + '.local'; const pool = await getPool(); - const query = sql` + const query = psql` INSERT INTO "oidc_integration_domains" ( "organization_id" , "oidc_integration_id" diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index e9a6a9424..e0bfe749a 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -1,8 +1,9 @@ import { formatISO, subHours } from 'date-fns'; import { humanId } from 'human-id'; -import { createPool, sql } from 'slonik'; +import z from 'zod'; import { NoopLogger } from '@hive/api/modules/shared/providers/logger'; import { createRedisClient } from '@hive/api/modules/shared/providers/redis'; +import { createPostgresDatabasePool, psql } from '@hive/postgres'; import type { Report } from '../../packages/libraries/core/src/client/usage.js'; import { authenticate, userEmail } from './auth'; import { @@ -82,9 +83,9 @@ function createConnectionPool() { db: ensureEnv('POSTGRES_DB'), }; - return createPool( - `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`, - ); + return createPostgresDatabasePool({ + connectionParameters: `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`, + }); } async function createDbConnection() { @@ -97,9 +98,9 @@ async function createDbConnection() { }; } -export function initSeed() { - let sharedDBPoolPromise: ReturnType; +let sharedDBPoolPromise: ReturnType; +export function initSeed() { function getPool() { if (!sharedDBPoolPromise) { sharedDBPoolPromise = createDbConnection(); @@ -118,7 +119,7 @@ export function initSeed() { if (opts?.verifyEmail ?? true) { const pool = await getPool(); - await pool.query(sql` + await pool.query(psql` INSERT INTO "email_verifications" ("user_identity_id", "email", "verified_at") VALUES (${auth.supertokensUserId}, ${email}, NOW()) `); @@ -142,18 +143,22 @@ export function initSeed() { pollForEmailVerificationLink, async purgeOIDCDomains() { const pool = await getPool(); - await pool.query(sql` + await pool.query(psql` TRUNCATE "oidc_integration_domains" `); }, async forgeOIDCDNSChallenge(orgSlug: string) { const pool = await getPool(); - const domainChallengeId = await pool.oneFirst(sql` + const domainChallengeId = await pool + .oneFirst( + psql` SELECT "oidc_integration_domains"."id" FROM "oidc_integration_domains" INNER JOIN "organizations" ON "oidc_integration_domains"."organization_id" = "organizations"."id" WHERE "organizations"."clean_id" = ${orgSlug} - `); + `, + ) + .then(z.string().parse); const key = `hive:oidcDomainChallenge:${domainChallengeId}`; const challenge = { @@ -208,7 +213,7 @@ export function initSeed() { async overrideOrgPlan(plan: 'PRO' | 'ENTERPRISE' | 'HOBBY') { const pool = await createConnectionPool(); - await pool.query(sql` + await pool.query(psql` UPDATE organizations SET plan_name = ${plan} WHERE id = ${organization.id} `); @@ -260,8 +265,8 @@ export function initSeed() { async setFeatureFlag(name: string, value: boolean | string[]) { const pool = await createConnectionPool(); - await pool.query(sql` - UPDATE organizations SET feature_flags = ${sql.jsonb({ + await pool.query(psql` + UPDATE organizations SET feature_flags = ${psql.jsonb({ [name]: value, })} WHERE id = ${organization.id} @@ -272,7 +277,7 @@ export function initSeed() { async setDataRetention(days: number) { const pool = await createConnectionPool(); - await pool.query(sql` + await pool.query(psql` UPDATE organizations SET limit_retention_days = ${days} WHERE id = ${organization.id} `); @@ -340,13 +345,15 @@ export function initSeed() { /** Expires tokens */ async forceExpireTokens(tokenIds: string[]) { const pool = await createConnectionPool(); - const result = await pool.query(sql` + const result = await pool.any(psql` UPDATE "organization_access_tokens" - SET "expires_at"=NOW() - WHERE id IN (${sql.join(tokenIds, sql`, `)}) AND organization_id=${organization.id} + SET "expires_at" = NOW() + WHERE id IN (${psql.join(tokenIds, psql`, `)}) AND organization_id = ${organization.id} + RETURNING + "id" `); await pool.end(); - expect(result.rowCount).toBe(tokenIds.length); + expect(result.length).toBe(tokenIds.length); for (const id of tokenIds) { await purgeOrganizationAccessTokenById(id); } @@ -390,7 +397,7 @@ export function initSeed() { async setNativeFederation(enabled: boolean) { const pool = await createConnectionPool(); - await pool.query(sql` + await pool.query(psql` UPDATE projects SET native_federation = ${enabled} WHERE id = ${project.id} `); diff --git a/integration-tests/tests/api/app-deployments.spec.ts b/integration-tests/tests/api/app-deployments.spec.ts index 4af2e94f8..54f7252e4 100644 --- a/integration-tests/tests/api/app-deployments.spec.ts +++ b/integration-tests/tests/api/app-deployments.spec.ts @@ -1,11 +1,12 @@ import { buildASTSchema, parse } from 'graphql'; import { createLogger } from 'graphql-yoga'; -import { sql } from 'slonik'; import { pollFor } from 'testkit/flow'; import { initSeed } from 'testkit/seed'; import { getServiceHost } from 'testkit/utils'; +import z from 'zod'; import { createHive } from '@graphql-hive/core'; -import { clickHouseInsert, clickHouseQuery } from '../../testkit/clickhouse'; +import { psql } from '@hive/postgres'; +import { clickHouseInsert } from '../../testkit/clickhouse'; import { graphql } from '../../testkit/gql'; import { execute } from '../../testkit/graphql'; @@ -2056,12 +2057,20 @@ test('activeAppDeployments works for > 1000 records with a date filter (neverUse ); // insert into postgres - const result = await conn.pool.query(sql` + const result = await conn.pool + .any( + psql` INSERT INTO app_deployments ("target_id", "name", "version", "activated_at") - SELECT * FROM ${sql.unnest(appDeploymentRows, ['uuid', 'text', 'text', 'timestamptz'])} + SELECT * FROM ${psql.unnest(appDeploymentRows, ['uuid', 'text', 'text', 'timestamptz'])} RETURNING "id", "target_id", "name", "version" - `); - expect(result.rowCount).toBe(1200); + `, + ) + .then( + z.array( + z.object({ id: z.string(), target_id: z.string(), name: z.string(), version: z.string() }), + ).parse, + ); + expect(result.length).toBe(1200); // insert into clickhouse and activate const query = `INSERT INTO app_deployments ( @@ -2071,7 +2080,7 @@ test('activeAppDeployments works for > 1000 records with a date filter (neverUse ,"app_version" ,"is_active" ) VALUES -${result.rows +${result .map( r => `( '${r['target_id']}' diff --git a/integration-tests/tests/api/schema/cleanup-tracker.spec.ts b/integration-tests/tests/api/schema/cleanup-tracker.spec.ts index 56de49223..2833d70d4 100644 --- a/integration-tests/tests/api/schema/cleanup-tracker.spec.ts +++ b/integration-tests/tests/api/schema/cleanup-tracker.spec.ts @@ -1,21 +1,28 @@ import 'reflect-metadata'; -import { sql, type CommonQueryMethods } from 'slonik'; /* eslint-disable no-process-env */ import { ProjectType } from 'testkit/gql/graphql'; import { test } from 'vitest'; +import z from 'zod'; +import { psql, type CommonQueryMethods } from '@hive/postgres'; import { initSeed } from '../../../testkit/seed'; async function fetchCoordinates(db: CommonQueryMethods, target: { id: string }) { - const result = await db.query<{ - coordinate: string; - created_in_version_id: string; - deprecated_in_version_id: string | null; - }>(sql` + const result = await db + .any( + psql` SELECT coordinate, created_in_version_id, deprecated_in_version_id FROM schema_coordinate_status WHERE target_id = ${target.id} - `); + `, + ) + .then( + z.object({ + coordinate: z.string(), + created_in_version_id: z.string(), + deprecated_in_version_id: z.string().nullable(), + }).parse, + ); - return result.rows; + return result; } describe.skip('schema cleanup tracker', () => { diff --git a/integration-tests/tests/api/schema/publish.spec.ts b/integration-tests/tests/api/schema/publish.spec.ts index 12c5217be..eef97e8fb 100644 --- a/integration-tests/tests/api/schema/publish.spec.ts +++ b/integration-tests/tests/api/schema/publish.spec.ts @@ -1,10 +1,11 @@ import 'reflect-metadata'; -import { createPool, sql } from 'slonik'; import { graphql } from 'testkit/gql'; /* eslint-disable no-process-env */ import { ProjectType } from 'testkit/gql/graphql'; import { execute } from 'testkit/graphql'; import { assertNonNull, getServiceHost } from 'testkit/utils'; +import z from 'zod'; +import { createPostgresDatabasePool, psql } from '@hive/postgres'; // eslint-disable-next-line import/no-extraneous-dependencies import { createStorage } from '@hive/storage'; import { createTarget, publishSchema, updateSchemaComposition } from '../../../testkit/flow'; @@ -3820,7 +3821,7 @@ test.concurrent( ); const insertLegacyVersion = async ( - pool: Awaited>, + pool: Awaited>, args: { sdl: string; projectId: string; @@ -3828,7 +3829,9 @@ const insertLegacyVersion = async ( serviceUrl: string; }, ) => { - const logId = await pool.oneFirst(sql` + const logId = await pool + .oneFirst( + psql` INSERT INTO schema_log ( author, @@ -3854,9 +3857,13 @@ const insertLegacyVersion = async ( 'PUSH' ) RETURNING id - `); + `, + ) + .then(z.string().parse); - const versionId = await pool.oneFirst(sql` + const versionId = await pool + .oneFirst( + psql` INSERT INTO schema_versions ( is_composable, @@ -3870,9 +3877,11 @@ const insertLegacyVersion = async ( ${logId} ) RETURNING "id" - `); + `, + ) + .then(z.string().parse); - await pool.query(sql` + await pool.query(psql` INSERT INTO schema_version_to_log (version_id, action_id) @@ -3886,7 +3895,7 @@ const insertLegacyVersion = async ( test.concurrent( 'service url change from legacy to new version is displayed correctly', async ({ expect }) => { - let pool: Awaited> | undefined; + let pool: Awaited> | undefined; try { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); @@ -3899,7 +3908,9 @@ test.concurrent( // We need to seed a legacy entry in the database const conn = connectionString(); - pool = await createPool(conn); + pool = await createPostgresDatabasePool({ + connectionParameters: conn, + }); const sdl = 'type Query { ping: String! }'; @@ -3950,7 +3961,7 @@ test.concurrent( test.concurrent( 'service url change from legacy to legacy version is displayed correctly', async ({ expect }) => { - let pool: Awaited> | undefined; + let pool: Awaited> | undefined; try { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); @@ -3963,7 +3974,7 @@ test.concurrent( // We need to seed a legacy entry in the database const conn = connectionString(); - pool = await createPool(conn); + pool = await createPostgresDatabasePool({ connectionParameters: conn }); const sdl = 'type Query { ping: String! }'; diff --git a/integration-tests/tests/apollo-router/apollo-router.test.ts b/integration-tests/tests/apollo-router/apollo-router.test.ts index f8bd227b2..553afdd39 100644 --- a/integration-tests/tests/apollo-router/apollo-router.test.ts +++ b/integration-tests/tests/apollo-router/apollo-router.test.ts @@ -2,12 +2,13 @@ import { existsSync, rmSync, writeFileSync } from 'node:fs'; import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { MaybePromise } from 'slonik/dist/src/types'; import { ProjectType } from 'testkit/gql/graphql'; import { initSeed } from 'testkit/seed'; import { getServiceHost } from 'testkit/utils'; import { execa } from '@esm2cjs/execa'; +type MaybePromise = T | Promise; + describe('Apollo Router Integration', () => { const getAvailablePort = () => new Promise(resolve => { diff --git a/integration-tests/tests/migrations/retention.spec.ts b/integration-tests/tests/migrations/retention.spec.ts index 62dc88107..e94b3d5b1 100644 --- a/integration-tests/tests/migrations/retention.spec.ts +++ b/integration-tests/tests/migrations/retention.spec.ts @@ -74,7 +74,6 @@ test('update-retention script skips gracefully when no env vars are set', async delete process.env.CLICKHOUSE_TTL_HOURLY_MV_TABLES; delete process.env.CLICKHOUSE_TTL_MINUTELY_MV_TABLES; - vi.resetModules(); const { updateRetention } = await import( '../../../packages/migrations/src/scripts/update-retention' ); diff --git a/packages/internal/postgres/README.md b/packages/internal/postgres/README.md new file mode 100644 index 000000000..7655ff40d --- /dev/null +++ b/packages/internal/postgres/README.md @@ -0,0 +1,3 @@ +# Internal Postgres Client + +This is a lightweight abstraction on top of Slonik, that sets up some things for ease of usage. diff --git a/packages/internal/postgres/package.json b/packages/internal/postgres/package.json new file mode 100644 index 000000000..53ea3a791 --- /dev/null +++ b/packages/internal/postgres/package.json @@ -0,0 +1,16 @@ +{ + "name": "@hive/postgres", + "type": "module", + "license": "MIT", + "private": true, + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "slonik": "30.4.4", + "slonik-interceptor-query-logging": "46.4.0" + }, + "devDependencies": { + "@hive/service-common": "workspace:*" + } +} diff --git a/packages/migrations/src/connection-string.ts b/packages/internal/postgres/src/connection-string.ts similarity index 76% rename from packages/migrations/src/connection-string.ts rename to packages/internal/postgres/src/connection-string.ts index 0bb58dfeb..aeef37119 100644 --- a/packages/migrations/src/connection-string.ts +++ b/packages/internal/postgres/src/connection-string.ts @@ -1,11 +1,14 @@ -export function createConnectionString(config: { +export type PostgresConnectionParamaters = { host: string; port: number; password: string | undefined; user: string; db: string; ssl: boolean; -}) { +}; + +/** Create a Postgres Connection String */ +export function createConnectionString(config: PostgresConnectionParamaters) { // prettier-ignore const encodedUser = encodeURIComponent(config.user); const encodedPassword = diff --git a/packages/internal/postgres/src/index.ts b/packages/internal/postgres/src/index.ts new file mode 100644 index 000000000..78058a1cb --- /dev/null +++ b/packages/internal/postgres/src/index.ts @@ -0,0 +1,18 @@ +export { + PostgresDatabasePool, + createPostgresDatabasePool, + type CommonQueryMethods, +} from './postgres-database-pool'; +export { type PostgresConnectionParamaters, createConnectionString } from './connection-string'; +export { psql } from './psql'; +export { + UniqueIntegrityConstraintViolationError, + ForeignKeyIntegrityConstraintViolationError, + type TaggedTemplateLiteralInvocation, + type PrimitiveValueExpression, + type SerializableValue, + type Interceptor, + type Query, + type QueryContext, +} from 'slonik'; +export { toDate } from './utils'; diff --git a/packages/internal/postgres/src/postgres-database-pool.ts b/packages/internal/postgres/src/postgres-database-pool.ts new file mode 100644 index 000000000..4d863de43 --- /dev/null +++ b/packages/internal/postgres/src/postgres-database-pool.ts @@ -0,0 +1,246 @@ +import { + createPool, + type DatabasePool, + type Interceptor, + type PrimitiveValueExpression, + type QueryResultRow, + type QueryResultRowColumn, + type CommonQueryMethods as SlonikCommonQueryMethods, + type TaggedTemplateLiteralInvocation, +} from 'slonik'; +import { createQueryLoggingInterceptor } from 'slonik-interceptor-query-logging'; +import { context, SpanKind, SpanStatusCode, trace } from '@hive/service-common'; +import { createConnectionString, type PostgresConnectionParamaters } from './connection-string'; + +const tracer = trace.getTracer('storage'); + +export interface CommonQueryMethods { + exists( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise; + any( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise>; + maybeOne( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise; + query(sql: TaggedTemplateLiteralInvocation, values?: PrimitiveValueExpression[]): Promise; + oneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise; + one(sql: TaggedTemplateLiteralInvocation, values?: PrimitiveValueExpression[]): Promise; + anyFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise>; + maybeOneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise; +} + +export class PostgresDatabasePool implements CommonQueryMethods { + constructor(private pool: DatabasePool) {} + + /** Retrieve the raw PgPool instance. Refrain from using this API. It only exists for postgraphile workers */ + getRawPgPool() { + return this.pool.pool; + } + + /** Retrieve the raw Slonik instance. Refrain from using this API. */ + getSlonikPool() { + return this.pool; + } + + async exists( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return this.pool.exists(sql, values); + } + + async any( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise> { + return this.pool.any(sql, values); + } + + async maybeOne( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return this.pool.maybeOne(sql, values); + } + + async query( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + await this.pool.query(sql, values); + } + + async oneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await this.pool.oneFirst(sql, values); + } + + async maybeOneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await this.pool.maybeOneFirst(sql, values); + } + + async one( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await this.pool.one(sql, values); + } + + async anyFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise> { + return await this.pool.anyFirst(sql, values); + } + + async transaction( + name: string, + handler: (methods: CommonQueryMethods) => Promise, + ): Promise { + const span = tracer.startSpan(`PG Transaction: ${name}`, { + kind: SpanKind.INTERNAL, + }); + + return context.with(trace.setSpan(context.active(), span), async () => { + return await this.pool.transaction(async methods => { + try { + return await handler({ + async exists( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return methods.exists(sql, values); + }, + async any( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise> { + return methods.any(sql, values); + }, + async maybeOne( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return methods.maybeOne(sql, values); + }, + async query( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + await methods.query(sql, values); + }, + async oneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await methods.oneFirst(sql, values); + }, + async maybeOneFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await methods.maybeOneFirst(sql, values); + }, + async anyFirst( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise> { + return await methods.anyFirst(sql, values); + }, + async one( + sql: TaggedTemplateLiteralInvocation, + values?: PrimitiveValueExpression[], + ): Promise { + return await methods.one(sql, values); + }, + }); + } catch (err) { + span.setAttribute('error', 'true'); + + if (err instanceof Error) { + span.setAttribute('error.type', err.name); + span.setAttribute('error.message', err.message); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + } + + throw err; + } finally { + span.end(); + } + }); + }); + } + + end(): Promise { + return this.pool.end(); + } +} + +const dbInterceptors: Interceptor[] = [createQueryLoggingInterceptor()]; + +export async function createPostgresDatabasePool(args: { + connectionParameters: PostgresConnectionParamaters | string; + maximumPoolSize?: number; + additionalInterceptors?: Interceptor[]; + statementTimeout?: number; +}) { + const connectionString = + typeof args.connectionParameters === 'string' + ? args.connectionParameters + : createConnectionString(args.connectionParameters); + const pool = await createPool(connectionString, { + interceptors: dbInterceptors.concat(args.additionalInterceptors ?? []), + captureStackTrace: false, + maximumPoolSize: args.maximumPoolSize, + idleTimeout: 30000, + statementTimeout: args.statementTimeout, + }); + + function interceptError>( + methodName: K, + ) { + const original: SlonikCommonQueryMethods[K] = pool[methodName]; + + function interceptor( + this: any, + sql: TaggedTemplateLiteralInvocation, + values?: QueryResultRowColumn[], + ): any { + return (original as any).call(this, sql, values).catch((error: any) => { + error.sql = sql.sql; + error.values = sql.values || values; + + return Promise.reject(error); + }); + } + + pool[methodName] = interceptor; + } + + interceptError('one'); + interceptError('many'); + + return new PostgresDatabasePool(pool); +} diff --git a/packages/internal/postgres/src/psql.ts b/packages/internal/postgres/src/psql.ts new file mode 100644 index 000000000..5342c5c08 --- /dev/null +++ b/packages/internal/postgres/src/psql.ts @@ -0,0 +1,3 @@ +import { createSqlTag } from 'slonik'; + +export const psql = createSqlTag(); diff --git a/packages/internal/postgres/src/utils.ts b/packages/internal/postgres/src/utils.ts new file mode 100644 index 000000000..609bc9e04 --- /dev/null +++ b/packages/internal/postgres/src/utils.ts @@ -0,0 +1,5 @@ +import { psql } from './psql'; + +export function toDate(date: Date) { + return psql`to_timestamp(${date.getTime() / 1000})`; +} diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 4c3db6e18..ce988a39c 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -13,10 +13,12 @@ "db:init": "pnpm db:create && pnpm migration:run", "db:migrator": "tsx src/index.ts", "migration:run": "pnpm db:migrator up", - "test": "WATCH=0 tsup-node --config ../../configs/tsup/dev.config.node.ts ./test/root.ts" + "test": "WATCH=0 tsup-node --config ../../configs/tsup/dev.config.node.ts ./test/root.ts", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@graphql-hive/core": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/service-common": "workspace:*", "@types/bcryptjs": "2.4.6", "@types/node": "24.10.9", @@ -30,7 +32,6 @@ "graphql": "16.9.0", "p-limit": "6.2.0", "pg-promise": "11.10.2", - "slonik": "30.4.4", "tslib": "2.8.1", "tsx": "4.19.2", "typescript": "5.7.3", diff --git a/packages/migrations/src/actions/2021-03-05T19-06-23.initial.ts b/packages/migrations/src/actions/2021-03-05T19-06-23.initial.ts index 4f79f21a8..f887b6de6 100644 --- a/packages/migrations/src/actions/2021-03-05T19-06-23.initial.ts +++ b/packages/migrations/src/actions/2021-03-05T19-06-23.initial.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-03-05T19-06-23.initial.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --initial (up) -- Extensions CREATE EXTENSION diff --git a/packages/migrations/src/actions/2021-03-08T11-02-26.urls.ts b/packages/migrations/src/actions/2021-03-08T11-02-26.urls.ts index 88c2e1784..65e2af048 100644 --- a/packages/migrations/src/actions/2021-03-08T11-02-26.urls.ts +++ b/packages/migrations/src/actions/2021-03-08T11-02-26.urls.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-03-08T11-02-26.urls.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --urls (up) ALTER TABLE projects diff --git a/packages/migrations/src/actions/2021-03-09T10-30-35.roles.ts b/packages/migrations/src/actions/2021-03-09T10-30-35.roles.ts index 3cead50b5..081cd4518 100644 --- a/packages/migrations/src/actions/2021-03-09T10-30-35.roles.ts +++ b/packages/migrations/src/actions/2021-03-09T10-30-35.roles.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-03-09T10-30-35.roles.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --roles (up) CREATE TYPE user_role AS ENUM('ADMIN', 'MEMBER'); diff --git a/packages/migrations/src/actions/2021-03-09T14-02-34.activities.ts b/packages/migrations/src/actions/2021-03-09T14-02-34.activities.ts index 92c9c931b..a227a0ab1 100644 --- a/packages/migrations/src/actions/2021-03-09T14-02-34.activities.ts +++ b/packages/migrations/src/actions/2021-03-09T14-02-34.activities.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-03-09T14-02-34.activities.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --activities (up) CREATE TABLE activities ( diff --git a/packages/migrations/src/actions/2021-03-15T19-32-01.commit-project-id.ts b/packages/migrations/src/actions/2021-03-15T19-32-01.commit-project-id.ts index 7dbb9895a..10bff587d 100644 --- a/packages/migrations/src/actions/2021-03-15T19-32-01.commit-project-id.ts +++ b/packages/migrations/src/actions/2021-03-15T19-32-01.commit-project-id.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-03-15T19-32-01.commit-project-id.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --commit-project-id (up) ALTER TABLE commits diff --git a/packages/migrations/src/actions/2021-04-20T11-30-30.tokens.ts b/packages/migrations/src/actions/2021-04-20T11-30-30.tokens.ts index 3e7849a3a..28ecac6ca 100644 --- a/packages/migrations/src/actions/2021-04-20T11-30-30.tokens.ts +++ b/packages/migrations/src/actions/2021-04-20T11-30-30.tokens.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-04-20T11-30-30.tokens.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --tokens (up) ALTER TABLE tokens diff --git a/packages/migrations/src/actions/2021-04-30T07-01-57.token-per-target.ts b/packages/migrations/src/actions/2021-04-30T07-01-57.token-per-target.ts index 6de9b6e35..687838948 100644 --- a/packages/migrations/src/actions/2021-04-30T07-01-57.token-per-target.ts +++ b/packages/migrations/src/actions/2021-04-30T07-01-57.token-per-target.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-04-30T07-01-57.token-per-target.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --token-per-target (up) ALTER TABLE tokens diff --git a/packages/migrations/src/actions/2021-04-30T11-47-26.validation.ts b/packages/migrations/src/actions/2021-04-30T11-47-26.validation.ts index 092330844..0ae3534a1 100644 --- a/packages/migrations/src/actions/2021-04-30T11-47-26.validation.ts +++ b/packages/migrations/src/actions/2021-04-30T11-47-26.validation.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-04-30T11-47-26.validation.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --validation (up) ALTER TABLE targets diff --git a/packages/migrations/src/actions/2021-04-30T18-30-00.persisted-operations.ts b/packages/migrations/src/actions/2021-04-30T18-30-00.persisted-operations.ts index d476e5243..f5f4791c7 100644 --- a/packages/migrations/src/actions/2021-04-30T18-30-00.persisted-operations.ts +++ b/packages/migrations/src/actions/2021-04-30T18-30-00.persisted-operations.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-04-30T18-30-00.persisted-operations.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --persisted_operations (up) CREATE TYPE operation_kind AS ENUM('query', 'mutation', 'subscription'); diff --git a/packages/migrations/src/actions/2021-05-07T07-28-07.token-last-used-at.ts b/packages/migrations/src/actions/2021-05-07T07-28-07.token-last-used-at.ts index 3f3cf0f4b..49075435d 100644 --- a/packages/migrations/src/actions/2021-05-07T07-28-07.token-last-used-at.ts +++ b/packages/migrations/src/actions/2021-05-07T07-28-07.token-last-used-at.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-05-07T07-28-07.token-last-used-at.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --token-last-used-at (up) ALTER TABLE tokens diff --git a/packages/migrations/src/actions/2021-06-11T10-46-24.slack-integration.ts b/packages/migrations/src/actions/2021-06-11T10-46-24.slack-integration.ts index 62eb0de85..ba22a879f 100644 --- a/packages/migrations/src/actions/2021-06-11T10-46-24.slack-integration.ts +++ b/packages/migrations/src/actions/2021-06-11T10-46-24.slack-integration.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-06-11T10-46-24.slack-integration.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --slack-integration (up) ALTER TABLE organizations diff --git a/packages/migrations/src/actions/2021-06-11T15-38-28.alerts.ts b/packages/migrations/src/actions/2021-06-11T15-38-28.alerts.ts index afab7f57f..907bd1bde 100644 --- a/packages/migrations/src/actions/2021-06-11T15-38-28.alerts.ts +++ b/packages/migrations/src/actions/2021-06-11T15-38-28.alerts.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-06-11T15-38-28.alerts.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --alerts (up) CREATE TYPE alert_channel_type AS ENUM('SLACK', 'WEBHOOK'); diff --git a/packages/migrations/src/actions/2021-08-18T13-20-45.urls.ts b/packages/migrations/src/actions/2021-08-18T13-20-45.urls.ts index c2d4b98f6..7190ee036 100644 --- a/packages/migrations/src/actions/2021-08-18T13-20-45.urls.ts +++ b/packages/migrations/src/actions/2021-08-18T13-20-45.urls.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-08-18T13-20-45.urls.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --urls (up) ALTER TABLE version_commit diff --git a/packages/migrations/src/actions/2021-08-27T14-19-48.non-unique-emails.ts b/packages/migrations/src/actions/2021-08-27T14-19-48.non-unique-emails.ts index 52405a3e5..4703b5424 100644 --- a/packages/migrations/src/actions/2021-08-27T14-19-48.non-unique-emails.ts +++ b/packages/migrations/src/actions/2021-08-27T14-19-48.non-unique-emails.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021-08-27T14-19-48.non-unique-emails.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --non-unique-emails (up) DROP INDEX email_idx; diff --git a/packages/migrations/src/actions/2021.09.17T14.45.36.token-deleted.ts b/packages/migrations/src/actions/2021.09.17T14.45.36.token-deleted.ts index 4dce09fbf..52bbd53a0 100644 --- a/packages/migrations/src/actions/2021.09.17T14.45.36.token-deleted.ts +++ b/packages/migrations/src/actions/2021.09.17T14.45.36.token-deleted.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021.09.17T14.45.36.token-deleted.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE tokens ADD COLUMN diff --git a/packages/migrations/src/actions/2021.10.07T12.11.13.access-scopes.ts b/packages/migrations/src/actions/2021.10.07T12.11.13.access-scopes.ts index 395190d3f..f6f43886b 100644 --- a/packages/migrations/src/actions/2021.10.07T12.11.13.access-scopes.ts +++ b/packages/migrations/src/actions/2021.10.07T12.11.13.access-scopes.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021.10.07T12.11.13.access-scopes.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Adds scopes to tokens ALTER TABLE tokens diff --git a/packages/migrations/src/actions/2021.11.22T11.23.44.base-schema.ts b/packages/migrations/src/actions/2021.11.22T11.23.44.base-schema.ts index 9e419e290..e50696752 100644 --- a/packages/migrations/src/actions/2021.11.22T11.23.44.base-schema.ts +++ b/packages/migrations/src/actions/2021.11.22T11.23.44.base-schema.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021.11.22T11.23.44.base-schema.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Adds a base schema column in target table and versions table ALTER TABLE targets diff --git a/packages/migrations/src/actions/2021.12.20T14.05.30.commits-with-targets.ts b/packages/migrations/src/actions/2021.12.20T14.05.30.commits-with-targets.ts index f2d3fa552..17cda7ad2 100644 --- a/packages/migrations/src/actions/2021.12.20T14.05.30.commits-with-targets.ts +++ b/packages/migrations/src/actions/2021.12.20T14.05.30.commits-with-targets.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2021.12.20T14.05.30.commits-with-targets.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --creates and fills a target_id column on commits ALTER TABLE commits diff --git a/packages/migrations/src/actions/2022.01.21T12.34.46.validation-targets.ts b/packages/migrations/src/actions/2022.01.21T12.34.46.validation-targets.ts index 1a3f122ce..a90dd1633 100644 --- a/packages/migrations/src/actions/2022.01.21T12.34.46.validation-targets.ts +++ b/packages/migrations/src/actions/2022.01.21T12.34.46.validation-targets.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.01.21T12.34.46.validation-targets.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE target_validation ( target_id UUID NOT NULL REFERENCES targets (id) ON DELETE CASCADE, diff --git a/packages/migrations/src/actions/2022.03.28T10.31.26.github-integration.ts b/packages/migrations/src/actions/2022.03.28T10.31.26.github-integration.ts index c7fa0bba6..cd79e7562 100644 --- a/packages/migrations/src/actions/2022.03.28T10.31.26.github-integration.ts +++ b/packages/migrations/src/actions/2022.03.28T10.31.26.github-integration.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.03.28T10.31.26.github-integration.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` --slack-integration (up) ALTER TABLE organizations diff --git a/packages/migrations/src/actions/2022.04.15T14.24.17.hash-tokens.ts b/packages/migrations/src/actions/2022.04.15T14.24.17.hash-tokens.ts index 8e0464ee8..adb86d251 100644 --- a/packages/migrations/src/actions/2022.04.15T14.24.17.hash-tokens.ts +++ b/packages/migrations/src/actions/2022.04.15T14.24.17.hash-tokens.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.04.15T14.24.17.hash-tokens.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE tokens ADD COLUMN diff --git a/packages/migrations/src/actions/2022.05.03T15.58.13.org_rate_limits.ts b/packages/migrations/src/actions/2022.05.03T15.58.13.org_rate_limits.ts index 0a19f5430..afd79d1bf 100644 --- a/packages/migrations/src/actions/2022.05.03T15.58.13.org_rate_limits.ts +++ b/packages/migrations/src/actions/2022.05.03T15.58.13.org_rate_limits.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.05.03T15.58.13.org_rate_limits.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE organizations ADD COLUMN diff --git a/packages/migrations/src/actions/2022.05.04T11.01.22.billing_plans.ts b/packages/migrations/src/actions/2022.05.04T11.01.22.billing_plans.ts index eb26f4bf4..2b0226bab 100644 --- a/packages/migrations/src/actions/2022.05.04T11.01.22.billing_plans.ts +++ b/packages/migrations/src/actions/2022.05.04T11.01.22.billing_plans.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.05.04T11.01.22.billing_plans.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE organizations_billing ( - organization_id UUID NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, -- org id + organization_id UUID NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, -- org id external_billing_reference_id VARCHAR(255) NOT NULL, -- stripe customer id billing_email_address VARCHAR(255), PRIMARY KEY (organization_id) diff --git a/packages/migrations/src/actions/2022.05.05T08.05.35.commits-metadata.ts b/packages/migrations/src/actions/2022.05.05T08.05.35.commits-metadata.ts index 2118b4506..d6ff7c2e7 100644 --- a/packages/migrations/src/actions/2022.05.05T08.05.35.commits-metadata.ts +++ b/packages/migrations/src/actions/2022.05.05T08.05.35.commits-metadata.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.05.05T08.05.35.commits-metadata.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE commits ADD COLUMN diff --git a/packages/migrations/src/actions/2022.07.07T12.15.10.no-schema-pushes-limit.ts b/packages/migrations/src/actions/2022.07.07T12.15.10.no-schema-pushes-limit.ts index 51e78d7b6..99c08b918 100644 --- a/packages/migrations/src/actions/2022.07.07T12.15.10.no-schema-pushes-limit.ts +++ b/packages/migrations/src/actions/2022.07.07T12.15.10.no-schema-pushes-limit.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.07.07T12.15.10.no-schema-pushes-limit.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE organizations DROP COLUMN diff --git a/packages/migrations/src/actions/2022.07.11T10.09.41.get-started-wizard.ts b/packages/migrations/src/actions/2022.07.11T10.09.41.get-started-wizard.ts index 3853244ba..ae2b80775 100644 --- a/packages/migrations/src/actions/2022.07.11T10.09.41.get-started-wizard.ts +++ b/packages/migrations/src/actions/2022.07.11T10.09.41.get-started-wizard.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.07.11T10.09.41.get-started-wizard.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Tracks feature discovery progress ALTER TABLE organizations diff --git a/packages/migrations/src/actions/2022.07.11T20.09.37.migrate-pro-hobby-retention.ts b/packages/migrations/src/actions/2022.07.11T20.09.37.migrate-pro-hobby-retention.ts index ccfe724de..7c668e28c 100644 --- a/packages/migrations/src/actions/2022.07.11T20.09.37.migrate-pro-hobby-retention.ts +++ b/packages/migrations/src/actions/2022.07.11T20.09.37.migrate-pro-hobby-retention.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.07.11T20.09.37.migrate-pro-hobby-retention.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Update Hobby with 3d to 7d UPDATE organizations diff --git a/packages/migrations/src/actions/2022.07.18T10.10.44.target-validation-client-exclusion.ts b/packages/migrations/src/actions/2022.07.18T10.10.44.target-validation-client-exclusion.ts index 4be3536c6..f854fe08e 100644 --- a/packages/migrations/src/actions/2022.07.18T10.10.44.target-validation-client-exclusion.ts +++ b/packages/migrations/src/actions/2022.07.18T10.10.44.target-validation-client-exclusion.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.07.18T10.10.44.target-validation-client-exclusion.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE targets ADD COLUMN diff --git a/packages/migrations/src/actions/2022.08.25T09.59.16.multiple-invitation-codes.ts b/packages/migrations/src/actions/2022.08.25T09.59.16.multiple-invitation-codes.ts index 58f31616b..42eeaa483 100644 --- a/packages/migrations/src/actions/2022.08.25T09.59.16.multiple-invitation-codes.ts +++ b/packages/migrations/src/actions/2022.08.25T09.59.16.multiple-invitation-codes.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.08.25T09.59.16.multiple-invitation-codes.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE organizations DROP COLUMN diff --git a/packages/migrations/src/actions/2022.08.26T06.23.24.add-supertokens-id.ts b/packages/migrations/src/actions/2022.08.26T06.23.24.add-supertokens-id.ts index d52869c21..2b47deb12 100644 --- a/packages/migrations/src/actions/2022.08.26T06.23.24.add-supertokens-id.ts +++ b/packages/migrations/src/actions/2022.08.26T06.23.24.add-supertokens-id.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.08.26T06.23.24.add-supertokens-id.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE users ADD COLUMN diff --git a/packages/migrations/src/actions/2022.09.14T16.09.43.external-projects.ts b/packages/migrations/src/actions/2022.09.14T16.09.43.external-projects.ts index 35bb481d5..0e14c18c3 100644 --- a/packages/migrations/src/actions/2022.09.14T16.09.43.external-projects.ts +++ b/packages/migrations/src/actions/2022.09.14T16.09.43.external-projects.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.09.14T16.09.43.external-projects.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE projects ADD COLUMN diff --git a/packages/migrations/src/actions/2022.10.20T08.00.46.oidc-integrations.ts b/packages/migrations/src/actions/2022.10.20T08.00.46.oidc-integrations.ts index 50a0c670c..d281218a2 100644 --- a/packages/migrations/src/actions/2022.10.20T08.00.46.oidc-integrations.ts +++ b/packages/migrations/src/actions/2022.10.20T08.00.46.oidc-integrations.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.10.20T08.00.46.oidc-integrations.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE IF NOT EXISTS "oidc_integrations" ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), diff --git a/packages/migrations/src/actions/2022.11.07T09.30.47.user-table-varchar-to-text.ts b/packages/migrations/src/actions/2022.11.07T09.30.47.user-table-varchar-to-text.ts index c57a82ebe..72e5efb25 100644 --- a/packages/migrations/src/actions/2022.11.07T09.30.47.user-table-varchar-to-text.ts +++ b/packages/migrations/src/actions/2022.11.07T09.30.47.user-table-varchar-to-text.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.11.07T09.30.47.user-table-varchar-to-text.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "users" ALTER COLUMN diff --git a/packages/migrations/src/actions/2022.12.03T09.12.28.organization-transfer.ts b/packages/migrations/src/actions/2022.12.03T09.12.28.organization-transfer.ts index 9f371b1e6..a6dd9ff85 100644 --- a/packages/migrations/src/actions/2022.12.03T09.12.28.organization-transfer.ts +++ b/packages/migrations/src/actions/2022.12.03T09.12.28.organization-transfer.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.12.03T09.12.28.organization-transfer.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE organizations ADD COLUMN diff --git a/packages/migrations/src/actions/2022.12.20T09.20.36.oidc-columns.ts b/packages/migrations/src/actions/2022.12.20T09.20.36.oidc-columns.ts index da011b23f..386006c91 100644 --- a/packages/migrations/src/actions/2022.12.20T09.20.36.oidc-columns.ts +++ b/packages/migrations/src/actions/2022.12.20T09.20.36.oidc-columns.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2022.12.20T09.20.36.oidc-columns.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "oidc_integrations" ADD COLUMN diff --git a/packages/migrations/src/actions/2023.01.04T17.00.23.hobby-7-by-default.ts b/packages/migrations/src/actions/2023.01.04T17.00.23.hobby-7-by-default.ts index a2f09fb98..88cc6b527 100644 --- a/packages/migrations/src/actions/2023.01.04T17.00.23.hobby-7-by-default.ts +++ b/packages/migrations/src/actions/2023.01.04T17.00.23.hobby-7-by-default.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.01.04T17.00.23.hobby-7-by-default.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Update Hobby with 3d to 7d - personal orgs were created with the default value of 3d UPDATE organizations diff --git a/packages/migrations/src/actions/2023.01.12T17.00.23.cdn-tokens.ts b/packages/migrations/src/actions/2023.01.12T17.00.23.cdn-tokens.ts index 02495adda..12d798bfe 100644 --- a/packages/migrations/src/actions/2023.01.12T17.00.23.cdn-tokens.ts +++ b/packages/migrations/src/actions/2023.01.12T17.00.23.cdn-tokens.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.01.12T17.00.23.cdn-tokens.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "cdn_access_tokens" ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), diff --git a/packages/migrations/src/actions/2023.01.17T10.46.28.import-legacy-s3-keys-to-database.ts b/packages/migrations/src/actions/2023.01.17T10.46.28.import-legacy-s3-keys-to-database.ts index 05a07c39d..95bed9f30 100644 --- a/packages/migrations/src/actions/2023.01.17T10.46.28.import-legacy-s3-keys-to-database.ts +++ b/packages/migrations/src/actions/2023.01.17T10.46.28.import-legacy-s3-keys-to-database.ts @@ -39,7 +39,7 @@ type Cursor = { lastCreatedAt: string; }; -const run: MigrationExecutor['run'] = async ({ connection, sql }) => { +const run: MigrationExecutor['run'] = async ({ connection, psql }) => { // eslint-disable-next-line no-process-env const eenv = shouldRunModel.parse(process.env); const shouldRun = eenv.RUN_S3_LEGACY_CDN_KEY_IMPORT === '1'; @@ -71,7 +71,7 @@ const run: MigrationExecutor['run'] = async ({ connection, sql }) => { // Also all this code runs inside a database transaction. // This will block any other writes to the table. // As the table should not be heavily in use when this is being run, it does not really matter. - const query = sql` + const query = psql` SELECT "id" , to_json("created_at") as "created_at_cursor" @@ -79,12 +79,12 @@ const run: MigrationExecutor['run'] = async ({ connection, sql }) => { "targets" ${ cursor - ? sql` + ? psql` WHERE ("created_at" = ${cursor.lastCreatedAt} AND "id" > ${cursor.lastId}) OR "created_at" > ${cursor.lastCreatedAt} ` - : sql`` + : psql`` } ORDER BY "created_at" ASC @@ -93,8 +93,8 @@ const run: MigrationExecutor['run'] = async ({ connection, sql }) => { 200 `; - const items = await connection.query(query); - return TargetsModel.parse(items.rows); + const items = await connection.any(query); + return TargetsModel.parse(items); } let lastCursor: null | Cursor = null; @@ -120,7 +120,7 @@ const run: MigrationExecutor['run'] = async ({ connection, sql }) => { throw new Error(`Unexpected Status for storing key. (status=${response.status})`); } - const query = sql` + const query = psql` INSERT INTO "cdn_access_tokens" ( diff --git a/packages/migrations/src/actions/2023.01.18T11.03.41.registry-v2.ts b/packages/migrations/src/actions/2023.01.18T11.03.41.registry-v2.ts index 063a34399..ed0c04482 100644 --- a/packages/migrations/src/actions/2023.01.18T11.03.41.registry-v2.ts +++ b/packages/migrations/src/actions/2023.01.18T11.03.41.registry-v2.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.01.18T11.03.41.registry-v2.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- CREATE INDEX IF NOT EXISTS version_commit_cid_vid_idx ON version_commit (commit_id, version_id); diff --git a/packages/migrations/src/actions/2023.02.22T09.27.02.delete-personal-org.ts b/packages/migrations/src/actions/2023.02.22T09.27.02.delete-personal-org.ts index b35397463..ddfd9bc7c 100644 --- a/packages/migrations/src/actions/2023.02.22T09.27.02.delete-personal-org.ts +++ b/packages/migrations/src/actions/2023.02.22T09.27.02.delete-personal-org.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.02.22T09.27.02.delete-personal-org.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` -- Find and delete all organizations of type PERSONAL that have no projects DELETE FROM organizations AS o diff --git a/packages/migrations/src/actions/2023.03.14T12.14.23.schema-policy.ts b/packages/migrations/src/actions/2023.03.14T12.14.23.schema-policy.ts index 2793c01a4..79f807d40 100644 --- a/packages/migrations/src/actions/2023.03.14T12.14.23.schema-policy.ts +++ b/packages/migrations/src/actions/2023.03.14T12.14.23.schema-policy.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.03.14T12.14.23.schema-policy.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TYPE schema_policy_resource AS ENUM('ORGANIZATION', 'PROJECT'); diff --git a/packages/migrations/src/actions/2023.03.29T11.42.44.feature-flags.ts b/packages/migrations/src/actions/2023.03.29T11.42.44.feature-flags.ts index 70bbfed7d..f7208af83 100644 --- a/packages/migrations/src/actions/2023.03.29T11.42.44.feature-flags.ts +++ b/packages/migrations/src/actions/2023.03.29T11.42.44.feature-flags.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.03.29T11.42.44.feature-flags.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE organizations ADD COLUMN diff --git a/packages/migrations/src/actions/2023.04.03T12.51.36.schema-versions-meta.ts b/packages/migrations/src/actions/2023.04.03T12.51.36.schema-versions-meta.ts index a061ae75e..afe3bc80b 100644 --- a/packages/migrations/src/actions/2023.04.03T12.51.36.schema-versions-meta.ts +++ b/packages/migrations/src/actions/2023.04.03T12.51.36.schema-versions-meta.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.04.03T12.51.36.schema-versions-meta.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "schema_version_changes" ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), diff --git a/packages/migrations/src/actions/2023.05.08T12.23.45.clean-invalid-schema-version-changes.ts b/packages/migrations/src/actions/2023.05.08T12.23.45.clean-invalid-schema-version-changes.ts index aacfee66f..7ac5479f4 100644 --- a/packages/migrations/src/actions/2023.05.08T12.23.45.clean-invalid-schema-version-changes.ts +++ b/packages/migrations/src/actions/2023.05.08T12.23.45.clean-invalid-schema-version-changes.ts @@ -2,11 +2,11 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.05.08T12.23.45.clean-invalid-schema-version-changes.sql', - run: ({ sql }) => sql` -DELETE + run: ({ psql }) => psql` +DELETE FROM "schema_version_changes" "svc" -WHERE +WHERE "svc"."change_type" = 'REGISTRY_SERVICE_URL_CHANGED' AND ( NOT "svc"."meta"->'serviceUrls' ? 'new' diff --git a/packages/migrations/src/actions/2023.05.12T08.29.06.store-supergraph-on-schema-versions.ts b/packages/migrations/src/actions/2023.05.12T08.29.06.store-supergraph-on-schema-versions.ts index 4ab773bd5..161c60526 100644 --- a/packages/migrations/src/actions/2023.05.12T08.29.06.store-supergraph-on-schema-versions.ts +++ b/packages/migrations/src/actions/2023.05.12T08.29.06.store-supergraph-on-schema-versions.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.05.12T08.29.06.store-supergraph-on-schema-versions.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_versions" ADD COLUMN "supergraph_sdl" text ; diff --git a/packages/migrations/src/actions/2023.06.01T09.07.53.create_collections.ts b/packages/migrations/src/actions/2023.06.01T09.07.53.create_collections.ts index 12de6412b..600edcb2b 100644 --- a/packages/migrations/src/actions/2023.06.01T09.07.53.create_collections.ts +++ b/packages/migrations/src/actions/2023.06.01T09.07.53.create_collections.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.06.01T09.07.53.create_collections.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "document_collections" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" text NOT NULL, diff --git a/packages/migrations/src/actions/2023.06.06T11.26.04.schema-checks.ts b/packages/migrations/src/actions/2023.06.06T11.26.04.schema-checks.ts index 2baa3f6be..6f5e57a5e 100644 --- a/packages/migrations/src/actions/2023.06.06T11.26.04.schema-checks.ts +++ b/packages/migrations/src/actions/2023.06.06T11.26.04.schema-checks.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.06.06T11.26.04.schema-checks.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "schema_checks" ( "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4() , "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() diff --git a/packages/migrations/src/actions/2023.07.10T11.26.04.schema-checks-manual-approval.ts b/packages/migrations/src/actions/2023.07.10T11.26.04.schema-checks-manual-approval.ts index 17243dda3..82191e7eb 100644 --- a/packages/migrations/src/actions/2023.07.10T11.26.04.schema-checks-manual-approval.ts +++ b/packages/migrations/src/actions/2023.07.10T11.26.04.schema-checks-manual-approval.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.07.10T11.26.04.schema-checks-manual-approval.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN "github_check_run_id" bigint , ADD COLUMN "is_manually_approved" boolean diff --git a/packages/migrations/src/actions/2023.07.27T11.44.36.graphql-endpoint.ts b/packages/migrations/src/actions/2023.07.27T11.44.36.graphql-endpoint.ts index 93804a1b6..9c7811f00 100644 --- a/packages/migrations/src/actions/2023.07.27T11.44.36.graphql-endpoint.ts +++ b/packages/migrations/src/actions/2023.07.27T11.44.36.graphql-endpoint.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.07.27T11.44.36.graphql-endpoint.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "targets" ADD COLUMN "graphql_endpoint_url" text ; diff --git a/packages/migrations/src/actions/2023.08.01T11.44.36.schema-checks-expires-at.ts b/packages/migrations/src/actions/2023.08.01T11.44.36.schema-checks-expires-at.ts index 0760d787d..c9e53d96d 100644 --- a/packages/migrations/src/actions/2023.08.01T11.44.36.schema-checks-expires-at.ts +++ b/packages/migrations/src/actions/2023.08.01T11.44.36.schema-checks-expires-at.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.08.01T11.44.36.schema-checks-expires-at.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN "expires_at" TIMESTAMP WITH TIME ZONE ; diff --git a/packages/migrations/src/actions/2023.09.01T09.54.00.zendesk-support.ts b/packages/migrations/src/actions/2023.09.01T09.54.00.zendesk-support.ts index 8b604108c..075c90cb2 100644 --- a/packages/migrations/src/actions/2023.09.01T09.54.00.zendesk-support.ts +++ b/packages/migrations/src/actions/2023.09.01T09.54.00.zendesk-support.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.09.01T09.54.00.zendesk-support.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "users" ADD COLUMN "zendesk_user_id" TEXT UNIQUE DEFAULT NULL; CREATE INDEX "users_by_zendesk_user_id" ON "users" ("zendesk_user_id" ASC); diff --git a/packages/migrations/src/actions/2023.09.25T15.23.00.github-check-with-project-name.ts b/packages/migrations/src/actions/2023.09.25T15.23.00.github-check-with-project-name.ts index 4454e9ed2..abace5a79 100644 --- a/packages/migrations/src/actions/2023.09.25T15.23.00.github-check-with-project-name.ts +++ b/packages/migrations/src/actions/2023.09.25T15.23.00.github-check-with-project-name.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.09.25T15.23.00.github-check-with-project-name.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "projects" ADD COLUMN "github_check_with_project_name" BOOLEAN; UPDATE "projects" SET "github_check_with_project_name" = FALSE WHERE "github_check_with_project_name" IS NULL; ALTER TABLE "projects" diff --git a/packages/migrations/src/actions/2023.09.28T14.14.14.native-fed-v2.ts b/packages/migrations/src/actions/2023.09.28T14.14.14.native-fed-v2.ts index 4fb2284ef..6606e591c 100644 --- a/packages/migrations/src/actions/2023.09.28T14.14.14.native-fed-v2.ts +++ b/packages/migrations/src/actions/2023.09.28T14.14.14.native-fed-v2.ts @@ -2,5 +2,6 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.09.28T14.14.14.native-fed-v2.ts', - run: ({ sql }) => sql`ALTER TABLE "projects" ADD COLUMN native_federation BOOLEAN DEFAULT FALSE;`, + run: ({ psql }) => + psql`ALTER TABLE "projects" ADD COLUMN native_federation BOOLEAN DEFAULT FALSE;`, } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2023.10.05T11.44.36.schema-checks-github-repository.ts b/packages/migrations/src/actions/2023.10.05T11.44.36.schema-checks-github-repository.ts index b1ba3a24d..2bbca07fa 100644 --- a/packages/migrations/src/actions/2023.10.05T11.44.36.schema-checks-github-repository.ts +++ b/packages/migrations/src/actions/2023.10.05T11.44.36.schema-checks-github-repository.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.08.03T11.44.36.schema-checks-github-repository.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN "github_repository" text , ADD COLUMN "github_sha" text diff --git a/packages/migrations/src/actions/2023.10.26T12.44.36.schema-checks-filters-index.ts b/packages/migrations/src/actions/2023.10.26T12.44.36.schema-checks-filters-index.ts index a2b3f8f48..b52264ddf 100644 --- a/packages/migrations/src/actions/2023.10.26T12.44.36.schema-checks-filters-index.ts +++ b/packages/migrations/src/actions/2023.10.26T12.44.36.schema-checks-filters-index.ts @@ -3,10 +3,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.10.26T12.44.36.schema-checks-filters-index.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'schema_checks_connection_pagination_with_changes', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_checks_connection_pagination_with_changes" ON "schema_checks" ( "target_id" ASC , "created_at" DESC @@ -20,7 +20,7 @@ export default { }, { name: 'schema_checks_connection_pagination_with_no_success', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_checks_connection_pagination_with_no_success" ON "schema_checks" ( "target_id" ASC , "created_at" DESC @@ -33,7 +33,7 @@ export default { }, { name: 'schema_checks_connection_pagination_with_no_success_and_changes', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_checks_connection_pagination_with_no_success_and_changes" ON "schema_checks" ( "target_id" ASC , "created_at" DESC diff --git a/packages/migrations/src/actions/2023.10.30T00-00-00.drop-persisted-operations.ts b/packages/migrations/src/actions/2023.10.30T00-00-00.drop-persisted-operations.ts index f447ae6e7..a432cefe2 100644 --- a/packages/migrations/src/actions/2023.10.30T00-00-00.drop-persisted-operations.ts +++ b/packages/migrations/src/actions/2023.10.30T00-00-00.drop-persisted-operations.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.10.30T00-00-00.drop-persisted-operations.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` DROP TABLE IF EXISTS "persisted_operations"; DROP TYPE IF EXISTS "operation_kind"; `, diff --git a/packages/migrations/src/actions/2023.11.02T14.41.41.schema-checks-dedup.ts b/packages/migrations/src/actions/2023.11.02T14.41.41.schema-checks-dedup.ts index f005b28c8..c100309e1 100644 --- a/packages/migrations/src/actions/2023.11.02T14.41.41.schema-checks-dedup.ts +++ b/packages/migrations/src/actions/2023.11.02T14.41.41.schema-checks-dedup.ts @@ -3,15 +3,15 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.10.25T14.41.41.schema-checks-dedup.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'create sdl_store and alter schema_checks', - query: sql` + query: psql` CREATE TABLE "sdl_store" ( "id" text PRIMARY KEY NOT NULL, "sdl" text NOT NULL ); - + ALTER TABLE "schema_checks" ADD COLUMN "schema_sdl_store_id" text REFERENCES "sdl_store" ("id"), ADD COLUMN "supergraph_sdl_store_id" text REFERENCES "sdl_store" ("id"), @@ -25,25 +25,25 @@ export default { }, { name: 'Create sdl_store_unique_id index', - query: sql` + query: psql` CREATE UNIQUE INDEX sdl_store_unique_id ON "sdl_store" ("id"); `, }, { name: 'Create schema_check_by_schema_sdl_store_id index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_check_by_schema_sdl_store_id" ON "schema_checks" ("schema_sdl_store_id" ASC) `, }, { name: 'Create schema_check_by_supergraph_sdl_store_id index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_check_by_supergraph_sdl_store_id" ON "schema_checks" ("supergraph_sdl_store_id" ASC) `, }, { name: 'Create schema_check_by_composite_schema_sdl_store_id index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "schema_check_by_composite_schema_sdl_store_id" ON "schema_checks" ("composite_schema_sdl_store_id" ASC); `, }, diff --git a/packages/migrations/src/actions/2023.11.09T00.00.00.schema-check-approval.ts b/packages/migrations/src/actions/2023.11.09T00.00.00.schema-check-approval.ts index 0d118e576..dbec354b6 100644 --- a/packages/migrations/src/actions/2023.11.09T00.00.00.schema-check-approval.ts +++ b/packages/migrations/src/actions/2023.11.09T00.00.00.schema-check-approval.ts @@ -2,7 +2,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2023.11.09T00.00.00.schema-check-approval.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "schema_change_approvals" ( "target_id" UUID NOT NULL REFERENCES "targets" ("id") ON DELETE CASCADE, "context_id" text NOT NULL, diff --git a/packages/migrations/src/actions/2023.11.20T10-00-00.organization-member-roles.ts b/packages/migrations/src/actions/2023.11.20T10-00-00.organization-member-roles.ts index 27cf5c866..703785ee1 100644 --- a/packages/migrations/src/actions/2023.11.20T10-00-00.organization-member-roles.ts +++ b/packages/migrations/src/actions/2023.11.20T10-00-00.organization-member-roles.ts @@ -3,10 +3,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2023.11.20T10-00-00.organization-member-roles.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'Create organization_roles and alter organization_member table', - query: sql` + query: psql` CREATE TABLE organization_member_roles ( "id" uuid NOT NULL UNIQUE DEFAULT uuid_generate_v4(), "organization_id" uuid NOT NULL REFERENCES "organizations" ("id") ON DELETE CASCADE, @@ -26,7 +26,7 @@ export default { }, { name: 'Create Admin role', - query: sql` + query: psql` INSERT INTO organization_member_roles ( organization_id, @@ -68,7 +68,7 @@ export default { }, { name: 'Create Contributor role', - query: sql` + query: psql` INSERT INTO organization_member_roles ( organization_id, @@ -104,7 +104,7 @@ export default { }, { name: 'Create Viewer role', - query: sql` + query: psql` INSERT INTO organization_member_roles ( organization_id, @@ -133,12 +133,12 @@ export default { }, { name: 'Assign roles to users with matching scopes', - query: sql` + query: psql` UPDATE organization_member SET role_id = ( SELECT id FROM organization_member_roles - WHERE + WHERE organization_member_roles.organization_id = organization_member.organization_id AND ARRAY(SELECT unnest(organization_member_roles.scopes) ORDER BY 1) @@ -150,14 +150,14 @@ export default { }, { name: 'Migrate organization_invitations table to use Viewer role', - query: sql` + query: psql` ALTER TABLE organization_invitations ADD COLUMN "role_id" uuid REFERENCES "organization_member_roles" ("id"); UPDATE organization_invitations SET role_id = ( SELECT id FROM organization_member_roles - WHERE + WHERE organization_member_roles.organization_id = organization_invitations.organization_id AND locked = true diff --git a/packages/migrations/src/actions/2024.01.08T10-00-00.schema-version-diff-schema-version-id.ts b/packages/migrations/src/actions/2024.01.08T10-00-00.schema-version-diff-schema-version-id.ts index 9a5ea6ba8..1b40d95b2 100644 --- a/packages/migrations/src/actions/2024.01.08T10-00-00.schema-version-diff-schema-version-id.ts +++ b/packages/migrations/src/actions/2024.01.08T10-00-00.schema-version-diff-schema-version-id.ts @@ -3,10 +3,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.01.08T10-00-00.schema-version-diff-schema-version-id', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'add diff_schema_version_id column', - query: sql` + query: psql` ALTER TABLE "schema_versions" ADD COLUMN IF NOT EXISTS "diff_schema_version_id" uuid REFERENCES "schema_versions" ("id") , ADD COLUMN IF NOT EXISTS "record_version" text @@ -15,7 +15,7 @@ export default { }, { name: 'create schema_versions_cursor_pagination index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_cursor_pagination" ON "schema_versions" ( "target_id" ASC , "created_at" DESC @@ -25,7 +25,7 @@ export default { }, { name: 'create schema_versions_cursor_pagination index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_cursor_pagination_composable" ON "schema_versions" ( "target_id" ASC , "created_at" DESC diff --git a/packages/migrations/src/actions/2024.01.26T00.00.00.contracts.ts b/packages/migrations/src/actions/2024.01.26T00.00.00.contracts.ts index 6de9f4f27..3d2e5250e 100644 --- a/packages/migrations/src/actions/2024.01.26T00.00.00.contracts.ts +++ b/packages/migrations/src/actions/2024.01.26T00.00.00.contracts.ts @@ -2,7 +2,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2024.01.26T00.00.00.contracts.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_versions" ADD COLUMN "tags" text[] , ADD COLUMN "has_contract_composition_errors" boolean diff --git a/packages/migrations/src/actions/2024.01.26T00.00.01.schema-check-pagination-index-update.ts b/packages/migrations/src/actions/2024.01.26T00.00.01.schema-check-pagination-index-update.ts index 08bdf873d..7792e7bf4 100644 --- a/packages/migrations/src/actions/2024.01.26T00.00.01.schema-check-pagination-index-update.ts +++ b/packages/migrations/src/actions/2024.01.26T00.00.01.schema-check-pagination-index-update.ts @@ -3,10 +3,10 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2024.01.26T00.00.01.schema-check-pagination-index-update', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'create index schema_checks_connection_pagination_with_changes_new', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_checks_connection_pagination_with_changes_new" ON "schema_checks" ( "target_id" ASC , "created_at" DESC @@ -21,7 +21,7 @@ export default { }, { name: 'create index schema_checks_connection_pagination_with_no_success_and_changes_new', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_checks_connection_pagination_with_no_success_and_changes_new" ON "schema_checks" ( "target_id" ASC , "created_at" DESC @@ -39,19 +39,19 @@ export default { }, { name: 'drop index schema_checks_connection_pagination_with_changes', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_checks_connection_pagination_with_changes"; `, }, { name: 'drop index schema_checks_connection_pagination_with_no_success_and_changes', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_checks_connection_pagination_with_no_success_and_changes"; `, }, { name: 'create index contract_checks_supergraph_sdl_store_id index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "contract_checks_supergraph_sdl_store_id" ON "contract_checks" ( "supergraph_sdl_store_id" ASC ); @@ -59,7 +59,7 @@ export default { }, { name: 'create index contract_checks_composite_schema_sdl_store_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY "contract_checks_composite_schema_sdl_store_id" ON "contract_checks" ( "composite_schema_sdl_store_id" ASC ); diff --git a/packages/migrations/src/actions/2024.02.19T00.00.01.schema-check-store-breaking-change-metadata.ts b/packages/migrations/src/actions/2024.02.19T00.00.01.schema-check-store-breaking-change-metadata.ts index fb8a5eee7..e0638288a 100644 --- a/packages/migrations/src/actions/2024.02.19T00.00.01.schema-check-store-breaking-change-metadata.ts +++ b/packages/migrations/src/actions/2024.02.19T00.00.01.schema-check-store-breaking-change-metadata.ts @@ -3,7 +3,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2024.02.19T00.00.01.schema-check-store-breaking-change-metadata.ts', noTransaction: true, - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN IF NOT EXISTS "conditional_breaking_change_metadata" JSONB ; diff --git a/packages/migrations/src/actions/2024.04.09T10-10-00.check-approval-comment.ts b/packages/migrations/src/actions/2024.04.09T10-10-00.check-approval-comment.ts index 6f812582d..baf8949cc 100644 --- a/packages/migrations/src/actions/2024.04.09T10-10-00.check-approval-comment.ts +++ b/packages/migrations/src/actions/2024.04.09T10-10-00.check-approval-comment.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.04.09T10.10.00.check-approval-comment.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN IF NOT EXISTS "manual_approval_comment" text; `, } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.06.11T10-10-00.ms-teams-webhook.ts b/packages/migrations/src/actions/2024.06.11T10-10-00.ms-teams-webhook.ts index b4f1f2ec5..284621ed0 100644 --- a/packages/migrations/src/actions/2024.06.11T10-10-00.ms-teams-webhook.ts +++ b/packages/migrations/src/actions/2024.06.11T10-10-00.ms-teams-webhook.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.06.11T10-10-00.ms-teams-webhook.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TYPE alert_channel_type ADD VALUE 'MSTEAMS_WEBHOOK'; `, } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.07.16T13-44-00.oidc-only-access.ts b/packages/migrations/src/actions/2024.07.16T13-44-00.oidc-only-access.ts index ef59fcd3d..7158b7c5b 100644 --- a/packages/migrations/src/actions/2024.07.16T13-44-00.oidc-only-access.ts +++ b/packages/migrations/src/actions/2024.07.16T13-44-00.oidc-only-access.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.07.16T13-44-00.oidc-only-access.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "oidc_integrations" ADD COLUMN "oidc_user_access_only" BOOLEAN NOT NULL DEFAULT TRUE; `, diff --git a/packages/migrations/src/actions/2024.07.17T00-00-00.app-deployments.ts b/packages/migrations/src/actions/2024.07.17T00-00-00.app-deployments.ts index 688812631..4e80cc229 100644 --- a/packages/migrations/src/actions/2024.07.17T00-00-00.app-deployments.ts +++ b/packages/migrations/src/actions/2024.07.17T00-00-00.app-deployments.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.07.17T00-00-00.app-deployments.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE IF NOT EXISTS "app_deployments" ( "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), "target_id" UUID NOT NULL REFERENCES "targets" ("id") ON DELETE CASCADE, diff --git a/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts b/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts index 4a4db7285..8c6a4abba 100644 --- a/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts +++ b/packages/migrations/src/actions/2024.07.23T09.36.00.schema-cleanup-tracker.ts @@ -10,14 +10,15 @@ import { isScalarType, isUnionType, } from 'graphql'; -import { sql, type CommonQueryMethods } from 'slonik'; +import z from 'zod'; +import { psql, type CommonQueryMethods } from '@hive/postgres'; import { env } from '../environment'; import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2024.07.23T09.36.00.schema-cleanup-tracker.ts', async run({ connection }) { - await connection.query(sql` + await connection.query(psql` CREATE TABLE IF NOT EXISTS "schema_coordinate_status" ( coordinate text NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), @@ -27,7 +28,7 @@ export default { "target_id" UUID NOT NULL REFERENCES "targets" ("id") ON DELETE CASCADE, PRIMARY KEY (coordinate, target_id) ); - + CREATE INDEX IF NOT EXISTS idx_schema_coordinate_status_by_target_timestamp ON schema_coordinate_status( target_id, @@ -40,7 +41,7 @@ export default { coordinate, created_at, deprecated_at - ); + ); `); if (env.isHiveCloud) { @@ -48,9 +49,13 @@ export default { return; } - const schemaVersionsTotal = await connection.oneFirst(sql` + const schemaVersionsTotal = await connection + .oneFirst( + psql` SELECT count(*) as total FROM schema_versions - `); + `, + ) + .then(z.number().parse); console.log(`Found ${schemaVersionsTotal} schema versions`); if (schemaVersionsTotal > 1000) { @@ -93,24 +98,24 @@ function diffSchemaCoordinates( export async function schemaCoordinateStatusMigration(connection: CommonQueryMethods) { // Fetch targets - const targetResult = await connection.query<{ id: string }>(sql` + const targetResult = await connection + .any( + psql` SELECT id FROM targets WHERE ID NOT IN (SELECT target_id FROM schema_coordinate_status) - `); + `, + ) + .then(z.array(z.object({ id: z.string() })).parse); - console.log(`Found ${targetResult.rowCount} targets`); + console.log(`Found ${targetResult.length} targets`); let i = 0; - for await (const target of targetResult.rows) { + for await (const target of targetResult) { try { - console.log(`Processing target (${i++}/${targetResult.rowCount}) - ${target.id}`); + console.log(`Processing target (${i++}/${targetResult.length}) - ${target.id}`); - const latestSchema = await connection.maybeOne<{ - id: string; - created_at: number; - is_composable: boolean; - sdl?: string; - previous_schema_version_id?: string; - }>(sql` + const latestSchema = await connection + .maybeOne( + psql` SELECT id, created_at, @@ -121,7 +126,19 @@ export async function schemaCoordinateStatusMigration(connection: CommonQueryMet WHERE target_id = ${target.id} AND is_composable = true ORDER BY created_at DESC LIMIT 1 - `); + `, + ) + .then( + z + .object({ + id: z.string(), + created_at: z.number(), + is_composable: z.boolean(), + sdl: z.string().nullable(), + previous_schema_version_id: z.string().nullable(), + }) + .nullable().parse, + ); if (!latestSchema) { console.log('[SKIPPING] No latest composable schema found for target %s', target.id); @@ -270,10 +287,10 @@ async function insertRemainingCoordinates( console.log( `Adding remaining ${targetCoordinates.coordinates.size} coordinates for target ${targetId}`, ); - await connection.query(sql` + await connection.query(psql` INSERT INTO schema_coordinate_status ( target_id, coordinate, created_at, created_in_version_id ) - SELECT * FROM ${sql.unnest( + SELECT * FROM ${psql.unnest( Array.from(targetCoordinates.coordinates).map(coordinate => [ targetId, coordinate, @@ -290,10 +307,10 @@ async function insertRemainingCoordinates( console.log( `Deprecating remaining ${remainingDeprecated.size} coordinates for target ${targetId}`, ); - await connection.query(sql` + await connection.query(psql` INSERT INTO schema_coordinate_status ( target_id, coordinate, created_at, created_in_version_id, deprecated_at, deprecated_in_version_id ) - SELECT * FROM ${sql.unnest( + SELECT * FROM ${psql.unnest( Array.from(remainingDeprecated).map(coordinate => [ targetId, coordinate, @@ -343,13 +360,9 @@ async function processVersion( return; } - const versionBefore = await connection.maybeOne<{ - id: string; - sdl?: string; - previous_schema_version_id?: string; - created_at: number; - is_composable: boolean; - }>(sql` + const versionBefore = await connection + .maybeOne( + psql` SELECT id, composite_schema_sdl as sdl, @@ -358,7 +371,19 @@ async function processVersion( is_composable FROM schema_versions WHERE id = ${previousVersionId} AND target_id = ${targetId} - `); + `, + ) + .then( + z + .object({ + id: z.string(), + created_at: z.number(), + is_composable: z.boolean(), + sdl: z.string().nullable(), + previous_schema_version_id: z.string().nullable(), + }) + .nullable().parse, + ); if (!versionBefore) { console.error( @@ -440,10 +465,10 @@ async function processVersion( if (added.length) { console.log(`Adding ${added.length} coordinates for target ${targetId}`); - await connection.query(sql` + await connection.query(psql` INSERT INTO schema_coordinate_status ( target_id, coordinate, created_at, created_in_version_id ) - SELECT * FROM ${sql.unnest( + SELECT * FROM ${psql.unnest( added.map(coordinate => [targetId, coordinate, datePG, after.versionId]), ['uuid', 'text', 'date', 'uuid'], )} @@ -456,10 +481,10 @@ async function processVersion( if (deprecated.length) { console.log(`deprecating ${deprecated.length} coordinates for target ${targetId}`); - await connection.query(sql` + await connection.query(psql` INSERT INTO schema_coordinate_status ( target_id, coordinate, created_at, created_in_version_id, deprecated_at, deprecated_in_version_id ) - SELECT * FROM ${sql.unnest( + SELECT * FROM ${psql.unnest( deprecated.map(coordinate => [ targetId, coordinate, diff --git a/packages/migrations/src/actions/2024.12.23T00-00-00.improve-version-index.ts b/packages/migrations/src/actions/2024.12.23T00-00-00.improve-version-index.ts index a1c0ce72c..25cd3d6fd 100644 --- a/packages/migrations/src/actions/2024.12.23T00-00-00.improve-version-index.ts +++ b/packages/migrations/src/actions/2024.12.23T00-00-00.improve-version-index.ts @@ -4,14 +4,14 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.12.23T00-00-00.improve-version-index.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: `create "schema_log"."action" with "created_at" sort index`, - query: sql`CREATE INDEX CONCURRENTLY idx_schema_log_action_created ON schema_log(action, created_at DESC);`, + query: psql`CREATE INDEX CONCURRENTLY idx_schema_log_action_created ON schema_log(action, created_at DESC);`, }, { name: `create "schema_log"."action" + "service_name" index`, - query: sql`CREATE INDEX CONCURRENTLY idx_schema_log_action_service ON schema_log(action, lower(service_name));`, + query: psql`CREATE INDEX CONCURRENTLY idx_schema_log_action_service ON schema_log(action, lower(service_name));`, }, ], } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.12.24T00-00-00.improve-version-index-2.ts b/packages/migrations/src/actions/2024.12.24T00-00-00.improve-version-index-2.ts index 64667566c..8b674e55e 100644 --- a/packages/migrations/src/actions/2024.12.24T00-00-00.improve-version-index-2.ts +++ b/packages/migrations/src/actions/2024.12.24T00-00-00.improve-version-index-2.ts @@ -3,10 +3,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.12.24T00-00-00.improve-version-index-2.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: `create "schema_version_changes"."schema_version_id" lookup index`, - query: sql`CREATE INDEX CONCURRENTLY idx_schema_version_changes_id ON schema_version_changes(schema_version_id);`, + query: psql`CREATE INDEX CONCURRENTLY idx_schema_version_changes_id ON schema_version_changes(schema_version_id);`, }, ], } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2024.12.27T00.00.00.create-preflight-scripts.ts b/packages/migrations/src/actions/2024.12.27T00.00.00.create-preflight-scripts.ts index 56868b901..0acb8c59f 100644 --- a/packages/migrations/src/actions/2024.12.27T00.00.00.create-preflight-scripts.ts +++ b/packages/migrations/src/actions/2024.12.27T00.00.00.create-preflight-scripts.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2024.12.27T00.00.00.create-preflight-scripts.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE IF NOT EXISTS "document_preflight_scripts" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "source_code" text NOT NULL, diff --git a/packages/migrations/src/actions/2025.01.02T00-00-00.cascade-deletion-indices.ts b/packages/migrations/src/actions/2025.01.02T00-00-00.cascade-deletion-indices.ts index 69ae70462..115410eb9 100644 --- a/packages/migrations/src/actions/2025.01.02T00-00-00.cascade-deletion-indices.ts +++ b/packages/migrations/src/actions/2025.01.02T00-00-00.cascade-deletion-indices.ts @@ -7,10 +7,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.01.02T00-00-00.cascade-deletion-indices.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'index schema_checks_manual_approval_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_checks_manual_approval_user_id" ON "schema_checks"("manual_approval_user_id") WHERE "manual_approval_user_id" is not null @@ -18,106 +18,106 @@ export default { }, { name: 'index organization_member_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_user_id" ON "organization_member"("user_id") `, }, { name: 'index organization_member_organization_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_organization_id" ON "organization_member"("organization_id") `, }, { name: 'index organization_member_roles_organization_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_roles_organization_id" ON "organization_member_roles"("organization_id") `, }, { name: 'index projects_org_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "projects_org_id" ON "projects"("org_id") `, }, { name: 'index targets_project_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "targets_project_id" ON "targets"("project_id") `, }, { name: 'index schema_versions_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_target_id" ON "schema_versions"("target_id") `, }, { name: 'index schema_checks_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_checks_target_id" ON "schema_checks"("target_id") `, }, { name: 'index schema_log_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_log_target_id" ON "schema_log"("target_id") `, }, { name: 'index schema_log_project_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_log_project_id" ON "schema_log"("project_id") `, }, { name: 'index contract_versions_schema_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "contract_versions_schema_version_id" ON "contract_versions"("schema_version_id") `, }, { name: 'index schema_version_to_log_action_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_version_to_log_action_id" ON "schema_version_to_log"("action_id") `, }, { name: 'index schema_version_to_log_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_version_to_log_version_id" ON "schema_version_to_log"("version_id") `, }, { name: 'index contract_schema_change_approvals_schema_change_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "contract_schema_change_approvals_schema_change_id" ON "contract_schema_change_approvals"("schema_change_id") `, }, { name: 'index schema_checks_schema_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_checks_schema_version_id" ON "schema_checks"("schema_version_id") `, }, { name: 'index schema_versions_diff_schema_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_diff_schema_version_id" ON "schema_versions"("diff_schema_version_id") `, }, { name: 'index organizations_ownership_transfer_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organizations_ownership_transfer_user_id" ON "organizations"("ownership_transfer_user_id") `, }, { name: 'index users_supertoken_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "users_supertoken_user_id_missing" ON "users"("supertoken_user_id") WHERE 'supertoken_user_id' IS NULL diff --git a/packages/migrations/src/actions/2025.01.02T00-00-00.legacy-user-org-cleanup.ts b/packages/migrations/src/actions/2025.01.02T00-00-00.legacy-user-org-cleanup.ts index 488e73b0f..472404f99 100644 --- a/packages/migrations/src/actions/2025.01.02T00-00-00.legacy-user-org-cleanup.ts +++ b/packages/migrations/src/actions/2025.01.02T00-00-00.legacy-user-org-cleanup.ts @@ -7,16 +7,16 @@ export default { // we do not run this in a transaction as each user processing delete takes around 300ms. // and we do not want to mess with live traffic. noTransaction: true, - async run({ sql, connection }) { + async run({ psql, connection }) { const userIds = await connection .anyFirst( - sql` + psql` SELECT "id" FROM "users" WHERE - "supertoken_user_id" IS NULL + "supertoken_user_id" IS NULL `, ) .then(value => z.array(z.string()).parse(value)); @@ -32,7 +32,7 @@ export default { `processing userId="${userId}" (${counter.toPrecision().padStart(padAmount, '0')}/${total})`, ); // ON DELETE SET null constraint is missing, so we need to first update it manually - await connection.query(sql` + await connection.query(psql` UPDATE "organizations" SET @@ -41,14 +41,14 @@ export default { "ownership_transfer_user_id" = ${userId} `); // Delete the organizations of these users - await connection.query(sql` + await connection.query(psql` DELETE FROM "organizations" WHERE "user_id" = ${userId} `); - await connection.query(sql` + await connection.query(psql` DELETE FROM "users" diff --git a/packages/migrations/src/actions/2025.01.09T00-00-00.legacy-member-scopes.ts b/packages/migrations/src/actions/2025.01.09T00-00-00.legacy-member-scopes.ts index 766ba5946..247393a21 100644 --- a/packages/migrations/src/actions/2025.01.09T00-00-00.legacy-member-scopes.ts +++ b/packages/migrations/src/actions/2025.01.09T00-00-00.legacy-member-scopes.ts @@ -24,8 +24,8 @@ const QUERY_RESULT = z.array( export default { name: '2025.01.09T00-00-00.legacy-member-scopes.ts', noTransaction: true, - async run({ sql, connection }) { - const queryResult = await connection.query(sql` + async run({ psql, connection }) { + const queryResult = await connection.any(psql` SELECT organization_id as "organizationId", sorted_scopes as "sortedScopes", @@ -49,7 +49,7 @@ export default { ORDER BY organization_id; `); - if (queryResult.rowCount === 0) { + if (queryResult.length === 0) { console.log('No members without role_id found.'); return; } @@ -57,7 +57,7 @@ export default { // rows are sorted by organization_id // and grouped by scopes // so we can process them in order - const rows = QUERY_RESULT.parse(queryResult.rows); + const rows = QUERY_RESULT.parse(queryResult); let counter = 1; let previousOrganizationId: string | null = null; @@ -70,12 +70,12 @@ export default { } console.log( - `processing organization_id="${row.organizationId}" (${counter}) with ${row.userIds.length} users | ${index + 1}/${queryResult.rowCount}`, + `processing organization_id="${row.organizationId}" (${counter}) with ${row.userIds.length} users | ${index + 1}/${queryResult.length}`, ); const startedAt = Date.now(); - await connection.query(sql` + await connection.query(psql` WITH new_role AS ( INSERT INTO organization_member_roles ( organization_id, name, description, scopes @@ -84,13 +84,13 @@ export default { ${row.organizationId}, 'Auto Role ' || substring(uuid_generate_v4()::text FROM 1 FOR 8), 'Auto generated role to assign to members without a role', - ${sql.array(row.sortedScopes, 'text')} + ${psql.array(row.sortedScopes, 'text')} ) RETURNING id ) UPDATE organization_member SET role_id = (SELECT id FROM new_role) - WHERE organization_id = ${row.organizationId} AND user_id = ANY(${sql.array(row.userIds, 'uuid')}) + WHERE organization_id = ${row.organizationId} AND user_id = ANY(${psql.array(row.userIds, 'uuid')}) `); console.log(`finished after ${Date.now() - startedAt}ms`); diff --git a/packages/migrations/src/actions/2025.01.10T00.00.00.breaking-changes-request-count.ts b/packages/migrations/src/actions/2025.01.10T00.00.00.breaking-changes-request-count.ts index 2e719b4ac..cf16000a5 100644 --- a/packages/migrations/src/actions/2025.01.10T00.00.00.breaking-changes-request-count.ts +++ b/packages/migrations/src/actions/2025.01.10T00.00.00.breaking-changes-request-count.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.01.10T00.00.00.breaking-changes-request-count.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TYPE breaking_change_formula AS ENUM('PERCENTAGE', 'REQUEST_COUNT'); diff --git a/packages/migrations/src/actions/2025.01.13T10-08-00.default-role.ts b/packages/migrations/src/actions/2025.01.13T10-08-00.default-role.ts index ed87fc89f..373cfac25 100644 --- a/packages/migrations/src/actions/2025.01.13T10-08-00.default-role.ts +++ b/packages/migrations/src/actions/2025.01.13T10-08-00.default-role.ts @@ -4,10 +4,10 @@ export default { name: '2025.01.13T10-08-00.default-role.ts', noTransaction: true, // Adds a default role to OIDC integration and set index on "oidc_integrations"."default_role_id" - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'Add a column', - query: sql` + query: psql` ALTER TABLE "oidc_integrations" ADD COLUMN IF NOT EXISTS "default_role_id" UUID REFERENCES organization_member_roles(id) ON DELETE SET NULL; @@ -15,7 +15,7 @@ export default { }, { name: 'Create an index', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "oidc_integrations_default_role_id_idx" ON "oidc_integrations"("default_role_id") WHERE "default_role_id" is not null; diff --git a/packages/migrations/src/actions/2025.01.17T10-08-00.drop-activities.ts b/packages/migrations/src/actions/2025.01.17T10-08-00.drop-activities.ts index 5660fe3f5..34c5341de 100644 --- a/packages/migrations/src/actions/2025.01.17T10-08-00.drop-activities.ts +++ b/packages/migrations/src/actions/2025.01.17T10-08-00.drop-activities.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.01.17T10-08-00.drop-activities.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` DROP TABLE IF EXISTS "activities"; `, } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2025.01.20T00-00-00.legacy-registry-model-removal.ts b/packages/migrations/src/actions/2025.01.20T00-00-00.legacy-registry-model-removal.ts index aa980a785..8294e12dd 100644 --- a/packages/migrations/src/actions/2025.01.20T00-00-00.legacy-registry-model-removal.ts +++ b/packages/migrations/src/actions/2025.01.20T00-00-00.legacy-registry-model-removal.ts @@ -6,7 +6,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.01.20T00-00-00.legacy-registry-model-removal.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE projects DROP COLUMN IF EXISTS legacy_registry_model; `, } satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts b/packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts index cf9941a0e..6088350dd 100644 --- a/packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts +++ b/packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025-01-30T00-00-00.granular-member-role-permissions.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "organization_member_roles" ALTER "scopes" DROP NOT NULL , ADD COLUMN "permissions" text[] diff --git a/packages/migrations/src/actions/2025.02.14T00-00-00.schema-versions-metadata.ts b/packages/migrations/src/actions/2025.02.14T00-00-00.schema-versions-metadata.ts index a098831a6..c3c9cbcac 100644 --- a/packages/migrations/src/actions/2025.02.14T00-00-00.schema-versions-metadata.ts +++ b/packages/migrations/src/actions/2025.02.14T00-00-00.schema-versions-metadata.ts @@ -2,7 +2,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2025.02.14T00.00.00.schema-versions-metadata.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_versions" ADD COLUMN "schema_metadata" JSONB DEFAULT NULL ; diff --git a/packages/migrations/src/actions/2025.02.20T00-00-00.organization-access-tokens.ts b/packages/migrations/src/actions/2025.02.20T00-00-00.organization-access-tokens.ts index 3a72f4358..622b08bfe 100644 --- a/packages/migrations/src/actions/2025.02.20T00-00-00.organization-access-tokens.ts +++ b/packages/migrations/src/actions/2025.02.20T00-00-00.organization-access-tokens.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.02.20T00-00-00.organization-access-tokens.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE IF NOT EXISTS "organization_access_tokens" ( "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4() , "organization_id" UUID NOT NULL REFERENCES "organizations" ("id") ON DELETE CASCADE diff --git a/packages/migrations/src/actions/2025.02.21T00-00-00.schema-versions-metadata-attributes.ts b/packages/migrations/src/actions/2025.02.21T00-00-00.schema-versions-metadata-attributes.ts index e7e5411da..a1da91b7d 100644 --- a/packages/migrations/src/actions/2025.02.21T00-00-00.schema-versions-metadata-attributes.ts +++ b/packages/migrations/src/actions/2025.02.21T00-00-00.schema-versions-metadata-attributes.ts @@ -2,7 +2,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2025.02.21T00.00.00.schema-versions-metadata-attributes.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_versions" ADD COLUMN "metadata_attributes" JSONB DEFAULT NULL ; diff --git a/packages/migrations/src/actions/2025.03.20T00-00-00.dangerous-breaking.ts b/packages/migrations/src/actions/2025.03.20T00-00-00.dangerous-breaking.ts index 8504ebe34..b6b94759b 100644 --- a/packages/migrations/src/actions/2025.03.20T00-00-00.dangerous-breaking.ts +++ b/packages/migrations/src/actions/2025.03.20T00-00-00.dangerous-breaking.ts @@ -3,7 +3,7 @@ import type { MigrationExecutor } from '../pg-migrator'; export default { name: '2025.03.20T00-00-00.dangerous_breaking.ts', noTransaction: true, - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE targets ADD COLUMN diff --git a/packages/migrations/src/actions/2025.05.14T00-00-00.cascade-deletion-indices-2.ts b/packages/migrations/src/actions/2025.05.14T00-00-00.cascade-deletion-indices-2.ts index 57f1dd797..e50c4b01d 100644 --- a/packages/migrations/src/actions/2025.05.14T00-00-00.cascade-deletion-indices-2.ts +++ b/packages/migrations/src/actions/2025.05.14T00-00-00.cascade-deletion-indices-2.ts @@ -7,143 +7,143 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.05.14T00-00-00.cascade-deletion-indices-2.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'index schema_versions_action_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_action_id" ON "schema_versions"("action_id") `, }, // For cascading delete from "schema_log" { name: 'index versions_commit_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "versions_commit_id" ON "versions"("commit_id") `, }, // For cascading delete from "targets" { name: 'index versions_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "versions_target_id" ON "versions"("target_id") `, }, // For cascading delete from "organizations" { name: 'index tokens_organization_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_organization_id" ON "tokens"("organization_id") `, }, // For cascading delete from "projects" { name: 'index tokens_project_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_project_id" ON "tokens"("project_id") `, }, // For cascading delete from "targets" { name: 'index tokens_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_target_id" ON "tokens"("target_id") `, }, // For cascading delete from "targets" { name: 'index target_validation_destination_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "target_validation_destination_target_id" ON "target_validation"("destination_target_id") `, }, // For cascading delete from "schema_versions" { name: 'index schema_coordinate_status_created_in_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_coordinate_status_created_in_version_id" ON "schema_coordinate_status"("created_in_version_id") `, }, // For cascading delete from "schema_versions" { name: 'index schema_coordinate_status_deprecated_in_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_coordinate_status_deprecated_in_version_id" ON "schema_coordinate_status"("deprecated_in_version_id") `, }, // For cascading set NULL from "users" { name: 'index document_preflight_scripts_created_by_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "document_preflight_scripts_created_by_user_id" ON "document_preflight_scripts"("created_by_user_id") WHERE "created_by_user_id" IS NOT NULL `, }, // For cascading set NULL from "users" { name: 'index document_collections_created_by_user_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "document_collections_created_by_user_id" ON "document_collections"("created_by_user_id") WHERE "created_by_user_id" IS NOT NULL `, }, // For cascading delete from "contract_versions" { name: 'index contract_version_changes_contract_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "contract_version_changes_contract_version_id" ON "contract_version_changes"("contract_version_id") `, }, // For cascading delete from "contract_versions" { name: 'index contract_checks_compared_contract_version_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "contract_checks_compared_contract_version_id" ON "contract_checks"("compared_contract_version_id") `, }, // For cascading delete from "contracts" { name: 'index contracts_contract_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "contracts_contract_id" ON "contract_checks"("contract_id") `, }, // For cascading delete from "targets" { name: 'index alerts_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "alerts_target_id" ON "alerts"("target_id") `, }, // For cascading delete from "projects" { name: 'index alerts_project_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "alerts_project_id" ON "alerts"("project_id") `, }, // For cascading delete from "alert_channels" { name: 'index alerts_alert_channel_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "alerts_alert_channel_id" ON "alerts"("alert_channel_id") `, }, // For cascading delete from "organizations" { name: 'index tokens_organization_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_organization_id" ON "tokens"("organization_id") `, }, // For cascading delete from "projects" { name: 'index tokens_project_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_project_id" ON "tokens"("project_id") `, }, // For cascading delete from "targets" { name: 'index tokens_target_id', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "tokens_target_id" ON "tokens"("target_id") `, }, diff --git a/packages/migrations/src/actions/2025.05.15T00-00-00.contracts-foreign-key-constraint-fix.ts b/packages/migrations/src/actions/2025.05.15T00-00-00.contracts-foreign-key-constraint-fix.ts index 9609fe8ea..45ae05e13 100644 --- a/packages/migrations/src/actions/2025.05.15T00-00-00.contracts-foreign-key-constraint-fix.ts +++ b/packages/migrations/src/actions/2025.05.15T00-00-00.contracts-foreign-key-constraint-fix.ts @@ -11,10 +11,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.05.15T00-00-00.contracts-foreign-key-constraint-fix.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'drop constraint contracts_target_id_fkey', - query: sql` + query: psql` ALTER TABLE "contracts" DROP CONSTRAINT IF EXISTS "contracts_target_id_fkey" , ADD CONSTRAINT "contracts_target_id_fkey" diff --git a/packages/migrations/src/actions/2025.05.15T00-00-00.redundant-indices.ts b/packages/migrations/src/actions/2025.05.15T00-00-00.redundant-indices.ts index 758863ca5..8c8f467d9 100644 --- a/packages/migrations/src/actions/2025.05.15T00-00-00.redundant-indices.ts +++ b/packages/migrations/src/actions/2025.05.15T00-00-00.redundant-indices.ts @@ -7,32 +7,32 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.05.15T00-00-00.redundant-indices.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ // redundant with schema_versions_cursor_pagination { name: 'drop index schema_versions_target_id', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_versions_target_id" `, }, // redundant with organization_member_pkey { name: 'drop index organization_member_organization_id', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "organization_member_organization_id" `, }, // redundant with schema_checks_connection_pagination { name: 'drop index schema_checks_target_id', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_checks_target_id" `, }, // redundant with schema_version_to_log_pkey { name: 'drop index schema_version_to_log_version_id', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_version_to_log_version_id" `, }, diff --git a/packages/migrations/src/actions/2025.05.15T00-00-01.organization-member-pagination.ts b/packages/migrations/src/actions/2025.05.15T00-00-01.organization-member-pagination.ts index 5b2bad46d..089338714 100644 --- a/packages/migrations/src/actions/2025.05.15T00-00-01.organization-member-pagination.ts +++ b/packages/migrations/src/actions/2025.05.15T00-00-01.organization-member-pagination.ts @@ -12,18 +12,18 @@ export default { name: '2025.05.15T00-00-01.organization-member-pagination.ts', noTransaction: true, // Adds a default role to OIDC integration and set index on "oidc_integrations"."default_role_id" - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'Add "organization_member"."created_at" column', - query: sql` + query: psql` ALTER TABLE "organization_member" - ADD COLUMN IF NOT EXISTS "created_at" timestamptz NOT NULL DEFAULT ${sql.literalValue(createdAt)}::timestamp + ADD COLUMN IF NOT EXISTS "created_at" timestamptz NOT NULL DEFAULT ${psql.literalValue(createdAt)}::timestamp ; `, }, { name: 'Create pagination index "organization_member_pagination_idx"', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_pagination_idx" ON "organization_member" ( "organization_id" DESC @@ -34,15 +34,15 @@ export default { }, { name: 'Add "organization_member_roles"."created_at" column', - query: sql` + query: psql` ALTER TABLE "organization_member_roles" - ADD COLUMN IF NOT EXISTS "created_at" timestamptz NOT NULL DEFAULT ${sql.literalValue(createdAt)}::timestamp + ADD COLUMN IF NOT EXISTS "created_at" timestamptz NOT NULL DEFAULT ${psql.literalValue(createdAt)}::timestamp ; `, }, { name: 'Create pagination index "organization_member_roles_pagination_idx"', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_roles_pagination_idx" ON "organization_member_roles" ( "organization_id" DESC diff --git a/packages/migrations/src/actions/2025.05.28T00-00-00.schema-log-by-ids.ts b/packages/migrations/src/actions/2025.05.28T00-00-00.schema-log-by-ids.ts index a3fcff3a7..3fa7acef7 100644 --- a/packages/migrations/src/actions/2025.05.28T00-00-00.schema-log-by-ids.ts +++ b/packages/migrations/src/actions/2025.05.28T00-00-00.schema-log-by-ids.ts @@ -6,10 +6,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.05.28T00-00-00.schema-log-by-ids.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'index schema_log_by_ids', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_log_by_ids" ON "schema_log"( "project_id" , "target_id" diff --git a/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts b/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts index e7f6d98bb..03b4da82e 100644 --- a/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts +++ b/packages/migrations/src/actions/2025.08.30T00-00-00.schema-proposals.ts @@ -5,10 +5,10 @@ import { type MigrationExecutor } from '../pg-migrator'; */ export default { name: '2025.05.29T00-00-00.schema-proposals.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'create schema_proposal tables', - query: sql` + query: psql` CREATE TYPE schema_proposal_stage AS ENUM('DRAFT', 'OPEN', 'APPROVED', 'IMPLEMENTED', 'CLOSED') ; @@ -40,7 +40,7 @@ export default { { // Associate schema checks with schema proposals name: 'Add "schema_checks"."schema_proposal_id" column and index', - query: sql` + query: psql` ALTER TABLE "schema_checks" ADD COLUMN IF NOT EXISTS "schema_proposal_id" UUID REFERENCES "schema_proposals" ("id") ON DELETE SET NULL ; diff --git a/packages/migrations/src/actions/2025.10.16T00-00-00.schema-log-by-commit-ordered.ts b/packages/migrations/src/actions/2025.10.16T00-00-00.schema-log-by-commit-ordered.ts index bebf1c196..fa8b32136 100644 --- a/packages/migrations/src/actions/2025.10.16T00-00-00.schema-log-by-commit-ordered.ts +++ b/packages/migrations/src/actions/2025.10.16T00-00-00.schema-log-by-commit-ordered.ts @@ -6,10 +6,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.10.16T00-00-00.schema-log-by-commit-ordered.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'index schema_log_by_commit_ordered', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_log_by_commit_ordered" ON "schema_log"( "project_id" , "target_id" @@ -20,7 +20,7 @@ export default { }, { name: 'drop index schema_log_by_ids', - query: sql` + query: psql` DROP INDEX CONCURRENTLY IF EXISTS "schema_log_by_ids"; `, }, diff --git a/packages/migrations/src/actions/2025.10.17T00-00-00.project-access-tokens.ts b/packages/migrations/src/actions/2025.10.17T00-00-00.project-access-tokens.ts index 95257a403..62339f272 100644 --- a/packages/migrations/src/actions/2025.10.17T00-00-00.project-access-tokens.ts +++ b/packages/migrations/src/actions/2025.10.17T00-00-00.project-access-tokens.ts @@ -3,10 +3,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.10.17T00-00-00.organization-access-tokens-project-scope.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'add new columns to "organization_access_tokens"', - query: sql` + query: psql` ALTER TABLE "organization_access_tokens" ADD COLUMN IF NOT EXISTS "project_id" UUID REFERENCES "projects" ("id") ON DELETE CASCADE , ADD COLUMN IF NOT EXISTS "user_id" UUID REFERENCES "users" ("id") ON DELETE CASCADE @@ -15,7 +15,7 @@ export default { }, { name: 'add index "organization_access_tokens_pagination_project"', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_access_tokens_pagination_project" ON "organization_access_tokens" ( "project_id" , "created_at" DESC @@ -25,7 +25,7 @@ export default { }, { name: 'add index "organization_access_tokens_pagination_user"', - query: sql` + query: psql` CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_access_tokens_pagination_user" ON "organization_access_tokens" ( "user_id" , "created_at" DESC diff --git a/packages/migrations/src/actions/2025.11.12T00-00-00.granular-oidc-role-permissions.ts b/packages/migrations/src/actions/2025.11.12T00-00-00.granular-oidc-role-permissions.ts index e8bce5301..8794363c3 100644 --- a/packages/migrations/src/actions/2025.11.12T00-00-00.granular-oidc-role-permissions.ts +++ b/packages/migrations/src/actions/2025.11.12T00-00-00.granular-oidc-role-permissions.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.11.12T00-00-00.granular-oidc-role-permissions.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "oidc_integrations" ADD COLUMN IF NOT EXISTS "default_assigned_resources" JSONB ; diff --git a/packages/migrations/src/actions/2025.11.25T00-00-00.members-search.ts b/packages/migrations/src/actions/2025.11.25T00-00-00.members-search.ts index a08f945e8..9ef685006 100644 --- a/packages/migrations/src/actions/2025.11.25T00-00-00.members-search.ts +++ b/packages/migrations/src/actions/2025.11.25T00-00-00.members-search.ts @@ -3,17 +3,17 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.11.25T00-00-00.members-search.ts', noTransaction: true, - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'Drop old "organization_member_pagination_idx" index', - query: sql` + query: psql` -- The order was wrong. This was sorting by org_id, user_id, then created_at... DROP INDEX IF EXISTS "organization_member_pagination_idx"; `, }, { name: 'Create new "organization_member_pagination" index with correct order', - query: sql` + query: psql` -- Replace "organization_member_pagination_idx" with a new index in the correct order CREATE INDEX CONCURRENTLY IF NOT EXISTS "organization_member_pagination" ON "organization_member" ( diff --git a/packages/migrations/src/actions/2025.12.12T00-00-00.workflows-deduplication.ts b/packages/migrations/src/actions/2025.12.12T00-00-00.workflows-deduplication.ts index 0a0a41359..a127d2b7d 100644 --- a/packages/migrations/src/actions/2025.12.12T00-00-00.workflows-deduplication.ts +++ b/packages/migrations/src/actions/2025.12.12T00-00-00.workflows-deduplication.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.12.12T00-00-00.workflows-deduplication.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE "graphile_worker_deduplication" ( "task_name" text NOT NULL, "dedupe_key" text NOT NULL, diff --git a/packages/migrations/src/actions/2025.12.17T00-00-00.custom-oidc-scopes.ts b/packages/migrations/src/actions/2025.12.17T00-00-00.custom-oidc-scopes.ts index 3480d2d52..6f29920a7 100644 --- a/packages/migrations/src/actions/2025.12.17T00-00-00.custom-oidc-scopes.ts +++ b/packages/migrations/src/actions/2025.12.17T00-00-00.custom-oidc-scopes.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.12.17T00-00-00.custom-oidc-scopes.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'Create new "additional_scopes" column', - query: sql` + query: psql` -- Alter the "oidc_integrations" table with the new column "additional_scopes" ALTER TABLE "oidc_integrations" ADD COLUMN "additional_scopes" TEXT[]; diff --git a/packages/migrations/src/actions/2025.12.30T00-00-00.schema-proposals-part-2.ts b/packages/migrations/src/actions/2025.12.30T00-00-00.schema-proposals-part-2.ts index e7ce08389..d2ab577cf 100644 --- a/packages/migrations/src/actions/2025.12.30T00-00-00.schema-proposals-part-2.ts +++ b/packages/migrations/src/actions/2025.12.30T00-00-00.schema-proposals-part-2.ts @@ -5,10 +5,10 @@ import { type MigrationExecutor } from '../pg-migrator'; */ export default { name: '2025.12.30T00-00-00.schema-proposals-part-2.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'create schema proposal reviews and comments tables', - query: sql` + query: psql` CREATE TABLE IF NOT EXISTS "schema_proposal_reviews" ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4 () diff --git a/packages/migrations/src/actions/2025.12.5T00-00-00.schema-check-url.ts b/packages/migrations/src/actions/2025.12.5T00-00-00.schema-check-url.ts index c828325fe..17c8242bb 100644 --- a/packages/migrations/src/actions/2025.12.5T00-00-00.schema-check-url.ts +++ b/packages/migrations/src/actions/2025.12.5T00-00-00.schema-check-url.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2025.12.5T00-00-00.schema-check-url.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "schema_checks" ADD COLUMN IF NOT EXISTS "service_url" TEXT ; diff --git a/packages/migrations/src/actions/2026.01.09T00-00-00.email-verifications.ts b/packages/migrations/src/actions/2026.01.09T00-00-00.email-verifications.ts index 3f68acf0d..f6b75c114 100644 --- a/packages/migrations/src/actions/2026.01.09T00-00-00.email-verifications.ts +++ b/packages/migrations/src/actions/2026.01.09T00-00-00.email-verifications.ts @@ -5,7 +5,7 @@ import { type MigrationExecutor } from '../pg-migrator'; */ export default { name: '2026.01.09T00-00-00.email-verifications.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` DO $$ BEGIN CREATE TABLE IF NOT EXISTS "email_verifications" ( diff --git a/packages/migrations/src/actions/2026.01.09T10.00.00.target-validation-app-deployment-exclusion.ts b/packages/migrations/src/actions/2026.01.09T10.00.00.target-validation-app-deployment-exclusion.ts index 6955b035b..d5e093127 100644 --- a/packages/migrations/src/actions/2026.01.09T10.00.00.target-validation-app-deployment-exclusion.ts +++ b/packages/migrations/src/actions/2026.01.09T10.00.00.target-validation-app-deployment-exclusion.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.01.09T10.00.00.target-validation-app-deployment-exclusion.sql', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE targets ADD COLUMN diff --git a/packages/migrations/src/actions/2026.01.25T00-00-00.checks-proposals-changes.ts b/packages/migrations/src/actions/2026.01.25T00-00-00.checks-proposals-changes.ts index 0d10947d7..59ca37a8d 100644 --- a/packages/migrations/src/actions/2026.01.25T00-00-00.checks-proposals-changes.ts +++ b/packages/migrations/src/actions/2026.01.25T00-00-00.checks-proposals-changes.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.01.25T00-00-00.checks-proposals-changes.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'add schema proposal changes to schema_checks table', - query: sql` + query: psql` ALTER TABLE IF EXISTS "schema_checks" ADD COLUMN IF NOT EXISTS "schema_proposal_changes" jsonb ; diff --git a/packages/migrations/src/actions/2026.01.27T00-00-00.app-deployment-protection.ts b/packages/migrations/src/actions/2026.01.27T00-00-00.app-deployment-protection.ts index 7c5c0cc9b..355f61cb5 100644 --- a/packages/migrations/src/actions/2026.01.27T00-00-00.app-deployment-protection.ts +++ b/packages/migrations/src/actions/2026.01.27T00-00-00.app-deployment-protection.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.01.27T00-00-00.app-deployment-protection.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE targets ADD COLUMN IF NOT EXISTS app_deployment_protection_enabled BOOLEAN NOT NULL DEFAULT FALSE , ADD COLUMN IF NOT EXISTS app_deployment_protection_min_days_inactive INT NOT NULL DEFAULT 7 diff --git a/packages/migrations/src/actions/2026.01.30T00-00-00.account-linking.ts b/packages/migrations/src/actions/2026.01.30T00-00-00.account-linking.ts index e89baf4b2..37e96edc2 100644 --- a/packages/migrations/src/actions/2026.01.30T00-00-00.account-linking.ts +++ b/packages/migrations/src/actions/2026.01.30T00-00-00.account-linking.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.01.30T00-00-00.account-linking.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'create `users_linked_identities` table', - query: sql` + query: psql` CREATE TABLE IF NOT EXISTS "users_linked_identities" ( "user_id" uuid NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE , "identity_id" uuid NOT NULL @@ -16,7 +16,7 @@ export default { }, { name: 'rename `oidc_user_access_only` to `oidc_user_join_only` and re-add `oidc_user_access_only` column', - query: sql` + query: psql` ALTER TABLE IF EXISTS "oidc_integrations" RENAME COLUMN "oidc_user_access_only" TO "oidc_user_join_only"; ALTER TABLE IF EXISTS "oidc_integrations" diff --git a/packages/migrations/src/actions/2026.01.30T10-00-00.oidc-require-invitation.ts b/packages/migrations/src/actions/2026.01.30T10-00-00.oidc-require-invitation.ts index b04233872..16c12cbab 100644 --- a/packages/migrations/src/actions/2026.01.30T10-00-00.oidc-require-invitation.ts +++ b/packages/migrations/src/actions/2026.01.30T10-00-00.oidc-require-invitation.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.01.30T10-00-00.oidc-require-invitation.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'add `require_invitation` column to `oidc_integrations` table', - query: sql` + query: psql` ALTER TABLE IF EXISTS "oidc_integrations" ADD COLUMN IF NOT EXISTS "require_invitation" boolean NOT NULL DEFAULT false ; diff --git a/packages/migrations/src/actions/2026.02.06T00-00-00.zendesk-unique.ts b/packages/migrations/src/actions/2026.02.06T00-00-00.zendesk-unique.ts index 415bd7bd5..2910bf055 100644 --- a/packages/migrations/src/actions/2026.02.06T00-00-00.zendesk-unique.ts +++ b/packages/migrations/src/actions/2026.02.06T00-00-00.zendesk-unique.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.06T00-00-00.zendesk-unique.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "users_zendesk_user_id_key" ; diff --git a/packages/migrations/src/actions/2026.02.07T00-00-00.saved-filters.ts b/packages/migrations/src/actions/2026.02.07T00-00-00.saved-filters.ts index d0341ee86..dc853ab42 100644 --- a/packages/migrations/src/actions/2026.02.07T00-00-00.saved-filters.ts +++ b/packages/migrations/src/actions/2026.02.07T00-00-00.saved-filters.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.07T00-00-00.saved-filters.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TYPE "saved_filter_visibility" AS ENUM ('private', 'shared'); CREATE TABLE "saved_filters" ( diff --git a/packages/migrations/src/actions/2026.02.18T00-00-00.ensure-supertokens-tables.ts b/packages/migrations/src/actions/2026.02.18T00-00-00.ensure-supertokens-tables.ts index bdcc07f37..f7238fba1 100644 --- a/packages/migrations/src/actions/2026.02.18T00-00-00.ensure-supertokens-tables.ts +++ b/packages/migrations/src/actions/2026.02.18T00-00-00.ensure-supertokens-tables.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.18T00-00-00.ensure-supertokens-tables.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'seed required tables', - query: sql` + query: psql` CREATE TABLE IF NOT EXISTS supertokens_apps ( app_id varchar(64) DEFAULT 'public'::character varying NOT NULL, @@ -138,7 +138,7 @@ CREATE INDEX IF NOT EXISTS emailpassword_pswd_reset_tokens_user_id_index ON supe }, { name: 'ensure app exists', - query: sql` + query: psql` INSERT INTO "supertokens_apps" ( app_id , created_at_time @@ -151,7 +151,7 @@ CREATE INDEX IF NOT EXISTS emailpassword_pswd_reset_tokens_user_id_index ON supe }, { name: 'ensure app exists', - query: sql` + query: psql` INSERT INTO "supertokens_tenants" ( app_id , tenant_id diff --git a/packages/migrations/src/actions/2026.02.19T00-00-00.saved-filter-permission.ts b/packages/migrations/src/actions/2026.02.19T00-00-00.saved-filter-permission.ts index 8e6bb3046..23367ce52 100644 --- a/packages/migrations/src/actions/2026.02.19T00-00-00.saved-filter-permission.ts +++ b/packages/migrations/src/actions/2026.02.19T00-00-00.saved-filter-permission.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.19T00-00-00.saved-filter-permission.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` UPDATE "organization_member_roles" SET "permissions" = array_append("permissions", 'sharedSavedFilter:modify') WHERE "permissions" @> ARRAY['project:modifySettings'] diff --git a/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts b/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts index c0d83243b..96cdce422 100644 --- a/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts +++ b/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts @@ -2,10 +2,10 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.24T00-00-00.proposal-composition.ts', - run: ({ sql }) => [ + run: ({ psql }) => [ { name: 'add schema proposal composition state', - query: sql` + query: psql` ALTER TABLE IF EXISTS "schema_proposals" ADD COLUMN IF NOT EXISTS "composition_status" TEXT , ADD COLUMN IF NOT EXISTS "composition_timestamp" TIMESTAMPTZ diff --git a/packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts b/packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts index a1b0626b8..78f25f8da 100644 --- a/packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts +++ b/packages/migrations/src/actions/2026.02.25T00-00-00.oidc-integration-domains.ts @@ -2,7 +2,7 @@ import { type MigrationExecutor } from '../pg-migrator'; export default { name: '2026.02.25T00-00-00.oidc-integration-domains.ts', - run: ({ sql }) => sql` + run: ({ psql }) => psql` CREATE TABLE IF NOT EXISTS "oidc_integration_domains" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4() , "organization_id" uuid NOT NULL REFERENCES "organizations"("id") ON DELETE CASCADE diff --git a/packages/migrations/src/actions/2026.03.25T00-00-00.access-token-expiration.ts b/packages/migrations/src/actions/2026.03.25T00-00-00.access-token-expiration.ts index 47ed7c72d..f2f08ce41 100644 --- a/packages/migrations/src/actions/2026.03.25T00-00-00.access-token-expiration.ts +++ b/packages/migrations/src/actions/2026.03.25T00-00-00.access-token-expiration.ts @@ -8,7 +8,7 @@ export default { * the timestamp via a filter. Since these lookups are always be done * via provider methods and not via a raw table query, this is safe. */ - run: ({ sql }) => sql` + run: ({ psql }) => psql` ALTER TABLE IF EXISTS organization_access_tokens ADD COLUMN IF NOT EXISTS "expires_at" TIMESTAMP WITH TIME ZONE ; diff --git a/packages/migrations/src/index.ts b/packages/migrations/src/index.ts index 57a98ba7f..01421a389 100644 --- a/packages/migrations/src/index.ts +++ b/packages/migrations/src/index.ts @@ -1,13 +1,13 @@ #!/usr/bin/env node -import { createPool } from 'slonik'; +import { createPostgresDatabasePool } from '@hive/postgres'; import { schemaCoordinateStatusMigration } from './actions/2024.07.23T09.36.00.schema-cleanup-tracker'; import { migrateClickHouse } from './clickhouse'; -import { createConnectionString } from './connection-string'; import { env } from './environment'; import { runPGMigrations } from './run-pg-migrations'; import { updateRetention } from './scripts/update-retention'; -const slonik = await createPool(createConnectionString(env.postgres), { +const slonik = await createPostgresDatabasePool({ + connectionParameters: env.postgres, // 10 minute timeout per statement statementTimeout: 10 * 60 * 1000, }); diff --git a/packages/migrations/src/pg-migrator.ts b/packages/migrations/src/pg-migrator.ts index 98119443e..aa39004fa 100644 --- a/packages/migrations/src/pg-migrator.ts +++ b/packages/migrations/src/pg-migrator.ts @@ -1,11 +1,9 @@ import { CommonQueryMethods, - sql, - type DatabasePool, - type DatabaseTransactionConnection, - type SqlTaggedTemplate, + PostgresDatabasePool, + psql, type TaggedTemplateLiteralInvocation, -} from 'slonik'; +} from '@hive/postgres'; export type MigrationExecutor = { name: string; @@ -18,7 +16,7 @@ export type MigrationExecutor = { * You can either return a SQL query to run or instead use the connection within the function to run custom logic. * You can also return an array of named steps so you can see the progress in the logs. */ - run: (args: { connection: CommonQueryMethods; sql: SqlTaggedTemplate }) => + run: (args: { connection: CommonQueryMethods; psql: typeof psql }) => | Promise | TaggedTemplateLiteralInvocation | Array<{ @@ -27,8 +25,8 @@ export type MigrationExecutor = { }>; }; -const seedMigrationsIfNotExists = async (args: { connection: DatabaseTransactionConnection }) => { - await args.connection.query(sql` +const seedMigrationsIfNotExists = async (args: { connection: CommonQueryMethods }) => { + await args.connection.query(psql` CREATE TABLE IF NOT EXISTS "migration" ( "name" text NOT NULL, "hash" text NOT NULL, @@ -39,7 +37,7 @@ const seedMigrationsIfNotExists = async (args: { connection: DatabaseTransaction }; async function runMigration(connection: CommonQueryMethods, migration: MigrationExecutor) { - const exists = await connection.maybeOneFirst(sql` + const exists = await connection.maybeOneFirst(psql` SELECT true FROM "migration" @@ -54,7 +52,7 @@ async function runMigration(connection: CommonQueryMethods, migration: Migration const startTime = Date.now(); console.log(`Running migration: ${migration.name}`); - const result = await migration.run({ connection, sql }); + const result = await migration.run({ connection, psql }); if (Array.isArray(result)) { for (const item of result) { console.log(` Starting step ${item.name}`); @@ -69,7 +67,7 @@ async function runMigration(connection: CommonQueryMethods, migration: Migration } // TODO: hash verification (but tbh nobody cares about that) - await connection.query(sql` + await connection.query(psql` INSERT INTO "migration" ("name", "hash") VALUES (${migration.name}, ${migration.name}); `); @@ -80,7 +78,7 @@ async function runMigration(connection: CommonQueryMethods, migration: Migration } export async function runMigrations(args: { - slonik: DatabasePool; + slonik: PostgresDatabasePool; migrations: Array; runTo?: string; }) { @@ -93,7 +91,9 @@ export async function runMigrations(args: { if (migration.noTransaction === true) { await runMigration(args.slonik, migration); } else { - await args.slonik.transaction(connection => runMigration(connection, migration)); + await args.slonik.transaction(migration.name, connection => + runMigration(connection, migration), + ); } if (args.runTo && args.runTo === migration.name) { diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index ec8e94c17..7e8d35806 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -1,5 +1,5 @@ import migration_2023_07_27T11_44_36_graphql_endpoint from './actions/2023.07.27T11.44.36.graphql-endpoint'; -import { type DatabasePool } from 'slonik'; +import { type PostgresDatabasePool } from '@hive/postgres'; import migration_2021_03_05T19_06_23_initial from './actions/2021-03-05T19-06-23.initial'; import migration_2021_03_08T11_02_26_urls from './actions/2021-03-08T11-02-26.urls'; import migration_2021_03_09T10_30_35_roles from './actions/2021-03-09T10-30-35.roles'; @@ -69,7 +69,7 @@ import migration_2024_07_17T00_00_00_app_deployments from './actions/2024.07.17T import migration_2024_07_23T_09_36_00_schema_cleanup_tracker from './actions/2024.07.23T09.36.00.schema-cleanup-tracker'; import { runMigrations } from './pg-migrator'; -export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: string }) => +export const runPGMigrations = async (args: { slonik: PostgresDatabasePool; runTo?: string }) => runMigrations({ slonik: args.slonik, runTo: args.runTo, diff --git a/packages/migrations/src/scripts/006-cloud.ts b/packages/migrations/src/scripts/006-cloud.ts index a7a64fcf2..d7ac2a718 100644 --- a/packages/migrations/src/scripts/006-cloud.ts +++ b/packages/migrations/src/scripts/006-cloud.ts @@ -1,8 +1,7 @@ import got from 'got'; import pLimit from 'p-limit'; -import { createPool, sql } from 'slonik'; import z from 'zod'; -import { createConnectionString } from '../connection-string.js'; +import { createPostgresDatabasePool, psql } from '@hive/postgres'; import { env } from './environment.js'; function log(...args: any[]) { @@ -60,7 +59,8 @@ async function main() { const limit = pLimit(poolSize); const startedAt = Date.now(); - const slonik = await createPool(createConnectionString(postgres), { + const slonik = await createPostgresDatabasePool({ + connectionParameters: postgres, // 30 seconds timeout per statement statementTimeout: 30 * 1000, maximumPoolSize: poolSize, @@ -106,19 +106,23 @@ async function main() { function fetchOrganizations() { log('Fetching organizations'); - return slonik.manyFirst(sql`SELECT id FROM organizations`); + return slonik.anyFirst(psql`SELECT id FROM organizations`).then(z.array(z.string()).parse); } function fetchTargets(organizationId: string) { log(`Fetching targets for organization ${organizationId}`); - return slonik.oneFirst(sql` + return slonik + .oneFirst( + psql` SELECT array_agg(DISTINCT t.id) as targets FROM organizations AS o LEFT JOIN projects AS p ON (p.org_id = o.id) LEFT JOIN targets as t ON (t.project_id = p.id) WHERE o.id = ${organizationId} GROUP BY o.id - `); + `, + ) + .then(z.array(z.string()).parse); } async function migrateOrganization(organizationId: string): Promise { @@ -138,7 +142,7 @@ async function main() { const currentMonthResponse = await execute( ` SELECT sum(total) as total FROM operations_daily - WHERE + WHERE target IN (${targetIdsArray}) AND toDate(timestamp) >= toDate(toStartOfMonth(toDate('${CLICKHOUSE_MIGRATION_006_DATE}'))) AND toDate(timestamp) < toDate('${CLICKHOUSE_MIGRATION_006_DATE}') @@ -202,7 +206,7 @@ async function main() { sum(total) as total, toDate(toStartOfMonth(timestamp)) as date FROM operations_daily - WHERE + WHERE target IN (${targetIdsArray}) AND toDate(timestamp) < toDate(toStartOfMonth(toDate('${CLICKHOUSE_MIGRATION_006_DATE}'))) GROUP BY date diff --git a/packages/migrations/src/scripts/008-cloud.ts b/packages/migrations/src/scripts/008-cloud.ts index 048efca87..e5395c0da 100644 --- a/packages/migrations/src/scripts/008-cloud.ts +++ b/packages/migrations/src/scripts/008-cloud.ts @@ -1,8 +1,7 @@ import got from 'got'; import pLimit from 'p-limit'; -import { createPool, sql } from 'slonik'; import z from 'zod'; -import { createConnectionString } from '../connection-string.js'; +import { createPostgresDatabasePool, psql } from '@hive/postgres'; import { env } from './environment.js'; function log(...args: any[]) { @@ -94,7 +93,8 @@ async function main() { const limit = pLimit(poolSize); const startedAt = Date.now(); - const slonik = await createPool(createConnectionString(postgres), { + const slonik = await createPostgresDatabasePool({ + connectionParameters: postgres, // 30 seconds timeout per statement statementTimeout: 30 * 1000, maximumPoolSize: poolSize, @@ -169,14 +169,18 @@ async function main() { async function fetchTargets(organizationId: string) { log(`Fetching targets for organization ${organizationId}`); - return slonik.oneFirst(sql` + return slonik + .oneFirst( + psql` SELECT array_agg(DISTINCT t.id) as targets FROM organizations AS o LEFT JOIN projects AS p ON (p.org_id = o.id) LEFT JOIN targets as t ON (t.project_id = p.id) WHERE o.id = ${organizationId} GROUP BY o.id - `); + `, + ) + .then(z.array(z.string()).parse); } async function migrateOrganization(organizationId: string, migrationDate: string): Promise { @@ -196,7 +200,7 @@ async function main() { const pastMonthResponse = await execute( ` SELECT sum(total) as total, toDate(timestamp) as date FROM operations_daily - WHERE + WHERE target IN (${targetIdsArray}) AND toDate(timestamp) >= (toDate('${migrationDate}') - INTERVAL 1 MONTH) AND toDate(timestamp) < toDate('${migrationDate}') diff --git a/packages/migrations/src/scripts/environment.ts b/packages/migrations/src/scripts/environment.ts index 8861ee17d..2481f6a4a 100644 --- a/packages/migrations/src/scripts/environment.ts +++ b/packages/migrations/src/scripts/environment.ts @@ -1,5 +1,6 @@ import { config as dotenv } from 'dotenv'; import zod from 'zod'; +import type { PostgresConnectionParamaters } from '@hive/postgres'; dotenv({ debug: true, @@ -83,5 +84,5 @@ export const env = { user: postgres.POSTGRES_USER, password: postgres.POSTGRES_PASSWORD, ssl: postgres.POSTGRES_SSL === '1', - }, + } satisfies PostgresConnectionParamaters, } as const; diff --git a/packages/migrations/src/scripts/update-retention.ts b/packages/migrations/src/scripts/update-retention.ts index 5d21f8d1d..89163263a 100644 --- a/packages/migrations/src/scripts/update-retention.ts +++ b/packages/migrations/src/scripts/update-retention.ts @@ -1,8 +1,7 @@ #!/usr/bin/env node import { got, HTTPError } from 'got'; -import { createPool, sql } from 'slonik'; import { z } from 'zod'; -import { createConnectionString } from '../connection-string'; +import { createPostgresDatabasePool, psql } from '@hive/postgres'; import { env } from '../environment'; interface QueryResponse { @@ -183,17 +182,18 @@ function createClickHouseHelpers(endpoint: string, username: string, password: s } async function updatePostgreSQLRetention(retention: RetentionValue) { - const pool = await createPool(createConnectionString(env.postgres), { + const pool = await createPostgresDatabasePool({ + connectionParameters: env.postgres, statementTimeout: 10 * 60 * 1000, // 10 minute timeout }); try { - const result = await pool.query(sql` + const result = await pool.any(psql` UPDATE organizations SET limit_retention_days = ${retention.days} `); - const updatedCount = result.rowCount; + const updatedCount = result.length; console.log( `Updated ${updatedCount} organization(s) (limit_retention_days: ${retention.days})`, ); diff --git a/packages/migrations/test/2023.02.22T09.27.02.delete-personal-org.test.ts b/packages/migrations/test/2023.02.22T09.27.02.delete-personal-org.test.ts index b112995da..4f4af7057 100644 --- a/packages/migrations/test/2023.02.22T09.27.02.delete-personal-org.test.ts +++ b/packages/migrations/test/2023.02.22T09.27.02.delete-personal-org.test.ts @@ -1,7 +1,8 @@ import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { sql } from 'slonik'; -import { DbTypes, initMigrationTestingEnvironment } from './utils/testkit'; +import z from 'zod'; +import { psql } from '@hive/postgres'; +import { initMigrationTestingEnvironment } from './utils/testkit'; await describe('drop-personal-org', async () => { await test('should remove all existing personal orgs that does not have projects', async () => { @@ -19,20 +20,28 @@ await describe('drop-personal-org', async () => { }, }); const emptyOrgs = await Promise.all([ - db.one( - sql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('personal-empty', 'personal-empty', ${user.id}, 'PERSONAL') RETURNING *;`, - ), - db.one( - sql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('regular-empty', 'regular-empty', ${user.id}, 'REGULAR') RETURNING *;`, - ), + db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('personal-empty', 'personal-empty', ${user.id}, 'PERSONAL') RETURNING *;`, + ) + .then(z.object({ id: z.string() }).parse), + db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('regular-empty', 'regular-empty', ${user.id}, 'REGULAR') RETURNING *;`, + ) + .then(z.object({ id: z.string() }).parse), ]); const orgsWithProjects = await Promise.all([ - await db.one( - sql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('personal-project', 'personal-project', ${user.id}, 'PERSONAL') RETURNING *;`, - ), - await db.one( - sql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('regular-project', 'regular-project', ${user.id}, 'PERSONAL') RETURNING *;`, - ), + await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('personal-project', 'personal-project', ${user.id}, 'PERSONAL') RETURNING *;`, + ) + .then(z.object({ id: z.string() }).parse), + await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id, type) VALUES ('regular-project', 'regular-project', ${user.id}, 'PERSONAL') RETURNING *;`, + ) + .then(z.object({ id: z.string() }).parse), ]); // Seed with projects @@ -56,19 +65,19 @@ await describe('drop-personal-org', async () => { // Only this one should be deleted, the rest should still exists assert.equal( - await db.maybeOne(sql`SELECT * FROM organizations WHERE id = ${emptyOrgs[0].id}`), + await db.maybeOne(psql`SELECT * FROM organizations WHERE id = ${emptyOrgs[0].id}`), null, ); assert.notEqual( - await db.maybeOne(sql`SELECT * FROM organizations WHERE id = ${emptyOrgs[1].id}`), + await db.maybeOne(psql`SELECT * FROM organizations WHERE id = ${emptyOrgs[1].id}`), null, ); assert.notEqual( - await db.maybeOne(sql`SELECT * FROM organizations WHERE id = ${orgsWithProjects[0].id}`), + await db.maybeOne(psql`SELECT * FROM organizations WHERE id = ${orgsWithProjects[0].id}`), null, ); assert.notEqual( - await db.maybeOne(sql`SELECT * FROM organizations WHERE id = ${orgsWithProjects[1].id}`), + await db.maybeOne(psql`SELECT * FROM organizations WHERE id = ${orgsWithProjects[1].id}`), null, ); } finally { diff --git a/packages/migrations/test/2023.09.25T15.23.00.github-check-with-project-name.test.ts b/packages/migrations/test/2023.09.25T15.23.00.github-check-with-project-name.test.ts index 98fb26571..bcf3c4a69 100644 --- a/packages/migrations/test/2023.09.25T15.23.00.github-check-with-project-name.test.ts +++ b/packages/migrations/test/2023.09.25T15.23.00.github-check-with-project-name.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { sql } from 'slonik'; +import { psql } from '@hive/postgres'; import { initMigrationTestingEnvironment } from './utils/testkit'; await describe('github-check-with-project-name', async () => { @@ -46,7 +46,7 @@ await describe('github-check-with-project-name', async () => { // Check that the old project has github_check_with_project_name = FALSE assert.equal( await db.oneFirst( - sql`SELECT github_check_with_project_name FROM projects WHERE id = ${oldProject.id}`, + psql`SELECT github_check_with_project_name FROM projects WHERE id = ${oldProject.id}`, ), false, ); @@ -54,7 +54,7 @@ await describe('github-check-with-project-name', async () => { // Check that the new project has github_check_with_project_name = TRUE assert.equal( await db.oneFirst( - sql`SELECT github_check_with_project_name FROM projects WHERE id = ${newProject.id}`, + psql`SELECT github_check_with_project_name FROM projects WHERE id = ${newProject.id}`, ), true, ); diff --git a/packages/migrations/test/2023.11.20T10-00-00.organization-member-roles.test.ts b/packages/migrations/test/2023.11.20T10-00-00.organization-member-roles.test.ts index 4ae4eeb14..e7b671d62 100644 --- a/packages/migrations/test/2023.11.20T10-00-00.organization-member-roles.test.ts +++ b/packages/migrations/test/2023.11.20T10-00-00.organization-member-roles.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { ForeignKeyIntegrityConstraintViolationError, sql } from 'slonik'; +import z from 'zod'; +import { ForeignKeyIntegrityConstraintViolationError, psql } from '@hive/postgres'; import { createStorage } from '../../services/storage/src/index'; import { initMigrationTestingEnvironment } from './utils/testkit'; @@ -93,40 +94,40 @@ await describe('migration: organization-member-roles', async () => { ]; // Create an invitation to simulate a pending invitation - await db.query(sql` + await db.query(psql` INSERT INTO organization_invitations (organization_id, email) VALUES (${organization.id}, 'invited@test.com') `); - await db.query(sql` + await db.query(psql` INSERT INTO organization_member (organization_id, user_id, scopes) VALUES - (${organization.id}, ${admin.id}, ${sql.array(adminScopes, 'text')}), - (${organization.id}, ${contributor.id}, ${sql.array(contributorScopes, 'text')}), - (${organization.id}, ${noRoleUser.id}, ${sql.array(noRoleUserScopes, 'text')}), - (${secondaryOrganization.id}, ${secondaryAdmin.id}, ${sql.array(adminScopes, 'text')}) + (${organization.id}, ${admin.id}, ${psql.array(adminScopes, 'text')}), + (${organization.id}, ${contributor.id}, ${psql.array(contributorScopes, 'text')}), + (${organization.id}, ${noRoleUser.id}, ${psql.array(noRoleUserScopes, 'text')}), + (${secondaryOrganization.id}, ${secondaryAdmin.id}, ${psql.array(adminScopes, 'text')}) `); // assert correct scopes assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${admin.id} `), adminScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${contributor.id} `), contributorScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${noRoleUser.id} `), noRoleUserScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${secondaryAdmin.id} `), adminScopes, @@ -137,25 +138,25 @@ await describe('migration: organization-member-roles', async () => { // assert scopes are still in place and identical assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${admin.id} `), adminScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${contributor.id} `), contributorScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${noRoleUser.id} `), noRoleUserScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${secondaryAdmin.id} `), adminScopes, @@ -163,7 +164,7 @@ await describe('migration: organization-member-roles', async () => { // assert assigned roles have identical scopes assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT omr.scopes FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id @@ -172,7 +173,7 @@ await describe('migration: organization-member-roles', async () => { adminScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT omr.scopes FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id @@ -182,13 +183,13 @@ await describe('migration: organization-member-roles', async () => { ); // assert no role user has no role assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT role_id FROM organization_member WHERE user_id = ${noRoleUser.id} `), null, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT omr.scopes FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id @@ -200,7 +201,7 @@ await describe('migration: organization-member-roles', async () => { // deleting a role with assigned members should not be possible const deletionError = await db .query( - sql` + psql` DELETE FROM organization_member_roles WHERE organization_id = ${organization.id} AND id IN ( SELECT role_id FROM organization_member WHERE organization_id = ${organization.id} AND user_id = ${contributor.id} ) @@ -214,48 +215,56 @@ await describe('migration: organization-member-roles', async () => { // locked roles should be Viewer and Admin assert.deepStrictEqual( - await db.manyFirst(sql` + await db.anyFirst(psql` SELECT name FROM organization_member_roles WHERE organization_id = ${organization.id} AND locked = true ORDER BY name ASC `), ['Admin', 'Viewer'], ); // deleting a member should not delete the role - const contributorRoleId = await db.oneFirst(sql` + const contributorRoleId = await db + .oneFirst( + psql` SELECT role_id FROM organization_member WHERE organization_id = ${organization.id} AND user_id = ${contributor.id} - `); + `, + ) + .then(z.string().parse); assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` DELETE FROM organization_member WHERE user_id = ${contributor.id} AND organization_id = ${organization.id} RETURNING user_id `), contributor.id, ); assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT id FROM organization_member_roles WHERE organization_id = ${organization.id} AND id = ${contributorRoleId} `), contributorRoleId, ); // deleting a user should not delete the role - const customRoleId = await db.oneFirst(sql` + const customRoleId = await db + .oneFirst( + psql` INSERT INTO organization_member_roles (organization_id, name, description, scopes) VALUES - (${organization.id}, 'Custom', 'Custom role', ${sql.array(noRoleUserScopes, 'text')}) + (${organization.id}, 'Custom', 'Custom role', ${psql.array(noRoleUserScopes, 'text')}) RETURNING id - `); - await db.query(sql` + `, + ) + .then(z.string().parse); + await db.query(psql` UPDATE organization_member SET role_id = ${customRoleId} WHERE user_id = ${noRoleUser.id} AND organization_id = ${organization.id} `); assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` DELETE FROM users WHERE id = ${noRoleUser.id} RETURNING id `), noRoleUser.id, ); assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT id FROM organization_member_roles WHERE organization_id = ${organization.id} AND id = ${customRoleId} `), customRoleId, @@ -263,7 +272,7 @@ await describe('migration: organization-member-roles', async () => { // pending invitations should have Viewer role assert.deepStrictEqual( - await db.manyFirst(sql` + await db.anyFirst(psql` SELECT omr.name FROM organization_invitations as oi LEFT JOIN organization_member_roles as omr ON (omr.id = oi.role_id AND omr.organization_id = oi.organization_id) diff --git a/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts b/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts index cffd50627..2dd41b535 100644 --- a/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts +++ b/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts @@ -3,8 +3,9 @@ import 'reflect-metadata'; import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { sql } from 'slonik'; +import z from 'zod'; import type { Logger } from '@hive/api'; +import { psql } from '@hive/postgres'; import { Contracts } from '../../services/api/src/modules/schema/providers/contracts'; import { createStorage, HiveSchemaChangeModel } from '../../services/storage/src/index'; import { initMigrationTestingEnvironment } from './utils/testkit'; @@ -41,21 +42,21 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); @@ -87,24 +88,28 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 1, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 1, 'SDL store count after creating schema check'); await storage.purgeExpiredSchemaChecks({ expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 0, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 0, 'SDL store count after purge'); } finally { await done(); @@ -126,21 +131,21 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); @@ -200,24 +205,28 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 2, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 2, 'SDL store count after creating schema check'); await storage.purgeExpiredSchemaChecks({ expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 1, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 1, 'SDL store count after purge'); } finally { await done(); @@ -239,21 +248,21 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); @@ -313,24 +322,28 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 2, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 2, 'SDL store count after creating schema check'); await storage.purgeExpiredSchemaChecks({ expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 1, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 1, 'SDL store count after purge'); } finally { await done(); @@ -352,21 +365,21 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); const expiresAt2 = new Date(expiresAt.getTime() + 10_000); @@ -436,17 +449,19 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 2, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 2, 'SDL store count after creating schema check'); - let schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, - ); + let schemaChangeApprovalCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_change_approvals`) + .then(z.number().parse); assert.equal( schemaChangeApprovalCount, 0, @@ -466,9 +481,9 @@ describe('schema check purging', async () => { } as any, }); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, - ); + schemaChangeApprovalCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_change_approvals`) + .then(z.number().parse); assert.equal( schemaChangeApprovalCount, 1, @@ -479,34 +494,38 @@ describe('schema check purging', async () => { expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 1, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 1, 'SDL store count after purge'); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, - ); + schemaChangeApprovalCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_change_approvals`) + .then(z.number().parse); assert.equal(schemaChangeApprovalCount, 1, 'schema change approval count after purge'); await storage.purgeExpiredSchemaChecks({ expiresAt: expiresAt2, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_checks`) + .then(z.number().parse); assert.equal(schemaCheckCount, 0, 'Schema check count after second purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db + .oneFirst(psql`SELECT count(*) as total FROM sdl_store`) + .then(z.number().parse); assert.equal(sdlStoreCount, 0, 'SDL store count after second purge'); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, - ); + schemaChangeApprovalCount = await db + .oneFirst(psql`SELECT count(*) as total FROM schema_change_approvals`) + .then(z.number().parse); assert.equal(schemaChangeApprovalCount, 0, 'schema change approval count after second purge'); } finally { await done(); @@ -528,26 +547,26 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); - const target2 = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target2 = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); const expiresAt2 = new Date(expiresAt.getTime() + 10_000); @@ -617,16 +636,14 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 2, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 2, 'SDL store count after creating schema check'); - let schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, + let schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -647,8 +664,8 @@ describe('schema check purging', async () => { } as any, }); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -660,16 +677,14 @@ describe('schema check purging', async () => { expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 1, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 1, 'SDL store count after purge'); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM schema_change_approvals`, ); assert.equal(schemaChangeApprovalCount, 0, 'schema change approval count after purge'); } finally { @@ -692,23 +707,25 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); - const contractId = await db.oneFirst(sql` + const contractId = await db + .oneFirst( + psql` INSERT INTO "contracts" ( "target_id" , "contract_name" @@ -718,13 +735,15 @@ describe('schema check purging', async () => { ) VALUES ( ${target.id} , 'contract-name' - , ${sql.array(['tag1', 'tag2'], 'text')} + , ${psql.array(['tag1', 'tag2'], 'text')} , ${null} , ${true} ) RETURNING "id" - `); + `, + ) + .then(z.string().parse); const expiresAt = new Date(); @@ -767,14 +786,14 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 4, 'SDL store count before purge'); await storage.purgeExpiredSchemaChecks({ expiresAt, }); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 0, 'SDL store count after purge'); } finally { await done(); @@ -796,26 +815,28 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); const contextId = 'context-id'; - const contractId = await db.oneFirst(sql` + const contractId = await db + .oneFirst( + psql` INSERT INTO "contracts" ( "target_id" , "contract_name" @@ -825,13 +846,15 @@ describe('schema check purging', async () => { ) VALUES ( ${target.id} , 'contract-name' - , ${sql.array(['tag1', 'tag2'], 'text')} + , ${psql.array(['tag1', 'tag2'], 'text')} , ${null} , ${true} ) RETURNING "id" - `); + `, + ) + .then(z.string().parse); const failedSchemaCheck = await storage.createSchemaCheck({ expiresAt, @@ -880,16 +903,14 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 1, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 4, 'SDL store count after creating schema check'); - let schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + let schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -908,8 +929,8 @@ describe('schema check purging', async () => { author: null, }); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -921,16 +942,14 @@ describe('schema check purging', async () => { expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 0, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 0, 'SDL store count after purge'); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -957,27 +976,29 @@ describe('schema check purging', async () => { email: 'test@test.com', }, }); - const organization = await db.one<{ - id: string; - }>( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, - ); - const project = await db.one<{ - id: string; - }>( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, - ); - const target = await db.one<{ - id: string; - }>( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, - ); + const organization = await db + .one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES ('org-1', 'org-1', ${user.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const project = await db + .one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'FEDERATION', ${organization.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); + const target = await db + .one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES ('proj-1', 'proj-1', ${project.id}) RETURNING id;`, + ) + .then(z.object({ id: z.string() }).parse); const expiresAt = new Date(); const expiresAt2 = new Date(expiresAt.getTime() + 10_000); const contextId = 'context-id'; - const contractId = await db.oneFirst(sql` + const contractId = await db + .oneFirst( + psql` INSERT INTO "contracts" ( "target_id" , "contract_name" @@ -987,13 +1008,15 @@ describe('schema check purging', async () => { ) VALUES ( ${target.id} , 'contract-name' - , ${sql.array(['tag1', 'tag2'], 'text')} + , ${psql.array(['tag1', 'tag2'], 'text')} , ${null} , ${true} ) RETURNING "id" - `); + `, + ) + .then(z.string().parse); const failedSchemaCheck = await storage.createSchemaCheck({ expiresAt, @@ -1089,16 +1112,14 @@ describe('schema check purging', async () => { schemaProposalChanges: null, }); - let schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + let schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 2, 'Schema check count after creating schema check'); - let sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + let sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 4, 'SDL store count after creating schema check'); - let schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + let schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -1117,8 +1138,8 @@ describe('schema check purging', async () => { author: null, }); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, @@ -1130,16 +1151,14 @@ describe('schema check purging', async () => { expiresAt, }); - schemaCheckCount = await db.oneFirst( - sql`SELECT count(*) as total FROM schema_checks`, - ); + schemaCheckCount = await db.oneFirst(psql`SELECT count(*) as total FROM schema_checks`); assert.equal(schemaCheckCount, 1, 'Schema check count after purge'); - sdlStoreCount = await db.oneFirst(sql`SELECT count(*) as total FROM sdl_store`); + sdlStoreCount = await db.oneFirst(psql`SELECT count(*) as total FROM sdl_store`); assert.equal(sdlStoreCount, 3, 'SDL store count after purge'); - schemaChangeApprovalCount = await db.oneFirst( - sql`SELECT count(*) as total FROM contract_schema_change_approvals`, + schemaChangeApprovalCount = await db.oneFirst( + psql`SELECT count(*) as total FROM contract_schema_change_approvals`, ); assert.equal( schemaChangeApprovalCount, diff --git a/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts b/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts index 87f3b2a45..219ba9df8 100644 --- a/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts +++ b/packages/migrations/test/2024.07.23T09.36.00.schema-cleanup-tracker.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { sql } from 'slonik'; +import z from 'zod'; +import { psql } from '@hive/postgres'; import { createStorage } from '../../services/storage/src/index'; import { initMigrationTestingEnvironment } from './utils/testkit'; @@ -47,7 +48,9 @@ await describe('migration: schema-cleanup-tracker', async () => { schema: string, previousSchemaVersionId: string | null, ): Promise { - const logId = await db.oneFirst(sql` + const logId = await db + .oneFirst( + psql` INSERT INTO schema_log ( author, @@ -73,9 +76,13 @@ await describe('migration: schema-cleanup-tracker', async () => { 'PUSH' ) RETURNING id - `); + `, + ) + .then(z.string().parse); - const versionId = await db.oneFirst(sql` + const versionId = await db + .oneFirst( + psql` INSERT INTO schema_versions ( record_version, @@ -117,7 +124,9 @@ await describe('migration: schema-cleanup-tracker', async () => { ${null} ) RETURNING id - `); + `, + ) + .then(z.string().parse); return versionId; } @@ -186,17 +195,29 @@ await describe('migration: schema-cleanup-tracker', async () => { // check that coordinates are correct - const versions = await db.manyFirst(sql` + const versions = await db + .anyFirst( + psql` SELECT id FROM schema_versions WHERE target_id = ${target.id} ORDER BY created_at ASC - `); + `, + ) + .then(z.array(z.string()).parse); - const coordinates = await db.many<{ - coordinate: string; - created_in_version_id: string; - deprecated_in_version_id: string | null; - }>(sql` + const coordinates = await db + .any( + psql` SELECT * FROM schema_coordinate_status WHERE target_id = ${target.id} - `); + `, + ) + .then( + z.array( + z.object({ + coordinate: z.string(), + created_in_version_id: z.string(), + deprecated_in_version_id: z.string().nullable(), + }), + ).parse, + ); assert.strictEqual(versions.length, 6); diff --git a/packages/migrations/test/2025.01.09T00-00-00.legacy-member-scopes.test.ts b/packages/migrations/test/2025.01.09T00-00-00.legacy-member-scopes.test.ts index cf09bce0f..21b42dded 100644 --- a/packages/migrations/test/2025.01.09T00-00-00.legacy-member-scopes.test.ts +++ b/packages/migrations/test/2025.01.09T00-00-00.legacy-member-scopes.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import { describe, test } from 'node:test'; -import { sql } from 'slonik'; +import z from 'zod'; +import { psql } from '@hive/postgres'; import { createStorage } from '../../services/storage/src/index'; import { initMigrationTestingEnvironment } from './utils/testkit'; @@ -81,33 +82,33 @@ await describe('migration: legacy-member-socpes', async () => { ]; // Create an invitation to simulate a pending invitation - await db.query(sql` + await db.query(psql` INSERT INTO organization_invitations (organization_id, email) VALUES (${organization.id}, 'invited@test.com') `); - await db.query(sql` + await db.query(psql` INSERT INTO organization_member (organization_id, user_id, scopes) VALUES - (${organization.id}, ${admin.id}, ${sql.array(adminScopes, 'text')}), - (${organization.id}, ${contributor.id}, ${sql.array(contributorScopes, 'text')}), - (${organization.id}, ${noRoleUser.id}, ${sql.array(noRoleUserScopes, 'text')}) + (${organization.id}, ${admin.id}, ${psql.array(adminScopes, 'text')}), + (${organization.id}, ${contributor.id}, ${psql.array(contributorScopes, 'text')}), + (${organization.id}, ${noRoleUser.id}, ${psql.array(noRoleUserScopes, 'text')}) `); // assert correct scopes assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${admin.id} `), adminScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${contributor.id} `), contributorScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${noRoleUser.id} `), noRoleUserScopes, @@ -118,19 +119,19 @@ await describe('migration: legacy-member-socpes', async () => { // assert scopes are still in place and identical assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${admin.id} `), adminScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${contributor.id} `), contributorScopes, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT scopes FROM organization_member WHERE user_id = ${noRoleUser.id} `), noRoleUserScopes, @@ -138,31 +139,43 @@ await describe('migration: legacy-member-socpes', async () => { // assert assigned roles have identical scopes - const adminRole = await db.one<{ - scopes: string[]; - id: string; - }>(sql` + const adminRole = await db + .one( + psql` SELECT omr.scopes, omr.id FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id WHERE om.user_id = ${admin.id} AND om.organization_id = ${organization.id} - `); + `, + ) + .then( + z.object({ + scopes: z.array(z.string()), + id: z.string(), + }).parse, + ); assert.deepStrictEqual(adminRole.scopes, adminScopes); - const contributorRole = await db.one<{ - scopes: string[]; - id: string; - }>(sql` + const contributorRole = await db + .one( + psql` SELECT omr.scopes, omr.id FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id WHERE om.user_id = ${contributor.id} AND om.organization_id = ${organization.id} - `); + `, + ) + .then( + z.object({ + scopes: z.array(z.string()), + id: z.string(), + }).parse, + ); assert.deepStrictEqual(contributorRole.scopes, contributorScopes); // assert no role user has no role assert.strictEqual( - await db.oneFirst(sql` + await db.oneFirst(psql` SELECT role_id FROM organization_member WHERE user_id = ${noRoleUser.id} `), null, @@ -172,15 +185,21 @@ await describe('migration: legacy-member-socpes', async () => { // assert that the migration created a new role for the non-role user // and the role has exact same scopes as the user - const previouslyNoRoleUser = await db.one<{ - scopes: string[]; - name: string; - }>(sql` + const previouslyNoRoleUser = await db + .one( + psql` SELECT omr.scopes, omr.name FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id WHERE om.user_id = ${noRoleUser.id} AND om.organization_id = ${organization.id} - `); + `, + ) + .then( + z.object({ + scopes: z.array(z.string()), + name: z.string(), + }).parse, + ); assert.deepStrictEqual(previouslyNoRoleUser.scopes, noRoleUserScopes); @@ -190,22 +209,30 @@ await describe('migration: legacy-member-socpes', async () => { // asser that users with roles were not affected, // meaning the role was exactly the same as before assert.deepStrictEqual( - await db.oneFirst(sql` + await db + .oneFirst( + psql` SELECT omr.id FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id WHERE om.user_id = ${admin.id} AND om.organization_id = ${organization.id} - `), + `, + ) + .then(z.string().parse), adminRole.id, ); assert.deepStrictEqual( - await db.oneFirst(sql` + await db + .oneFirst( + psql` SELECT omr.id FROM organization_member as om LEFT JOIN organization_member_roles as omr ON omr.id = om.role_id WHERE om.user_id = ${contributor.id} AND om.organization_id = ${organization.id} - `), + `, + ) + .then(z.string().parse), contributorRole.id, ); } finally { diff --git a/packages/migrations/test/utils/testkit.ts b/packages/migrations/test/utils/testkit.ts index 976018023..cd5ab5108 100644 --- a/packages/migrations/test/utils/testkit.ts +++ b/packages/migrations/test/utils/testkit.ts @@ -1,12 +1,11 @@ /* eslint-disable import/first */ - + import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { config } from 'dotenv'; import pgpFactory from 'pg-promise'; -import { createPool, sql } from 'slonik'; import type * as DbTypes from '../../../services/storage/src/db/types'; -import { createConnectionString } from '../../src/connection-string'; +import { createConnectionString, psql, createPostgresDatabasePool } from '@hive/postgres'; import { runPGMigrations } from '../../src/run-pg-migrations' export { type DbTypes } @@ -18,6 +17,7 @@ config({ }); import { env } from '../../src/environment'; +import z from 'zod'; export async function initMigrationTestingEnvironment() { const pgp = pgpFactory(); @@ -25,17 +25,18 @@ export async function initMigrationTestingEnvironment() { ...env.postgres, db: 'postgres', })); - + const dbName = 'migration_test_' + Date.now() + Math.random().toString(16).substring(2); console.log('db name:', dbName) await db.query(`CREATE DATABASE ${dbName};`); - + const connectionString = createConnectionString({ ...env.postgres, db: dbName, }); - const slonik = await createPool( - connectionString + const slonik = await createPostgresDatabasePool({ + connectionParameters: connectionString + } ); const actionsDirectory = resolve(__dirname + '/../../src/actions/'); @@ -58,9 +59,9 @@ export async function initMigrationTestingEnvironment() { email: string; } }) { - return await slonik.one( - sql`INSERT INTO users (email, display_name, full_name, supertoken_user_id) VALUES (${user.email}, ${user.name} , ${user.name}, ${superTokenUserIdCounter++}) RETURNING *;`, - ); + return await slonik.one( + psql`INSERT INTO users (email, display_name, full_name, supertoken_user_id) VALUES (${user.email}, ${user.name} , ${user.name}, ${superTokenUserIdCounter++}) RETURNING *;`, + ).then(z.object({ id: z.string()}).parse); }, async organization({ organization, @@ -73,9 +74,9 @@ export async function initMigrationTestingEnvironment() { id: string; } }) { - return await slonik.one( - sql`INSERT INTO organizations (clean_id, name, user_id) VALUES (${organization.name}, ${organization.name}, ${user.id}) RETURNING *;`, - ); + return await slonik.one( + psql`INSERT INTO organizations (clean_id, name, user_id) VALUES (${organization.name}, ${organization.name}, ${user.id}) RETURNING *;`, + ).then(z.object({ id: z.string()}).parse); }, async project({ project, @@ -89,9 +90,9 @@ export async function initMigrationTestingEnvironment() { id: string; } }) { - return await slonik.one( - sql`INSERT INTO projects (clean_id, name, type, org_id) VALUES (${project.name}, ${project.name}, ${project.type}, ${organization.id}) RETURNING *;`, - ) + return await slonik.one( + psql`INSERT INTO projects (clean_id, name, type, org_id) VALUES (${project.name}, ${project.name}, ${project.type}, ${organization.id}) RETURNING *;`, + ).then(z.object({ id: z.string()}).parse); }, async target({ project, @@ -104,9 +105,9 @@ export async function initMigrationTestingEnvironment() { name: string; } }) { - return await slonik.one( - sql`INSERT INTO targets (clean_id, name, project_id) VALUES (${target.name}, ${target.name}, ${project.id}) RETURNING *;`, - ) + return await slonik.one( + psql`INSERT INTO targets (clean_id, name, project_id) VALUES (${target.name}, ${target.name}, ${project.id}) RETURNING *;`, + ).then(z.object({ id: z.string()}).parse); }, }, async complete() { diff --git a/packages/migrations/tsconfig.json b/packages/migrations/tsconfig.json index 764684e8f..a289bc408 100644 --- a/packages/migrations/tsconfig.json +++ b/packages/migrations/tsconfig.json @@ -1,27 +1,10 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "rootDir": "./", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "importHelpers": true, - "allowJs": true, - "skipLibCheck": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "sourceMap": true, - "declaration": true, - "resolveJsonModule": true, - "moduleResolution": "node", - "strict": true, - "paths": { - "@hive/cdn-script/artifact-storage-reader": [ - "../services/cdn-worker/src/artifact-storage-reader.ts" - ], - "@hive/storage": ["../services/storage/src/index.ts"], - "@hive/service-common": ["../services/service-common/src/index.ts"] - } + "target": "ES2020", + "module": "esnext", + "rootDir": "../.." }, - "include": ["src"] + "files": ["src/index.ts"], + "include": ["src/**/*"] } diff --git a/packages/services/api/package.json b/packages/services/api/package.json index 7d56f635b..918db15d8 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -26,6 +26,7 @@ "@graphql-inspector/core": "7.1.2", "@graphql-tools/merge": "9.1.1", "@hive/cdn-script": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/pubsub": "workspace:*", "@hive/schema": "workspace:*", "@hive/service-common": "workspace:*", @@ -73,7 +74,6 @@ "p-queue": "8.0.1", "prom-client": "15.1.3", "redlock": "5.0.0-beta.2", - "slonik": "30.4.4", "stripe": "17.5.0", "tslib": "2.8.1", "undici": "7.24.0", diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index b418631ac..f38732615 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -1,5 +1,6 @@ import { CONTEXT, createApplication, Provider, Scope } from 'graphql-modules'; import { Redis } from 'ioredis'; +import { PostgresDatabasePool } from '@hive/postgres'; import { TaskScheduler } from '@hive/workflows/kit'; import { adminModule } from './modules/admin'; import { alertsModule } from './modules/alerts'; @@ -58,7 +59,6 @@ import { } from './modules/shared/providers/in-memory-rate-limiter'; import { Logger } from './modules/shared/providers/logger'; import { Mutex } from './modules/shared/providers/mutex'; -import { PG_POOL_CONFIG } from './modules/shared/providers/pg-pool'; import { PrometheusConfig } from './modules/shared/providers/prometheus-config'; import { HivePubSub, PUB_SUB_CONFIG } from './modules/shared/providers/pub-sub'; import { REDIS_INSTANCE } from './modules/shared/providers/redis'; @@ -312,7 +312,7 @@ export function createRegistry({ scope: Scope.Singleton, }, { - provide: PG_POOL_CONFIG, + provide: PostgresDatabasePool, scope: Scope.Singleton, useValue: storage.pool, }, diff --git a/packages/services/api/src/modules/app-deployments/providers/app-deployments.ts b/packages/services/api/src/modules/app-deployments/providers/app-deployments.ts index 0e1ec0c47..aff2d3772 100644 --- a/packages/services/api/src/modules/app-deployments/providers/app-deployments.ts +++ b/packages/services/api/src/modules/app-deployments/providers/app-deployments.ts @@ -1,8 +1,12 @@ import { differenceInCalendarDays, startOfDay, subDays } from 'date-fns'; import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, UniqueIntegrityConstraintViolationError, type DatabasePool } from 'slonik'; import { z } from 'zod'; import { buildAppDeploymentIsEnabledKey } from '@hive/cdn-script/artifact-storage-reader'; +import { + PostgresDatabasePool, + psql, + UniqueIntegrityConstraintViolationError, +} from '@hive/postgres'; import { AffectedAppDeployments, decodeAppDeploymentSortCursor, @@ -15,7 +19,6 @@ import { import { ClickHouse, sql as cSql } from '../../operations/providers/clickhouse-client'; import { SchemaVersionHelper } from '../../schema/providers/schema-version-helper'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { S3_CONFIG, type S3Config } from '../../shared/providers/s3-config'; import { Storage } from '../../shared/providers/storage'; import { APP_DEPLOYMENTS_ENABLED } from './app-deployments-enabled-token'; @@ -46,7 +49,7 @@ export class AppDeployments { constructor( logger: Logger, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, @Inject(S3_CONFIG) private s3: S3Config, private clickhouse: ClickHouse, private storage: Storage, @@ -62,8 +65,8 @@ export class AppDeployments { }): Promise { this.logger.debug('get app deployment by id (appDeploymentId=%s)', args.appDeploymentId); - const record = await this.pool.maybeOne( - sql` + const record = await this.pool.maybeOne( + psql` SELECT ${appDeploymentFields} FROM @@ -92,8 +95,8 @@ export class AppDeployments { args.version, ); - const record = await this.pool.maybeOne( - sql` + const record = await this.pool.maybeOne( + psql` SELECT ${appDeploymentFields} FROM @@ -188,7 +191,7 @@ export class AppDeployments { try { const result = await this.pool.maybeOne( - sql` + psql` INSERT INTO "app_deployments" ( "target_id" , "name" @@ -458,7 +461,7 @@ export class AppDeployments { const updatedAppDeployment = await this.pool .maybeOne( - sql` + psql` UPDATE "app_deployments" SET @@ -744,7 +747,7 @@ export class AppDeployments { const updatedAppDeployment = await this.pool .one( - sql` + psql` UPDATE "app_deployments" SET @@ -772,9 +775,13 @@ export class AppDeployments { } async countAppDeployments(targetId: string): Promise { - return this.pool.oneFirst(sql` + return this.pool + .oneFirst( + psql` SELECT count(*) FROM "app_deployments" WHERE "target_id" = ${targetId} - `); + `, + ) + .then(z.number().parse); } async getPaginatedAppDeployments(args: { @@ -816,33 +823,33 @@ export class AppDeployments { cursor = null; } - const col = sql.identifier([sortField === 'ACTIVATED_AT' ? 'activated_at' : 'created_at']); + const col = psql.identifier([sortField === 'ACTIVATED_AT' ? 'activated_at' : 'created_at']); const isNullable = sortField === 'ACTIVATED_AT'; const isDesc = sortDirection === 'DESC'; - let cursorCondition = sql``; + let cursorCondition = psql``; if (cursor) { const cv = cursor.sortValue; - const tiebreakOp = isDesc ? sql`<` : sql`>`; + const tiebreakOp = isDesc ? psql`<` : psql`>`; if (cv === null) { - cursorCondition = sql`AND (${col} IS NULL AND "id" ${tiebreakOp} ${cursor.id})`; + cursorCondition = psql`AND (${col} IS NULL AND "id" ${tiebreakOp} ${cursor.id})`; } else if (isNullable) { cursorCondition = isDesc - ? sql`AND ((${col} = ${cv} AND "id" < ${cursor.id}) OR ${col} < ${cv} OR ${col} IS NULL)` - : sql`AND ((${col} = ${cv} AND "id" > ${cursor.id}) OR ${col} > ${cv} OR ${col} IS NULL)`; + ? psql`AND ((${col} = ${cv} AND "id" < ${cursor.id}) OR ${col} < ${cv} OR ${col} IS NULL)` + : psql`AND ((${col} = ${cv} AND "id" > ${cursor.id}) OR ${col} > ${cv} OR ${col} IS NULL)`; } else { cursorCondition = isDesc - ? sql`AND ((${col} = ${cv} AND "id" < ${cursor.id}) OR ${col} < ${cv})` - : sql`AND ((${col} = ${cv} AND "id" > ${cursor.id}) OR ${col} > ${cv})`; + ? psql`AND ((${col} = ${cv} AND "id" < ${cursor.id}) OR ${col} < ${cv})` + : psql`AND ((${col} = ${cv} AND "id" > ${cursor.id}) OR ${col} > ${cv})`; } } - const dirSql = isDesc ? sql`DESC` : sql`ASC`; - const nullsLast = isNullable ? sql`NULLS LAST` : sql``; - const orderBy = sql`ORDER BY ${col} ${dirSql} ${nullsLast}, "id" ${dirSql}`; + const dirSql = isDesc ? psql`DESC` : psql`ASC`; + const nullsLast = isNullable ? psql`NULLS LAST` : psql``; + const orderBy = psql`ORDER BY ${col} ${dirSql} ${nullsLast}, "id" ${dirSql}`; - const result = await this.pool.query(sql` + const result = await this.pool.any(psql` SELECT ${appDeploymentFields} FROM @@ -854,7 +861,7 @@ export class AppDeployments { LIMIT ${limit + 1} `); - let items = result.rows.map(row => { + let items = result.map(row => { const node = AppDeploymentModel.parse(row); const sortValue = sortField === 'ACTIVATED_AT' ? node.activatedAt : node.createdAt; @@ -972,13 +979,13 @@ export class AppDeployments { const deploymentByPair = new Map(); if (usagePairsForPage.length > 0) { - const pgResult = await this.pool.query(sql` + const pgResult = await this.pool.any(psql` SELECT ${appDeploymentFields} FROM "app_deployments" WHERE "target_id" = ${args.targetId} - AND ("name" || ':' || "version") = ANY(${sql.array(usagePairsForPage, 'text')}) + AND ("name" || ':' || "version") = ANY(${psql.array(usagePairsForPage, 'text')}) `); - for (const row of pgResult.rows) { + for (const row of pgResult) { const d = AppDeploymentModel.parse(row); deploymentByPair.set(`${d.name}:${d.version}`, d); } @@ -1008,20 +1015,20 @@ export class AppDeployments { const remaining = limit + 1 - pageItems.length; let fillHasMore = false; if (remaining > 0) { - const dirSql = isDesc ? sql`DESC` : sql`ASC`; - let fillCursorCondition = sql``; + const dirSql = isDesc ? psql`DESC` : psql`ASC`; + let fillCursorCondition = psql``; if (cursorInNoUsageSection) { fillCursorCondition = isDesc - ? sql`AND "id" < ${cursor!.id}` - : sql`AND "id" > ${cursor!.id}`; + ? psql`AND "id" < ${cursor!.id}` + : psql`AND "id" > ${cursor!.id}`; } const excludeCurrentPage = usagePairsForPage.length > 0 - ? sql`AND NOT (("name" || ':' || "version") = ANY(${sql.array(usagePairsForPage, 'text')}))` - : sql``; + ? psql`AND NOT (("name" || ':' || "version") = ANY(${psql.array(usagePairsForPage, 'text')}))` + : psql``; - const fillResult = await this.pool.query(sql` + const fillResult = await this.pool.any(psql` SELECT ${appDeploymentFields} FROM "app_deployments" WHERE "target_id" = ${args.targetId} @@ -1031,7 +1038,7 @@ export class AppDeployments { LIMIT ${limit + 1} `); - const candidates = fillResult.rows.map(row => AppDeploymentModel.parse(row)); + const candidates = fillResult.map(row => AppDeploymentModel.parse(row)); fillHasMore = candidates.length > limit; if (candidates.length > 0) { @@ -1341,7 +1348,7 @@ export class AppDeployments { private async getAffectedAppDeploymentsMetadata(appDeploymentIds: Array) { return await this.pool .any( - sql` + psql` SELECT "id" , "name" @@ -1351,7 +1358,7 @@ export class AppDeployments { , to_json("retired_at") AS "retiredAt" FROM "app_deployments" - WHERE "id" = ANY(${sql.array(appDeploymentIds, 'uuid')}) + WHERE "id" = ANY(${psql.array(appDeploymentIds, 'uuid')}) `, ) .then( @@ -1652,13 +1659,13 @@ export class AppDeployments { const batchResults = await Promise.all( batches.map(batchIds => - this.pool.query(sql` + this.pool.any(psql` SELECT ${appDeploymentFields} FROM "app_deployments" WHERE - "id" = ANY(${sql.array(batchIds, 'uuid')}) + "id" = ANY(${psql.array(batchIds, 'uuid')}) AND "activated_at" IS NOT NULL AND "retired_at" IS NULL `), @@ -1667,7 +1674,7 @@ export class AppDeployments { // Merge and sort results activeDeployments = batchResults - .flatMap(result => result.rows.map(row => AppDeploymentModel.parse(row))) + .flatMap(result => result.map(row => AppDeploymentModel.parse(row))) .sort((a, b) => { // Sort by created_at DESC, id ASC if (a.createdAt > b.createdAt) return -1; @@ -1675,21 +1682,21 @@ export class AppDeployments { return a.id.localeCompare(b.id); }); } else { - const activeDeploymentsResult = await this.pool.query(sql` + const activeDeploymentsResult = await this.pool.any(psql` SELECT ${appDeploymentFields} FROM "app_deployments" WHERE "target_id" = ${args.targetId} - ${args.filter.name ? sql`AND "name" ILIKE ${'%' + args.filter.name + '%'}` : sql``} + ${args.filter.name ? psql`AND "name" ILIKE ${'%' + args.filter.name + '%'}` : psql``} AND "activated_at" IS NOT NULL AND "retired_at" IS NULL ORDER BY "created_at" DESC, "id" ASC LIMIT ${maxDeployments} `); - activeDeployments = activeDeploymentsResult.rows.map(row => AppDeploymentModel.parse(row)); + activeDeployments = activeDeploymentsResult.map(row => AppDeploymentModel.parse(row)); } } catch (error) { const batchCount = staleDeploymentIds ? Math.ceil(staleDeploymentIds.length / BATCH_SIZE) : 0; @@ -1881,7 +1888,7 @@ export class AppDeployments { } } -const appDeploymentFields = sql` +const appDeploymentFields = psql` "id" , "target_id" AS "targetId" , "name" diff --git a/packages/services/api/src/modules/auth/providers/email-verification.ts b/packages/services/api/src/modules/auth/providers/email-verification.ts index a0be94da8..750e41359 100644 --- a/packages/services/api/src/modules/auth/providers/email-verification.ts +++ b/packages/services/api/src/modules/auth/providers/email-verification.ts @@ -1,13 +1,12 @@ import { randomBytes } from 'node:crypto'; import bcrypt from 'bcryptjs'; import { Inject, Injectable } from 'graphql-modules'; -import { sql, TaggedTemplateLiteralInvocation, type DatabasePool } from 'slonik'; import zod from 'zod'; +import { PostgresDatabasePool, psql, TaggedTemplateLiteralInvocation } from '@hive/postgres'; import { TaskScheduler } from '@hive/workflows/kit'; import { EmailVerificationTask } from '@hive/workflows/tasks/email-verification'; import { HiveError } from '../../../shared/errors'; import { InMemoryRateLimiter } from '../../shared/providers/in-memory-rate-limiter'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { WEB_APP_URL } from '../../shared/providers/tokens'; const EmailVerificationModelBase = zod.object({ @@ -34,7 +33,7 @@ const EmailVerificationModel = zod.union([ VerifiedEmailVerificationModel, ]); -const emailVerificationFields = (r: TaggedTemplateLiteralInvocation) => sql` +const emailVerificationFields = (r: TaggedTemplateLiteralInvocation) => psql` ${r}"id" AS "id" , ${r}"user_identity_id" AS "userIdentityId" , ${r}"email" AS "email" @@ -56,13 +55,13 @@ export class EmailVerification { private taskScheduler: TaskScheduler, private rateLimiter: InMemoryRateLimiter, @Inject(WEB_APP_URL) private appBaseUrl: string, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, ) {} async checkUserEmailVerified(input: { userIdentityId: string; email: string }) { const { provider } = await this.pool .one( - sql` + psql` SELECT COALESCE("stu"."third_party_id", 'emailpassword') "provider" FROM "supertokens_all_auth_recipe_users" "saaru" LEFT JOIN "supertokens_thirdparty_users" "stu" @@ -80,8 +79,8 @@ export class EmailVerification { const emailVerification = await this.pool .maybeOne( - sql` - SELECT ${emailVerificationFields(sql`"ev".`)} + psql` + SELECT ${emailVerificationFields(psql`"ev".`)} FROM "email_verifications" "ev" WHERE "ev"."user_identity_id" = ${input.userIdentityId} @@ -121,7 +120,7 @@ export class EmailVerification { const superTokensUser = await this.pool .maybeOne( - sql` + psql` SELECT COALESCE("seu"."email", "stu"."email") "email" FROM "supertokens_all_auth_recipe_users" "saaru" LEFT JOIN "supertokens_emailpassword_users" "seu" @@ -139,8 +138,8 @@ export class EmailVerification { const existingVerification = await this.pool .maybeOne( - sql` - SELECT ${emailVerificationFields(sql`"ev".`)} + psql` + SELECT ${emailVerificationFields(psql`"ev".`)} FROM "email_verifications" "ev" WHERE "ev"."user_identity_id" = ${input.userIdentityId} @@ -168,7 +167,7 @@ export class EmailVerification { const tokenHash = await bcrypt.hash(token, await bcrypt.genSalt()); const newVerification = await this.pool .one( - sql` + psql` INSERT INTO "email_verifications" AS "ev" ( "user_identity_id" , "email" @@ -185,7 +184,7 @@ export class EmailVerification { "token_hash" = EXCLUDED.token_hash , "expires_at" = EXCLUDED.expires_at , "verified_at" = NULL - RETURNING ${emailVerificationFields(sql`"ev".`)} + RETURNING ${emailVerificationFields(psql`"ev".`)} `, ) .then>(v => @@ -212,8 +211,8 @@ export class EmailVerification { }): Promise<{ ok: true; verified: boolean } | { ok: false; message: string }> { const emailVerification = await this.pool .maybeOne( - sql` - SELECT ${emailVerificationFields(sql`"ev".`)} + psql` + SELECT ${emailVerificationFields(psql`"ev".`)} FROM "email_verifications" "ev" WHERE "user_identity_id" = ${input.userIdentityId} @@ -236,7 +235,7 @@ export class EmailVerification { } if (emailVerification.expiresAt.getTime() <= Date.now()) { - await this.pool.query(sql` + await this.pool.query(psql` DELETE FROM "email_verifications" WHERE "id" = ${emailVerification.id} `); @@ -247,7 +246,7 @@ export class EmailVerification { }; } - await this.pool.query(sql` + await this.pool.query(psql` UPDATE "email_verifications" SET "verified_at" = now() diff --git a/packages/services/api/src/modules/auth/providers/supertokens-store.ts b/packages/services/api/src/modules/auth/providers/supertokens-store.ts index f8dd4b9fa..a0587191e 100644 --- a/packages/services/api/src/modules/auth/providers/supertokens-store.ts +++ b/packages/services/api/src/modules/auth/providers/supertokens-store.ts @@ -1,8 +1,6 @@ -import { Inject } from 'graphql-modules'; -import { sql, type DatabasePool } from 'slonik'; import z from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; const SessionInfoModel = z.object({ sessionHandle: z.string(), @@ -54,7 +52,7 @@ const EmailPasswordResetTokenModel = z.object({ export class SuperTokensStore { private logger: Logger; constructor( - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, logger: Logger, ) { this.logger = logger.child({ module: 'SuperTokensStore' }); @@ -63,7 +61,7 @@ export class SuperTokensStore { async getSessionInfo(sessionHandle: string) { this.logger.debug('Lookup session. (sessionHandle=%s)', sessionHandle); - const query = sql` + const query = psql` SELECT "session_handle" AS "sessionHandle" , "user_id" AS "userId" @@ -93,7 +91,7 @@ export class SuperTokensStore { async deleteSession(sessionHandle: string) { this.logger.debug('Delete session. (sessionHandle=%s)', sessionHandle); - const query = sql` + const query = psql` DELETE FROM "supertokens_session_info" WHERE @@ -106,7 +104,7 @@ export class SuperTokensStore { } async findEmailPasswordUserByEmail(email: string) { - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "email" AS "email" @@ -124,7 +122,7 @@ export class SuperTokensStore { } private async lookupEmailUserByUserId(userId: string) { - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "email" AS "email" @@ -142,7 +140,7 @@ export class SuperTokensStore { } public async lookupEmailUserByEmail(email: string) { - const userToTenantQuery = sql` + const userToTenantQuery = psql` SELECT "user_id" AS "userId" FROM @@ -161,7 +159,7 @@ export class SuperTokensStore { return null; } - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "email" AS "email" @@ -179,7 +177,7 @@ export class SuperTokensStore { } private async lookupThirdPartyUserByUserId(userId: string) { - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "email" AS "email" @@ -200,7 +198,7 @@ export class SuperTokensStore { async lookupUserByUserId(userId: string) { this.logger.debug('Lookup user. (userId=%s)', userId); - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "recipe_id" AS "recipeId" @@ -237,7 +235,7 @@ export class SuperTokensStore { refreshTokenHash2: string, expiresAt: number, ) { - const query = sql` + const query = psql` INSERT INTO "supertokens_session_info" ( "app_id" , "tenant_id" @@ -278,7 +276,7 @@ export class SuperTokensStore { lastRefreshTokenHash2: string, newRefreshTokenHash2: string, ) { - const query = sql` + const query = psql` UPDATE "supertokens_session_info" SET @@ -301,7 +299,7 @@ export class SuperTokensStore { } async findThirdPartyUser(args: { thirdPartyId: string; thirdPartyUserId: string }) { - const query = sql` + const query = psql` SELECT "user_id" AS "userId" , "email" AS "email" @@ -327,7 +325,7 @@ export class SuperTokensStore { } async updateOIDCUserEmail(args: { userId: string; newEmail: string }) { - const query = sql` + const query = psql` UPDATE "supertokens_thirdparty_users" SET @@ -354,7 +352,7 @@ export class SuperTokensStore { const userId = crypto.randomUUID(); const now = Date.now(); - const allRecipeUsersQuery = sql` + const allRecipeUsersQuery = psql` INSERT INTO "supertokens_all_auth_recipe_users" ( "app_id" , "tenant_id" @@ -376,7 +374,7 @@ export class SuperTokensStore { ) `; - const oidcUserQuery = sql` + const oidcUserQuery = psql` INSERT INTO "supertokens_thirdparty_users" ( "app_id" , "third_party_id" @@ -400,7 +398,7 @@ export class SuperTokensStore { , "time_joined" AS "timeJoined" `; - const appIdToUserIdQuery = sql` + const appIdToUserIdQuery = psql` INSERT INTO "supertokens_app_id_to_user_id" ( "app_id" , "user_id" @@ -416,7 +414,7 @@ export class SuperTokensStore { ) `; - const thirdpartyUserToTenant = sql` + const thirdpartyUserToTenant = psql` INSERT INTO "supertokens_thirdparty_user_to_tenant" ( "app_id" , "tenant_id" @@ -433,7 +431,7 @@ export class SuperTokensStore { `; return await this.pool - .transaction(async t => { + .transaction('createThirdPartyUser', async t => { await t.query(appIdToUserIdQuery); const result = await t.one(oidcUserQuery); await t.query(allRecipeUsersQuery); @@ -454,7 +452,7 @@ export class SuperTokensStore { async createEmailPasswordUser(args: { email: string; passwordHash: string }) { const userId = crypto.randomUUID(); const now = Date.now(); - const allRecipeUsersQuery = sql` + const allRecipeUsersQuery = psql` INSERT INTO "supertokens_all_auth_recipe_users" ( "app_id" , "tenant_id" @@ -476,7 +474,7 @@ export class SuperTokensStore { ) `; - const emailPasswordUserQuery = sql` + const emailPasswordUserQuery = psql` INSERT INTO "supertokens_emailpassword_users" ( "app_id" , "user_id" @@ -497,7 +495,7 @@ export class SuperTokensStore { , "time_joined" AS "timeJoined" `; - const appIdToUserIdQuery = sql` + const appIdToUserIdQuery = psql` INSERT INTO "supertokens_app_id_to_user_id" ( "app_id" , "user_id" @@ -513,7 +511,7 @@ export class SuperTokensStore { ) `; - const userToTenantQuery = sql` + const userToTenantQuery = psql` INSERT INTO "supertokens_emailpassword_user_to_tenant" ( "app_id" , "tenant_id" @@ -528,7 +526,7 @@ export class SuperTokensStore { `; return await this.pool - .transaction(async t => { + .transaction('createEmailPasswordUser', async t => { await t.query(appIdToUserIdQuery); const result = await t.one(emailPasswordUserQuery); await t.query(allRecipeUsersQuery); @@ -543,7 +541,7 @@ export class SuperTokensStore { token: string; expiresAt: number; }) { - const deletePendingRequestsQuery = sql` + const deletePendingRequestsQuery = psql` DELETE FROM "supertokens_emailpassword_pswd_reset_tokens" WHERE @@ -551,7 +549,7 @@ export class SuperTokensStore { AND "user_id" =${args.user.userId} `; - const query = sql` + const query = psql` INSERT INTO "supertokens_emailpassword_pswd_reset_tokens" ( "app_id" , "user_id" @@ -572,14 +570,14 @@ export class SuperTokensStore { , "token_expiry" AS "tokenExpiry" `; - return await this.pool.transaction(async t => { + return await this.pool.transaction('createEmailPasswordResetToken', async t => { await t.query(deletePendingRequestsQuery); return await t.one(query).then(EmailPasswordResetTokenModel.parse); }); } async updateEmailPasswordBasedOnResetToken(args: { token: string; newPasswordHash: string }) { - const emailPasswordResetTokenQuery = sql` + const emailPasswordResetTokenQuery = psql` DELETE FROM "supertokens_emailpassword_pswd_reset_tokens" @@ -592,7 +590,7 @@ export class SuperTokensStore { , "token_expiry" AS "tokenExpiry" `; - const updatePasswordHash = (userId: string) => sql` + const updatePasswordHash = (userId: string) => psql` UPDATE "supertokens_emailpassword_users" SET "password_hash" = ${args.newPasswordHash} @@ -606,7 +604,7 @@ export class SuperTokensStore { , "time_joined" AS "timeJoined" `; - return await this.pool.transaction(async t => { + return await this.pool.transaction('updateEmailPasswordBasedOnResetToken', async t => { const resetToken = await t .maybeOne(emailPasswordResetTokenQuery) .then(EmailPasswordResetTokenModel.parse); diff --git a/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts b/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts index 1b9bbb9aa..9cd176cba 100644 --- a/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts +++ b/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts @@ -1,12 +1,11 @@ -import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, type DatabasePool } from 'slonik'; +import { Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import type { Target } from '../../../shared/entities'; import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder'; import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { Storage } from '../../shared/providers/storage'; const SourceCodeModel = z.string().max(5_000); @@ -40,13 +39,13 @@ export class PreflightScriptProvider { private session: Session, private idTranslator: IdTranslator, private auditLogs: AuditLogRecorder, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, ) { this.logger = logger.child({ source: 'PreflightScriptProvider' }); } async getPreflightScript(targetId: string) { - const result = await this.pool.maybeOne(sql`/* getPreflightScript */ + const result = await this.pool.maybeOne(psql`/* getPreflightScript */ SELECT "id" , "source_code" AS "sourceCode" @@ -114,7 +113,7 @@ export class PreflightScriptProvider { } const currentUser = await this.session.getViewer(); - const result = await this.pool.maybeOne(sql`/* createPreflightScript */ + const result = await this.pool.maybeOne(psql`/* createPreflightScript */ INSERT INTO "document_preflight_scripts" ( "source_code" , "target_id" diff --git a/packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts b/packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts index a6f05c4d4..2cc74aeef 100644 --- a/packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts +++ b/packages/services/api/src/modules/oidc-integrations/providers/oidc-integration.store.ts @@ -1,9 +1,8 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, type DatabasePool } from 'slonik'; import { z } from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import { sha256 } from '../../auth/lib/supertokens-at-home/crypto'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { REDIS_INSTANCE, type Redis } from '../../shared/providers/redis'; const SharedOIDCIntegrationDomainFieldsModel = z.object({ @@ -29,7 +28,7 @@ const OIDCIntegrationDomainModel = z.union([ export type OIDCIntegrationDomain = z.TypeOf; -const oidcIntegrationDomainsFields = sql` +const oidcIntegrationDomainsFields = psql` "id" , "organization_id" AS "organizationId" , "oidc_integration_id" AS "oidcIntegrationId" @@ -48,7 +47,7 @@ export class OIDCIntegrationStore { private logger: Logger; constructor( - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, @Inject(REDIS_INSTANCE) private redis: Redis, logger: Logger, ) { @@ -61,7 +60,7 @@ export class OIDCIntegrationStore { oidcIntegrationId, ); - const query = sql` + const query = psql` SELECT ${oidcIntegrationDomainsFields} FROM @@ -79,7 +78,7 @@ export class OIDCIntegrationStore { oidcIntegrationId, ); - const query = sql` + const query = psql` INSERT INTO "oidc_integration_domains" ( "organization_id" , "oidc_integration_id" @@ -101,7 +100,7 @@ export class OIDCIntegrationStore { async deleteDomain(domainId: string) { this.logger.debug('delete domain on oidc integration. (oidcIntegrationId=%s)', domainId); - const query = sql` + const query = psql` DELETE FROM "oidc_integration_domains" @@ -113,7 +112,7 @@ export class OIDCIntegrationStore { } async findDomainById(domainId: string) { - const query = sql` + const query = psql` SELECT ${oidcIntegrationDomainsFields} FROM @@ -126,7 +125,7 @@ export class OIDCIntegrationStore { } async findVerifiedDomainByName(domainName: string) { - const query = sql` + const query = psql` SELECT ${oidcIntegrationDomainsFields} FROM @@ -143,7 +142,7 @@ export class OIDCIntegrationStore { oidcIntegrationId: string, domainName: string, ) { - const query = sql` + const query = psql` SELECT ${oidcIntegrationDomainsFields} FROM @@ -165,7 +164,7 @@ export class OIDCIntegrationStore { // The NOT EXISTS statement is to avoid verifying the domain twice for two different otganizations // only one org can own a domain - const query = sql` + const query = psql` UPDATE "oidc_integration_domains" SET diff --git a/packages/services/api/src/modules/organization/providers/organization-access-tokens-cache.ts b/packages/services/api/src/modules/organization/providers/organization-access-tokens-cache.ts index 669b97ede..ecb2f27b3 100644 --- a/packages/services/api/src/modules/organization/providers/organization-access-tokens-cache.ts +++ b/packages/services/api/src/modules/organization/providers/organization-access-tokens-cache.ts @@ -3,11 +3,10 @@ import { memoryDriver } from 'bentocache/build/src/drivers/memory'; import { redisDriver } from 'bentocache/build/src/drivers/redis'; import { Inject, Injectable, Scope } from 'graphql-modules'; import type Redis from 'ioredis'; -import type { DatabasePool } from 'slonik'; import { prometheusPlugin } from '@bentocache/plugin-prometheus'; +import { PostgresDatabasePool } from '@hive/postgres'; import { AuthorizationPolicyStatement } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { PrometheusConfig } from '../../shared/providers/prometheus-config'; import { REDIS_INSTANCE } from '../../shared/providers/redis'; import { @@ -42,7 +41,7 @@ export class OrganizationAccessTokensCache { constructor( @Inject(REDIS_INSTANCE) redis: Redis, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, prometheusConfig: PrometheusConfig, ) { this.cache = new BentoCache({ diff --git a/packages/services/api/src/modules/organization/providers/organization-access-tokens.ts b/packages/services/api/src/modules/organization/providers/organization-access-tokens.ts index 4cfa13703..81367aa8c 100644 --- a/packages/services/api/src/modules/organization/providers/organization-access-tokens.ts +++ b/packages/services/api/src/modules/organization/providers/organization-access-tokens.ts @@ -1,7 +1,7 @@ import { addDays, addMonths, addYears } from 'date-fns'; import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, type CommonQueryMethods, type DatabasePool } from 'slonik'; import { z } from 'zod'; +import { PostgresDatabasePool, psql, type CommonQueryMethods } from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -28,7 +28,6 @@ import { OTEL_TRACING_ENABLED } from '../../operations/providers/traces'; import { SCHEMA_PROPOSALS_ENABLED } from '../../proposals/providers/schema-proposals-enabled-token'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { Storage } from '../../shared/providers/storage'; import * as OrganizationAccessKey from '../lib/organization-access-key'; import * as OrganizationAccessTokensPermissions from '../lib/organization-access-token-permissions'; @@ -173,7 +172,7 @@ export class OrganizationAccessTokens { private findById: ReturnType; constructor( - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, private cache: OrganizationAccessTokensCache, private resourceAssignments: ResourceAssignments, private idTranslator: IdTranslator, @@ -468,7 +467,7 @@ export class OrganizationAccessTokens { : OrganizationAccessKey.AccessTokenCategory.organization, ); - const result = await this.pool.maybeOne(sql` + const result = await this.pool.maybeOne(psql` INSERT INTO "organization_access_tokens" ( "id" , "organization_id" @@ -489,8 +488,8 @@ export class OrganizationAccessTokens { , ${args.userId ?? null} , ${args.title} , ${args.description} - , ${args.permissions !== null ? sql.array(args.permissions, 'text') : null} - , ${sql.jsonb(args.assignedResources)} + , ${args.permissions !== null ? psql.array(args.permissions, 'text') : null} + , ${psql.jsonb(args.assignedResources)} , ${accessKey.hash} , ${accessKey.firstCharacters} , ${args.expiresAt?.toISOString() ?? null} @@ -570,7 +569,7 @@ export class OrganizationAccessTokens { } } - await this.pool.query(sql` + await this.pool.query(psql` DELETE FROM "organization_access_tokens" @@ -625,7 +624,7 @@ export class OrganizationAccessTokens { cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.after); } - const result = await this.pool.any(sql` /* OrganizationAccessTokens.getPaginated */ + const result = await this.pool.any(psql` /* OrganizationAccessTokens.getPaginated */ SELECT ${organizationAccessTokenFields} FROM @@ -634,7 +633,7 @@ export class OrganizationAccessTokens { "organization_id" = ${organization.id} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -643,15 +642,15 @@ export class OrganizationAccessTokens { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ${ args.includeOnlyOrganizationScoped - ? sql` + ? psql` AND "project_id" IS NULL AND "user_id" IS NULL ` - : sql`` + : psql`` } ORDER BY "organization_id" ASC @@ -714,7 +713,7 @@ export class OrganizationAccessTokens { cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.after); } - const result = await this.pool.any(sql` /* OrganizationAccessTokens.getPaginated */ + const result = await this.pool.any(psql` /* OrganizationAccessTokens.getPaginated */ SELECT ${organizationAccessTokenFields} FROM @@ -723,7 +722,7 @@ export class OrganizationAccessTokens { "project_id" = ${project.id} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -732,7 +731,7 @@ export class OrganizationAccessTokens { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "project_id" ASC @@ -791,7 +790,7 @@ export class OrganizationAccessTokens { } const result = await this.pool - .any(sql` /* OrganizationAccessTokens.getPaginatedForMembership */ + .any(psql` /* OrganizationAccessTokens.getPaginatedForMembership */ SELECT ${organizationAccessTokenFields} FROM @@ -800,7 +799,7 @@ export class OrganizationAccessTokens { "user_id" = ${member.userId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -809,13 +808,13 @@ export class OrganizationAccessTokens { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ${ args.includeExpired - ? sql`` - : sql` - AND (expires_at IS NULL OR expires_at > NOW()) + ? psql`` + : psql` + AND (expires_at IS NULL OR expires_at > NOW()) ` } ORDER BY @@ -1288,7 +1287,7 @@ export function findById(deps: { pool: CommonQueryMethods; logger: Logger }) { return null; } - const data = await deps.pool.maybeOne(sql` /* OrganizationAccessTokens.findById */ + const data = await deps.pool.maybeOne(psql` /* OrganizationAccessTokens.findById */ SELECT ${organizationAccessTokenFields} FROM @@ -1297,9 +1296,9 @@ export function findById(deps: { pool: CommonQueryMethods; logger: Logger }) { "id" = ${organizationAccessTokenId} ${ opts.includeExpired - ? sql`` - : sql` - AND (expires_at IS NULL OR expires_at > NOW()) + ? psql`` + : psql` + AND (expires_at IS NULL OR expires_at > NOW()) ` } LIMIT 1 @@ -1325,7 +1324,7 @@ export function findById(deps: { pool: CommonQueryMethods; logger: Logger }) { }; } -const organizationAccessTokenFields = sql` +const organizationAccessTokenFields = psql` "id" , "organization_id" AS "organizationId" , "project_id" AS "projectId" diff --git a/packages/services/api/src/modules/organization/providers/organization-member-roles.ts b/packages/services/api/src/modules/organization/providers/organization-member-roles.ts index c2665a71e..cb40c404a 100644 --- a/packages/services/api/src/modules/organization/providers/organization-member-roles.ts +++ b/packages/services/api/src/modules/organization/providers/organization-member-roles.ts @@ -1,6 +1,6 @@ -import { Inject, Injectable, Scope } from 'graphql-modules'; -import { CommonQueryMethods, sql, type DatabasePool } from 'slonik'; +import { Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; +import { CommonQueryMethods, PostgresDatabasePool, psql } from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -19,7 +19,6 @@ import { TargetAccessScope, } from '../../auth/providers/scopes'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import * as OrganizationMemberPermissions from '../lib/organization-member-permissions'; function omit(obj: T, key: K): Omit { @@ -89,7 +88,7 @@ export class OrganizationMemberRoles { private logger: Logger; constructor( - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, logger: Logger, ) { this.logger = logger.child({ @@ -110,7 +109,7 @@ export class OrganizationMemberRoles { } const limit = args.first ? (args.first > 0 ? Math.min(args.first, 50) : 50) : 50; - const query = sql` + const query = psql` SELECT ${organizationMemberRoleFields} FROM @@ -119,7 +118,7 @@ export class OrganizationMemberRoles { "organization_id" = ${organizationId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -128,7 +127,7 @@ export class OrganizationMemberRoles { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "created_at" DESC @@ -136,7 +135,7 @@ export class OrganizationMemberRoles { LIMIT ${limit + 1} `; - const records = await this.pool.any(query); + const records = await this.pool.any(query); let edges = records.map(row => { const node = MemberRoleModel.parse(row); @@ -171,16 +170,16 @@ export class OrganizationMemberRoles { async findMemberRolesByIds(roleIds: Array): Promise> { this.logger.debug('Find organization membership roles. (roleIds=%o)', roleIds); - const query = sql` + const query = psql` SELECT ${organizationMemberRoleFields} FROM "organization_member_roles" WHERE - "id" = ANY(${sql.array(roleIds, 'uuid')}) + "id" = ANY(${psql.array(roleIds, 'uuid')}) `; - const result = await this.pool.any(query); + const result = await this.pool.any(query); const rowsById = new Map(); @@ -201,7 +200,7 @@ export class OrganizationMemberRoles { organizationId: string, name: string, ): Promise { - const result = await this.pool.maybeOne(sql`/* findViewerRoleForOrganizationId */ + const result = await this.pool.maybeOne(psql`/* findViewerRoleForOrganizationId */ SELECT ${organizationMemberRoleFields} FROM @@ -235,7 +234,7 @@ export class OrganizationMemberRoles { OrganizationMemberPermissions.permissions.assignable.has(permission as Permission), ); const role = await this.pool.one( - sql`/* createOrganizationMemberRole */ + psql`/* createOrganizationMemberRole */ INSERT INTO "organization_member_roles" ( "organization_id" , "name" @@ -248,7 +247,7 @@ export class OrganizationMemberRoles { , ${args.name} , ${args.description} , NULL - , ${sql.array(permissions, 'text')} + , ${psql.array(permissions, 'text')} ) RETURNING ${organizationMemberRoleFields} @@ -270,14 +269,14 @@ export class OrganizationMemberRoles { ); const role = await this.pool.one( - sql`/* updateOrganizationMemberRole */ + psql`/* updateOrganizationMemberRole */ UPDATE "organization_member_roles" SET "name" = ${args.name} , "description" = ${args.description} , "scopes" = NULL - , "permissions" = ${sql.array(permissions, 'text')} + , "permissions" = ${psql.array(permissions, 'text')} WHERE "organization_id" = ${args.organizationId} AND id = ${args.roleId} RETURNING @@ -294,7 +293,7 @@ export class OrganizationMemberRoles { ): Promise { deps.logger.debug('Find organization membership role by id. (roleId=%s)', roleId); - const query = sql` + const query = psql` SELECT ${organizationMemberRoleFields} FROM @@ -303,7 +302,7 @@ export class OrganizationMemberRoles { "id" = ${roleId} `; - const result = await deps.pool.maybeOne(query); + const result = await deps.pool.maybeOne(query); if (result == null) { deps.logger.debug('Organization membership role not found. (roleId=%s)', roleId); @@ -405,7 +404,7 @@ function transformOrganizationMemberLegacyScopesIntoPermissionGroup( ]); } -const organizationMemberRoleFields = sql` +const organizationMemberRoleFields = psql` "organization_member_roles"."id" , "organization_member_roles"."name" , "organization_member_roles"."description" diff --git a/packages/services/api/src/modules/organization/providers/organization-members.ts b/packages/services/api/src/modules/organization/providers/organization-members.ts index e2b9be9c1..cc9687ac8 100644 --- a/packages/services/api/src/modules/organization/providers/organization-members.ts +++ b/packages/services/api/src/modules/organization/providers/organization-members.ts @@ -1,6 +1,6 @@ -import { Inject, Injectable, Scope } from 'graphql-modules'; -import { CommonQueryMethods, sql, type DatabasePool } from 'slonik'; +import { Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; +import { CommonQueryMethods, PostgresDatabasePool, psql } from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -9,7 +9,6 @@ import { type Organization } from '../../../shared/entities'; import { batchBy } from '../../../shared/helpers'; import { AuthorizationPolicyStatement } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { ResourceAssignmentModel, type ResourceAssignmentGroup, @@ -68,7 +67,7 @@ export class OrganizationMembers { private logger: Logger; constructor( - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, private organizationMemberRoles: OrganizationMemberRoles, logger: Logger, ) { @@ -81,17 +80,17 @@ export class OrganizationMembers { organizationId: string, userIds: Array | null = null, ) { - const query = sql` + const query = psql` SELECT - ${organizationMemberFields(sql`"om"`)} + ${organizationMemberFields(psql`"om"`)} FROM "organization_member" AS "om" WHERE "om"."organization_id" = ${organizationId} - ${userIds ? sql`AND "om"."user_id" = ANY(${sql.array(userIds, 'uuid')})` : sql``} + ${userIds ? psql`AND "om"."user_id" = ANY(${psql.array(userIds, 'uuid')})` : psql``} `; - const result = await this.pool.any(query); + const result = await this.pool.any(query); return result.map(row => RawOrganizationMembershipModel.parse(row)); } @@ -160,24 +159,24 @@ export class OrganizationMembers { const searchTerm = args.searchTerm?.trim() ?? ''; const searching = searchTerm.length > 0; - const query = sql` + const query = psql` SELECT - ${organizationMemberFields(sql`"om"`)} + ${organizationMemberFields(psql`"om"`)} FROM "organization_member" AS "om" ${ searching - ? sql` + ? psql` JOIN "users" as "u" ON "om"."user_id" = "u"."id" ` - : sql`` + : psql`` } WHERE "om"."organization_id" = ${organization.id} ${ cursor - ? sql` + ? psql` AND ( "om"."created_at" < ${cursor.createdAt} OR ( @@ -186,9 +185,9 @@ export class OrganizationMembers { ) ) ` - : sql`` + : psql`` } - ${searching ? sql`AND "u"."display_name" || ' ' || "u"."email" ILIKE ${'%' + searchTerm + '%'}` : sql``} + ${searching ? psql`AND "u"."display_name" || ' ' || "u"."email" ILIKE ${'%' + searchTerm + '%'}` : psql``} ORDER BY "om"."organization_id" DESC , "om"."created_at" DESC @@ -196,7 +195,7 @@ export class OrganizationMembers { LIMIT ${first + 1} `; - const result = await this.pool.any(query); + const result = await this.pool.any(query); const hasNextPage = first !== null ? result.length > first : false; const organizationMembers = result @@ -273,9 +272,9 @@ export class OrganizationMembers { organization.id, email, ); - const query = sql` + const query = psql` SELECT - ${organizationMemberFields(sql`"om"`)} + ${organizationMemberFields(psql`"om"`)} FROM "organization_member" AS "om" INNER JOIN "users" AS "u" @@ -286,7 +285,7 @@ export class OrganizationMembers { LIMIT 1 `; - const result = await this.pool.maybeOne(query); + const result = await this.pool.maybeOne(query); if (result === null) { return null; } @@ -303,7 +302,7 @@ export class OrganizationMembers { resourceAssignmentGroup: ResourceAssignmentGroup; }) { await this.pool.query( - sql`/* assignOrganizationMemberRole */ + psql`/* assignOrganizationMemberRole */ UPDATE "organization_member" SET @@ -368,9 +367,9 @@ export class OrganizationMembers { userId, ); - const query = sql` + const query = psql` SELECT - ${organizationMemberFields(sql`"om"`)} + ${organizationMemberFields(psql`"om"`)} FROM "organization_member" AS "om" WHERE @@ -378,7 +377,7 @@ export class OrganizationMembers { AND "om"."user_id" = ${userId} `; - const result = await deps.pool.maybeOne(query); + const result = await deps.pool.maybeOne(query); if (result == null) { deps.logger.debug( @@ -415,7 +414,7 @@ export class OrganizationMembers { } } -const organizationMemberFields = (prefix = sql`"organization_member"`) => sql` +const organizationMemberFields = (prefix = psql`"organization_member"`) => psql` ${prefix}."organization_id" AS "organizationId" , ${prefix}."user_id" AS "userId" , ${prefix}."role_id" AS "roleId" diff --git a/packages/services/api/src/modules/organization/providers/resource-selector.ts b/packages/services/api/src/modules/organization/providers/resource-selector.ts index d4d196e60..33ebe0dac 100644 --- a/packages/services/api/src/modules/organization/providers/resource-selector.ts +++ b/packages/services/api/src/modules/organization/providers/resource-selector.ts @@ -1,5 +1,6 @@ import { Injectable, Scope } from 'graphql-modules'; -import { sql } from 'slonik'; +import z from 'zod'; +import { psql } from '@hive/postgres'; import * as GraphQLSchema from '../../../__generated__/types'; import { Organization, ProjectType } from '../../../shared/entities'; import { AccessError } from '../../../shared/errors'; @@ -164,8 +165,9 @@ export class ResourceSelector { } const latest = await this.storage.getMaybeLatestValidVersion({ targetId: target.targetId }); if (latest) { - return await this.storage.pool.anyFirst( - sql`/* getServicesFromTargetForResourceSelector */ + return await this.storage.pool + .anyFirst( + psql`/* getServicesFromTargetForResourceSelector */ SELECT lower(sl.service_name) as service_name FROM schema_version_to_log AS svl @@ -178,15 +180,17 @@ export class ResourceSelector { ORDER BY sl.created_at DESC `, - ); + ) + .then(z.array(z.string()).parse); } return []; } async getAppDeploymentsFromTargetForResourceSelector(target: TargetForResourceSelector) { - const apps = await this.storage.pool.anyFirst( - sql` + const apps = await this.storage.pool + .anyFirst( + psql` SELECT DISTINCT ON ("name") "name" FROM @@ -195,7 +199,8 @@ export class ResourceSelector { "target_id" = ${target.targetId} AND "retired_at" IS NULL `, - ); + ) + .then(z.array(z.string()).parse); return apps; } diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts index 2916c4948..8faafdb09 100644 --- a/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts +++ b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts @@ -2,8 +2,8 @@ * This wraps the database calls for schema proposals and required validation */ import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, type DatabasePool } from 'slonik'; import { z } from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -12,7 +12,6 @@ import { TaskScheduler } from '@hive/workflows/kit'; import { SchemaProposalCompositionTask } from '@hive/workflows/tasks/schema-proposal-composition'; import { SchemaProposalStage } from '../../../__generated__/types'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { Storage } from '../../shared/providers/storage'; import { SCHEMA_PROPOSALS_ENABLED } from './schema-proposals-enabled-token'; @@ -38,7 +37,7 @@ export class SchemaProposalStorage { constructor( logger: Logger, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, private storage: Storage, @Inject(SCHEMA_PROPOSALS_ENABLED) private schemaProposalsEnabled: boolean, private taskScheduler: TaskScheduler, @@ -118,15 +117,15 @@ export class SchemaProposalStorage { }, }; } - const review = await this.pool.transaction(async conn => { - await conn.maybeOne( - sql` + const review = await this.pool.transaction('manuallyTransitionProposal', async conn => { + await conn.query( + psql` UPDATE "schema_proposals" SET "stage" = ${args.stage} WHERE "id" = ${args.id} AND "stage" <> 'IMPLEMENTED' `, ); - const row = await conn.maybeOne(sql` + const row = await conn.maybeOne(psql` INSERT INTO schema_proposal_reviews ("schema_proposal_id", "stage_transition", "author", "service_name") VALUES ( @@ -182,8 +181,8 @@ export class SchemaProposalStorage { }; } const proposal = await this.pool - .maybeOne( - sql` + .maybeOne( + psql` INSERT INTO "schema_proposals" as "sp" ("target_id", "title", "description", "stage", "author") VALUES @@ -212,8 +211,8 @@ export class SchemaProposalStorage { async getProposalTargetId(args: { id: string }) { this.logger.debug('Get proposal target ID (proposal=%s)', args.id); const result = await this.pool - .maybeOne( - sql` + .maybeOne( + psql` SELECT id , target_id as "targetId" @@ -231,8 +230,8 @@ export class SchemaProposalStorage { async getProposal(args: { id: string }) { this.logger.debug('Get proposal (proposal=%s)', args.id); const result = await this.pool - .maybeOne( - sql` + .maybeOne( + psql` SELECT ${schemaProposalFields} FROM @@ -267,7 +266,7 @@ export class SchemaProposalStorage { cursor, limit, ); - const result = await this.pool.query(sql` + const result = await this.pool.any(psql` SELECT ${schemaProposalFields} FROM @@ -276,7 +275,7 @@ export class SchemaProposalStorage { sp."target_id" = ${args.targetId} ${ cursor - ? sql` + ? psql` AND ( ( sp."created_at" = ${cursor.createdAt} @@ -285,22 +284,22 @@ export class SchemaProposalStorage { OR sp."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ${ args.stages.length > 0 - ? sql` + ? psql` AND ( - sp."stage" = ANY(${sql.array(args.stages, 'schema_proposal_stage')}) + sp."stage" = ANY(${psql.array(args.stages, 'schema_proposal_stage')}) ) ` - : sql`` + : psql`` } ORDER BY sp."created_at" DESC, sp."id" LIMIT ${limit + 1} `); - let items = result.rows.map(row => { + let items = result.map(row => { const node = SchemaProposalModel.parse(row); return { @@ -334,7 +333,7 @@ export class SchemaProposalStorage { cursor, limit, ); - const result = await this.pool.query(sql` + const result = await this.pool.any(psql` SELECT ${schemaProposalReviewFields} FROM @@ -343,7 +342,7 @@ export class SchemaProposalStorage { "schema_proposal_id" = ${args.proposalId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -352,13 +351,13 @@ export class SchemaProposalStorage { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "created_at" DESC, "id" LIMIT ${limit + 1} `); - let items = result.rows.map(row => { + let items = result.map(row => { const node = SchemaProposalReviewModel.parse(row); return { @@ -382,7 +381,7 @@ export class SchemaProposalStorage { } } -const schemaProposalFields = sql` +const schemaProposalFields = psql` sp."id" , to_json(sp."created_at") as "createdAt" , to_json(sp."updated_at") as "updatedAt" @@ -396,7 +395,7 @@ const schemaProposalFields = sql` , sp."composition_status_reason" as "compositionStatusReason" `; -const schemaProposalReviewFields = sql` +const schemaProposalReviewFields = psql` "id" , "schema_proposal_id" , to_json("created_at") as "createdAt" diff --git a/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts b/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts index a219c71a0..01b10eec8 100644 --- a/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts +++ b/packages/services/api/src/modules/proposals/resolvers/SchemaProposal.ts @@ -43,20 +43,22 @@ export const SchemaProposal: SchemaProposalResolvers = { (!node.serviceName && s.kind === 'single') || (s.kind === 'composite' && s.service_name === node.serviceName), ); + return { kind: schema?.kind ?? 'composite', + type: schema?.type ?? 'FEDERATION', action: 'PUSH', // no idea why this is required for `__isTypeOf` in CompositeSchema. sdl: node.schemaSDL ?? '', // @todo patch schema changes onto latest id: node.id, service_name: node.serviceName ?? '', target: node.targetId ?? schema?.target, service_url: - node.serviceUrl ?? (schema?.kind === 'composite' ? schema.service_url : null) ?? null, + node.serviceUrl ?? (schema?.kind === 'composite' ? schema.service_url : '') ?? '', author: node.meta?.author ?? '', date: new Date(node.createdAt).getTime(), commit: node.meta?.commit ?? node.id, metadata: node.meta ? JSON.stringify(node.meta) : null, - }; + } as Schema; }); } } diff --git a/packages/services/api/src/modules/saved-filters/providers/saved-filters-storage.ts b/packages/services/api/src/modules/saved-filters/providers/saved-filters-storage.ts index 4363c4d57..9cbc0c219 100644 --- a/packages/services/api/src/modules/saved-filters/providers/saved-filters-storage.ts +++ b/packages/services/api/src/modules/saved-filters/providers/saved-filters-storage.ts @@ -1,6 +1,6 @@ -import { Inject, Injectable, Scope } from 'graphql-modules'; -import { sql, type DatabasePool } from 'slonik'; +import { Injectable, Scope } from 'graphql-modules'; import * as zod from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -10,7 +10,6 @@ import type { SavedFilter, SavedFilterVisibility, } from '../../../shared/entities'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; const SavedFilterModel = zod.object({ id: zod.string(), @@ -46,10 +45,10 @@ const SavedFilterModel = zod.object({ scope: Scope.Operation, }) export class SavedFiltersStorage { - constructor(@Inject(PG_POOL_CONFIG) private pool: DatabasePool) {} + constructor(private pool: PostgresDatabasePool) {} async getSavedFilter(args: { id: string }): Promise { - const result = await this.pool.maybeOne(sql`/* getSavedFilter */ + const result = await this.pool.maybeOne(psql`/* getSavedFilter */ SELECT "id" , "project_id" as "projectId" @@ -99,15 +98,15 @@ export class SavedFiltersStorage { // that allows better index usage than OR conditions const visibilityCondition = args.visibility === 'shared' - ? sql`"visibility" = 'shared'` + ? psql`"visibility" = 'shared'` : args.visibility === 'private' - ? sql`"visibility" = 'private' AND "created_by_user_id" = ${args.userId}` - : sql`( + ? psql`"visibility" = 'private' AND "created_by_user_id" = ${args.userId}` + : psql`( "visibility" = 'shared' OR ("visibility" = 'private' AND "created_by_user_id" = ${args.userId}) )`; - const result = await this.pool.any(sql`/* getPaginatedSavedFiltersForProject */ + const result = await this.pool.any(psql`/* getPaginatedSavedFiltersForProject */ SELECT "id" , "project_id" as "projectId" @@ -127,17 +126,17 @@ export class SavedFiltersStorage { AND ${visibilityCondition} ${ args.search - ? sql` + ? psql` AND ( "name" ILIKE ${'%' + args.search + '%'} OR "description" ILIKE ${'%' + args.search + '%'} ) ` - : sql`` + : psql`` } ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -146,7 +145,7 @@ export class SavedFiltersStorage { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "project_id" ASC @@ -193,7 +192,7 @@ export class SavedFiltersStorage { filters: InsightsFilterData; visibility: SavedFilterVisibility; }): Promise { - const result = await this.pool.one(sql`/* createSavedFilter */ + const result = await this.pool.one(psql`/* createSavedFilter */ INSERT INTO "saved_filters" ( "project_id" , "created_by_user_id" @@ -235,7 +234,7 @@ export class SavedFiltersStorage { filters: InsightsFilterData | null; visibility: SavedFilterVisibility | null; }): Promise { - const result = await this.pool.maybeOne(sql`/* updateSavedFilter */ + const result = await this.pool.maybeOne(psql`/* updateSavedFilter */ UPDATE "saved_filters" SET @@ -269,7 +268,7 @@ export class SavedFiltersStorage { } async deleteSavedFilter(args: { id: string }): Promise { - const result = await this.pool.maybeOneFirst(sql`/* deleteSavedFilter */ + const result = await this.pool.maybeOneFirst(psql`/* deleteSavedFilter */ DELETE FROM "saved_filters" @@ -287,7 +286,7 @@ export class SavedFiltersStorage { } async incrementSavedFilterViews(args: { id: string }): Promise { - await this.pool.query(sql`/* incrementSavedFilterViews */ + await this.pool.query(psql`/* incrementSavedFilterViews */ UPDATE "saved_filters" SET diff --git a/packages/services/api/src/modules/schema/providers/contracts.ts b/packages/services/api/src/modules/schema/providers/contracts.ts index f26e99ba9..9d2fcc52f 100644 --- a/packages/services/api/src/modules/schema/providers/contracts.ts +++ b/packages/services/api/src/modules/schema/providers/contracts.ts @@ -1,11 +1,11 @@ -import { Inject, Injectable, Scope } from 'graphql-modules'; -import { - sql, - UniqueIntegrityConstraintViolationError, - type DatabasePool, - type PrimitiveValueExpression, -} from 'slonik'; +import { Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; +import { + PostgresDatabasePool, + psql, + UniqueIntegrityConstraintViolationError, + type PrimitiveValueExpression, +} from '@hive/postgres'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -17,7 +17,6 @@ import { } from '@hive/storage'; import { isUUID } from '../../../shared/is-uuid'; import { Logger } from '../../shared/providers/logger'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { ArtifactStorageWriter } from './artifact-storage-writer'; @Injectable({ @@ -28,7 +27,7 @@ export class Contracts { private logger: Logger; constructor( logger: Logger, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, private artifactStorageWriter: ArtifactStorageWriter, ) { this.logger = logger.child({ source: 'Contracts' }); @@ -63,7 +62,7 @@ export class Contracts { let result: unknown; try { - result = await this.pool.maybeOne(sql` + result = await this.pool.maybeOne(psql` INSERT INTO "contracts" ( "target_id" , "contract_name" @@ -121,7 +120,7 @@ export class Contracts { return null; } - const record = await this.pool.maybeOne(sql` + const record = await this.pool.maybeOne(psql` SELECT ${contractFields} FROM @@ -148,7 +147,7 @@ export class Contracts { }; } - const record = await this.pool.maybeOne(sql` + const record = await this.pool.maybeOne(psql` UPDATE "contracts" SET @@ -200,7 +199,7 @@ export class Contracts { targetId: string; }): Promise> { this.logger.debug('Load active contracts for target. (targetId=%s)', args.targetId); - const result = await this.pool.any(sql` + const result = await this.pool.any(psql` SELECT ${contractFields} FROM @@ -237,13 +236,13 @@ export class Contracts { args.contractIds.join(','), ); - const result = await this.pool.any(sql` + const result = await this.pool.any(psql` SELECT DISTINCT ON ("contract_id") ${contractVersionsFields} FROM "contract_versions" WHERE - "contract_id" = ANY(${sql.array(args.contractIds, 'uuid')}) + "contract_id" = ANY(${psql.array(args.contractIds, 'uuid')}) AND "schema_composition_errors" IS NULL ORDER BY "contract_id" ASC @@ -307,17 +306,17 @@ export class Contracts { cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const result = await this.pool.any(sql` + const result = await this.pool.any(psql` SELECT ${contractFields} FROM "contracts" WHERE "target_id" = ${args.targetId} - ${args.onlyActive ? sql`AND "is_disabled" = false` : sql``} + ${args.onlyActive ? psql`AND "is_disabled" = false` : psql``} ${ cursor - ? sql` + ? psql` AND ( ( c."created_at" = ${cursor.createdAt} @@ -326,7 +325,7 @@ export class Contracts { OR c."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "target_id" ASC, @@ -374,7 +373,7 @@ export class Contracts { args.schemaCheckId, ); - const result = await this.pool.any(sql` + const result = await this.pool.any(psql` SELECT "contract_checks"."id" , "contract_checks"."schema_check_id" as "schemaCheckId" @@ -399,14 +398,14 @@ export class Contracts { "contract_checks"."schema_check_id" = ${args.schemaCheckId} ${ args.onlyFailedWithBreakingChanges - ? sql` + ? psql` AND ( "contract_checks"."is_success" = FALSE AND "contract_checks"."schema_composition_errors" IS NULL AND "contract_checks"."breaking_schema_changes" IS NOT NULL )` - : sql`` + : psql`` } ORDER BY "contract_checks"."schema_check_id" ASC @@ -462,7 +461,7 @@ export class Contracts { > = []; for (const contractCheck of contractChecks) { - await this.pool.maybeOne(sql` + await this.pool.maybeOne(psql` UPDATE "contract_checks" SET @@ -471,7 +470,7 @@ export class Contracts { SELECT json_agg( CASE WHEN (COALESCE(jsonb_typeof("change"->'approvalMetadata'), 'null') = 'null' AND "change"->>'isSafeBasedOnUsage' = 'false') - THEN jsonb_set("change", '{approvalMetadata}', ${sql.jsonb( + THEN jsonb_set("change", '{approvalMetadata}', ${psql.jsonb( args.approvalMetadata, )}) ELSE "change" @@ -516,14 +515,14 @@ export class Contracts { args.contextId, ); // Try to approve and claim all the breaking schema changes for this context - await this.pool.query(sql` + await this.pool.query(psql` INSERT INTO "contract_schema_change_approvals" ( "contract_id" , "context_id" , "schema_change_id" , "schema_change" ) - SELECT * FROM ${sql.unnest(breakingChangeApprovalInserts, [ + SELECT * FROM ${psql.unnest(breakingChangeApprovalInserts, [ 'uuid', 'text', 'text', @@ -540,14 +539,14 @@ export class Contracts { contractIds: Array; contextId: string; }) { - const records = await this.pool.any(sql` + const records = await this.pool.any(psql` SELECT "contract_id" as "contractId", "schema_change" as "schemaChange" FROM "contract_schema_change_approvals" WHERE - "contract_id" = ANY(${sql.array(args.contractIds, 'uuid')}) + "contract_id" = ANY(${psql.array(args.contractIds, 'uuid')}) AND "context_id" = ${args.contextId} `); @@ -611,7 +610,7 @@ export class Contracts { args.contractVersionId, ); - const result = await this.pool.maybeOne(sql` + const result = await this.pool.maybeOne(psql` SELECT ${contractVersionsFields} FROM @@ -633,7 +632,7 @@ export class Contracts { public async getPreviousContractVersionForContractVersion(args: { contractVersion: ContractVersion; }) { - const result = await this.pool.maybeOne(sql` + const result = await this.pool.maybeOne(psql` SELECT ${contractVersionsFields} FROM @@ -663,7 +662,7 @@ export class Contracts { public async getDiffableContractVersionForContractVersion(args: { contractVersion: ContractVersion; }) { - const result = await this.pool.maybeOne(sql` + const result = await this.pool.maybeOne(psql` SELECT ${contractVersionsFields} FROM @@ -697,7 +696,7 @@ export class Contracts { args.schemaVersionId, ); - const result = await this.pool.any(sql` + const result = await this.pool.any(psql` SELECT ${contractVersionsFields} FROM @@ -749,7 +748,7 @@ export class Contracts { } public async getBreakingChangesForContractVersion(args: { contractVersionId: string }) { - const changes = await this.pool.query(sql` + const changes = await this.pool.any(psql` SELECT "change_type" as "type", "meta", @@ -761,15 +760,15 @@ export class Contracts { AND "severity_level" = 'BREAKING' `); - if (changes.rows.length === 0) { + if (changes.length === 0) { return null; } - return changes.rows.map(row => HiveSchemaChangeModel.parse(row)); + return changes.map(row => HiveSchemaChangeModel.parse(row)); } public async getSafeChangesForContractVersion(args: { contractVersionId: string }) { - const changes = await this.pool.query(sql` + const changes = await this.pool.any(psql` SELECT "change_type" as "type", "meta", @@ -781,15 +780,15 @@ export class Contracts { AND "severity_level" <> 'BREAKING' `); - if (changes.rows.length === 0) { + if (changes.length === 0) { return null; } - return changes.rows.map(row => HiveSchemaChangeModel.parse(row)); + return changes.map(row => HiveSchemaChangeModel.parse(row)); } public async getAllChangesForContractVersion(args: { contractVersionId: string }) { - const changes = await this.pool.query(sql` + const changes = await this.pool.any(psql` SELECT "change_type" as "type", "meta", @@ -800,11 +799,11 @@ export class Contracts { "contract_version_id" = ${args.contractVersionId} `); - if (changes.rows.length === 0) { + if (changes.length === 0) { return null; } - return changes.rows.map(row => HiveSchemaChangeModel.parse(row)); + return changes.map(row => HiveSchemaChangeModel.parse(row)); } } @@ -813,10 +812,10 @@ function toNullableTextArray(value: T[] | nu return null; } - return sql.array(value, 'text'); + return psql.array(value, 'text'); } -const contractFields = sql` +const contractFields = psql` "id" , "target_id" as "targetId" , "contract_name" as "contractName" @@ -888,7 +887,7 @@ function hasIntersection(a: Set, b: Set): boolean { return false; } -const contractVersionsFields = sql` +const contractVersionsFields = psql` "id" , "schema_version_id" as "schemaVersionId" , "contract_id" as "contractId" diff --git a/packages/services/api/src/modules/schema/providers/schema-helper.ts b/packages/services/api/src/modules/schema/providers/schema-helper.ts index b028a4f42..26a04f310 100644 --- a/packages/services/api/src/modules/schema/providers/schema-helper.ts +++ b/packages/services/api/src/modules/schema/providers/schema-helper.ts @@ -35,7 +35,6 @@ export function ensureSingleSchema(schema: Schema | Schema[]): SingleSchema { if (schema.length > 1) { throw new Error(`Expected a single schema, got ${schema.length}`); } - return ensureSingleSchema(schema[0]); } diff --git a/packages/services/api/src/modules/schema/providers/schema-manager.ts b/packages/services/api/src/modules/schema/providers/schema-manager.ts index 5895dc957..06c1cb04b 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -203,7 +203,7 @@ export class SchemaManager { input.services.map(service => ({ serviceName: service.name, sdl: service.sdl, - serviceUrl: service.url ?? null, + serviceUrl: service.url ?? '', })), ) .map(service => this.schemaHelper.createSchemaObject(service)); diff --git a/packages/services/api/src/modules/shared/providers/pg-pool.ts b/packages/services/api/src/modules/shared/providers/pg-pool.ts deleted file mode 100644 index 8960bb464..000000000 --- a/packages/services/api/src/modules/shared/providers/pg-pool.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InjectionToken } from 'graphql-modules'; -import type { DatabasePool } from 'slonik'; - -export const PG_POOL_CONFIG = new InjectionToken('PG_POOL'); diff --git a/packages/services/api/src/modules/shared/providers/storage.ts b/packages/services/api/src/modules/shared/providers/storage.ts index 0f67d7fd1..9382a81e7 100644 --- a/packages/services/api/src/modules/shared/providers/storage.ts +++ b/packages/services/api/src/modules/shared/providers/storage.ts @@ -1,6 +1,6 @@ import { Injectable } from 'graphql-modules'; -import type { DatabasePool } from 'slonik'; import type { PolicyConfigurationObject } from '@hive/policy'; +import { PostgresDatabasePool } from '@hive/postgres'; import type { ConditionalBreakingChangeMetadata, PaginatedOrganizationInvitationConnection, @@ -67,7 +67,7 @@ type CreateContractVersionInput = { // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface Storage { - pool: DatabasePool; + pool: PostgresDatabasePool; destroy(): Promise; isReady(): Promise; ensureUserExists(_: { diff --git a/packages/services/api/src/modules/target/providers/targets-by-id-cache.ts b/packages/services/api/src/modules/target/providers/targets-by-id-cache.ts index 2602097af..9d9a6b271 100644 --- a/packages/services/api/src/modules/target/providers/targets-by-id-cache.ts +++ b/packages/services/api/src/modules/target/providers/targets-by-id-cache.ts @@ -3,12 +3,11 @@ import { memoryDriver } from 'bentocache/build/src/drivers/memory'; import { redisDriver } from 'bentocache/build/src/drivers/redis'; import { Inject, Injectable, Scope } from 'graphql-modules'; import type Redis from 'ioredis'; -import type { DatabasePool } from 'slonik'; import { prometheusPlugin } from '@bentocache/plugin-prometheus'; +import { PostgresDatabasePool } from '@hive/postgres'; import { findTargetById } from '@hive/storage'; import type { Target } from '../../../shared/entities'; import { isUUID } from '../../../shared/is-uuid'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { PrometheusConfig } from '../../shared/providers/prometheus-config'; import { REDIS_INSTANCE } from '../../shared/providers/redis'; @@ -24,7 +23,7 @@ export class TargetsByIdCache { constructor( @Inject(REDIS_INSTANCE) redis: Redis, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, prometheusConfig: PrometheusConfig, ) { this.cache = new BentoCache({ diff --git a/packages/services/api/src/modules/target/providers/targets-by-slug-cache.ts b/packages/services/api/src/modules/target/providers/targets-by-slug-cache.ts index 5e8215c9e..757816fa9 100644 --- a/packages/services/api/src/modules/target/providers/targets-by-slug-cache.ts +++ b/packages/services/api/src/modules/target/providers/targets-by-slug-cache.ts @@ -3,10 +3,9 @@ import { memoryDriver } from 'bentocache/build/src/drivers/memory'; import { redisDriver } from 'bentocache/build/src/drivers/redis'; import { Inject, Injectable, Scope } from 'graphql-modules'; import type Redis from 'ioredis'; -import type { DatabasePool } from 'slonik'; import { prometheusPlugin } from '@bentocache/plugin-prometheus'; +import { PostgresDatabasePool } from '@hive/postgres'; import { findTargetBySlug } from '@hive/storage'; -import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { PrometheusConfig } from '../../shared/providers/prometheus-config'; import { REDIS_INSTANCE } from '../../shared/providers/redis'; @@ -22,7 +21,7 @@ export class TargetsBySlugCache { constructor( @Inject(REDIS_INSTANCE) redis: Redis, - @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + private pool: PostgresDatabasePool, prometheusConfig: PrometheusConfig, ) { this.cache = new BentoCache({ diff --git a/packages/services/api/src/shared/entities.ts b/packages/services/api/src/shared/entities.ts index 6df4ec141..3b1de8439 100644 --- a/packages/services/api/src/shared/entities.ts +++ b/packages/services/api/src/shared/entities.ts @@ -3,7 +3,12 @@ import { DocumentNode, GraphQLError, parse, print, SourceLocation } from 'graphq import { z } from 'zod'; import type { AvailableRulesResponse, PolicyConfigurationObject } from '@hive/policy'; import type { CompositionFailureError } from '@hive/schema'; -import type { schema_policy_resource } from '@hive/storage'; +import type { + CompositeDeletedSchemaLog, + CompositePushSchemaLog, + schema_policy_resource, + SinglePushSchemaLog, +} from '@hive/storage'; import type { AlertChannelType, AlertType, @@ -22,49 +27,9 @@ export const NameModel = z `Name restricted to alphanumerical characters, spaces and . , _ - / &`, ); -export const SingleSchemaModel = z - .object({ - kind: z.literal('single'), - id: z.string(), - author: z.string(), - date: z.number(), - commit: z.string(), - target: z.string(), - sdl: z.string(), - metadata: z.string().nullish(), - }) - .required(); - -export const DeletedCompositeSchemaModel = z - .object({ - kind: z.literal('composite'), - id: z.string(), - date: z.number(), - target: z.string(), - service_name: z.string(), - action: z.literal('DELETE'), - }) - .required(); - -export const PushedCompositeSchemaModel = z - .object({ - kind: z.literal('composite'), - id: z.string(), - author: z.string(), - date: z.number(), - commit: z.string(), - target: z.string(), - sdl: z.string(), - service_name: z.string(), - service_url: z.string().nullable(), - action: z.literal('PUSH'), - metadata: z.string().nullish(), - }) - .required(); - -export type SingleSchema = z.infer; -export type DeletedCompositeSchema = z.infer; -export type PushedCompositeSchema = z.infer; +export type SingleSchema = SinglePushSchemaLog; +export type DeletedCompositeSchema = CompositeDeletedSchemaLog; +export type PushedCompositeSchema = CompositePushSchemaLog; export type CompositeSchema = PushedCompositeSchema; export type Schema = SingleSchema | CompositeSchema; diff --git a/packages/services/commerce/package.json b/packages/services/commerce/package.json index e6220e9e8..0a2a5f7da 100644 --- a/packages/services/commerce/package.json +++ b/packages/services/commerce/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@hive/api": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", "@hive/workflows": "workspace:*", diff --git a/packages/services/commerce/src/index.ts b/packages/services/commerce/src/index.ts index 4c585a612..a951f45ed 100644 --- a/packages/services/commerce/src/index.ts +++ b/packages/services/commerce/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import 'reflect-metadata'; +import { createConnectionString } from '@hive/postgres'; import { configureTracing, createServer, @@ -10,7 +11,7 @@ import { startMetrics, TracingInstance, } from '@hive/service-common'; -import { createConnectionString, createStorage as createPostgreSQLStorage } from '@hive/storage'; +import { createStorage } from '@hive/storage'; import { TaskScheduler } from '@hive/workflows/kit'; import * as Sentry from '@sentry/node'; import { commerceRouter } from './api'; @@ -52,13 +53,13 @@ async function main() { }); try { - const postgres = await createPostgreSQLStorage( + const storage = await createStorage( createConnectionString(env.postgres), 5, tracing ? [tracing.instrumentSlonik()] : undefined, ); - const taskScheduler = new TaskScheduler(postgres.pool.pool); + const taskScheduler = new TaskScheduler(storage.pool.getRawPgPool()); const usageEstimator = createEstimator({ logger: server.log, @@ -78,7 +79,7 @@ async function main() { }, usageEstimator, taskScheduler, - storage: postgres, + storage, }); const stripeBilling = createStripeBilling({ @@ -88,7 +89,7 @@ async function main() { syncIntervalMs: env.stripe.syncIntervalMs, }, usageEstimator, - storage: postgres, + storage, }); registerShutdown({ @@ -96,7 +97,7 @@ async function main() { async onShutdown() { await server.close(); await Promise.all([usageEstimator.stop(), rateLimiter.stop(), stripeBilling.stop()]); - await postgres.destroy(); + await storage.destroy(); }, }); @@ -127,7 +128,7 @@ async function main() { const readinessChecks = await Promise.all([ usageEstimator.readiness(), rateLimiter.readiness(), - postgres.isReady(), + storage.isReady(), ]); const isReady = readinessChecks.every(val => val === true); reportReadiness(isReady); diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 15e0e1198..dd71ca11b 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -31,6 +31,7 @@ "@graphql-yoga/redis-event-target": "3.0.3", "@hive/api": "workspace:*", "@hive/cdn-script": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/pubsub": "workspace:*", "@hive/schema": "workspace:*", "@hive/service-common": "workspace:*", diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index d8bd27973..97d7f7595 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -26,6 +26,7 @@ import { ArtifactStorageReader } from '@hive/cdn-script/artifact-storage-reader' import { AwsClient } from '@hive/cdn-script/aws'; import { createIsAppDeploymentActive } from '@hive/cdn-script/is-app-deployment-active'; import { createIsKeyValid } from '@hive/cdn-script/key-validation'; +import { createConnectionString } from '@hive/postgres'; import { createHivePubSub } from '@hive/pubsub'; import { configureTracing, @@ -37,7 +38,7 @@ import { startMetrics, TracingInstance, } from '@hive/service-common'; -import { createConnectionString, createStorage as createPostgreSQLStorage } from '@hive/storage'; +import { createStorage as createPostgreSQLStorage } from '@hive/storage'; import { TaskScheduler } from '@hive/workflows/kit'; import { captureException, SeverityLevel } from '@sentry/node'; import { createServerAdapter } from '@whatwg-node/server'; @@ -168,7 +169,7 @@ export async function main() { 10, tracing ? [tracing.instrumentSlonik()] : [], ); - const taskScheduler = new TaskScheduler(storage.pool.pool); + const taskScheduler = new TaskScheduler(storage.pool.getRawPgPool()); const redis = createRedisClient('Redis', env.redis, server.log.child({ source: 'Redis' })); diff --git a/packages/services/service-common/package.json b/packages/services/service-common/package.json index f624c73d8..4245000b7 100644 --- a/packages/services/service-common/package.json +++ b/packages/services/service-common/package.json @@ -34,7 +34,6 @@ "opentelemetry-instrumentation-fetch-node": "1.2.3", "p-retry": "6.2.1", "prom-client": "15.1.3", - "slonik": "30.4.4", "zod": "3.25.76" } } diff --git a/packages/services/service-common/src/tracing.ts b/packages/services/service-common/src/tracing.ts index 7f651ecbd..e7d970719 100644 --- a/packages/services/service-common/src/tracing.ts +++ b/packages/services/service-common/src/tracing.ts @@ -3,13 +3,10 @@ import { FetchInstrumentation, type FetchInstrumentationConfig, } from 'opentelemetry-instrumentation-fetch-node'; -import type { Interceptor, Query, QueryContext } from 'slonik'; import zod from 'zod'; -import { - HiveTracingSpanProcessor, - HiveTracingSpanProcessorOptions, - openTelemetrySetup, -} from '@graphql-hive/plugin-opentelemetry/setup'; +import { type HiveTracingSpanProcessorOptions } from '@graphql-hive/plugin-opentelemetry/setup'; +import * as hiveOtel from '@graphql-hive/plugin-opentelemetry/setup'; +import type { Interceptor, Query, QueryContext } from '@hive/postgres'; import { Attributes, AttributeValue, @@ -41,6 +38,8 @@ import { } from '@opentelemetry/semantic-conventions'; import openTelemetryPlugin, { OpenTelemetryPluginOptions } from './fastify-tracing'; +const { HiveTracingSpanProcessor, openTelemetrySetup } = hiveOtel; + export { trace, context, Span, SpanKind, SamplingDecision, SpanStatusCode }; export class TracingInstance { diff --git a/packages/services/storage/package.json b/packages/services/storage/package.json index 2a8d84859..acb48c5b4 100644 --- a/packages/services/storage/package.json +++ b/packages/services/storage/package.json @@ -8,8 +8,7 @@ }, "main": "./src/index.ts", "exports": { - ".": "./src/index.ts", - "./db/pool": "./src/db/pool.ts" + ".": "./src/index.ts" }, "scripts": { "build": "tsx ../../../scripts/runify.ts", @@ -17,6 +16,7 @@ }, "devDependencies": { "@graphql-inspector/core": "7.1.2", + "@hive/postgres": "workspace:*", "@hive/service-common": "workspace:*", "@sentry/node": "7.120.2", "@sentry/types": "7.120.2", @@ -28,8 +28,6 @@ "got": "14.4.7", "graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "pg-promise": "11.10.2", - "slonik": "30.4.4", - "slonik-interceptor-query-logging": "46.4.0", "slonik-utilities": "1.9.4", "tslib": "2.8.1", "typescript": "5.7.3", diff --git a/packages/services/storage/src/db/index.ts b/packages/services/storage/src/db/index.ts index d727cf34a..fcb073fef 100644 --- a/packages/services/storage/src/db/index.ts +++ b/packages/services/storage/src/db/index.ts @@ -1,3 +1 @@ export * from './types'; -export * from './pool'; -export * from './utils'; diff --git a/packages/services/storage/src/db/pool.ts b/packages/services/storage/src/db/pool.ts deleted file mode 100644 index e3bda938a..000000000 --- a/packages/services/storage/src/db/pool.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - CommonQueryMethods, - createPool, - Interceptor, - QueryResultRow, - QueryResultRowColumn, - TaggedTemplateLiteralInvocation, -} from 'slonik'; -import { createQueryLoggingInterceptor } from 'slonik-interceptor-query-logging'; - -const dbInterceptors: Interceptor[] = [createQueryLoggingInterceptor()]; - -export async function getPool( - connection: string, - maximumPoolSize: number, - additionalInterceptors: Interceptor[] = [], -) { - const pool = await createPool(connection, { - interceptors: [...dbInterceptors, ...additionalInterceptors], - captureStackTrace: false, - maximumPoolSize, - idleTimeout: 30000, - }); - - function interceptError>( - methodName: K, - ) { - const original: CommonQueryMethods[K] = pool[methodName]; - - function interceptor( - this: any, - sql: TaggedTemplateLiteralInvocation, - values?: QueryResultRowColumn[], - ): any { - return (original as any).call(this, sql, values).catch((error: any) => { - error.sql = sql.sql; - error.values = sql.values || values; - - return Promise.reject(error); - }); - } - - pool[methodName] = interceptor; - } - - interceptError('one'); - interceptError('many'); - - return pool; -} diff --git a/packages/services/storage/src/db/utils.ts b/packages/services/storage/src/db/utils.ts deleted file mode 100644 index bbda95d2f..000000000 --- a/packages/services/storage/src/db/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { sql } from 'slonik'; - -export function createConnectionString(config: { - host: string; - port: number; - password: string | undefined; - user: string; - db: string; - ssl: boolean; -}) { - // prettier-ignore - const encodedUser = encodeURIComponent(config.user); - const encodedPassword = - typeof config.password === 'string' ? `:${encodeURIComponent(config.password)}` : ''; - const encodedHost = encodeURIComponent(config.host); - const encodedDb = encodeURIComponent(config.db); - - return `postgres://${encodedUser}${encodedPassword}@${encodedHost}:${config.port}/${encodedDb}${config.ssl ? '?sslmode=require' : '?sslmode=disable'}`; -} - -export function toDate(date: Date) { - return sql`to_timestamp(${date.getTime() / 1000})`; -} diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index 0e80076a2..5055cde3f 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -1,56 +1,27 @@ -import { - DatabasePool, - DatabaseTransactionConnection, - Interceptor, - SerializableValue, - sql, - UniqueIntegrityConstraintViolationError, -} from 'slonik'; import { update } from 'slonik-utilities'; -import { TaggedTemplateLiteralInvocation, TransactionFunction } from 'slonik/dist/src/types'; -import zod from 'zod'; +import { z } from 'zod'; import type { - Alert, - AlertChannel, Member, Organization, - OrganizationBilling, OrganizationInvitation, Project, - Schema, Storage, Target, - TargetSettings, } from '@hive/api'; -import { context, SpanKind, SpanStatusCode, trace } from '@hive/service-common'; +import { + CommonQueryMethods, + createPostgresDatabasePool, + Interceptor, + PostgresDatabasePool, + psql, + SerializableValue, + TaggedTemplateLiteralInvocation, + UniqueIntegrityConstraintViolationError, +} from '@hive/postgres'; import type { SchemaCoordinatesDiffResult } from '../../api/src/modules/schema/providers/inspector'; -import { - createSDLHash, - ProjectType, - type CDNAccessToken, - type OIDCIntegration, - type SchemaLog, - type SchemaPolicy, -} from '../../api/src/shared/entities'; +import { createSDLHash, ProjectType } from '../../api/src/shared/entities'; import { batch, batchBy } from '../../api/src/shared/helpers'; -import { - alert_channels, - alerts, - getPool, - organization_invitations, - organization_member, - organization_member_roles, - organizations, - organizations_billing, - projects, - schema_log as schema_log_in_db, - schema_policy_config, - schema_versions, - target_validation, - targets, - tokens, - users, -} from './db'; +import { type organizations } from './db'; import { AffectedAppDeployments, ConditionalBreakingChangeMetadata, @@ -65,25 +36,12 @@ import { type SchemaCheckApprovalMetadata, type SchemaCompositionError, } from './schema-change-model'; -import type { Slonik } from './shared'; export type { Interceptor }; -export { ConnectionError } from 'slonik'; -export { createConnectionString } from './db/utils'; export { createTokenStorage } from './tokens'; export type { tokens, schema_policy_resource } from './db/types'; -type Connection = DatabasePool | DatabaseTransactionConnection; - -type OverrideProp, K extends keyof T, V extends T[K]> = Omit & { - [_P in K]: V; -}; - -type schema_log = Omit & { - action: 'PUSH' | 'DELETE'; -}; - const organizationGetStartedMapping: Record< Exclude, keyof organizations @@ -96,362 +54,25 @@ const organizationGetStartedMapping: Record< enablingUsageBasedBreakingChanges: 'get_started_usage_breaking', }; -function ensureDefined(value: T | null | undefined, propertyName: string): T { - if (value == null) { - throw new Error(`${propertyName} is null or undefined`); - } - - return value; -} - -const tracer = trace.getTracer('storage'); - -async function tracedTransaction( - name: string, - pool: DatabasePool, - handler: TransactionFunction, - transactionRetryLimit?: number, -): Promise { - const span = tracer.startSpan(`PG Transaction: ${name}`, { - kind: SpanKind.INTERNAL, - }); - - return context.with(trace.setSpan(context.active(), span), async () => { - try { - return await pool.transaction(handler, transactionRetryLimit); - } catch (e) { - span.setAttribute('error', 'true'); - - if (e instanceof Error) { - span.setAttribute('error.type', e.name); - span.setAttribute('error.message', e.message); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: e.message, - }); - } - - throw e; - } finally { - span.end(); - } - }); -} - -type MemberRoleColumns = { - role_id: organization_member_roles['id']; - role_name: organization_member_roles['name']; - role_description: organization_member_roles['description']; - role_locked: organization_member_roles['locked']; - role_scopes: organization_member_roles['scopes']; -}; - export async function createStorage( connection: string, maximumPoolSize: number, - additionalInterceptors: Interceptor[] = [], + additionalInterceptors?: Array, ): Promise { - const pool = await getPool(connection, maximumPoolSize, additionalInterceptors); - - function transformSchemaPolicy(schema_policy: schema_policy_config): SchemaPolicy { - return { - id: `${schema_policy.resource_type}_${schema_policy.resource_id}`, - config: schema_policy.config, - createdAt: schema_policy.created_at, - updatedAt: schema_policy.updated_at, - resource: schema_policy.resource_type, - resourceId: schema_policy.resource_id, - allowOverrides: schema_policy.allow_overriding, - }; - } - - function transformMember( - user: users & - Pick & { - is_owner: boolean; - } & MemberRoleColumns & { - provider: string | null; - }, - ): Member { - return { - id: user.id, - isOwner: user.is_owner, - user: UserModel.parse(user), - // This allows us to have a fallback for users that don't have a role, remove this once we all users have a role - scopes: (user.scopes as Member['scopes']) || [], - organization: user.organization_id, - oidcIntegrationId: user.oidc_integration_id ?? null, - connectedToZendesk: user.connected_to_zendesk ?? false, - role: { - id: user.role_id, - name: user.role_name, - locked: user.role_locked, - description: user.role_description, - scopes: user.role_scopes as Member['scopes'], - organizationId: user.organization_id, - membersCount: undefined, // if it's not defined, the resolver will fetch it - }, - }; - } - - function transformOrganization(organization: organizations): Organization { - return { - id: organization.id, - slug: organization.clean_id, - name: organization.name, - monthlyRateLimit: { - retentionInDays: parseInt(organization.limit_retention_days), - operations: parseInt(organization.limit_operations_monthly), - }, - billingPlan: organization.plan_name, - getStarted: { - id: organization.id, - creatingProject: organization.get_started_creating_project, - publishingSchema: organization.get_started_publishing_schema, - checkingSchema: organization.get_started_checking_schema, - invitingMembers: organization.get_started_inviting_members, - reportingOperations: organization.get_started_reporting_operations, - enablingUsageBasedBreakingChanges: organization.get_started_usage_breaking, - }, - featureFlags: decodeFeatureFlags(organization.feature_flags), - zendeskId: organization.zendesk_organization_id ?? null, - ownerId: organization.user_id, - }; - } - - function transformOrganizationInvitation( - invitation: organization_invitations & { - role: organization_member_roles; - }, - ): OrganizationInvitation { - return { - get id() { - return getOrganizationInvitationId({ - organizationId: invitation.organization_id, - email: invitation.email, - code: invitation.code, - }); - }, - email: invitation.email, - organizationId: invitation.organization_id, - code: invitation.code, - createdAt: invitation.created_at as any, - expiresAt: invitation.expires_at as any, - roleId: invitation.role.id, - assignedResources: invitation.assigned_resources as any, - }; - } - - function transformProject(project: projects): Project { - return { - id: project.id, - slug: project.clean_id, - orgId: project.org_id, - name: project.name, - type: project.type as ProjectType, - createdAt: new Date(project.created_at).toISOString(), - buildUrl: project.build_url, - validationUrl: project.validation_url, - gitRepository: project.git_repository as `${string}/${string}` | null, - useProjectNameInGithubCheck: project.github_check_with_project_name === true, - externalComposition: { - enabled: project.external_composition_enabled, - endpoint: project.external_composition_endpoint, - encryptedSecret: project.external_composition_secret, - }, - nativeFederation: project.native_federation === true, - }; - } - - function transformSchema( - schema: Pick< - OverrideProp, - | 'id' - | 'action' - | 'commit' - | 'author' - | 'sdl' - | 'created_at' - | 'project_id' - | 'service_name' - | 'service_url' - | 'target_id' - | 'metadata' - > & - Pick, - ): Schema { - const isSingleProject = (schema.type as ProjectType) === ProjectType.SINGLE; - const record: Schema = isSingleProject - ? { - kind: 'single', - id: schema.id, - author: schema.author, - sdl: ensureDefined(schema.sdl, 'sdl'), - commit: schema.commit, - date: schema.created_at as any, - target: schema.target_id, - metadata: schema.metadata ?? null, - } - : { - kind: 'composite', - id: schema.id, - author: schema.author, - sdl: ensureDefined(schema.sdl, 'sdl'), - commit: schema.commit, - date: schema.created_at as any, - service_name: schema.service_name!, - service_url: schema.service_url, - target: schema.target_id, - action: 'PUSH', - metadata: schema.metadata ?? null, - }; - - return record; - } - - function transformSchemaLog( - schema: Pick< - schema_log, - | 'id' - | 'action' - | 'commit' - | 'author' - | 'sdl' - | 'created_at' - | 'project_id' - | 'service_name' - | 'service_url' - | 'target_id' - | 'metadata' - > & - Pick, - ): SchemaLog { - const isSingleProject = (schema.type as ProjectType) === ProjectType.SINGLE; - const record: SchemaLog = isSingleProject - ? { - kind: 'single', - id: schema.id, - author: schema.author, - sdl: ensureDefined(schema.sdl, 'sdl'), - commit: schema.commit, - date: schema.created_at as any, - target: schema.target_id, - metadata: schema.metadata ?? null, - } - : schema.action === 'PUSH' - ? { - kind: 'composite', - id: schema.id, - author: schema.author, - sdl: ensureDefined(schema.sdl, 'sdl'), - commit: schema.commit, - date: schema.created_at as any, - service_name: schema.service_name!, - service_url: schema.service_url, - target: schema.target_id, - action: 'PUSH', - metadata: schema.metadata ?? null, - } - : { - kind: 'composite', - id: schema.id, - date: schema.created_at as any, - service_name: schema.service_name!, - target: schema.target_id, - action: 'DELETE', - }; - - return record; - } - - function transformTargetSettings( - row: Pick< - targets, - | 'validation_enabled' - | 'validation_percentage' - | 'validation_period' - | 'validation_excluded_clients' - | 'validation_excluded_app_deployments' - | 'validation_request_count' - | 'validation_breaking_change_formula' - | 'fail_diff_on_dangerous_change' - | 'app_deployment_protection_enabled' - | 'app_deployment_protection_min_days_inactive' - | 'app_deployment_protection_max_traffic_percentage' - | 'app_deployment_protection_traffic_period_days' - | 'app_deployment_protection_rule_logic' - | 'app_deployment_protection_min_days_since_creation' - > & { - targets: target_validation['destination_target_id'][] | null; - }, - ): TargetSettings { - return { - failDiffOnDangerousChange: row.fail_diff_on_dangerous_change, - validation: { - isEnabled: row.validation_enabled, - percentage: row.validation_percentage, - period: row.validation_period, - requestCount: row.validation_request_count ?? 1, - breakingChangeFormula: row.validation_breaking_change_formula ?? 'PERCENTAGE', - targets: Array.isArray(row.targets) ? row.targets.filter(isDefined) : [], - excludedClients: Array.isArray(row.validation_excluded_clients) - ? row.validation_excluded_clients.filter(isDefined) - : [], - excludedAppDeployments: Array.isArray(row.validation_excluded_app_deployments) - ? row.validation_excluded_app_deployments.filter(isDefined) - : [], - }, - appDeploymentProtection: { - isEnabled: row.app_deployment_protection_enabled, - minDaysInactive: row.app_deployment_protection_min_days_inactive, - minDaysSinceCreation: row.app_deployment_protection_min_days_since_creation, - maxTrafficPercentage: Number(row.app_deployment_protection_max_traffic_percentage), - trafficPeriodDays: row.app_deployment_protection_traffic_period_days, - ruleLogic: row.app_deployment_protection_rule_logic as 'AND' | 'OR', - }, - }; - } - - function transformOrganizationBilling(orgBilling: organizations_billing): OrganizationBilling { - return { - organizationId: orgBilling.organization_id, - externalBillingReference: orgBilling.external_billing_reference_id, - billingEmailAddress: orgBilling.billing_email_address, - }; - } - - function transformAlertChannel(channel: alert_channels): AlertChannel { - return { - id: channel.id, - projectId: channel.project_id, - name: channel.name, - type: channel.type, - createdAt: channel.created_at as any, - slackChannel: channel.slack_channel, - webhookEndpoint: channel.webhook_endpoint, - }; - } - - function transformAlert(alert: alerts, organization: string): Alert { - return { - id: alert.id, - type: alert.type, - createdAt: alert.created_at as any, - channelId: alert.alert_channel_id, - organizationId: organization, - projectId: alert.project_id, - targetId: alert.target_id, - }; - } + const pool = await createPostgresDatabasePool({ + connectionParameters: connection, + maximumPoolSize, + additionalInterceptors, + }); const shared = { async getUserBySuperTokenId( { superTokensUserId }: { superTokensUserId: string }, - connection: Connection, + connection: CommonQueryMethods, ) { - const record = await connection.maybeOne(sql`/* getUserBySuperTokenId */ + const record = await connection.maybeOne(psql`/* getUserBySuperTokenId */ SELECT - ${userFields(sql`"users".`)} + ${userFields(psql`"users".`)} FROM "users" WHERE @@ -466,16 +87,16 @@ export async function createStorage( return UserModel.parse(record); }, getUserById: batchBy( - (item: { id: string; connection: Connection }) => item.connection, + (item: { id: string; connection: CommonQueryMethods }) => item.connection, async input => { const userIds = input.map(i => i.id); - const records = await input[0].connection.any(sql`/* getUserById */ + const records = await input[0].connection.any(psql`/* getUserById */ SELECT - ${userFields(sql`"users".`)} + ${userFields(psql`"users".`)} FROM "users" WHERE - "users"."id" = ANY(${sql.array(userIds, 'uuid')}) + "users"."id" = ANY(${psql.array(userIds, 'uuid')}) `); const mappings = new Map(); @@ -501,19 +122,21 @@ export async function createStorage( superTokensUserId: string; oidcIntegrationId: string | null; }, - connection: DatabaseTransactionConnection, + connection: CommonQueryMethods, ) { - const { id } = await connection.one<{ id: string }>( - sql`/* createUser */ - INSERT INTO users - ("email", "full_name", "display_name", "supertoken_user_id", "oidc_integration_id") - VALUES - (${email}, ${fullName}, ${displayName}, ${superTokensUserId}, ${oidcIntegrationId}) - RETURNING id - `, - ); + const { id } = await connection + .maybeOne( + psql`/* createUser */ + INSERT INTO users + ("email", "full_name", "display_name", "supertoken_user_id", "oidc_integration_id") + VALUES + (${email}, ${fullName}, ${displayName}, ${superTokensUserId}, ${oidcIntegrationId}) + RETURNING id + `, + ) + .then(z.object({ id: z.string() }).parse); - await connection.query(sql` + await connection.query(psql` INSERT INTO "users_linked_identities" ("user_id", "identity_id") VALUES (${id}, ${superTokensUserId}) `); @@ -525,12 +148,12 @@ export async function createStorage( return user; }, - async getOrganization(userId: string, connection: Connection) { - const org = await connection.maybeOne>( - sql`/* getOrganization */ SELECT * FROM organizations WHERE user_id = ${userId} AND type = ${'PERSONAL'} LIMIT 1`, - ); - - return org ? transformOrganization(org) : null; + async getOrganization(userId: string, connection: CommonQueryMethods) { + return connection + .maybeOne( + psql`/* getOrganization */ SELECT ${organizationFields(psql``)} FROM organizations WHERE user_id = ${userId} AND type = ${'PERSONAL'} LIMIT 1`, + ) + .then(OrganizationModel.nullable().parse); }, async addOrganizationMemberViaOIDCIntegrationId( args: { @@ -538,17 +161,20 @@ export async function createStorage( userId: string; invitation: OrganizationInvitation | null; }, - connection: Connection, + connection: CommonQueryMethods, ) { - const linkedOrganizationId = - await connection.maybeOneFirst(sql`/* addOrganizationMemberViaOIDCIntegrationId */ - SELECT - "linked_organization_id" - FROM - "oidc_integrations" - WHERE - "id" = ${args.oidcIntegrationId} - `); + const linkedOrganizationId = await connection + .maybeOneFirst( + psql`/* addOrganizationMemberViaOIDCIntegrationId */ + SELECT + "linked_organization_id" + FROM + "oidc_integrations" + WHERE + "id" = ${args.oidcIntegrationId} + `, + ) + .then(z.string().nullable().parse); if (linkedOrganizationId === null) { return; @@ -556,7 +182,7 @@ export async function createStorage( // Add user and assign role (either the invited role, custom default role, or Viewer) await connection.query( - sql`/* addOrganizationMemberViaOIDCIntegrationId */ + psql`/* addOrganizationMemberViaOIDCIntegrationId */ INSERT INTO organization_member (organization_id, user_id, role_id, assigned_resources, created_at) VALUES @@ -574,13 +200,13 @@ export async function createStorage( ), ${ args.invitation?.assignedResources - ? sql.jsonb(args.invitation.assignedResources) - : sql`(SELECT default_assigned_resources FROM oidc_integrations + ? psql.jsonb(args.invitation.assignedResources) + : psql`(SELECT default_assigned_resources FROM oidc_integrations WHERE id = ${args.oidcIntegrationId})` }, now() ) - ${args.invitation ? sql`` : sql`ON CONFLICT DO NOTHING`} + ${args.invitation ? psql`` : psql`ON CONFLICT DO NOTHING`} RETURNING * `, ); @@ -590,18 +216,22 @@ export async function createStorage( organizationId: string; roleName: string; }, - connection: Connection, + connection: CommonQueryMethods, ) { - const roleId = await connection.oneFirst(sql`/* getOrganizationMemberRoleByName */ - SELECT - "id" - FROM - "organization_member_roles" - WHERE - "organization_id" = ${args.organizationId} - AND "name" = ${args.roleName} - LIMIT 1 - `); + const roleId = await connection + .oneFirst( + psql`/* getOrganizationMemberRoleByName */ + SELECT + "id" + FROM + "organization_member_roles" + WHERE + "organization_id" = ${args.organizationId} + AND "name" = ${args.roleName} + LIMIT 1 + `, + ) + .then(z.string().parse); return roleId; }, @@ -635,7 +265,7 @@ export async function createStorage( }, async isReady() { try { - await pool.exists(sql`/* Heartbeat */ SELECT 1`); + await pool.exists(psql`/* Heartbeat */ SELECT 1`); return true; } catch { return false; @@ -659,15 +289,15 @@ export async function createStorage( class EnsureUserExistsError extends Error {} try { - return await tracedTransaction('ensureUserExists', pool, async t => { + return await pool.transaction('ensureUserExists', async t => { let action: 'created' | 'no_action' = 'no_action'; // try searching existing user first let internalUser = await t - .maybeOne( - sql` + .maybeOne( + psql` SELECT - ${userFields(sql`"users".`)} + ${userFields(psql`"users".`)} FROM "users" WHERE "users"."supertoken_user_id" = ${superTokensUserId} @@ -678,14 +308,14 @@ export async function createStorage( ) `, ) - .then(v => UserModel.nullable().parse(v)); + .then(UserModel.nullable().parse); if (!internalUser) { // try automatic account linking const sameEmailUsers = await t - .any( - sql`/* ensureUserExists */ - SELECT ${userFields(sql`"users".`)} + .any( + psql`/* ensureUserExists */ + SELECT ${userFields(psql`"users".`)} FROM "users" WHERE "users"."email" = ${email} ORDER BY "users"."created_at"; @@ -695,7 +325,7 @@ export async function createStorage( if (sameEmailUsers.length === 1) { internalUser = sameEmailUsers[0]; - await t.query(sql` + await t.query(psql` INSERT INTO "users_linked_identities" ("user_id", "identity_id") VALUES (${internalUser.id}, ${superTokensUserId}) `); @@ -711,8 +341,8 @@ export async function createStorage( if (oidcConfig) { invitation = await t - .maybeOne( - sql` + .maybeOne( + psql` DELETE FROM "organization_invitations" AS "oi" WHERE "oi"."organization_id" = ${oidcConfig.linkedOrganizationId} @@ -722,13 +352,13 @@ export async function createStorage( "oi"."organization_id" "organizationId" , "oi"."code" "code" , "oi"."email" "email" - , "oi"."created_at" "createdAt" - , "oi"."expires_at" "expiresAt" + , to_json("oi"."created_at") "createdAt" + , to_json("oi"."expires_at") "expiresAt" , "oi"."role_id" "roleId" , "oi"."assigned_resources" "assignedResources" `, ) - .then(v => OrganizationInvitationModel.nullable().parse(v)); + .then(OrganizationInvitationModel.nullable().parse); } if (oidcConfig?.requireInvitation && !invitation) { @@ -762,7 +392,7 @@ export async function createStorage( } if (internalUser.email !== email) { - await t.query(sql` + await t.query(psql` UPDATE "users" SET "email" = ${email} WHERE "id" = ${internalUser.id} @@ -805,7 +435,7 @@ export async function createStorage( return shared.getUserById({ id, connection: pool }); }, async updateUser({ id, displayName, fullName }) { - await pool.query(sql`/* updateUser */ + await pool.query(psql`/* updateUser */ UPDATE "users" SET "display_name" = ${displayName} @@ -823,7 +453,7 @@ export async function createStorage( return user; }, createOrganization(input) { - return tracedTransaction('createOrganization', pool, async t => { + return pool.transaction('createOrganization', async t => { if (input.reservedSlugs.includes(input.slug)) { return { ok: false, @@ -832,7 +462,7 @@ export async function createStorage( } const orgSlugExists = await t.exists( - sql`/* orgSlugExists */ SELECT 1 FROM organizations WHERE clean_id = ${input.slug} LIMIT 1`, + psql`/* orgSlugExists */ SELECT 1 FROM organizations WHERE clean_id = ${input.slug} LIMIT 1`, ); if (orgSlugExists) { @@ -842,50 +472,54 @@ export async function createStorage( }; } - const org = await t.one( - sql`/* createOrganization */ - INSERT INTO organizations - ("name", "clean_id", "user_id") - VALUES - (${input.slug}, ${input.slug}, ${input.userId}) - RETURNING * - `, - ); + const org = await t + .maybeOne( + psql`/* createOrganization */ + INSERT INTO organizations + ("name", "clean_id", "user_id") + VALUES + (${input.slug}, ${input.slug}, ${input.userId}) + RETURNING ${organizationFields(psql``)} + `, + ) + .then(OrganizationModel.parse); // Create default roles for the organization - const roles = await t.query< - Pick - >(sql`/* createOrganizationRoles */ - INSERT INTO organization_member_roles - ( - organization_id, - name, - description, - locked + const roles = await t + .any( + psql`/* createOrganizationRoles */ + INSERT INTO organization_member_roles + ( + organization_id, + name, + description, + locked + ) + VALUES ( + ${org.id}, + 'Admin', + 'Full access to all organization resources', + true + ), ( + ${org.id}, + 'Viewer', + 'Read-only access to all organization resources', + true + ) + RETURNING id, name + `, ) - VALUES ( - ${org.id}, - 'Admin', - 'Full access to all organization resources', - true - ), ( - ${org.id}, - 'Viewer', - 'Read-only access to all organization resources', - true - ) - RETURNING id, name - `); + .then(z.array(z.object({ id: z.string(), name: z.string() })).parse); - const adminRole = roles.rows.find(role => role.name === 'Admin'); + const adminRole = roles.find(role => role.name === 'Admin'); if (!adminRole) { throw new Error('Admin role not found'); } // Assign the admin role to the user - await t.query( - sql`/* assignAdminRole */ + await t.query( + psql`/* assignAdminRole */ INSERT INTO organization_member ("organization_id", "user_id", "role_id", "created_at") VALUES @@ -895,38 +529,44 @@ export async function createStorage( return { ok: true, - organization: transformOrganization(org), + organization: org, }; }); }, async deleteOrganization({ organizationId: organization }) { - const result = await tracedTransaction('DeleteOrganization', pool, async t => { - const tokensResult = await t.query>(sql`/* findTokensForDeletion */ - SELECT token FROM tokens WHERE organization_id = ${organization} AND deleted_at IS NULL - `); + const result = await pool.transaction('DeleteOrganization', async t => { + const tokens = await t + .any( + psql`/* findTokensForDeletion */ + SELECT token FROM tokens WHERE organization_id = ${organization} AND deleted_at IS NULL + `, + ) + .then(z.array(z.object({ token: z.string() })).parse); return { - organization: await t.one( - sql`/* deleteOrganization */ - DELETE FROM organizations - WHERE id = ${organization} - RETURNING * - `, - ), - tokens: tokensResult.rows.map(row => row.token), + organization: await t + .maybeOne( + psql`/* deleteOrganization */ + DELETE FROM organizations + WHERE id = ${organization} + RETURNING ${organizationFields(psql``)} + `, + ) + .then(OrganizationModel.parse), + tokens: tokens.map(row => row.token), }; }); return { - ...transformOrganization(result.organization), + ...result.organization, tokens: result.tokens, }; }, async createProject({ organizationId: organization, slug, type }) { // Native Composition is enabled by default for fresh Federation-type projects - return pool.transaction(async t => { + return pool.transaction('createProject', async t => { const projectSlugExists = await t.exists( - sql`/* projectSlugExists */ SELECT 1 FROM projects WHERE clean_id = ${slug} AND org_id = ${organization} LIMIT 1`, + psql`/* projectSlugExists */ SELECT 1 FROM projects WHERE clean_id = ${slug} AND org_id = ${organization} LIMIT 1`, ); if (projectSlugExists) { @@ -936,29 +576,33 @@ export async function createStorage( }; } - const project = await t.one( - sql`/* createProject */ - INSERT INTO projects - ("name", "clean_id", "type", "org_id", "native_federation") - VALUES - (${slug}, ${slug}, ${type}, ${organization}, ${type === ProjectType.FEDERATION}) - RETURNING * - `, - ); + const project = await t + .maybeOne( + psql`/* createProject */ + INSERT INTO projects + ("name", "clean_id", "type", "org_id", "native_federation") + VALUES + (${slug}, ${slug}, ${type}, ${organization}, ${type === ProjectType.FEDERATION}) + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse); return { ok: true, - project: transformProject(project), + project, }; }); }, async getOrganizationId({ organizationSlug }) { // Based on clean_id, resolve id - const result = await pool.maybeOne>( - sql`/* getOrganizationId */ - SELECT id FROM organizations WHERE clean_id = ${organizationSlug} LIMIT 1 - `, - ); + const result = await pool + .maybeOne( + psql`/* getOrganizationId */ + SELECT id FROM organizations WHERE clean_id = ${organizationSlug} LIMIT 1 + `, + ) + .then(z.object({ id: z.string() }).nullable().parse); if (!result) { return null; @@ -968,15 +612,17 @@ export async function createStorage( }, getOrganizationOwnerId: batch(async selectors => { const organizations = selectors.map(s => s.organizationId); - const owners = await pool.query>>( - sql`/* getOrganizationOwnerId */ + const owners = await pool + .any( + psql`/* getOrganizationOwnerId */ SELECT id, user_id FROM organizations - WHERE id IN (${sql.join(organizations, sql`, `)})`, - ); + WHERE id IN (${psql.join(organizations, psql`, `)})`, + ) + .then(z.array(OrganizationUserIdAndIdModel).parse); return organizations.map(async organization => { - const owner = owners.rows.find(row => row.id === organization); + const owner = owners.find(row => row.id === organization); if (owner) { return owner.user_id; @@ -987,93 +633,82 @@ export async function createStorage( }), getOrganizationOwner: batch(async selectors => { const organizations = selectors.map(s => s.organizationId); - const owners = await pool.query< - users & - Pick & - MemberRoleColumns & { - provider: string | null; - } - >( - sql`/* getOrganizationOwner */ + const owners = await pool.any( + psql`/* getOrganizationOwner */ SELECT - ${userFields(sql`"u".`)}, + ${userFields(psql`"u".`)}, omr.scopes as scopes, - om.organization_id, - om.connected_to_zendesk, - omr.id as role_id, - omr.name as role_name, - omr.locked as role_locked, - omr.scopes as role_scopes, - omr.description as role_description + om.organization_id AS "organizationId", + om.connected_to_zendesk AS "connectedToZendesk", + true AS "isOwner", + omr.id as "roleId", + omr.name as "roleName", + omr.locked as "roleLocked", + omr.scopes as "roleScopes", + omr.description as "roleDescription" FROM organizations as o LEFT JOIN users as u ON (u.id = o.user_id) LEFT JOIN organization_member as om ON (om.user_id = u.id AND om.organization_id = o.id) LEFT JOIN organization_member_roles as omr ON (omr.organization_id = o.id AND omr.id = om.role_id) - WHERE o.id = ANY(${sql.array(organizations, 'uuid')})`, + WHERE o.id = ANY(${psql.array(organizations, 'uuid')})`, ); + const parsedOwners = z.array(MemberModel).parse(owners); + return organizations.map(organization => { - const owner = owners.rows.find(row => row.organization_id === organization); + const owner = parsedOwners.find(row => row.organization === organization); if (owner) { - return Promise.resolve( - transformMember({ - ...owner, - is_owner: true, - }), - ); + return Promise.resolve(owner); } return Promise.reject(new Error(`Owner not found (organization=${organization})`)); }); }), async countOrganizationMembers({ organizationId: organization }) { - const { total } = await pool.one<{ total: number }>( - sql`/* countOrganizationMembers */ SELECT COUNT(*) as total FROM organization_member WHERE organization_id = ${organization}`, - ); + const { total } = await pool + .maybeOne( + psql`/* countOrganizationMembers */ SELECT COUNT(*) as total FROM organization_member WHERE organization_id = ${organization}`, + ) + .then(z.object({ total: z.number() }).parse); return total; }, getOrganizationMember: batch(async selectors => { - const membersResult = await pool.query< - users & - Pick & { - is_owner: boolean; - } & MemberRoleColumns & { - provider: string | null; - } - >( - sql`/* getOrganizationMember */ + const membersResult = await pool.any( + psql`/* getOrganizationMember */ SELECT - ${userFields(sql`"u".`)}, + ${userFields(psql`"u".`)}, omr.scopes as scopes, - om.organization_id, - om.connected_to_zendesk, - CASE WHEN o.user_id = om.user_id THEN true ELSE false END AS is_owner, - omr.id as role_id, - omr.name as role_name, - omr.locked as role_locked, - omr.scopes as role_scopes, - omr.description as role_description + om.organization_id AS "organizationId", + om.connected_to_zendesk AS "connectedToZendesk", + CASE WHEN o.user_id = om.user_id THEN true ELSE false END AS "isOwner", + omr.id as "roleId", + omr.name as "roleName", + omr.locked as "roleLocked", + omr.scopes as "roleScopes", + omr.description as "roleDescription" FROM organization_member as om LEFT JOIN organizations as o ON (o.id = om.organization_id) LEFT JOIN users as u ON (u.id = om.user_id) LEFT JOIN organization_member_roles as omr ON (omr.organization_id = o.id AND omr.id = om.role_id) - WHERE (om.organization_id, om.user_id) IN ((${sql.join( - selectors.map(s => sql`${s.organizationId}, ${s.userId}`), - sql`), (`, + WHERE (om.organization_id, om.user_id) IN ((${psql.join( + selectors.map(s => psql`${s.organizationId}, ${s.userId}`), + psql`), (`, )})) ORDER BY u.created_at DESC `, ); + const parsedMembers = z.array(MemberModel).parse(membersResult); + return selectors.map(selector => { - const member = membersResult.rows.find( - row => row.organization_id === selector.organizationId && row.id === selector.userId, + const member = parsedMembers.find( + row => row.organization === selector.organizationId && row.id === selector.userId, ); if (member) { - return Promise.resolve(transformMember(member)); + return Promise.resolve(member); } return Promise.resolve(null); @@ -1093,15 +728,15 @@ export async function createStorage( cursor = decodeCreatedAtAndHashBasedCursor(args.after); } - const query = sql` + const query = psql` SELECT - oi.organization_id + oi.organization_id AS "organizationId" , oi.code , oi.email - , to_json(oi.created_at) as "created_at" - , to_json(oi.expires_at) as "expires_at" - , oi.role_id as "role_id" - , to_jsonb(omr.*) as role + , to_json(oi.created_at) as "createdAt" + , to_json(oi.expires_at) as "expiresAt" + , oi.role_id as "roleId" + , oi.assigned_resources as "assignedResources" FROM organization_invitations as oi LEFT JOIN organization_member_roles as omr ON (omr.organization_id = oi.organization_id AND omr.id = oi.role_id) @@ -1109,7 +744,7 @@ export async function createStorage( oi.organization_id = ${organizationId} ${ cursor - ? sql` + ? psql` AND ( ( oi.created_at = ${cursor.createdAt} @@ -1118,7 +753,7 @@ export async function createStorage( OR oi.created_at < ${cursor.createdAt} ) ` - : sql`` + : psql`` } AND oi.expires_at > NOW() ORDER BY @@ -1127,15 +762,9 @@ export async function createStorage( LIMIT ${limit + 1} `; - const result = await pool.any< - organization_invitations & { - role: organization_member_roles; - } - >(query); - - let edges = result.map(row => { - const node = transformOrganizationInvitation(row); + const result = await pool.any(query).then(z.array(OrganizationInvitationModel).parse); + let edges = result.map(node => { return { node, get cursor() { @@ -1166,21 +795,25 @@ export async function createStorage( }, async deleteOrganizationMemberRole({ organizationId, roleId }) { - await tracedTransaction('deleteOrganizationMemberRole', pool, async t => { - const viewerRoleId = await t.oneFirst(sql`/* getViewerRoleId */ + await pool.transaction('deleteOrganizationMemberRole', async t => { + const viewerRoleId = await t + .oneFirst( + psql`/* getViewerRoleId */ SELECT id FROM organization_member_roles WHERE organization_id = ${organizationId} AND locked = true AND name = 'Viewer' LIMIT 1 - `); + `, + ) + .then(z.string().parse); // move all invitations to the viewer role - await t.query(sql`/* moveInvitations */ + await t.query(psql`/* moveInvitations */ UPDATE organization_invitations SET role_id = ${viewerRoleId} WHERE role_id = ${roleId} AND organization_id = ${organizationId} `); - await t.query(sql`/* deleteOrganizationMemberRole */ + await t.query(psql`/* deleteOrganizationMemberRole */ DELETE FROM organization_member_roles WHERE organization_id = ${organizationId} @@ -1195,28 +828,27 @@ export async function createStorage( }); }, async getOrganizationMemberAccessPairs(pairs) { - const results = await pool.query< - Slonik> - >( - sql`/* getOrganizationMemberAccessPairs */ + const results = await pool + .any( + psql`/* getOrganizationMemberAccessPairs */ SELECT om.organization_id, om.user_id, omr.scopes as scopes FROM organization_member as om LEFT JOIN organization_member_roles as omr ON (omr.organization_id = om.organization_id AND omr.id = om.role_id) - WHERE (om.organization_id, om.user_id) IN ((${sql.join( - pairs.map(p => sql`${p.organizationId}, ${p.userId}`), - sql`), (`, + WHERE (om.organization_id, om.user_id) IN ((${psql.join( + pairs.map(p => psql`${p.organizationId}, ${p.userId}`), + psql`), (`, )})) `, - ); + ) + .then(z.array(OrganizationMemberAccessModel).parse); return pairs.map(({ organizationId: organization, userId: user }) => { - return (results.rows.find( - row => row.organization_id === organization && row.user_id === user, - )?.scopes || []) as Member['scopes']; + return (results.find(row => row.organization_id === organization && row.user_id === user) + ?.scopes || []) as Member['scopes']; }); }, async updateOrganizationSlug({ slug, organizationId: organization, reservedSlugs }) { - return pool.transaction(async t => { + return pool.transaction('updateOrganizationSlug', async t => { if (reservedSlugs.includes(slug)) { return { ok: false, @@ -1225,7 +857,7 @@ export async function createStorage( } const orgSlugExists = await t.exists( - sql`/* orgSlugExists */ SELECT 1 FROM organizations WHERE clean_id = ${slug} AND id != ${organization} LIMIT 1`, + psql`/* orgSlugExists */ SELECT 1 FROM organizations WHERE clean_id = ${slug} AND id != ${organization} LIMIT 1`, ); if (orgSlugExists) { @@ -1237,48 +869,54 @@ export async function createStorage( return { ok: true, - organization: transformOrganization( - await t.one>(sql`/* updateOrganizationSlug */ + organization: await t + .maybeOne( + psql`/* updateOrganizationSlug */ UPDATE organizations SET clean_id = ${slug}, name = ${slug} WHERE id = ${organization} - RETURNING * - `), - ), + RETURNING ${organizationFields(psql``)} + `, + ) + .then(OrganizationModel.parse), }; }); }, async updateOrganizationPlan({ billingPlan, organizationId: organization }) { - return transformOrganization( - await pool.one>(sql`/* updateOrganizationPlan */ + return pool + .maybeOne( + psql`/* updateOrganizationPlan */ UPDATE organizations SET plan_name = ${billingPlan} WHERE id = ${organization} - RETURNING * - `), - ); + RETURNING ${organizationFields(psql``)} + `, + ) + .then(OrganizationModel.parse); }, async updateOrganizationRateLimits(args, action) { - return await tracedTransaction('updateOrganizationRateLimits', pool, async t => { - const org = transformOrganization( - await t.one>(sql`/* updateOrganizationRateLimits */ + return await pool.transaction('updateOrganizationRateLimits', async t => { + const org = await t + .maybeOne( + psql`/* updateOrganizationRateLimits */ UPDATE organizations SET limit_operations_monthly = ${args.monthlyRateLimit.operations}, limit_retention_days = ${args.monthlyRateLimit.retentionInDays} WHERE id = ${args.organizationId} - RETURNING * - `), - ); + RETURNING ${organizationFields(psql``)} + `, + ) + .then(OrganizationModel.parse); await action?.(); return org; }); }, async createOrganizationInvitation(args) { - return transformOrganizationInvitation( - await tracedTransaction('createOrganizationInvitation', pool, async trx => { - const invitation = - await trx.one(sql`/* createOrganizationInvitation */ + return pool.transaction('createOrganizationInvitation', async trx => { + const invitation = await trx + .maybeOne( + psql`/* createOrganizationInvitation */ INSERT INTO "organization_invitations" ( "organization_id" , "email" @@ -1289,83 +927,72 @@ export async function createStorage( ${args.organizationId} , ${args.email} , ${args.roleId} - , ${args.resourceAssignments === null ? null : sql.jsonb(args.resourceAssignments)} + , ${args.resourceAssignments === null ? null : psql.jsonb(args.resourceAssignments)} ) - RETURNING * - `); + RETURNING + "organization_id" AS "organizationId" + , "code" + , "email" + , to_json("created_at") AS "createdAt" + , to_json("expires_at") AS "expiresAt" + , "role_id" AS "roleId" + , "assigned_resources" AS "assignedResources" + `, + ) + .then(OrganizationInvitationModel.parse); - const role = await trx.one(sql`/* getOrganizationRole */ - SELECT * - FROM - "organization_member_roles" - WHERE - "id" = ${args.roleId} - LIMIT 1 - `); - - return { - ...invitation, - role, - }; - }), - ); + return invitation; + }); }, async deleteOrganizationInvitationByEmail({ organizationId: organization, email }) { - const result = await tracedTransaction( - 'deleteOrganizationInvitationByEmail', - pool, - async trx => { - const deleted = - await trx.maybeOne(sql`/* deleteOrganizationInvitationByEmail */ - DELETE FROM organization_invitations - WHERE organization_id = ${organization} AND email = ${email} - RETURNING * - `); - - if (!deleted) { - return null; - } - - const role = await trx.one(sql`/* getOrganizationRole */ - SELECT * FROM organization_member_roles WHERE id = ${deleted.role_id} LIMIT 1 - `); - - return { - ...deleted, - role, - }; - }, - ); - - if (!result) { - return null; - } - - return transformOrganizationInvitation(result); + return pool.transaction('deleteOrganizationInvitationByEmail', async trx => { + return trx + .maybeOne( + psql`/* deleteOrganizationInvitationByEmail */ + DELETE FROM organization_invitations + WHERE organization_id = ${organization} AND email = ${email} + RETURNING + "organization_id" AS "organizationId" + , "code" + , "email" + , to_json("created_at") AS "createdAt" + , to_json("expires_at") AS "expiresAt" + , "role_id" AS "roleId" + , "assigned_resources" AS "assignedResources" + `, + ) + .then(OrganizationInvitationModel.nullable().parse); + }); }, async addOrganizationMemberViaInvitationCode({ code, userId: user, organizationId: organization, }) { - await tracedTransaction('addOrganizationMemberViaInvitationCode', pool, async trx => { - const { roleId, assignedResources } = await trx.one<{ - roleId: string; - assignedResources: null | Record; - }>(sql`/* deleteInviteAndGetRoleId */ - DELETE - FROM - "organization_invitations" - WHERE - "organization_id" = ${organization} - AND "code" = ${code} - RETURNING - role_id as "roleId" - , assigned_resources as "assignedResources" - `); + await pool.transaction('addOrganizationMemberViaInvitationCode', async trx => { + const { roleId, assignedResources } = await trx + .maybeOne( + psql`/* deleteInviteAndGetRoleId */ + DELETE + FROM + "organization_invitations" + WHERE + "organization_id" = ${organization} + AND "code" = ${code} + RETURNING + role_id as "roleId" + , assigned_resources as "assignedResources" + `, + ) + .then( + z.object({ + roleId: z.string(), + assignedResources: z.record(z.any()).nullable(), + }).parse, + ); await trx.query( - sql`/* addOrganizationMemberViaInvitationCode */ + psql`/* addOrganizationMemberViaInvitationCode */ INSERT INTO "organization_member" ( "organization_id" , "user_id" @@ -1377,7 +1004,7 @@ export async function createStorage( ${organization} , ${user} , ${roleId} - , ${assignedResources === null ? null : sql.json(assignedResources)} + , ${assignedResources === null ? null : psql.json(assignedResources)} , now() ) `, @@ -1387,8 +1014,8 @@ export async function createStorage( async createOrganizationTransferRequest({ organizationId: organization, userId: user }) { const code = Math.random().toString(16).substring(2, 12); - await pool.query>>( - sql`/* createOrganizationTransferRequest */ + await pool.any( + psql`/* createOrganizationTransferRequest */ UPDATE organizations SET ownership_transfer_user_id = ${user}, @@ -1403,16 +1030,18 @@ export async function createStorage( }; }, async getOrganizationTransferRequest({ code, userId: user, organizationId: organization }) { - return pool.maybeOne<{ - code: string; - }>(sql`/* getOrganizationTransferRequest */ - SELECT ownership_transfer_code as code FROM organizations - WHERE - ownership_transfer_user_id = ${user} - AND id = ${organization} - AND ownership_transfer_code = ${code} - AND ownership_transfer_expires_at > NOW() - `); + return pool + .maybeOne( + psql`/* getOrganizationTransferRequest */ + SELECT ownership_transfer_code as code FROM organizations + WHERE + ownership_transfer_user_id = ${user} + AND id = ${organization} + AND ownership_transfer_code = ${code} + AND ownership_transfer_expires_at > NOW() + `, + ) + .then(z.object({ code: z.string() }).nullable().parse); }, async answerOrganizationTransferRequest({ organizationId: organization, @@ -1420,10 +1049,9 @@ export async function createStorage( code, accept, }) { - await tracedTransaction('answerOrganizationTransferRequest', pool, async tsx => { - const owner = await tsx.maybeOne< - Slonik> - >(sql`/* findOrganizationTransferRequest */ + await pool.transaction('answerOrganizationTransferRequest', async tsx => { + const owner = await tsx.maybeOne( + psql`/* findOrganizationTransferRequest */ SELECT user_id FROM organizations WHERE @@ -1431,7 +1059,8 @@ export async function createStorage( AND ownership_transfer_user_id = ${user} AND ownership_transfer_code = ${code} AND ownership_transfer_expires_at > NOW() - `); + `, + ); if (!owner) { throw new Error('No organization transfer request found'); @@ -1439,7 +1068,7 @@ export async function createStorage( if (!accept) { // NULL out the transfer request - await tsx.query(sql`/* rejectOrganizationTransferRequest */ + await tsx.query(psql`/* rejectOrganizationTransferRequest */ UPDATE organizations SET ownership_transfer_user_id = NULL, @@ -1461,7 +1090,7 @@ export async function createStorage( ); // set admin role - await tsx.query(sql`/* setAdminRole */ + await tsx.query(psql`/* setAdminRole */ UPDATE organization_member SET role_id = ${adminRoleId} WHERE organization_id = ${organization} AND user_id = ${user} @@ -1469,7 +1098,7 @@ export async function createStorage( // NULL out the transfer request // assign the new owner - await tsx.query(sql`/* acceptOrganizationTransferRequest */ + await tsx.query(psql`/* acceptOrganizationTransferRequest */ UPDATE organizations SET ownership_transfer_user_id = NULL, @@ -1481,8 +1110,8 @@ export async function createStorage( }); }, async deleteOrganizationMember({ userId: user, organizationId: organization }) { - await pool.query( - sql`/* deleteOrganizationMember */ + await pool.query( + psql`/* deleteOrganizationMember */ DELETE FROM organization_member WHERE organization_id = ${organization} AND user_id = ${user} `, @@ -1490,56 +1119,61 @@ export async function createStorage( }, async getProjectId({ projectSlug, organizationSlug }) { // Based on project's clean_id and organization's clean_id, resolve the actual uuid of the project - const result = await pool.one>( - sql`/* getProjectId */ - SELECT p.id as id - FROM projects as p - LEFT JOIN organizations as org ON (p.org_id = org.id) - WHERE p.clean_id = ${projectSlug} AND org.clean_id = ${organizationSlug} AND p.type != 'CUSTOM' LIMIT 1`, - ); + const result = await pool + .maybeOne( + psql`/* getProjectId */ + SELECT p.id as id + FROM projects as p + LEFT JOIN organizations as org ON (p.org_id = org.id) + WHERE p.clean_id = ${projectSlug} AND org.clean_id = ${organizationSlug} AND p.type != 'CUSTOM' LIMIT 1`, + ) + .then(z.object({ id: z.string() }).parse); return result.id; }, async getTargetId(selector) { - const result = await pool.one>( - sql`/* getTargetId (slug) */ - SELECT t.id FROM targets as t - LEFT JOIN projects AS p ON (p.id = t.project_id) - LEFT JOIN organizations AS o ON (o.id = p.org_id) - WHERE - t.clean_id = ${selector.targetSlug} AND - p.clean_id = ${selector.projectSlug} AND - o.clean_id = ${selector.organizationSlug} AND - p.type != 'CUSTOM' - LIMIT 1`, - ); + const result = await pool + .maybeOne( + psql`/* getTargetId (slug) */ + SELECT t.id FROM targets as t + LEFT JOIN projects AS p ON (p.id = t.project_id) + LEFT JOIN organizations AS o ON (o.id = p.org_id) + WHERE + t.clean_id = ${selector.targetSlug} AND + p.clean_id = ${selector.projectSlug} AND + o.clean_id = ${selector.organizationSlug} AND + p.type != 'CUSTOM' + LIMIT 1`, + ) + .then(z.object({ id: z.string() }).parse); return result.id; }, async getOrganization({ organizationId }) { - return transformOrganization( - await pool.one>( - sql`/* getOrganization */ SELECT * FROM organizations WHERE id = ${organizationId} LIMIT 1`, - ), - ); + return pool + .maybeOne( + psql`/* getOrganization */ SELECT ${organizationFields(psql``)} FROM organizations WHERE id = ${organizationId} LIMIT 1`, + ) + .then(OrganizationModel.parse); }, async getOrganizations({ userId: user }) { - const results = await pool.query>( - sql`/* getOrganizations */ - SELECT o.* + return pool + .any( + psql`/* getOrganizations */ + SELECT ${organizationFields(psql`o.`)} FROM organizations as o LEFT JOIN organization_member as om ON (om.organization_id = o.id) WHERE om.user_id = ${user} ORDER BY o.created_at DESC `, - ); - - return results.rows.map(transformOrganization); + ) + .then(z.array(OrganizationModel).parse); }, async getOrganizationByInviteCode({ inviteCode, email }) { - const result = await pool.maybeOne>( - sql`/* getOrganizationByInviteCode */ - SELECT o.* FROM organizations as o + return pool + .maybeOne( + psql`/* getOrganizationByInviteCode */ + SELECT ${organizationFields(psql`o.`)} FROM organizations as o LEFT JOIN organization_invitations as i ON (i.organization_id = o.id) WHERE i.code = ${inviteCode} @@ -1548,64 +1182,47 @@ export async function createStorage( GROUP BY o.id LIMIT 1 `, - ); - - if (result) { - return transformOrganization(result); - } - - return null; + ) + .then(OrganizationModel.nullable().parse); }, async getOrganizationBySlug({ slug }) { - const result = await pool.maybeOne>( - sql`/* getOrganizationBySlug */ SELECT * FROM organizations WHERE clean_id = ${slug} LIMIT 1`, - ); - - if (!result) { - return null; - } - - return transformOrganization(result); + return pool + .maybeOne( + psql`/* getOrganizationBySlug */ SELECT ${organizationFields(psql``)} FROM organizations WHERE clean_id = ${slug} LIMIT 1`, + ) + .then(OrganizationModel.nullable().parse); }, async getOrganizationByGitHubInstallationId({ installationId }) { - const result = await pool.maybeOne>( - sql`/* getOrganizationByGitHubInstallationId */ - SELECT * FROM organizations + return pool + .maybeOne( + psql`/* getOrganizationByGitHubInstallationId */ + SELECT ${organizationFields(psql``)} FROM organizations WHERE github_app_installation_id = ${installationId} LIMIT 1 `, - ); - - if (result) { - return transformOrganization(result); - } - - return null; + ) + .then(OrganizationModel.nullable().parse); }, async getProject({ projectId: project }) { - return transformProject( - await pool.one>( - sql`/* getProject */ SELECT * FROM projects WHERE id = ${project} AND type != 'CUSTOM' LIMIT 1`, - ), - ); + return pool + .maybeOne( + psql`/* getProject */ SELECT ${projectFields(psql``)} FROM projects WHERE id = ${project} AND type != 'CUSTOM' LIMIT 1`, + ) + .then(ProjectModel.parse); }, async getProjectBySlug({ slug, organizationId: organization }) { - const result = await pool.maybeOne>( - sql`/* getProjectBySlug */ SELECT * FROM projects WHERE clean_id = ${slug} AND org_id = ${organization} AND type != 'CUSTOM' LIMIT 1`, - ); - - if (!result) { - return null; - } - - return transformProject(result); + return pool + .maybeOne( + psql`/* getProjectBySlug */ SELECT ${projectFields(psql``)} FROM projects WHERE clean_id = ${slug} AND org_id = ${organization} AND type != 'CUSTOM' LIMIT 1`, + ) + .then(ProjectModel.nullable().parse); }, async getProjects({ organizationId: organization }) { - const result = await pool.query>( - sql`/* getProjects */ SELECT * FROM projects WHERE org_id = ${organization} AND type != 'CUSTOM' ORDER BY created_at DESC`, - ); - - return result.rows.map(transformProject); + return pool + .any( + psql`/* getProjects */ SELECT ${projectFields(psql``)} FROM projects WHERE org_id = ${organization} AND type != 'CUSTOM' ORDER BY created_at DESC`, + ) + .then(z.array(ProjectModel).parse); }, findProjectsByIds: batch<{ projectIds: Array }, Map>( async function FindProjectByIdsBatchHandler(args) { @@ -1616,12 +1233,13 @@ export async function createStorage( return args.map(async () => allProjectsLookupMap); } - const result = await pool.query>( - sql`/* findProjectsByIds */ SELECT * FROM projects WHERE id = ANY(${sql.array(allProjectIds, 'uuid')}) AND type != 'CUSTOM'`, - ); + const result = await pool + .any( + psql`/* findProjectsByIds */ SELECT ${projectFields(psql``)} FROM projects WHERE id = ANY(${psql.array(allProjectIds, 'uuid')}) AND type != 'CUSTOM'`, + ) + .then(z.array(ProjectModel).parse); - result.rows.forEach(row => { - const project = transformProject(row); + result.forEach(project => { allProjectsLookupMap.set(project.id, project); }); @@ -1643,9 +1261,9 @@ export async function createStorage( ); }, async updateProjectSlug({ slug, organizationId: organization, projectId: project }) { - return pool.transaction(async t => { + return pool.transaction('updateProjectSlug', async t => { const projectSlugExists = await t.exists( - sql`/* projectSlugExists */ SELECT 1 FROM projects WHERE clean_id = ${slug} AND id != ${project} AND org_id = ${organization} LIMIT 1`, + psql`/* projectSlugExists */ SELECT 1 FROM projects WHERE clean_id = ${slug} AND id != ${project} AND org_id = ${organization} LIMIT 1`, ); if (projectSlugExists) { @@ -1657,32 +1275,37 @@ export async function createStorage( return { ok: true, - project: transformProject( - await t.one>(sql`/* updateProjectSlug */ + project: await t + .maybeOne( + psql`/* updateProjectSlug */ UPDATE projects SET clean_id = ${slug}, name = ${slug} WHERE id = ${project} AND org_id = ${organization} - RETURNING * - `), - ), + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse), }; }); }, async updateNativeSchemaComposition({ projectId: project, enabled }) { - return transformProject( - await pool.one(sql`/* updateNativeSchemaComposition */ + return pool + .maybeOne( + psql`/* updateNativeSchemaComposition */ UPDATE projects SET native_federation = ${enabled}, external_composition_enabled = FALSE WHERE id = ${project} - RETURNING * - `), - ); + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse); }, async enableExternalSchemaComposition({ projectId: project, endpoint, encryptedSecret }) { - return transformProject( - await pool.one>(sql`/* enableExternalSchemaComposition */ + return pool + .maybeOne( + psql`/* enableExternalSchemaComposition */ UPDATE projects SET native_federation = FALSE, @@ -1690,48 +1313,57 @@ export async function createStorage( external_composition_endpoint = ${endpoint}, external_composition_secret = ${encryptedSecret} WHERE id = ${project} - RETURNING * - `), - ); + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse); }, async enableProjectNameInGithubCheck({ projectId: project }) { - return transformProject( - await pool.one(sql`/* enableProjectNameInGithubCheck */ + return pool + .maybeOne( + psql`/* enableProjectNameInGithubCheck */ UPDATE projects SET github_check_with_project_name = true WHERE id = ${project} - RETURNING * - `), - ); + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse); }, async deleteProject({ organizationId: organization, projectId: project }) { - const result = await tracedTransaction('deleteProject', pool, async t => { - const tokensResult = await t.query>(sql`/* deleteProject */ - SELECT token FROM tokens WHERE project_id = ${project} AND deleted_at IS NULL - `); + const result = await pool.transaction('deleteProject', async t => { + const tokens = await t + .any( + psql`/* deleteProject */ + SELECT token FROM tokens WHERE project_id = ${project} AND deleted_at IS NULL + `, + ) + .then(z.array(z.object({ token: z.string() })).parse); return { - project: await t.one( - sql`/* deleteProject */ - DELETE FROM projects - WHERE id = ${project} AND org_id = ${organization} - RETURNING * - `, - ), - tokens: tokensResult.rows.map(row => row.token), + project: await t + .maybeOne( + psql`/* deleteProject */ + DELETE FROM projects + WHERE id = ${project} AND org_id = ${organization} + RETURNING ${projectFields(psql``)} + `, + ) + .then(ProjectModel.parse), + tokens: tokens.map(row => row.token), }; }); return { - ...transformProject(result.project), + ...result.project, tokens: result.tokens, }; }, async createTarget({ organizationId: organization, projectId: project, slug }) { - return pool.transaction(async t => { + return pool.transaction('createTarget', async t => { const targetSlugExists = await t.exists( - sql`/* targetSlugExists */ SELECT 1 FROM targets WHERE clean_id = ${slug} AND project_id = ${project} LIMIT 1`, + psql`/* targetSlugExists */ SELECT 1 FROM targets WHERE clean_id = ${slug} AND project_id = ${project} LIMIT 1`, ); if (targetSlugExists) { @@ -1741,7 +1373,7 @@ export async function createStorage( }; } - const result = await t.maybeOne(sql`/* createTarget */ + const result = await t.maybeOne(psql`/* createTarget */ INSERT INTO targets (name, clean_id, project_id) VALUES @@ -1765,9 +1397,9 @@ export async function createStorage( projectId: project, targetId: target, }) { - return pool.transaction(async t => { + return pool.transaction('updateTargetSlug', async t => { const targetSlugExists = await t.exists( - sql`/* targetSlugExists */ SELECT 1 FROM targets WHERE clean_id = ${slug} AND id != ${target} AND project_id = ${project} LIMIT 1`, + psql`/* targetSlugExists */ SELECT 1 FROM targets WHERE clean_id = ${slug} AND id != ${target} AND project_id = ${project} LIMIT 1`, ); if (targetSlugExists) { @@ -1777,49 +1409,59 @@ export async function createStorage( }; } - const result = await t.one(sql`/* updateTargetSlug */ - UPDATE targets - SET clean_id = ${slug}, name = ${slug} - WHERE id = ${target} AND project_id = ${project} - RETURNING ${targetSQLFields} - `); + const result = await t + .maybeOne( + psql`/* updateTargetSlug */ + UPDATE targets + SET clean_id = ${slug}, name = ${slug} + WHERE id = ${target} AND project_id = ${project} + RETURNING ${targetSQLFields} + `, + ) + .then(TargetModel.parse); return { ok: true, target: { - ...TargetModel.parse(result), + ...result, orgId: organization, }, }; }); }, async deleteTarget({ organizationId: organization, targetId: target }) { - const result = await tracedTransaction('deleteTarget', pool, async t => { - const tokensResult = await t.query>(sql`/* findTokensForDeletion */ - SELECT token FROM tokens WHERE target_id = ${target} AND deleted_at IS NULL - `); - - const targetResult = await t.one( - sql`/* deleteTarget */ - DELETE FROM targets - WHERE id = ${target} - RETURNING - ${targetSQLFields} + const result = await pool.transaction('deleteTarget', async t => { + const tokens = await t + .any( + psql`/* findTokensForDeletion */ + SELECT token FROM tokens WHERE target_id = ${target} AND deleted_at IS NULL `, - ); + ) + .then(z.array(z.object({ token: z.string() })).parse); + + const targetResult = await t + .maybeOne( + psql`/* deleteTarget */ + DELETE FROM targets + WHERE id = ${target} + RETURNING + ${targetSQLFields} + `, + ) + .then(TargetModel.parse); await t.query( - sql`/* deleteTargetSchemaVersions */ DELETE FROM schema_versions WHERE target_id = ${target}`, + psql`/* deleteTargetSchemaVersions */ DELETE FROM schema_versions WHERE target_id = ${target}`, ); return { target: targetResult, - tokens: tokensResult.rows.map(row => row.token), + tokens: tokens.map(row => row.token), }; }); return { - ...TargetModel.parse(result.target), + ...result.target, orgId: organization, tokens: result.tokens, }; @@ -1847,22 +1489,22 @@ export async function createStorage( const uniqueSelectors = Array.from(uniqueSelectorsMap.values()); const rows = await pool - .any( - sql`/* getTarget */ + .any( + psql`/* getTarget */ SELECT ${targetSQLFields} FROM targets WHERE (id, project_id) IN ( - (${sql.join( - uniqueSelectors.map(s => sql`${s.targetId}, ${s.projectId}`), - sql`), (`, + (${psql.join( + uniqueSelectors.map(s => psql`${s.targetId}, ${s.projectId}`), + psql`), (`, )}) ) `, ) - .then(rows => rows.map(row => TargetModel.parse(row))); + .then(z.array(TargetModel).parse); return selectors.map(selector => { const row = rows.find( @@ -1885,7 +1527,7 @@ export async function createStorage( }, ), async getTargetBySlug({ organizationId: organization, projectId: project, slug }) { - const result = await pool.maybeOne(sql`/* getTargetBySlug */ + const result = await pool.maybeOne(psql`/* getTargetBySlug */ SELECT ${targetSQLFields} FROM @@ -1906,7 +1548,9 @@ export async function createStorage( }; }, async getTargets({ organizationId, projectId }) { - const results = await pool.query(sql`/* getTargets */ + const results = await pool + .any( + psql`/* getTargets */ SELECT ${targetSQLFields} FROM @@ -1915,10 +1559,12 @@ export async function createStorage( project_id = ${projectId} ORDER BY created_at DESC - `); + `, + ) + .then(z.array(TargetModel).parse); - return results.rows.map(r => ({ - ...TargetModel.parse(r), + return results.map(r => ({ + ...r, orgId: organizationId, })); }, @@ -1941,17 +1587,21 @@ export async function createStorage( const orgId = args[0].organizationId; - const results = await pool.query(sql`/* getTargets */ + const results = await pool + .any( + psql`/* getTargets */ SELECT ${targetSQLFields} FROM "targets" WHERE - "id" = ANY(${sql.array(allTargetIds, 'uuid')}) - `); + "id" = ANY(${psql.array(allTargetIds, 'uuid')}) + `, + ) + .then(z.array(TargetModel).parse); - for (const row of results.rows) { - const target: Target = { ...TargetModel.parse(row), orgId }; + for (const row of results) { + const target: Target = { ...row, orgId }; resultLookupMap.set(target.id, target); } @@ -1967,81 +1617,56 @@ export async function createStorage( }, ), async getTargetIdsOfOrganization({ organizationId: organization }) { - const results = await pool.query>>( - sql`/* getTargetIdsOfOrganization */ + const results = await pool + .any( + psql`/* getTargetIdsOfOrganization */ SELECT t.id as id FROM targets as t LEFT JOIN projects as p ON (p.id = t.project_id) WHERE p.org_id = ${organization} GROUP BY t.id `, - ); + ) + .then(z.array(TargetIdModel).parse); - return results.rows.map(r => r.id); + return results.map(r => r.id); }, async getTargetIdsOfProject({ projectId: project }) { - const results = await pool.query>>( - sql`/* getTargetIdsOfProject */ + const results = await pool + .any( + psql`/* getTargetIdsOfProject */ SELECT id FROM targets WHERE project_id = ${project} `, - ); + ) + .then(z.array(TargetIdModel).parse); - return results.rows.map(r => r.id); + return results.map(r => r.id); }, async getTargetSettings({ targetId: target, projectId: project }) { - const row = await pool.one< - Pick< - targets, - | 'validation_enabled' - | 'validation_percentage' - | 'validation_period' - | 'validation_excluded_clients' - | 'validation_excluded_app_deployments' - | 'validation_request_count' - | 'validation_breaking_change_formula' - | 'fail_diff_on_dangerous_change' - | 'app_deployment_protection_enabled' - | 'app_deployment_protection_min_days_inactive' - | 'app_deployment_protection_max_traffic_percentage' - | 'app_deployment_protection_traffic_period_days' - | 'app_deployment_protection_rule_logic' - | 'app_deployment_protection_min_days_since_creation' - > & { - targets: target_validation['destination_target_id'][]; - } - >(sql`/* getTargetSettings */ - SELECT - t.validation_enabled, - t.validation_percentage, - t.validation_period, - t.validation_excluded_clients, - t.validation_excluded_app_deployments, - t.validation_request_count, - t.validation_breaking_change_formula, - array_agg(tv.destination_target_id) as targets, - t.fail_diff_on_dangerous_change, - t.app_deployment_protection_enabled, - t.app_deployment_protection_min_days_inactive, - t.app_deployment_protection_min_days_since_creation, - t.app_deployment_protection_max_traffic_percentage, - t.app_deployment_protection_traffic_period_days, - t.app_deployment_protection_rule_logic - FROM targets AS t - LEFT JOIN target_validation AS tv ON (tv.target_id = t.id) - WHERE t.id = ${target} AND t.project_id = ${project} - GROUP BY t.id - LIMIT 1 - `); - - return transformTargetSettings(row); + return pool + .maybeOne( + psql`/* getTargetSettings */ + SELECT + ${targetSettingsFields(psql`t.`)} + , array_agg(DISTINCT tv.destination_target_id) + FILTER (WHERE tv.destination_target_id IS NOT NULL) + AS "targets" + FROM targets AS t + LEFT JOIN target_validation AS tv ON (tv.target_id = t.id) + WHERE t.id = ${target} AND t.project_id = ${project} + GROUP BY t.id + LIMIT 1 + `, + ) + .then(TargetSettingsModel.parse); }, async updateTargetDangerousChangeClassification({ targetId: target, projectId: project, failDiffOnDangerousChange, }) { - return transformTargetSettings( - await tracedTransaction('updateTargetDangerousChangeClassification', pool, async trx => { - return trx.one(sql`/* updateTargetValidationSettings */ + return pool + .transaction('updateTargetDangerousChangeClassification', async trx => { + return trx.maybeOne(psql`/* updateTargetValidationSettings */ UPDATE targets as t SET fail_diff_on_dangerous_change = ${failDiffOnDangerousChange} FROM ( @@ -2055,10 +1680,12 @@ export async function createStorage( LIMIT 1 ) ret WHERE t.id = ret.id - RETURNING t.id, t.validation_enabled, t.validation_percentage, t.validation_period, t.validation_excluded_clients, t.validation_excluded_app_deployments, ret.targets, t.validation_request_count, t.validation_breaking_change_formula, t.fail_diff_on_dangerous_change, t.app_deployment_protection_enabled, t.app_deployment_protection_min_days_inactive, t.app_deployment_protection_max_traffic_percentage, t.app_deployment_protection_traffic_period_days, t.app_deployment_protection_rule_logic; + RETURNING + ${targetSettingsFields(psql`t.`)} + , ret.targets `); - }), - ); + }) + .then(TargetSettingsModel.parse); }, async updateTargetValidationSettings({ targetId: target, @@ -2072,48 +1699,49 @@ export async function createStorage( requestCount, isEnabled, }) { - return transformTargetSettings( - await tracedTransaction('updateTargetValidationSettings', pool, async trx => { - if (targets) { - await trx.query(sql`/* deleteTargetValidation */ + return ( + await pool + .transaction('updateTargetValidationSettings', async trx => { + if (targets) { + await trx.query(psql`/* deleteTargetValidation */ DELETE FROM target_validation - WHERE destination_target_id NOT IN (${sql.join(targets, sql`, `)}) + WHERE destination_target_id NOT IN (${psql.join(targets, psql`, `)}) AND target_id = ${target} `); - await trx.query(sql`/* insertTargetValidation */ + await trx.query(psql`/* insertTargetValidation */ INSERT INTO target_validation (target_id, destination_target_id) VALUES ( - ${sql.join( - targets.map(dest => sql.join([target, dest], sql`, `)), - sql`), (`, + ${psql.join( + targets.map(dest => psql.join([target, dest], psql`, `)), + psql`), (`, )} ) ON CONFLICT (target_id, destination_target_id) DO NOTHING `); - } else { - const targetValidationRowExists = await trx.exists(sql`/* findTargetValidation */ + } else { + const targetValidationRowExists = await trx.exists(psql`/* findTargetValidation */ SELECT 1 FROM target_validation WHERE target_id = ${target} `); - if (!targetValidationRowExists) { - await trx.query(sql`/* insertTargetValidation */ + if (!targetValidationRowExists) { + await trx.query(psql`/* insertTargetValidation */ INSERT INTO target_validation (target_id, destination_target_id) VALUES (${target}, ${target}) `); + } } - } - return trx.one(sql`/* updateTargetValidationSettings */ + return trx.maybeOne(psql`/* updateTargetValidationSettings */ UPDATE targets as t SET validation_percentage = COALESCE(${percentage ?? null}, validation_percentage) , validation_period = COALESCE(${period ?? null}, validation_period) - , validation_excluded_clients = COALESCE(${excludedClients?.length ? sql.array(excludedClients, 'text') : null}, validation_excluded_clients) - , validation_excluded_app_deployments = COALESCE(${excludedAppDeployments?.length ? sql.array(excludedAppDeployments, 'text') : null}, validation_excluded_app_deployments) + , validation_excluded_clients = COALESCE(${excludedClients?.length ? psql.array(excludedClients, 'text') : null}, validation_excluded_clients) + , validation_excluded_app_deployments = COALESCE(${excludedAppDeployments?.length ? psql.array(excludedAppDeployments, 'text') : null}, validation_excluded_app_deployments) , validation_request_count = COALESCE(${requestCount ?? null}, validation_request_count) , validation_breaking_change_formula = COALESCE(${breakingChangeFormula ?? null}, validation_breaking_change_formula) , validation_enabled = COALESCE(${isEnabled ?? null}, validation_enabled) @@ -2133,23 +1761,11 @@ export async function createStorage( WHERE t.id = ret.id RETURNING - t.id - , t.validation_enabled - , t.validation_percentage - , t.validation_period - , t.validation_excluded_clients - , t.validation_excluded_app_deployments + ${targetSettingsFields(psql`t.`)} , ret.targets - , t.validation_request_count - , t.validation_breaking_change_formula - , t.fail_diff_on_dangerous_change - , t.app_deployment_protection_enabled - , t.app_deployment_protection_min_days_inactive - , t.app_deployment_protection_max_traffic_percentage - , t.app_deployment_protection_traffic_period_days - , t.app_deployment_protection_rule_logic `); - }), + }) + .then(TargetSettingsModel.parse) ).validation; }, @@ -2172,8 +1788,9 @@ export async function createStorage( trafficPeriodDays?: number | null; ruleLogic?: 'AND' | 'OR' | null; }) { - return transformTargetSettings( - await pool.one(sql`/* updateTargetAppDeploymentProtectionSettings */ + return pool + .maybeOne( + psql`/* updateTargetAppDeploymentProtectionSettings */ UPDATE targets SET @@ -2187,81 +1804,82 @@ export async function createStorage( id = ${target} AND project_id = ${project} RETURNING - id - , validation_enabled - , validation_percentage - , validation_period - , validation_excluded_clients + ${targetSettingsFields(psql``)} , null as targets - , validation_request_count - , validation_breaking_change_formula - , fail_diff_on_dangerous_change - , app_deployment_protection_enabled - , app_deployment_protection_min_days_inactive - , app_deployment_protection_max_traffic_percentage - , app_deployment_protection_traffic_period_days - , app_deployment_protection_rule_logic - , app_deployment_protection_min_days_since_creation - `), - ).appDeploymentProtection; + `, + ) + .then(TargetSettingsModel.parse) + .then(r => r.appDeploymentProtection); }, async countSchemaVersionsOfProject({ projectId: project, period }) { if (period) { - const result = await pool.maybeOne<{ - total: number; - }>(sql`/* countPeriodSchemaVersionsOfProject */ - SELECT COUNT(*) as total FROM schema_versions as sv - LEFT JOIN targets as t ON (t.id = sv.target_id) - WHERE - t.project_id = ${project} - AND sv.created_at >= ${period.from.toISOString()} - AND sv.created_at < ${period.to.toISOString()} - `); + const result = await pool + .maybeOne( + psql`/* countPeriodSchemaVersionsOfProject */ + SELECT COUNT(*) as total FROM schema_versions as sv + LEFT JOIN targets as t ON (t.id = sv.target_id) + WHERE + t.project_id = ${project} + AND sv.created_at >= ${period.from.toISOString()} + AND sv.created_at < ${period.to.toISOString()} + `, + ) + .then(z.object({ total: z.number() }).nullable().parse); return result?.total ?? 0; } - const result = await pool.maybeOne<{ total: number }>(sql`/* countSchemaVersionsOfProject */ - SELECT COUNT(*) as total FROM schema_versions as sv - LEFT JOIN targets as t ON (t.id = sv.target_id) - WHERE t.project_id = ${project} - `); + const result = await pool + .maybeOne( + psql`/* countSchemaVersionsOfProject */ + SELECT COUNT(*) as total FROM schema_versions as sv + LEFT JOIN targets as t ON (t.id = sv.target_id) + WHERE t.project_id = ${project} + `, + ) + .then(z.object({ total: z.number() }).nullable().parse); return result?.total ?? 0; }, async countSchemaVersionsOfTarget({ targetId: target, period }) { if (period) { - const result = await pool.maybeOne<{ - total: number; - }>(sql`/* countPeriodSchemaVersionsOfTarget */ - SELECT COUNT(*) as total FROM schema_versions - WHERE - target_id = ${target} - AND created_at >= ${period.from.toISOString()} - AND created_at < ${period.to.toISOString()} - `); + const result = await pool + .maybeOne( + psql`/* countPeriodSchemaVersionsOfTarget */ + SELECT COUNT(*) as total FROM schema_versions + WHERE + target_id = ${target} + AND created_at >= ${period.from.toISOString()} + AND created_at < ${period.to.toISOString()} + `, + ) + .then(z.object({ total: z.number() }).nullable().parse); return result?.total ?? 0; } - const result = await pool.maybeOne<{ total: number }>(sql`/* countSchemaVersionsOfTarget */ - SELECT COUNT(*) as total FROM schema_versions WHERE target_id = ${target} - `); + const result = await pool + .maybeOne( + psql`/* countSchemaVersionsOfTarget */ + SELECT COUNT(*) as total FROM schema_versions WHERE target_id = ${target} + `, + ) + .then(z.object({ total: z.number() }).nullable().parse); return result?.total ?? 0; }, async hasSchema({ targetId: target }) { return pool.exists( - sql`/* hasSchema */ + psql`/* hasSchema */ SELECT 1 FROM schema_versions as v WHERE v.target_id = ${target} LIMIT 1 `, ); }, async getMaybeLatestValidVersion(args) { - const version = await pool.maybeOne( - sql`/* getMaybeLatestValidVersion */ + const version = await pool.maybeOne( + psql`/* getMaybeLatestValidVersion */ SELECT - ${schemaVersionSQLFields(sql`sv.`)} + ${schemaVersionSQLFields(psql`sv.`)} FROM schema_versions as sv WHERE sv.target_id = ${args.targetId} AND sv.is_composable IS TRUE ORDER BY sv.created_at DESC @@ -2276,10 +1894,10 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, async getLatestValidVersion({ targetId: target }) { - const version = await pool.maybeOne( - sql`/* getLatestValidVersion */ + const version = await pool.maybeOne( + psql`/* getLatestValidVersion */ SELECT - ${schemaVersionSQLFields(sql`sv.`)} + ${schemaVersionSQLFields(psql`sv.`)} FROM schema_versions as sv WHERE sv.target_id = ${target} AND sv.is_composable IS TRUE ORDER BY sv.created_at DESC @@ -2290,10 +1908,10 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, async getMaybeLatestVersion(args) { - const version = await pool.maybeOne( - sql`/* getMaybeLatestVersion */ + const version = await pool.maybeOne( + psql`/* getMaybeLatestVersion */ SELECT - ${schemaVersionSQLFields(sql`sv.`)} + ${schemaVersionSQLFields(psql`sv.`)} FROM "schema_versions" AS "sv" WHERE @@ -2311,8 +1929,8 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, async getVersionBeforeVersionId(args) { - const version = await pool.maybeOne( - sql`/* getVersionBeforeVersionId */ + const version = await pool.maybeOne( + psql`/* getVersionBeforeVersionId */ SELECT ${schemaVersionSQLFields()} FROM "schema_versions" @@ -2325,7 +1943,7 @@ export async function createStorage( ) OR "created_at" < ${args.beforeVersionCreatedAt} ) - ${args.onlyComposable ? sql`AND "is_composable" = TRUE` : sql``} + ${args.onlyComposable ? psql`AND "is_composable" = TRUE` : psql``} ORDER BY "created_at" DESC LIMIT 1 @@ -2339,180 +1957,101 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, async getSchemaByNameOfVersion(args) { - const result = await pool.maybeOne< - Pick< - OverrideProp, - | 'id' - | 'commit' - | 'action' - | 'author' - | 'sdl' - | 'created_at' - | 'project_id' - | 'service_name' - | 'service_url' - | 'target_id' - | 'metadata' - > & - Pick - >( - sql`/* getSchemaByNameOfVersion */ - SELECT - sl.id, - sl.commit, - sl.author, - sl.action, - sl.sdl, - sl.created_at, - sl.project_id, - lower(sl.service_name) as service_name, - sl.service_url, - sl.target_id, - p.type - FROM schema_version_to_log AS svl - LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) - LEFT JOIN projects as p ON (p.id = sl.project_id) - WHERE - svl.version_id = ${args.versionId} - AND sl.action = 'PUSH' - AND p.type != 'CUSTOM' - AND lower(sl.service_name) = lower(${args.serviceName}) - ORDER BY - sl.created_at DESC - `, - ); - - if (!result) { - return null; - } - - return transformSchema(result); + return pool + .maybeOne( + psql`/* getSchemaByNameOfVersion */ + SELECT + ${schemaLogFields(psql`sl.`)} + , p.type + FROM schema_version_to_log AS svl + LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) + LEFT JOIN projects as p ON (p.id = sl.project_id) + WHERE + svl.version_id = ${args.versionId} + AND sl.action = 'PUSH' + AND p.type != 'CUSTOM' + AND lower(sl.service_name) = lower(${args.serviceName}) + ORDER BY + sl.created_at DESC + `, + ) + .then(SchemaPushLogModel.nullable().parse); }, - async getSchemasOfVersion({ versionId: version, includeMetadata = false }) { - const result = await pool.query< - Pick< - OverrideProp, - | 'id' - | 'commit' - | 'action' - | 'author' - | 'sdl' - | 'created_at' - | 'project_id' - | 'service_name' - | 'service_url' - | 'target_id' - | 'metadata' - > & - Pick - >( - sql`/* getSchemasOfVersion */ - SELECT - sl.id, - sl.commit, - sl.author, - sl.action, - sl.sdl, - sl.created_at, - sl.project_id, - lower(sl.service_name) as service_name, - sl.service_url, - ${includeMetadata ? sql`sl.metadata,` : sql``} - sl.target_id, - p.type - FROM schema_version_to_log AS svl - LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) - LEFT JOIN projects as p ON (p.id = sl.project_id) - WHERE - svl.version_id = ${version} - AND sl.action = 'PUSH' - AND p.type != 'CUSTOM' - ORDER BY - sl.created_at DESC - `, - ); - - return result.rows.map(transformSchema); + async getSchemasOfVersion({ versionId: version, includeMetadata: _includeMetadata = false }) { + return pool + .any( + psql`/* getSchemasOfVersion */ + SELECT + ${schemaLogFields(psql`sl.`)} + , p.type + FROM schema_version_to_log AS svl + LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) + LEFT JOIN projects as p ON (p.id = sl.project_id) + WHERE + svl.version_id = ${version} + AND sl.action = 'PUSH' + AND p.type != 'CUSTOM' + ORDER BY + sl.created_at DESC + `, + ) + .then(z.array(SchemaPushLogModel).parse); }, async getServiceSchemaOfVersion(args) { - const result = await pool.maybeOne< - Pick< - OverrideProp, - | 'id' - | 'commit' - | 'action' - | 'author' - | 'sdl' - | 'created_at' - | 'project_id' - | 'service_name' - | 'service_url' - | 'target_id' - | 'metadata' - > & - Pick - >(sql`/* getServiceSchemaOfVersion */ - SELECT - sl.id, - sl.commit, - sl.author, - sl.action, - sl.sdl, - sl.created_at, - sl.project_id, - lower(sl.service_name) as service_name, - sl.service_url, - sl.target_id, - p.type - FROM schema_version_to_log AS svl - LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) - LEFT JOIN projects as p ON (p.id = sl.project_id) - WHERE - svl.version_id = ${args.schemaVersionId} - AND sl.action = 'PUSH' - AND p.type != 'CUSTOM' - AND lower(sl.service_name) = lower(${args.serviceName}) - `); - - if (!result) { - return null; - } - - return transformSchema(result); + return pool + .maybeOne( + psql`/* getServiceSchemaOfVersion */ + SELECT + ${schemaLogFields(psql`sl.`)} + , p.type + FROM schema_version_to_log AS svl + LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) + LEFT JOIN projects as p ON (p.id = sl.project_id) + WHERE + svl.version_id = ${args.schemaVersionId} + AND sl.action = 'PUSH' + AND p.type != 'CUSTOM' + AND lower(sl.service_name) = lower(${args.serviceName}) + `, + ) + .then(SchemaPushLogModel.nullable().parse); }, async getMatchingServiceSchemaOfVersions(versions) { - const after = await pool.one<{ - sdl: string; - service_name: string; - }>(sql`/* getMatchingServiceSchemaOfVersions */ - SELECT sl.service_name, sl.sdl - FROM schema_versions as sv - LEFT JOIN schema_log as sl ON sv.action_id = sl.id - WHERE sv.id = ${versions.after} AND service_name IS NOT NULL - `); + const after = await pool + .maybeOne( + psql`/* getMatchingServiceSchemaOfVersions */ + SELECT sl.service_name, sl.sdl + FROM schema_versions as sv + LEFT JOIN schema_log as sl ON sv.action_id = sl.id + WHERE sv.id = ${versions.after} AND service_name IS NOT NULL + `, + ) + .then(z.object({ service_name: z.string(), sdl: z.string() }).parse); // It's an initial version, so we just need to fetch a single version if (!versions.before) { return { serviceName: after.service_name, after: after.sdl, before: null }; } - const before = await pool.maybeOne<{ - sdl: string | null; - }>(sql`/* getMatchingServiceSchemaOfVersions */ - SELECT sl.sdl - FROM schema_version_to_log as svtl - LEFT JOIN schema_log as sl ON svtl.action_id = sl.id - WHERE svtl.version_id = ${versions.before} AND sl.service_name = ${after.service_name} - `); + const before = await pool + .maybeOne( + psql`/* getMatchingServiceSchemaOfVersions */ + SELECT sl.sdl + FROM schema_version_to_log as svtl + LEFT JOIN schema_log as sl ON svtl.action_id = sl.id + WHERE svtl.version_id = ${versions.before} AND sl.service_name = ${after.service_name} + `, + ) + .then(z.object({ sdl: z.string().nullable() }).nullable().parse); return { serviceName: after.service_name, after: after.sdl, before: before?.sdl ?? null }; }, async getMaybeVersion({ projectId: project, targetId: target, versionId: version }) { - const result = await pool.maybeOne(sql`/* getMaybeVersion */ + const result = await pool.maybeOne(psql`/* getMaybeVersion */ SELECT - ${schemaVersionSQLFields(sql`sv.`)} + ${schemaVersionSQLFields(psql`sv.`)} FROM schema_versions as sv LEFT JOIN schema_log as sl ON (sl.id = sv.action_id) LEFT JOIN targets as t ON (t.id = sv.target_id) @@ -2541,7 +2080,7 @@ export async function createStorage( cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const query = sql`/* getPaginatedSchemaVersionsForTargetId */ + const query = psql`/* getPaginatedSchemaVersionsForTargetId */ SELECT ${schemaVersionSQLFields()} FROM @@ -2550,7 +2089,7 @@ export async function createStorage( "target_id" = ${args.targetId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -2559,7 +2098,7 @@ export async function createStorage( OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "created_at" DESC @@ -2567,7 +2106,7 @@ export async function createStorage( LIMIT ${limit + 1} `; - const result = await pool.any(query); + const result = await pool.any(query); let edges = result.map(row => { const node = SchemaVersionModel.parse(row); @@ -2598,40 +2137,57 @@ export async function createStorage( }; }, async deleteSchema(args) { - return tracedTransaction('deleteSchema', pool, async trx => { + return pool.transaction('deleteSchema', async trx => { // fetch the latest version - const latestVersion = await trx.one>( - sql`/* findLatestSchemaVersion */ - SELECT sv.id, sv.base_schema - FROM schema_versions as sv - WHERE sv.target_id = ${args.targetId} - ORDER BY sv.created_at DESC - LIMIT 1 - `, - ); + const latestVersion = await trx + .maybeOne( + psql`/* findLatestSchemaVersion */ + SELECT sv.id, sv.base_schema + FROM schema_versions as sv + WHERE sv.target_id = ${args.targetId} + ORDER BY sv.created_at DESC + LIMIT 1 + `, + ) + .then(z.object({ id: z.string(), base_schema: z.string().nullable() }).parse); // create a new action - const deleteActionResult = await trx.one(sql`/* createDeleteActionSchemaLog */ - INSERT INTO schema_log - ( - author, - commit, - service_name, - project_id, - target_id, - action - ) - VALUES - ( - ${'system'}::text, - ${'system'}::text, - lower(${args.serviceName}::text), - ${args.projectId}, - ${args.targetId}, - 'DELETE' - ) - RETURNING * - `); + const deleteActionResult = await trx + .maybeOne( + psql`/* createDeleteActionSchemaLog */ + INSERT INTO schema_log + ( + author, + commit, + service_name, + project_id, + target_id, + action + ) + VALUES + ( + ${'system'}::text, + ${'system'}::text, + lower(${args.serviceName}::text), + ${args.projectId}, + ${args.targetId}, + 'DELETE' + ) + RETURNING + id + , to_json("created_at") AS "createdAt" + , "service_name" AS "serviceName" + , "target_id" AS "targetId" + `, + ) + .then( + z.object({ + id: z.string(), + createdAt: z.string(), + serviceName: z.string(), + targetId: z.string(), + }).parse, + ); // creates a new version const newVersion = await insertSchemaVersion(trx, { @@ -2655,7 +2211,7 @@ export async function createStorage( }); // Move all the schema_version_to_log entries of the previous version to the new version - await trx.query(sql`/* moveSchemaVersionToLog */ + await trx.query(psql`/* moveSchemaVersionToLog */ INSERT INTO schema_version_to_log (version_id, action_id) SELECT ${newVersion.id}::uuid as version_id, svl.action_id @@ -2664,7 +2220,7 @@ export async function createStorage( WHERE svl.version_id = ${latestVersion.id} AND sl.action = 'PUSH' AND lower(sl.service_name) != lower(${args.serviceName}) `); - await trx.query(sql`/* insertSchemaVersionToLog */ + await trx.query(psql`/* insertSchemaVersionToLog */ INSERT INTO schema_version_to_log (version_id, action_id) VALUES @@ -2706,11 +2262,13 @@ export async function createStorage( return { kind: 'composite', id: deleteActionResult.id, - versionId: deleteActionResult.id, - date: deleteActionResult.created_at as any, - service_name: deleteActionResult.service_name!, - target: deleteActionResult.target_id, + date: deleteActionResult.createdAt as any, + service_name: deleteActionResult.serviceName, + target: deleteActionResult.targetId, action: 'DELETE', + versionId: newVersion.id, + } satisfies CompositeDeletedSchemaLog & { + versionId: string; }; }); }, @@ -2718,34 +2276,38 @@ export async function createStorage( const url = input.url ?? null; const service = input.service ?? null; - const output = await tracedTransaction('createVersion', pool, async trx => { - const log = await pool.one>(sql`/* createVersion */ - INSERT INTO schema_log - ( - author, - service_name, - service_url, - commit, - sdl, - project_id, - target_id, - metadata, - action - ) - VALUES - ( - ${input.author}, - lower(${service}::text), - ${url}::text, - ${input.commit}::text, - ${input.schema}::text, - ${input.projectId}, - ${input.targetId}, - ${input.metadata}, - 'PUSH' - ) - RETURNING id - `); + const output = await pool.transaction('createVersion', async trx => { + const log = await pool + .maybeOne( + psql`/* createVersion */ + INSERT INTO schema_log + ( + author, + service_name, + service_url, + commit, + sdl, + project_id, + target_id, + metadata, + action + ) + VALUES + ( + ${input.author}, + lower(${service}::text), + ${url}::text, + ${input.commit}::text, + ${input.schema}::text, + ${input.projectId}, + ${input.targetId}, + ${input.metadata}, + 'PUSH' + ) + RETURNING id + `, + ) + .then(z.object({ id: z.string() }).parse); // creates a new version const version = await insertSchemaVersion(trx, { @@ -2767,11 +2329,11 @@ export async function createStorage( conditionalBreakingChangeMetadata: input.conditionalBreakingChangeMetadata, }); - await trx.query(sql`/* insertSchemaVersionToLog */ + await trx.query(psql`/* insertSchemaVersionToLog */ INSERT INTO schema_version_to_log (version_id, action_id) SELECT * FROM - ${sql.unnest( + ${psql.unnest( input.logIds.concat(log.id).map(actionId => // Note: change.criticality.level is actually a computed value from meta [version.id, actionId], @@ -2821,7 +2383,9 @@ export async function createStorage( async getSchemaChangesForVersion(args) { // TODO: should this be paginated? - const changes = await pool.query(sql`/* getSchemaChangesForVersion */ + const changes = await pool + .any( + psql`/* getSchemaChangesForVersion */ SELECT "change_type" as "type", "meta", @@ -2831,28 +2395,32 @@ export async function createStorage( "schema_version_changes" WHERE "schema_version_id" = ${args.versionId} - `); + `, + ) + .then(z.array(HiveSchemaChangeModel).parse); - if (changes.rows.length === 0) { + if (changes.length === 0) { return null; } - return changes.rows.map(row => HiveSchemaChangeModel.parse(row)); + return changes; }, getSchemaLog: batch(async selectors => { - const rows = await pool.many>( - sql`/* getSchemaLog */ - SELECT sl.*, lower(sl.service_name) as service_name, p.type + const rows = await pool.any( + psql`/* getSchemaLog */ + SELECT + ${schemaLogFields(psql`sl.`)} + , p.type FROM schema_log as sl LEFT JOIN projects as p ON (p.id = sl.project_id) - WHERE (sl.id, sl.target_id) IN ((${sql.join( - selectors.map(s => sql`${s.commit}, ${s.targetId}`), - sql`), (`, + WHERE (sl.id, sl.target_id) IN ((${psql.join( + selectors.map(s => psql`${s.commit}, ${s.targetId}`), + psql`), (`, )})) `, ); - const schemas = rows.map(transformSchemaLog); + const schemas = z.array(SchemaLogModel).parse(rows); return selectors.map(selector => { const schema = schemas.find( @@ -2871,8 +2439,8 @@ export async function createStorage( }); }), async addSlackIntegration({ organizationId: organization, token }) { - await pool.query>( - sql`/* addSlackIntegration */ + await pool.any( + psql`/* addSlackIntegration */ UPDATE organizations SET slack_token = ${token} WHERE id = ${organization} @@ -2880,8 +2448,8 @@ export async function createStorage( ); }, async deleteSlackIntegration({ organizationId: organization }) { - await pool.query>( - sql`/* deleteSlackIntegration */ + await pool.any( + psql`/* deleteSlackIntegration */ UPDATE organizations SET slack_token = NULL WHERE id = ${organization} @@ -2889,19 +2457,21 @@ export async function createStorage( ); }, async getSlackIntegrationToken({ organizationId: organization }) { - const result = await pool.maybeOne>( - sql`/* getSlackIntegrationToken */ - SELECT slack_token - FROM organizations - WHERE id = ${organization} - `, - ); + const result = await pool + .maybeOne( + psql`/* getSlackIntegrationToken */ + SELECT slack_token + FROM organizations + WHERE id = ${organization} + `, + ) + .then(z.object({ slack_token: z.string().nullable() }).nullable().parse); return result?.slack_token; }, async addGitHubIntegration({ organizationId: organization, installationId }) { - await pool.query>( - sql`/* addGitHubIntegration */ + await pool.any( + psql`/* addGitHubIntegration */ UPDATE organizations SET github_app_installation_id = ${installationId} WHERE id = ${organization} @@ -2909,15 +2479,15 @@ export async function createStorage( ); }, async deleteGitHubIntegration({ organizationId: organization }) { - await pool.query>( - sql`/* deleteGitHubIntegration */ + await pool.any( + psql`/* deleteGitHubIntegration */ UPDATE organizations SET github_app_installation_id = NULL WHERE id = ${organization} `, ); - await pool.query>( - sql`/* resetProjectsGitHubRepository */ + await pool.any( + psql`/* resetProjectsGitHubRepository */ UPDATE projects SET git_repository = NULL WHERE org_id = ${organization} @@ -2925,92 +2495,108 @@ export async function createStorage( ); }, async getGitHubIntegrationInstallationId({ organizationId: organization }) { - const result = await pool.maybeOne>( - sql`/* getGitHubIntegrationInstallationId */ - SELECT github_app_installation_id - FROM organizations - WHERE id = ${organization} - `, - ); - - return result?.github_app_installation_id; + return await pool + .maybeOneFirst( + psql`/* getGitHubIntegrationInstallationId */ + SELECT github_app_installation_id + FROM organizations + WHERE id = ${organization} + `, + ) + .then(z.string().nullable().parse); }, async addAlertChannel({ projectId, name, type, slackChannel, webhookEndpoint }) { - return transformAlertChannel( - await pool.one>( - sql`/* addAlertChannel */ + return AlertChannelModel.parse( + await pool.maybeOne( + psql`/* addAlertChannel */ INSERT INTO alert_channels ("name", "type", "project_id", "slack_channel", "webhook_endpoint") VALUES (${name}, ${type}, ${projectId}, ${slackChannel ?? null}, ${webhookEndpoint ?? null}) - RETURNING * + RETURNING + ${alertChannelFields()} `, ), ); }, async deleteAlertChannels({ projectId, channelIds }) { - const result = await pool.query( - sql`/* deleteAlertChannels */ + return pool + .any( + psql`/* deleteAlertChannels */ DELETE FROM alert_channels WHERE project_id = ${projectId} AND - id IN (${sql.join(channelIds, sql`, `)}) - RETURNING * + id IN (${psql.join(channelIds, psql`, `)}) + RETURNING + ${alertChannelFields()} `, - ); - - return result.rows.map(transformAlertChannel); + ) + .then(z.array(AlertChannelModel).parse); }, async getAlertChannels({ projectId: project }) { - const result = await pool.query>( - sql`/* getAlertChannels */ SELECT * FROM alert_channels WHERE project_id = ${project} ORDER BY created_at DESC`, - ); - - return result.rows.map(transformAlertChannel); + return pool + .any( + psql`/* getAlertChannels */ + SELECT + ${alertChannelFields()} + FROM alert_channels + WHERE project_id = ${project} + ORDER BY created_at DESC`, + ) + .then(z.array(AlertChannelModel).parse); }, async addAlert({ organizationId, projectId, targetId, channelId, type }) { - return transformAlert( - await pool.one>( - sql`/* addAlert */ + return { + ...(await pool + .maybeOne( + psql`/* addAlert */ INSERT INTO alerts ("type", "alert_channel_id", "target_id", "project_id") VALUES (${type}, ${channelId}, ${targetId}, ${projectId}) - RETURNING * + RETURNING + ${alertFields()} `, - ), + ) + .then(AlertModel.parse)), organizationId, - ); + }; }, async deleteAlerts({ organizationId: organization, projectId: project, alertIds: alerts }) { - const result = await pool.query>( - sql`/* deleteAlerts */ + const result = await pool + .any( + psql`/* deleteAlerts */ DELETE FROM alerts WHERE project_id = ${project} AND - id IN (${sql.join(alerts, sql`, `)}) - RETURNING * + id IN (${psql.join(alerts, psql`, `)}) + RETURNING + ${alertFields()} `, - ); + ) + .then(z.array(AlertModel).parse); - return result.rows.map(row => transformAlert(row, organization)); + return result.map(row => ({ ...row, organizationId: organization })); }, async getAlerts({ organizationId: organization, projectId: project }) { - const result = await pool.query>( - sql`/* getAlerts */ SELECT * FROM alerts WHERE project_id = ${project} ORDER BY created_at DESC`, - ); + const result = await pool + .any( + psql`/* getAlerts */ + SELECT + ${alertFields()} + FROM alerts + WHERE project_id = ${project} + ORDER BY created_at DESC`, + ) + .then(z.array(AlertModel).parse); - return result.rows.map(row => transformAlert(row, organization)); + return result.map(row => ({ ...row, organizationId: organization })); }, async adminGetOrganizationsTargetPairs() { - const results = await pool.query< - Slonik<{ - organization: string; - target: string; - }> - >( - sql`/* adminGetOrganizationsTargetPairs */ + const results = await pool + .any( + psql`/* adminGetOrganizationsTargetPairs */ SELECT o.id as organization, t.id as target @@ -3018,54 +2604,61 @@ export async function createStorage( LEFT JOIN projects AS p ON (p.id = t.project_id) LEFT JOIN organizations AS o ON (o.id = p.org_id) `, - ); - return results.rows; + ) + .then(z.array(OrganizationTargetPairModel).parse); + return results; }, async getGetOrganizationsAndTargetsWithLimitInfo() { - const results = await pool.query<{ - organization: string; - org_name: string; - org_clean_id: string; - org_plan_name: string; - owner_email: string; - targets: string[]; - limit_operations_monthly: number; - limit_retention_days: number; - }>( - sql`/* getGetOrganizationsAndTargetsWithLimitInfo */ - SELECT - o.id as organization, - o.clean_id as org_clean_id, - o.name as org_name, - o.limit_operations_monthly, - o.limit_retention_days, - o.plan_name as org_plan_name, - array_agg(DISTINCT t.id) as targets, - split_part( - string_agg( - DISTINCT u.email, ',' - ), - ',', - 1 - ) AS owner_email - FROM organizations AS o - LEFT JOIN projects AS p ON (p.org_id = o.id) - LEFT JOIN targets as t ON (t.project_id = p.id) - LEFT JOIN users AS u ON (u.id = o.user_id) - GROUP BY o.id - `, - ); - return results.rows; + return pool + .any( + psql`/* getGetOrganizationsAndTargetsWithLimitInfo */ + SELECT + o.id as organization, + o.clean_id as org_clean_id, + o.name as org_name, + o.limit_operations_monthly, + o.limit_retention_days, + o.plan_name as org_plan_name, + array_agg(DISTINCT t.id) + FILTER (WHERE t.id IS NOT NULL) + as targets, + split_part( + string_agg( + DISTINCT u.email, ',' + ), + ',', + 1 + ) AS owner_email + FROM organizations AS o + LEFT JOIN projects AS p ON (p.org_id = o.id) + LEFT JOIN targets as t ON (t.project_id = p.id) + LEFT JOIN users AS u ON (u.id = o.user_id) + GROUP BY o.id + `, + ) + .then( + z.array( + z.object({ + organization: z.string(), + org_name: z.string(), + org_clean_id: z.string(), + org_plan_name: z.string(), + owner_email: z.string(), + targets: z + .array(z.string()) + .nullable() + .transform(value => value ?? []), + limit_operations_monthly: z.number(), + limit_retention_days: z.number(), + }), + ).parse, + ); }, async adminGetStats(period: { from: Date; to: Date }) { // count schema versions by organization - const versionsResult = pool.query< - Slonik< - Pick & { - total: number; - } - > - >(sql`/* adminCountSchemaVersionsByOrg */ + const versionsResult = pool + .any( + psql`/* adminCountSchemaVersionsByOrg */ SELECT COUNT(*) as total, o.id @@ -3078,48 +2671,42 @@ export async function createStorage( AND v.created_at < ${period.to.toISOString()} GROUP by o.id - `); + `, + ) + .then(z.array(OrganizationStatModel).parse); // count users by organization - const usersResult = pool.query< - Slonik< - Pick & { - total: number; - } - > - >(sql`/* adminCountUsersByOrg */ + const usersResult = pool + .any( + psql`/* adminCountUsersByOrg */ SELECT COUNT(*) as total, o.id FROM organization_member AS om LEFT JOIN organizations AS o ON (o.id = om.organization_id) GROUP by o.id - `); + `, + ) + .then(z.array(OrganizationStatModel).parse); // count projects by organization - const projectsResult = pool.query< - Slonik< - Pick & { - total: number; - } - > - >(sql`/* adminCountProjectsByOrg */ + const projectsResult = pool + .any( + psql`/* adminCountProjectsByOrg */ SELECT COUNT(*) as total, o.id FROM projects AS p LEFT JOIN organizations AS o ON (o.id = p.org_id) GROUP by o.id - `); + `, + ) + .then(z.array(OrganizationStatModel).parse); // count targets by organization - const targetsResult = pool.query< - Slonik< - Pick & { - total: number; - } - > - >(sql`/* adminCountTargetsByOrg */ + const targetsResult = pool + .any( + psql`/* adminCountTargetsByOrg */ SELECT COUNT(*) as total, o.id @@ -3127,12 +2714,18 @@ export async function createStorage( LEFT JOIN projects AS p ON (p.id = t.project_id) LEFT JOIN organizations AS o ON (o.id = p.org_id) GROUP by o.id - `); + `, + ) + .then(z.array(OrganizationStatModel).parse); // get organizations data - const organizationsResult = pool.query>(sql`/* adminGetOrganizations */ - SELECT * FROM organizations - `); + const organizationsResult = pool + .any( + psql`/* adminGetOrganizations */ + SELECT ${organizationFields(psql``)} FROM organizations + `, + ) + .then(z.array(OrganizationModel).parse); const [versions, users, projects, targets, organizations] = await Promise.all([ versionsResult, @@ -3164,13 +2757,13 @@ export async function createStorage( return nodes.find(node => node.id === id)?.total ?? 0; } - for (const organization of organizations.rows) { + for (const organization of organizations) { rows.push({ - organization: transformOrganization(organization), - versions: extractTotal(versions.rows, organization.id), - users: extractTotal(users.rows, organization.id), - projects: extractTotal(projects.rows, organization.id), - targets: extractTotal(targets.rows, organization.id), + organization, + versions: extractTotal(versions, organization.id), + users: extractTotal(users, organization.id), + projects: extractTotal(projects, organization.id), + targets: extractTotal(targets, organization.id), persistedOperations: 0, period, }); @@ -3179,41 +2772,56 @@ export async function createStorage( return rows; }, async getBaseSchema({ projectId: project, targetId: target }) { - const data = await pool.maybeOne>( - sql`/* getBaseSchema */ SELECT base_schema FROM targets WHERE id=${target} AND project_id=${project}`, - ); + const data = await pool + .maybeOne( + psql`/* getBaseSchema */ SELECT base_schema FROM targets WHERE id=${target} AND project_id=${project}`, + ) + .then(z.object({ base_schema: z.string().nullable() }).nullable().parse); return data?.base_schema ?? null; }, async updateBaseSchema({ projectId: project, targetId: target }, base) { if (base) { await pool.query( - sql`/* updateBaseSchema */ UPDATE targets SET base_schema = ${base} WHERE id = ${target} AND project_id = ${project}`, + psql`/* updateBaseSchema */ UPDATE targets SET base_schema = ${base} WHERE id = ${target} AND project_id = ${project}`, ); } else { await pool.query( - sql`/* resetBaseSchema */ UPDATE targets SET base_schema = null WHERE id = ${target} AND project_id = ${project}`, + psql`/* resetBaseSchema */ UPDATE targets SET base_schema = null WHERE id = ${target} AND project_id = ${project}`, ); } }, async getBillingParticipants() { - const results = await pool.query>( - sql`/* getBillingParticipants */ SELECT * FROM organizations_billing`, - ); + const results = await pool + .any( + psql`/* getBillingParticipants */ + SELECT + "organization_id" AS "organizationId", + "external_billing_reference_id" AS "externalBillingReference", + "billing_email_address" AS "billingEmailAddress" + FROM organizations_billing`, + ) + .then(z.array(OrganizationBillingModel).parse); - return results.rows.map(transformOrganizationBilling); + return results; }, async getOrganizationBilling(selector) { - const results = await pool.query>( - sql`/* getOrganizationBilling */ SELECT * FROM organizations_billing WHERE organization_id = ${selector.organizationId}`, - ); + const results = await pool + .any( + psql`/* getOrganizationBilling */ + SELECT + "organization_id" AS "organizationId", + "external_billing_reference_id" AS "externalBillingReference", + "billing_email_address" AS "billingEmailAddress" + FROM organizations_billing + WHERE organization_id = ${selector.organizationId}`, + ) + .then(z.array(OrganizationBillingModel).parse); - const mapped = results.rows.map(transformOrganizationBilling); - - return mapped[0] || null; + return results[0] || null; }, async deleteOrganizationBilling(selector) { - await pool.query>( - sql`/* deleteOrganizationBilling */ + await pool.any( + psql`/* deleteOrganizationBilling */ DELETE FROM organizations_billing WHERE organization_id = ${selector.organizationId}`, ); @@ -3223,21 +2831,24 @@ export async function createStorage( organizationId, externalBillingReference, }) { - return transformOrganizationBilling( - await pool.one>( - sql`/* createOrganizationBilling */ + return OrganizationBillingModel.parse( + await pool.maybeOne( + psql`/* createOrganizationBilling */ INSERT INTO organizations_billing ("organization_id", "external_billing_reference_id", "billing_email_address") VALUES (${organizationId}, ${externalBillingReference}, ${billingEmailAddress || null}) - RETURNING * + RETURNING + "organization_id" AS "organizationId", + "external_billing_reference_id" AS "externalBillingReference", + "billing_email_address" AS "billingEmailAddress" `, ), ); }, async completeGetStartedStep({ organizationId: organization, step }) { await update( - pool, + pool.getSlonikPool(), 'organizations', { [organizationGetStartedMapping[step]]: true, @@ -3249,64 +2860,39 @@ export async function createStorage( }, async getOIDCIntegrationById({ oidcIntegrationId: integrationId }) { - const result = await pool.maybeOne(sql`/* getOIDCIntegrationById */ + return await pool + .maybeOne( + psql`/* getOIDCIntegrationById */ SELECT - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" + ${oidcIntegrationFields()} FROM "oidc_integrations" WHERE "id" = ${integrationId} LIMIT 1 - `); - - if (result === null) { - return null; - } - - return decodeOktaIntegrationRecord(result); + `, + ) + .then(OIDCIntegrationModel.nullable().parse); }, getOIDCIntegrationForOrganization: batch(async selectors => { - const result = await pool.query(sql`/* getOIDCIntegrationForOrganization */ + const rows = await pool + .any( + psql`/* getOIDCIntegrationForOrganization */ SELECT - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" + ${oidcIntegrationFields()} FROM "oidc_integrations" WHERE - "linked_organization_id" = ANY(${sql.array( + "linked_organization_id" = ANY(${psql.array( selectors.map(s => s.organizationId), 'uuid', )}) - `); + `, + ) + .then(z.array(OIDCIntegrationModel).parse); const integrations = new Map( - result.rows.map(row => { - const integration = decodeOktaIntegrationRecord(row); + rows.map(integration => { return [integration.linkedOrganizationId, integration] as const; }), ); @@ -3315,27 +2901,33 @@ export async function createStorage( }), async getOIDCIntegrationIdForOrganizationSlug({ slug }) { - const id = await pool.maybeOneFirst(sql`/* getOIDCIntegrationIdForOrganizationSlug */ - SELECT - "id" - FROM - "oidc_integrations" - WHERE - "linked_organization_id" = ( - SELECT "id" - FROM "organizations" - WHERE "clean_id" = ${slug} - LIMIT 1 - ) - LIMIT 1 - `); + const id = await pool + .maybeOneFirst( + psql`/* getOIDCIntegrationIdForOrganizationSlug */ + SELECT + "id" + FROM + "oidc_integrations" + WHERE + "linked_organization_id" = ( + SELECT "id" + FROM "organizations" + WHERE "clean_id" = ${slug} + LIMIT 1 + ) + LIMIT 1 + `, + ) + .then(z.string().nullable().parse); return id; }, async createOIDCIntegrationForOrganization(args) { try { - const result = await pool.maybeOne(sql`/* createOIDCIntegrationForOrganization */ + const oidcIntegration = await pool + .maybeOne( + psql`/* createOIDCIntegrationForOrganization */ INSERT INTO "oidc_integrations" ( "linked_organization_id", "client_id", @@ -3352,28 +2944,17 @@ export async function createStorage( ${args.tokenEndpoint}, ${args.userinfoEndpoint}, ${args.authorizationEndpoint}, - ${sql.array(args.additionalScopes, 'text')} + ${psql.array(args.additionalScopes, 'text')} ) RETURNING - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" - `); + ${oidcIntegrationFields()} + `, + ) + .then(OIDCIntegrationModel.parse); return { type: 'ok', - oidcIntegration: decodeOktaIntegrationRecord(result), + oidcIntegration, }; } catch (error) { if ( @@ -3390,153 +2971,113 @@ export async function createStorage( }, async updateOIDCIntegration(args) { - const result = await pool.maybeOne(sql`/* updateOIDCIntegration */ + return await pool + .maybeOne( + psql`/* updateOIDCIntegration */ UPDATE "oidc_integrations" SET - "client_id" = ${args.clientId ?? sql`"client_id"`} - , "client_secret" = ${args.encryptedClientSecret ?? sql`"client_secret"`} + "client_id" = ${args.clientId ?? psql`"client_id"`} + , "client_secret" = ${args.encryptedClientSecret ?? psql`"client_secret"`} , "token_endpoint" = ${ args.tokenEndpoint ?? /** update existing columns to the old legacy values if not yet stored */ - sql`COALESCE("token_endpoint", CONCAT("oauth_api_url", "/token"))` + psql`COALESCE("token_endpoint", CONCAT("oauth_api_url", "/token"))` } , "userinfo_endpoint" = ${ args.userinfoEndpoint ?? /** update existing columns to the old legacy values if not yet stored */ - sql`COALESCE("userinfo_endpoint", CONCAT("oauth_api_url", "/userinfo"))` + psql`COALESCE("userinfo_endpoint", CONCAT("oauth_api_url", "/userinfo"))` } , "authorization_endpoint" = ${ args.authorizationEndpoint ?? /** update existing columns to the old legacy values if not yet stored */ - sql`COALESCE("authorization_endpoint", CONCAT("oauth_api_url", "/authorize"))` + psql`COALESCE("authorization_endpoint", CONCAT("oauth_api_url", "/authorize"))` } - , "additional_scopes" = ${args.additionalScopes ? sql.array(args.additionalScopes, 'text') : sql`"additional_scopes"`} + , "additional_scopes" = ${args.additionalScopes ? psql.array(args.additionalScopes, 'text') : psql`"additional_scopes"`} , "oauth_api_url" = NULL WHERE "id" = ${args.oidcIntegrationId} RETURNING - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" - `); - - return decodeOktaIntegrationRecord(result); + ${oidcIntegrationFields()} + `, + ) + .then(OIDCIntegrationModel.parse); }, async updateOIDCRestrictions(args) { - const result = await pool.one(sql`/* updateOIDCRestrictions */ + return await pool + .maybeOne( + psql`/* updateOIDCRestrictions */ UPDATE "oidc_integrations" SET - "oidc_user_join_only" = ${args.oidcUserJoinOnly ?? sql`"oidc_user_join_only"`} - , "oidc_user_access_only" = ${args.oidcUserAccessOnly ?? sql`"oidc_user_access_only"`} - , "require_invitation" = ${args.requireInvitation ?? sql`"require_invitation"`} + "oidc_user_join_only" = ${args.oidcUserJoinOnly ?? psql`"oidc_user_join_only"`} + , "oidc_user_access_only" = ${args.oidcUserAccessOnly ?? psql`"oidc_user_access_only"`} + , "require_invitation" = ${args.requireInvitation ?? psql`"require_invitation"`} WHERE "id" = ${args.oidcIntegrationId} RETURNING - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" - `); - - return decodeOktaIntegrationRecord(result); + ${oidcIntegrationFields()} + `, + ) + .then(OIDCIntegrationModel.parse); }, async updateOIDCDefaultAssignedResources(args) { - return tracedTransaction('updateOIDCDefaultAssignedResources', pool, async _ => { - const result = await pool.one(sql`/* updateOIDCDefaultAssignedResources */ + return pool.transaction('updateOIDCDefaultAssignedResources', async trx => { + return await trx + .maybeOne( + psql`/* updateOIDCDefaultAssignedResources */ UPDATE "oidc_integrations" SET - "default_assigned_resources" = ${sql.jsonb(args.assignedResources)} + "default_assigned_resources" = ${psql.jsonb(args.assignedResources)} WHERE "id" = ${args.oidcIntegrationId} RETURNING - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "additional_scopes" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" - `); - - return decodeOktaIntegrationRecord(result); + ${oidcIntegrationFields()} + `, + ) + .then(OIDCIntegrationModel.parse); }); }, async updateOIDCDefaultMemberRole(args) { - return tracedTransaction('updateOIDCDefaultMemberRole', pool, async trx => { + return pool.transaction('updateOIDCDefaultMemberRole', async trx => { // Make sure the role exists and is associated with the organization - const roleId = await trx.oneFirst(sql`/* checkRoleExists */ - SELECT id FROM "organization_member_roles" - WHERE - "id" = ${args.roleId} AND - "organization_id" = ( - SELECT "linked_organization_id" FROM "oidc_integrations" WHERE "id" = ${args.oidcIntegrationId} - ) - `); + const roleId = await trx + .oneFirst( + psql`/* checkRoleExists */ + SELECT id FROM "organization_member_roles" + WHERE + "id" = ${args.roleId} AND + "organization_id" = ( + SELECT "linked_organization_id" FROM "oidc_integrations" WHERE "id" = ${args.oidcIntegrationId} + ) + `, + ) + .then(z.string().parse); if (!roleId) { throw new Error('Role does not exist'); } - const result = await pool.one(sql`/* updateOIDCDefaultMemberRole */ + return await pool + .maybeOne( + psql`/* updateOIDCDefaultMemberRole */ UPDATE "oidc_integrations" SET "default_role_id" = ${roleId} WHERE "id" = ${args.oidcIntegrationId} RETURNING - "id" - , "linked_organization_id" - , "client_id" - , "client_secret" - , "oauth_api_url" - , "token_endpoint" - , "userinfo_endpoint" - , "authorization_endpoint" - , "additional_scopes" - , "oidc_user_join_only" - , "oidc_user_access_only" - , "default_role_id" - , "default_assigned_resources" - , "require_invitation" - `); - - return decodeOktaIntegrationRecord(result); + ${oidcIntegrationFields()} + `, + ) + .then(OIDCIntegrationModel.parse); }); }, async deleteOIDCIntegration(args) { - await pool.query(sql`/* deleteOIDCIntegration */ + await pool.any(psql`/* deleteOIDCIntegration */ DELETE FROM "oidc_integrations" WHERE "id" = ${args.oidcIntegrationId} @@ -3544,7 +3085,9 @@ export async function createStorage( }, async createCDNAccessToken(args) { - const result = await pool.maybeOne(sql`/* createCDNAccessToken */ + return await pool + .maybeOne( + psql`/* createCDNAccessToken */ INSERT INTO "cdn_access_tokens" ( "id" , "target_id" @@ -3563,46 +3106,29 @@ export async function createStorage( ) ON CONFLICT ("s3_key") DO NOTHING RETURNING - "id" - , "target_id" - , "s3_key" - , "first_characters" - , "last_characters" - , "alias" - , to_json("created_at") as "created_at" - `); - - if (result === null) { - return null; - } - - return decodeCDNAccessTokenRecord(result); + ${cdnAccessTokenFields()} + `, + ) + .then(CDNAccessTokenModel.nullable().parse); }, async getCDNAccessTokenById(args) { - const result = await pool.maybeOne(sql`/* getCDNAccessTokenById */ + return await pool + .maybeOne( + psql`/* getCDNAccessTokenById */ SELECT - "id" - , "target_id" - , "s3_key" - , "first_characters" - , "last_characters" - , "alias" - , to_json("created_at") as "created_at" + ${cdnAccessTokenFields()} FROM "cdn_access_tokens" WHERE "id" = ${args.cdnAccessTokenId} - `); - - if (result == null) { - return null; - } - return decodeCDNAccessTokenRecord(result); + `, + ) + .then(CDNAccessTokenModel.nullable().parse); }, async deleteCDNAccessToken(args) { - const result = await pool.maybeOne(sql`/* deleteCDNAccessToken */ + const result = await pool.maybeOne(psql`/* deleteCDNAccessToken */ DELETE FROM "cdn_access_tokens" @@ -3627,22 +3153,18 @@ export async function createStorage( cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const result = await pool.any(sql`/* getPaginatedCDNAccessTokensForTarget */ + const result = await pool + .any( + psql`/* getPaginatedCDNAccessTokensForTarget */ SELECT - "id" - , "target_id" - , "s3_key" - , "first_characters" - , "last_characters" - , "alias" - , to_json("created_at") as "created_at" + ${cdnAccessTokenFields()} FROM "cdn_access_tokens" WHERE "target_id" = ${args.targetId} ${ cursor - ? sql` + ? psql` AND ( ( "cdn_access_tokens"."created_at" = ${cursor.createdAt} @@ -3651,18 +3173,18 @@ export async function createStorage( OR "cdn_access_tokens"."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "target_id" ASC , "cdn_access_tokens"."created_at" DESC , "id" DESC LIMIT ${limit + 1} - `); - - let items = result.map(row => { - const node = decodeCDNAccessTokenRecord(row); + `, + ) + .then(z.array(CDNAccessTokenModel).parse); + let items = result.map(node => { return { node, get cursor() { @@ -3690,77 +3212,86 @@ export async function createStorage( }; }, - async setSchemaPolicyForOrganization(input): Promise { - const result = await pool.one(sql`/* setSchemaPolicyForOrganization */ + async setSchemaPolicyForOrganization(input) { + return pool + .maybeOne( + psql`/* setSchemaPolicyForOrganization */ INSERT INTO "schema_policy_config" ("resource_type", "resource_id", "config", "allow_overriding") - VALUES ('ORGANIZATION', ${input.organizationId}, ${sql.jsonb(input.policy)}, ${ + VALUES ('ORGANIZATION', ${input.organizationId}, ${psql.jsonb(input.policy)}, ${ input.allowOverrides }) ON CONFLICT (resource_type, resource_id) DO UPDATE - SET "config" = ${sql.jsonb(input.policy)}, + SET "config" = ${psql.jsonb(input.policy)}, "allow_overriding" = ${input.allowOverrides}, "updated_at" = now() - RETURNING *; - `); - - return transformSchemaPolicy(result); + RETURNING ${schemaPolicyFields(psql``)}; + `, + ) + .then(SchemaPolicyModel.parse); }, - async setSchemaPolicyForProject(input): Promise { - const result = await pool.one(sql`/* setSchemaPolicyForProject */ + async setSchemaPolicyForProject(input) { + return pool + .maybeOne( + psql`/* setSchemaPolicyForProject */ INSERT INTO "schema_policy_config" ("resource_type", "resource_id", "config") - VALUES ('PROJECT', ${input.projectId}, ${sql.jsonb(input.policy)}) + VALUES ('PROJECT', ${input.projectId}, ${psql.jsonb(input.policy)}) ON CONFLICT (resource_type, resource_id) DO UPDATE - SET "config" = ${sql.jsonb(input.policy)}, + SET "config" = ${psql.jsonb(input.policy)}, "updated_at" = now() - RETURNING *; - `); - - return transformSchemaPolicy(result); + RETURNING ${schemaPolicyFields(psql``)}; + `, + ) + .then(SchemaPolicyModel.parse); }, - async findInheritedPolicies(selector): Promise { + async findInheritedPolicies(selector) { const { organizationId: organization, projectId: project } = selector; - const result = await pool.any(sql`/* findInheritedPolicies */ - SELECT * + return pool + .any( + psql`/* findInheritedPolicies */ + SELECT ${schemaPolicyFields(psql``)} FROM "schema_policy_config" WHERE ("resource_type" = 'ORGANIZATION' AND "resource_id" = ${organization}) OR ("resource_type" = 'PROJECT' AND "resource_id" = ${project}); - `); - - return result.map(transformSchemaPolicy); + `, + ) + .then(z.array(SchemaPolicyModel).parse); }, - async getSchemaPolicyForOrganization(organizationId: string): Promise { - const result = - await pool.maybeOne(sql`/* getSchemaPolicyForOrganization */ - SELECT * + async getSchemaPolicyForOrganization(organizationId: string) { + return pool + .maybeOne( + psql`/* getSchemaPolicyForOrganization */ + SELECT ${schemaPolicyFields(psql``)} FROM "schema_policy_config" WHERE "resource_type" = 'ORGANIZATION' AND "resource_id" = ${organizationId}; - `); - - return result ? transformSchemaPolicy(result) : null; + `, + ) + .then(SchemaPolicyModel.nullable().parse); }, - async getSchemaPolicyForProject(projectId: string): Promise { - const result = await pool.maybeOne(sql`/* getSchemaPolicyForProject */ - SELECT * + async getSchemaPolicyForProject(projectId: string) { + return pool + .maybeOne( + psql`/* getSchemaPolicyForProject */ + SELECT ${schemaPolicyFields(psql``)} FROM "schema_policy_config" WHERE "resource_type" = 'PROJECT' AND "resource_id" = ${projectId}; - `); - - return result ? transformSchemaPolicy(result) : null; + `, + ) + .then(SchemaPolicyModel.nullable().parse); }, async getPaginatedDocumentCollectionsForTarget(args) { let cursor: null | { @@ -3774,7 +3305,7 @@ export async function createStorage( cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const result = await pool.any(sql`/* getPaginatedDocumentCollectionsForTarget */ + const result = await pool.any(psql`/* getPaginatedDocumentCollectionsForTarget */ SELECT "id" , "title" @@ -3789,7 +3320,7 @@ export async function createStorage( "target_id" = ${args.targetId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -3798,7 +3329,7 @@ export async function createStorage( OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "target_id" ASC @@ -3838,7 +3369,7 @@ export async function createStorage( }, async createDocumentCollection(args) { - const result = await pool.maybeOne(sql`/* createDocumentCollection */ + const result = await pool.maybeOne(psql`/* createDocumentCollection */ INSERT INTO "document_collections" ( "title" , "description" @@ -3864,7 +3395,7 @@ export async function createStorage( return DocumentCollectionModel.parse(result); }, async deleteDocumentCollection(args) { - const result = await pool.maybeOneFirst(sql`/* deleteDocumentCollection */ + const result = await pool.maybeOneFirst(psql`/* deleteDocumentCollection */ DELETE FROM "document_collections" @@ -3878,11 +3409,11 @@ export async function createStorage( return null; } - return zod.string().parse(result); + return z.string().parse(result); }, async updateDocumentCollection(args) { - const result = await pool.maybeOne(sql`/* updateDocumentCollection */ + const result = await pool.maybeOne(psql`/* updateDocumentCollection */ UPDATE "document_collections" SET @@ -3923,7 +3454,7 @@ export async function createStorage( cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const result = await pool.any(sql`/* getPaginatedDocumentsForDocumentCollection */ + const result = await pool.any(psql`/* getPaginatedDocumentsForDocumentCollection */ SELECT "id" , "title" @@ -3940,7 +3471,7 @@ export async function createStorage( "document_collection_id" = ${args.documentCollectionId} ${ cursor - ? sql` + ? psql` AND ( ( "created_at" = ${cursor.createdAt} @@ -3949,7 +3480,7 @@ export async function createStorage( OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY "document_collection_id" ASC @@ -3989,7 +3520,7 @@ export async function createStorage( }, async createDocumentCollectionDocument(args) { - const result = await pool.one(sql`/* createDocumentCollectionDocument */ + const result = await pool.maybeOne(psql`/* createDocumentCollectionDocument */ INSERT INTO "document_collection_documents" ( "title" , "contents" @@ -4022,7 +3553,7 @@ export async function createStorage( }, async deleteDocumentCollectionDocument(args) { - const result = await pool.maybeOneFirst(sql`/* deleteDocumentCollectionDocument */ + const result = await pool.maybeOneFirst(psql`/* deleteDocumentCollectionDocument */ DELETE FROM "document_collection_documents" @@ -4036,11 +3567,11 @@ export async function createStorage( return null; } - return zod.string().parse(result); + return z.string().parse(result); }, async getDocumentCollectionDocument(args) { - const result = await pool.maybeOne(sql`/* getDocumentCollectionDocument */ + const result = await pool.maybeOne(psql`/* getDocumentCollectionDocument */ SELECT "id" , "title" @@ -4065,7 +3596,7 @@ export async function createStorage( }, async getDocumentCollection(args) { - const result = await pool.maybeOne(sql`/* getDocumentCollection */ + const result = await pool.maybeOne(psql`/* getDocumentCollection */ SELECT "id" , "title" @@ -4088,7 +3619,7 @@ export async function createStorage( }, async updateDocumentCollectionDocument(args) { - const result = await pool.maybeOne(sql`/* updateDocumentCollectionDocument */ + const result = await pool.maybeOne(psql`/* updateDocumentCollectionDocument */ UPDATE "document_collection_documents" SET @@ -4119,11 +3650,11 @@ export async function createStorage( }, async createSchemaCheck(args) { - const result = await tracedTransaction('createSchemaCheck', pool, async trx => { + const result = await pool.transaction('createSchemaCheck', async trx => { const sdlStoreInserts: Array> = []; function insertSdl(hash: string, sdl: string) { - return trx.query(sql`/* insertToSdlStore */ + return trx.any(psql`/* insertToSdlStore */ INSERT INTO "sdl_store" (id, sdl) VALUES (${hash}, ${sdl}) ON CONFLICT (id) DO NOTHING; @@ -4148,7 +3679,9 @@ export async function createStorage( await Promise.all(sdlStoreInserts); - const schemaCheck = await trx.one<{ id: string }>(sql`/* createSchemaCheck */ + const schemaCheck = await trx + .maybeOne( + psql`/* createSchemaCheck */ INSERT INTO "schema_checks" ( "schema_sdl_store_id" , "service_name" @@ -4209,7 +3742,9 @@ export async function createStorage( ) RETURNING "id" - `); + `, + ) + .then(z.object({ id: z.string() }).parse); if (args.contracts?.length) { for (const contract of args.contracts) { @@ -4226,7 +3761,7 @@ export async function createStorage( await insertSdl(compositeSchemaSdlHash, contract.compositeSchemaSdl); } - await trx.query(sql`/* createContractChecks */ + await trx.query(psql`/* createContractChecks */ INSERT INTO "contract_checks" ( "schema_check_id" , "compared_contract_version_id" @@ -4268,7 +3803,7 @@ export async function createStorage( return check; }, async findSchemaCheck(args) { - const result = await pool.maybeOne(sql`/* findSchemaCheck */ + const result = await pool.maybeOne(psql`/* findSchemaCheck */ SELECT ${schemaCheckSQLFields} FROM @@ -4307,14 +3842,14 @@ export async function createStorage( if (schemaCheck.contextId !== null && !!schemaCheck.breakingSchemaChanges) { // Try to approve and claim all the breaking schema changes for this context - await pool.query(sql`/* approveFailedSchemaCheck */ + await pool.query(psql`/* approveFailedSchemaCheck */ INSERT INTO "schema_change_approvals" ( "target_id" , "context_id" , "schema_change_id" , "schema_change" ) - SELECT * FROM ${sql.unnest( + SELECT * FROM ${psql.unnest( schemaCheck.breakingSchemaChanges .filter(change => !change.isSafeBasedOnUsage) .map(change => [ @@ -4346,60 +3881,64 @@ export async function createStorage( } | null = null; if (schemaCheck.breakingSchemaChanges) { - updateResult = await pool.maybeOne<{ - id: string; - }>(sql`/* approveFailedSchemaCheck (breakingSchemaChanges) */ - UPDATE - "schema_checks" - SET - "is_success" = true - , "is_manually_approved" = true - , "manual_approval_user_id" = ${args.userId} - , "manual_approval_comment" = ${args.comment ?? null} - , "breaking_schema_changes" = ( - SELECT json_agg( - CASE - WHEN (COALESCE(jsonb_typeof("change"->'approvalMetadata'), 'null') = 'null' AND "change"->>'isSafeBasedOnUsage' = 'false') - THEN jsonb_set("change", '{approvalMetadata}', ${sql.jsonb(approvalMetadata)}) - ELSE "change" - END + updateResult = await pool + .maybeOne( + psql`/* approveFailedSchemaCheck (breakingSchemaChanges) */ + UPDATE + "schema_checks" + SET + "is_success" = true + , "is_manually_approved" = true + , "manual_approval_user_id" = ${args.userId} + , "manual_approval_comment" = ${args.comment ?? null} + , "breaking_schema_changes" = ( + SELECT json_agg( + CASE + WHEN (COALESCE(jsonb_typeof("change"->'approvalMetadata'), 'null') = 'null' AND "change"->>'isSafeBasedOnUsage' = 'false') + THEN jsonb_set("change", '{approvalMetadata}', ${psql.jsonb(approvalMetadata)}) + ELSE "change" + END + ) + FROM jsonb_array_elements("breaking_schema_changes") AS "change" ) - FROM jsonb_array_elements("breaking_schema_changes") AS "change" - ) - WHERE - "id" = ${args.schemaCheckId} - AND "is_success" = false - AND "schema_composition_errors" IS NULL - AND "schema_policy_errors" IS NULL - RETURNING - "id" - `); + WHERE + "id" = ${args.schemaCheckId} + AND "is_success" = false + AND "schema_composition_errors" IS NULL + AND "schema_policy_errors" IS NULL + RETURNING + "id" + `, + ) + .then(z.object({ id: z.string() }).nullable().parse); } else if (didUpdateContractChecks) { - updateResult = await pool.maybeOne<{ - id: string; - }>(sql`/* approveFailedSchemaCheck (didUpdateContractChecks) */ - UPDATE - "schema_checks" - SET - "is_success" = true - , "is_manually_approved" = true - , "manual_approval_comment" = ${args.comment ?? null} - , "manual_approval_user_id" = ${args.userId} - WHERE - "id" = ${args.schemaCheckId} - AND "is_success" = false - AND "schema_composition_errors" IS NULL - AND "schema_policy_errors" IS NULL - RETURNING - "id" - `); + updateResult = await pool + .maybeOne( + psql`/* approveFailedSchemaCheck (didUpdateContractChecks) */ + UPDATE + "schema_checks" + SET + "is_success" = true + , "is_manually_approved" = true + , "manual_approval_comment" = ${args.comment ?? null} + , "manual_approval_user_id" = ${args.userId} + WHERE + "id" = ${args.schemaCheckId} + AND "is_success" = false + AND "schema_composition_errors" IS NULL + AND "schema_policy_errors" IS NULL + RETURNING + "id" + `, + ) + .then(z.object({ id: z.string() }).nullable().parse); } if (updateResult == null) { return null; } - const result = await pool.maybeOne(sql`/* getApprovedSchemaCheck */ + const result = await pool.maybeOne(psql`/* getApprovedSchemaCheck */ SELECT ${schemaCheckSQLFields} FROM @@ -4414,7 +3953,7 @@ export async function createStorage( return SchemaCheckModel.parse(result); }, async getApprovedSchemaChangesForContextId(args) { - const result = await pool.anyFirst(sql`/* getApprovedSchemaChangesForContextId */ + const result = await pool.anyFirst(psql`/* getApprovedSchemaChangesForContextId */ SELECT "schema_change" FROM @@ -4445,7 +3984,7 @@ export async function createStorage( cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); } - const result = await pool.any(sql`/* getPaginatedSchemaChecksForTarget */ + const result = await pool.any(psql`/* getPaginatedSchemaChecksForTarget */ SELECT ${schemaCheckSQLFields} FROM @@ -4457,7 +3996,7 @@ export async function createStorage( c."target_id" = ${args.targetId} ${ cursor - ? sql` + ? psql` AND ( ( c."created_at" = ${cursor.createdAt} @@ -4466,27 +4005,27 @@ export async function createStorage( OR c."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ${ failed - ? sql` + ? psql` AND ( "is_success" = false ) ` - : sql`` + : psql`` } ${ changed - ? sql` + ? psql` AND ( jsonb_typeof("safe_schema_changes") = 'array' OR jsonb_typeof("breaking_schema_changes") = 'array' OR "has_contract_schema_changes" = true ) ` - : sql`` + : psql`` } ORDER BY c."target_id" ASC @@ -4541,20 +4080,20 @@ export async function createStorage( } // gets the most recently created schema checks per service name - const result = await pool.any(sql`/* getPaginatedSchemaChecksForSchemaProposal */ + const result = await pool.any(psql`/* getPaginatedSchemaChecksForSchemaProposal */ SELECT ${schemaCheckSQLFields} FROM "schema_checks" as c ${ args.latest - ? sql` + ? psql` INNER JOIN ( SELECT COALESCE("service_name", '') as "service", "schema_proposal_id", max("created_at") as "maxdate" FROM schema_checks ${ cursor - ? sql` + ? psql` WHERE "schema_proposal_id" = ${args.proposalId} AND ( ( @@ -4564,7 +4103,7 @@ export async function createStorage( OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } GROUP BY "service", "schema_proposal_id" ) as cc @@ -4572,7 +4111,7 @@ export async function createStorage( AND COALESCE(c."service_name", '') = cc."service" AND c."created_at" = cc."maxdate" ` - : sql`` + : psql`` } LEFT JOIN "sdl_store" as s_schema ON s_schema."id" = c."schema_sdl_store_id" @@ -4584,7 +4123,7 @@ export async function createStorage( c."schema_proposal_id" = ${args.proposalId} ${ cursor - ? sql` + ? psql` AND ( ( c."created_at" = ${cursor.createdAt} @@ -4593,7 +4132,7 @@ export async function createStorage( OR c."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY c."created_at" DESC @@ -4634,7 +4173,7 @@ export async function createStorage( }, async getTargetBreadcrumbForTargetId(args) { - const result = await pool.maybeOne(sql`/* getTargetBreadcrumbForTargetId */ + const result = await pool.maybeOne(psql`/* getTargetBreadcrumbForTargetId */ SELECT o."clean_id" AS "organization_slug", p."clean_id" AS "project_slug", @@ -4655,8 +4194,8 @@ export async function createStorage( }, getTargetById: batch(async targetIds => { const rows = await pool - .any( - sql`/* getTarget */ + .any( + psql`/* getTarget */ SELECT "t".* , "p"."org_id" AS "orgId" @@ -4666,12 +4205,12 @@ export async function createStorage( FROM "targets" WHERE - "id" = ANY(${sql.array(targetIds, 'uuid')}) + "id" = ANY(${psql.array(targetIds, 'uuid')}) ) AS "t" INNER JOIN "projects" "p" ON "t"."projectId" = "p"."id" `, ) - .then(rows => rows.map(row => TargetWithOrgIdModel.parse(row))); + .then(z.array(TargetWithOrgIdModel).parse); const resultLookupMap = new Map(); for (const target of rows) { @@ -4684,7 +4223,7 @@ export async function createStorage( }), async updateTargetGraphQLEndpointUrl(args) { - const result = await pool.maybeOne(sql`/* updateTargetGraphQLEndpointUrl */ + const result = await pool.maybeOne(psql`/* updateTargetGraphQLEndpointUrl */ UPDATE "targets" SET @@ -4706,16 +4245,16 @@ export async function createStorage( }, async purgeExpiredSchemaChecks(args) { - const SchemaCheckModel = zod.object({ - schemaCheckIds: zod.array(zod.string()), - sdlStoreIds: zod.array(zod.string()), - contextIds: zod.array(zod.string()), - targetIds: zod.array(zod.string()), - contractIds: zod.array(zod.string()), + const SchemaCheckModel = z.object({ + schemaCheckIds: z.array(z.string()), + sdlStoreIds: z.array(z.string()), + contextIds: z.array(z.string()), + targetIds: z.array(z.string()), + contractIds: z.array(z.string()), }); - return await tracedTransaction('purgeExpiredSchemaChecks', pool, async pool => { + return await pool.transaction('purgeExpiredSchemaChecks', async pool => { const date = args.expiresAt.toISOString(); - const rawData = await pool.maybeOne(sql`/* findSchemaChecksToPurge */ + const rawData = await pool.maybeOne(psql`/* findSchemaChecksToPurge */ WITH "filtered_schema_checks" AS ( SELECT * FROM "schema_checks" @@ -4773,22 +4312,24 @@ export async function createStorage( let deletedSchemaChangeApprovalCount = 0; let deletedContractSchemaChangeApprovalCount = 0; - await pool.any(sql`/* purgeExpiredSchemaChecks */ + await pool.any(psql`/* purgeExpiredSchemaChecks */ DELETE FROM "schema_checks" WHERE - "id" = ANY(${sql.array(data.schemaCheckIds, 'uuid')}) + "id" = ANY(${psql.array(data.schemaCheckIds, 'uuid')}) `); if (data.sdlStoreIds.length) { - deletedSdlStoreCount = await pool.oneFirst(sql`/* purgeExpiredSdlStore */ + deletedSdlStoreCount = await pool + .oneFirst( + psql`/* purgeExpiredSdlStore */ WITH "deleted" AS ( DELETE FROM "sdl_store" WHERE "id" = ANY( - ${sql.array(data.sdlStoreIds, 'text')} + ${psql.array(data.sdlStoreIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -4812,22 +4353,25 @@ export async function createStorage( RETURNING "id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } if (data.targetIds.length && data.contextIds.length) { - deletedSchemaChangeApprovalCount = - await pool.oneFirst(sql`/* purgeExpiredSchemaChangeApprovals */ + deletedSchemaChangeApprovalCount = await pool + .oneFirst( + psql`/* purgeExpiredSchemaChangeApprovals */ WITH "deleted" AS ( DELETE FROM "schema_change_approvals" WHERE "target_id" = ANY( - ${sql.array(data.targetIds, 'uuid')} + ${psql.array(data.targetIds, 'uuid')} ) AND "context_id" = ANY( - ${sql.array(data.contextIds, 'text')} + ${psql.array(data.contextIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -4840,22 +4384,25 @@ export async function createStorage( RETURNING "target_id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } if (data.contractIds.length && data.contextIds.length) { - deletedContractSchemaChangeApprovalCount = - await pool.oneFirst(sql`/* purgeExpiredContractSchemaChangeApprovals */ + deletedContractSchemaChangeApprovalCount = await pool + .oneFirst( + psql`/* purgeExpiredContractSchemaChangeApprovals */ WITH "deleted" AS ( DELETE FROM "contract_schema_change_approvals" WHERE "contract_id" = ANY( - ${sql.array(data.contractIds, 'uuid')} + ${psql.array(data.contractIds, 'uuid')} ) AND "context_id" = ANY( - ${sql.array(data.contextIds, 'text')} + ${psql.array(data.contextIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -4871,7 +4418,9 @@ export async function createStorage( RETURNING "contract_id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } return { @@ -4884,7 +4433,7 @@ export async function createStorage( }, async getSchemaVersionByCommit(args) { - const record = await pool.maybeOne(sql`/* getSchemaVersionByCommit */ + const record = await pool.maybeOne(psql`/* getSchemaVersionByCommit */ SELECT ${schemaVersionSQLFields()} FROM @@ -4913,7 +4462,7 @@ export async function createStorage( }, // Zendesk async setZendeskOrganizationId({ organizationId, zendeskId }) { - await pool.query(sql`/* setZendeskOrganizationId */ + await pool.query(psql`/* setZendeskOrganizationId */ UPDATE "organizations" SET @@ -4923,7 +4472,7 @@ export async function createStorage( `); }, async setZendeskUserId({ userId, zendeskId }) { - await pool.query(sql`/* setZendeskUserId */ + await pool.query(psql`/* setZendeskUserId */ UPDATE "users" SET @@ -4933,7 +4482,7 @@ export async function createStorage( `); }, async setZendeskOrganizationUserConnection({ organizationId, userId }) { - await pool.query(sql`/* setZendeskOrganizationUserConnection */ + await pool.query(psql`/* setZendeskOrganizationUserConnection */ UPDATE "organization_member" SET @@ -4946,14 +4495,14 @@ export async function createStorage( async updateTargetSchemaComposition(args) { // I could do it in one query, but the amount of SQL needed to do it in one go is just not worth it... // It is just too complex to understand. - await pool.transaction(async t => { - const { feature_flags } = await t.one< - Pick - >(sql`/* updateTargetSchemaComposition_select */ + await pool.transaction('updateTargetSchemaComposition', async t => { + const ff = await t + .maybeOneFirst( + psql`/* updateTargetSchemaComposition_select */ SELECT feature_flags FROM organizations WHERE id = ${args.organizationId}; - `); - - const ff = decodeFeatureFlags(feature_flags); + `, + ) + .then(FeatureFlagsModel.parse); let modify = false; const includesTarget = ff.forceLegacyCompositionInTargets.includes(args.targetId); @@ -4975,9 +4524,9 @@ export async function createStorage( } if (modify) { - await t.query(sql`/* updateTargetSchemaComposition_update */ + await t.query(psql`/* updateTargetSchemaComposition_update */ UPDATE organizations - SET feature_flags = ${sql.jsonb(ff)} + SET feature_flags = ${psql.jsonb(ff)} WHERE id = ${args.organizationId}; `); } @@ -5078,115 +4627,81 @@ export function decodeCreatedAtAndHashBasedCursor(cursor: string) { }; } -function isDefined(val: T | undefined | null): val is T { - return val !== undefined && val !== null; -} - -const OktaIntegrationBaseModel = zod.object({ - id: zod.string(), - linked_organization_id: zod.string(), - client_id: zod.string(), - client_secret: zod.string(), - additional_scopes: zod - .array(zod.string()) +const OIDCIntegrationBaseModel = z.object({ + id: z.string(), + linkedOrganizationId: z.string(), + clientId: z.string(), + clientSecret: z.string(), + additionalScopes: z + .array(z.string()) .nullable() .transform(value => (value === null ? [] : value)), - oidc_user_join_only: zod.boolean(), - oidc_user_access_only: zod.boolean(), - default_role_id: zod.string().nullable(), - default_assigned_resources: zod.any().nullable(), - require_invitation: zod.boolean(), + oidcUserJoinOnly: z.boolean(), + oidcUserAccessOnly: z.boolean(), + defaultRoleId: z.string().nullable(), + defaultAssignedResources: z.any().nullable(), + requireInvitation: z.boolean(), }); -const OktaIntegrationLegacyModel = zod.intersection( - OktaIntegrationBaseModel, - zod.object({ - oauth_api_url: zod.string().url(), - }), -); +const OIDCIntegrationLegacyModel = OIDCIntegrationBaseModel.extend({ + oauthApiUrl: z.string().url(), +}).transform(record => ({ + id: record.id, + clientId: record.clientId, + encryptedClientSecret: record.clientSecret, + linkedOrganizationId: record.linkedOrganizationId, + tokenEndpoint: `${record.oauthApiUrl}/token`, + userinfoEndpoint: `${record.oauthApiUrl}/userinfo`, + authorizationEndpoint: `${record.oauthApiUrl}/authorize`, + additionalScopes: record.additionalScopes, + oidcUserJoinOnly: record.oidcUserJoinOnly, + oidcUserAccessOnly: record.oidcUserAccessOnly, + requireInvitation: record.requireInvitation, + defaultMemberRoleId: record.defaultRoleId, + defaultResourceAssignment: record.defaultAssignedResources, +})); -const OktaIntegrationModel = zod.intersection( - OktaIntegrationBaseModel, - zod.object({ - token_endpoint: zod.string().url(), - userinfo_endpoint: zod.string().url(), - authorization_endpoint: zod.string().url(), - }), -); +const LatestOIDCIntegrationModel = OIDCIntegrationBaseModel.extend({ + oauthApiUrl: z.null(), + tokenEndpoint: z.string().url(), + userinfoEndpoint: z.string().url(), + authorizationEndpoint: z.string().url(), +}).transform(record => ({ + id: record.id, + clientId: record.clientId, + encryptedClientSecret: record.clientSecret, + linkedOrganizationId: record.linkedOrganizationId, + tokenEndpoint: record.tokenEndpoint, + userinfoEndpoint: record.userinfoEndpoint, + authorizationEndpoint: record.authorizationEndpoint, + additionalScopes: record.additionalScopes, + oidcUserJoinOnly: record.oidcUserJoinOnly, + oidcUserAccessOnly: record.oidcUserAccessOnly, + requireInvitation: record.requireInvitation, + defaultMemberRoleId: record.defaultRoleId, + defaultResourceAssignment: record.defaultAssignedResources, +})); -const OktaIntegrationModelUnion = zod.union([OktaIntegrationLegacyModel, OktaIntegrationModel]); +const OIDCIntegrationModel = z.union([OIDCIntegrationLegacyModel, LatestOIDCIntegrationModel]); -const decodeOktaIntegrationRecord = (result: unknown): OIDCIntegration => { - const rawRecord = OktaIntegrationModelUnion.parse(result); - - // handle legacy case - if ('oauth_api_url' in rawRecord) { - return { - id: rawRecord.id, - clientId: rawRecord.client_id, - encryptedClientSecret: rawRecord.client_secret, - linkedOrganizationId: rawRecord.linked_organization_id, - tokenEndpoint: `${rawRecord.oauth_api_url}/token`, - userinfoEndpoint: `${rawRecord.oauth_api_url}/userinfo`, - authorizationEndpoint: `${rawRecord.oauth_api_url}/authorize`, - additionalScopes: rawRecord.additional_scopes, - oidcUserJoinOnly: rawRecord.oidc_user_join_only, - oidcUserAccessOnly: rawRecord.oidc_user_access_only, - requireInvitation: rawRecord.require_invitation, - defaultMemberRoleId: rawRecord.default_role_id, - defaultResourceAssignment: rawRecord.default_assigned_resources, - }; - } - - return { - id: rawRecord.id, - clientId: rawRecord.client_id, - encryptedClientSecret: rawRecord.client_secret, - linkedOrganizationId: rawRecord.linked_organization_id, - tokenEndpoint: rawRecord.token_endpoint, - userinfoEndpoint: rawRecord.userinfo_endpoint, - authorizationEndpoint: rawRecord.authorization_endpoint, - additionalScopes: rawRecord.additional_scopes, - oidcUserJoinOnly: rawRecord.oidc_user_join_only, - oidcUserAccessOnly: rawRecord.oidc_user_access_only, - requireInvitation: rawRecord.require_invitation, - defaultMemberRoleId: rawRecord.default_role_id, - defaultResourceAssignment: rawRecord.default_assigned_resources, - }; -}; - -const CDNAccessTokenModel = zod.object({ - id: zod.string(), - target_id: zod.string(), - s3_key: zod.string(), - first_characters: zod.string(), - last_characters: zod.string(), - alias: zod.string(), - created_at: zod.string(), +const CDNAccessTokenModel = z.object({ + id: z.string(), + targetId: z.string(), + s3Key: z.string(), + firstCharacters: z.string(), + lastCharacters: z.string(), + alias: z.string(), + createdAt: z.string(), }); -const decodeCDNAccessTokenRecord = (result: unknown): CDNAccessToken => { - const rawRecord = CDNAccessTokenModel.parse(result); - - return { - id: rawRecord.id, - targetId: rawRecord.target_id, - s3Key: rawRecord.s3_key, - firstCharacters: rawRecord.first_characters, - lastCharacters: rawRecord.last_characters, - alias: rawRecord.alias, - createdAt: rawRecord.created_at, - }; -}; - -const FeatureFlagsModel = zod +const FeatureFlagsModel = z .object({ - forceLegacyCompositionInTargets: zod.array(zod.string()).default([]), + forceLegacyCompositionInTargets: z.array(z.string()).default([]), /** whether app deployments are enabled for the given organization */ - appDeployments: zod.boolean().default(false), + appDeployments: z.boolean().default(false), /** whether otel tracing is enabled for the given organization */ - otelTracing: zod.boolean().default(false), - schemaProposals: zod.boolean().default(false), + otelTracing: z.boolean().default(false), + schemaProposals: z.boolean().default(false), }) .optional() .nullable() @@ -5201,53 +4716,49 @@ const FeatureFlagsModel = zod }, ); -function decodeFeatureFlags(column: unknown) { - return FeatureFlagsModel.parse(column); -} - /** This version introduced the "diffSchemaVersionId" column. */ -const SchemaVersionRecordVersion_2024_01_10_Model = zod.literal('2024-01-10'); +const SchemaVersionRecordVersion_2024_01_10_Model = z.literal('2024-01-10'); const SchemaVersionRecordVersionModel = SchemaVersionRecordVersion_2024_01_10_Model; -const SchemaMetadataModel = zod.object({ - name: zod.string(), - content: zod.string(), - source: zod.nullable(zod.string()).default(null), +const SchemaMetadataModel = z.object({ + name: z.string(), + content: z.string(), + source: z.nullable(z.string()).default(null), }); -const SchemaVersionModel = zod.intersection( - zod.object({ - id: zod.string(), - isComposable: zod.boolean(), - createdAt: zod.string(), - baseSchema: zod.nullable(zod.string()), - actionId: zod.string(), - hasPersistedSchemaChanges: zod.nullable(zod.boolean()).transform(val => val ?? false), - previousSchemaVersionId: zod.nullable(zod.string()), - diffSchemaVersionId: zod.nullable(zod.string()), - compositeSchemaSDL: zod.nullable(zod.string()), - supergraphSDL: zod.nullable(zod.string()), - schemaCompositionErrors: zod.nullable(zod.array(SchemaCompositionErrorModel)), - recordVersion: zod.nullable(SchemaVersionRecordVersionModel), - tags: zod.nullable(zod.array(zod.string())), - schemaMetadata: zod.nullable(zod.record(zod.string(), zod.array(SchemaMetadataModel))), - metadataAttributes: zod.nullable(zod.record(zod.string(), zod.array(zod.string()))), - hasContractCompositionErrors: zod +const SchemaVersionModel = z.intersection( + z.object({ + id: z.string(), + isComposable: z.boolean(), + createdAt: z.string(), + baseSchema: z.nullable(z.string()), + actionId: z.string(), + hasPersistedSchemaChanges: z.nullable(z.boolean()).transform(val => val ?? false), + previousSchemaVersionId: z.nullable(z.string()), + diffSchemaVersionId: z.nullable(z.string()), + compositeSchemaSDL: z.nullable(z.string()), + supergraphSDL: z.nullable(z.string()), + schemaCompositionErrors: z.nullable(z.array(SchemaCompositionErrorModel)), + recordVersion: z.nullable(SchemaVersionRecordVersionModel), + tags: z.nullable(z.array(z.string())), + schemaMetadata: z.nullable(z.record(z.string(), z.array(SchemaMetadataModel))), + metadataAttributes: z.nullable(z.record(z.string(), z.array(z.string()))), + hasContractCompositionErrors: z .boolean() .nullable() .transform(val => val ?? false), conditionalBreakingChangeMetadata: ConditionalBreakingChangeMetadataModel.nullable(), }), - zod + z .union([ - zod.object({ - githubRepository: zod.string(), - githubSha: zod.string(), + z.object({ + githubRepository: z.string(), + githubSha: z.string(), }), - zod.object({ - githubRepository: zod.null(), - githubSha: zod.null(), + z.object({ + githubRepository: z.null(), + githubSha: z.null(), }), ]) .transform(val => ({ @@ -5260,35 +4771,35 @@ const SchemaVersionModel = zod.intersection( })), ); -export type SchemaVersion = zod.infer; +export type SchemaVersion = z.infer; -const DocumentCollectionModel = zod.object({ - id: zod.string(), - title: zod.string(), - description: zod.union([zod.string(), zod.null()]), - targetId: zod.string(), - createdByUserId: zod.union([zod.string(), zod.null()]), - createdAt: zod.string(), - updatedAt: zod.string(), +const DocumentCollectionModel = z.object({ + id: z.string(), + title: z.string(), + description: z.union([z.string(), z.null()]), + targetId: z.string(), + createdByUserId: z.union([z.string(), z.null()]), + createdAt: z.string(), + updatedAt: z.string(), }); -const DocumentCollectionDocumentModel = zod.object({ - id: zod.string(), - title: zod.string(), - contents: zod.string(), - variables: zod.string().nullable(), - headers: zod.string().nullable(), - createdByUserId: zod.union([zod.string(), zod.null()]), - documentCollectionId: zod.string(), - createdAt: zod.string(), - updatedAt: zod.string(), +const DocumentCollectionDocumentModel = z.object({ + id: z.string(), + title: z.string(), + contents: z.string(), + variables: z.string().nullable(), + headers: z.string().nullable(), + createdByUserId: z.union([z.string(), z.null()]), + documentCollectionId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), }); /** * Insert a schema version changes into the database. */ async function insertSchemaVersionContractChanges( - trx: DatabaseTransactionConnection, + trx: CommonQueryMethods, args: { changes: Array | null; schemaVersionContractId: string; @@ -5298,7 +4809,7 @@ async function insertSchemaVersionContractChanges( return; } - await trx.query(sql`/* insertSchemaVersionContractChanges */ + await trx.query(psql`/* insertSchemaVersionContractChanges */ INSERT INTO "contract_version_changes" ( "contract_version_id", "change_type", @@ -5307,7 +4818,7 @@ async function insertSchemaVersionContractChanges( "is_safe_based_on_usage" ) SELECT * FROM - ${sql.unnest( + ${psql.unnest( args.changes.map(change => // Note: change.criticality.level is actually a computed value from meta [ @@ -5327,7 +4838,7 @@ async function insertSchemaVersionContractChanges( * Insert a schema version changes into the database. */ async function insertSchemaVersionChanges( - trx: DatabaseTransactionConnection, + trx: CommonQueryMethods, args: { changes: Array; versionId: string; @@ -5337,7 +4848,7 @@ async function insertSchemaVersionChanges( return; } - await trx.query(sql`/* insertSchemaVersionChanges */ + await trx.query(psql`/* insertSchemaVersionChanges */ INSERT INTO schema_version_changes ( "schema_version_id", "change_type", @@ -5346,7 +4857,7 @@ async function insertSchemaVersionChanges( "is_safe_based_on_usage" ) SELECT * FROM - ${sql.unnest( + ${psql.unnest( args.changes.map(change => // Note: change.criticality.level is actually a computed value from meta [ @@ -5366,7 +4877,7 @@ async function insertSchemaVersionChanges( * Insert a new schema version into the database. */ async function insertSchemaVersion( - trx: DatabaseTransactionConnection, + trx: CommonQueryMethods, args: { isComposable: boolean; targetId: string; @@ -5391,7 +4902,7 @@ async function insertSchemaVersion( conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata; }, ) { - const query = sql`/* insertSchemaVersion */ + const query = psql`/* insertSchemaVersion */ INSERT INTO schema_versions ( record_version, @@ -5428,7 +4939,7 @@ async function insertSchemaVersion( ${jsonify(args.schemaCompositionErrors)}, ${args.github?.repository ?? null}, ${args.github?.sha ?? null}, - ${Array.isArray(args.tags) ? sql.array(args.tags, 'text') : null}, + ${Array.isArray(args.tags) ? psql.array(args.tags, 'text') : null}, ${args.hasContractCompositionErrors}, ${jsonify(InsertConditionalBreakingChangeMetadataModel.parse(args.conditionalBreakingChangeMetadata))}, ${jsonify(args.schemaMetadata)}, @@ -5438,11 +4949,11 @@ async function insertSchemaVersion( ${schemaVersionSQLFields()} `; - return await trx.one(query).then(SchemaVersionModel.parse); + return await trx.maybeOne(query).then(SchemaVersionModel.parse); } async function insertSchemaVersionContract( - trx: DatabaseTransactionConnection, + trx: CommonQueryMethods, args: { schemaVersionId: string; contractId: string; @@ -5452,7 +4963,7 @@ async function insertSchemaVersionContract( schemaCompositionErrors: Array | null; }, ): Promise { - const id = await trx.oneFirst(sql`/* insertSchemaVersionContract */ + const id = await trx.oneFirst(psql`/* insertSchemaVersionContract */ INSERT INTO "contract_versions" ( "schema_version_id" , "contract_id" @@ -5473,11 +4984,11 @@ async function insertSchemaVersionContract( "id" `); - return zod.string().parse(id); + return z.string().parse(id); } async function updateSchemaCoordinateStatus( - trx: DatabaseTransactionConnection, + trx: CommonQueryMethods, args: { targetId: string; versionId: string; @@ -5555,7 +5066,7 @@ async function updateSchemaCoordinateStatus( */ function jsonify(obj: T | null | undefined) { if (obj == null) return null; - return sql`${JSON.stringify(obj)}::jsonb`; + return psql`${JSON.stringify(obj)}::jsonb`; } /** @@ -5596,7 +5107,7 @@ export function toSerializableSchemaChange(change: SchemaChangeType): { }; } -const schemaCheckSQLFields = sql` +const schemaCheckSQLFields = psql` c."id" , to_json(c."created_at") as "createdAt" , to_json(c."updated_at") as "updatedAt" @@ -5626,7 +5137,7 @@ const schemaCheckSQLFields = sql` , c."schema_proposal_changes" as "schemaProposalChanges" `; -const schemaVersionSQLFields = (t = sql``) => sql` +const schemaVersionSQLFields = (t = psql``) => psql` ${t}"id" , ${t}"is_composable" as "isComposable" , to_json(${t}"created_at") as "createdAt" @@ -5648,7 +5159,7 @@ const schemaVersionSQLFields = (t = sql``) => sql` , ${t}"metadata_attributes" as "metadataAttributes" `; -const targetSQLFields = sql` +const targetSQLFields = psql` "id", "clean_id" as "slug", "name", @@ -5657,10 +5168,10 @@ const targetSQLFields = sql` "fail_diff_on_dangerous_change" as "failDiffOnDangerousChange" `; -export function findTargetById(deps: { pool: DatabasePool }) { +export function findTargetById(deps: { pool: PostgresDatabasePool }) { return async function findByIdImplementation(id: string): Promise { - const data = await deps.pool.maybeOne( - sql`/* getTarget */ + const data = await deps.pool.maybeOne( + psql`/* getTarget */ SELECT "t".* , "p"."org_id" AS "orgId" @@ -5684,14 +5195,14 @@ export function findTargetById(deps: { pool: DatabasePool }) { }; } -export function findTargetBySlug(deps: { pool: DatabasePool }) { +export function findTargetBySlug(deps: { pool: PostgresDatabasePool }) { return async function findTargetsBySlugImplementation(args: { organizationSlug: string; projectSlug: string; targetSlug: string; }): Promise { - const data = await deps.pool.maybeOne( - sql`/* getTargetBySlug */ + const data = await deps.pool.maybeOne( + psql`/* getTargetBySlug */ SELECT "t".* , "p"."org_id" AS "orgId" @@ -5718,17 +5229,17 @@ export function findTargetBySlug(deps: { pool: DatabasePool }) { }; } -const TargetModel = zod.object({ - id: zod.string(), - slug: zod.string(), - name: zod.string(), - projectId: zod.string(), - graphqlEndpointUrl: zod.string().nullable(), - failDiffOnDangerousChange: zod.boolean(), +const TargetModel = z.object({ + id: z.string(), + slug: z.string(), + name: z.string(), + projectId: z.string(), + graphqlEndpointUrl: z.string().nullable(), + failDiffOnDangerousChange: z.boolean(), }); const TargetWithOrgIdModel = TargetModel.extend({ - orgId: zod.string(), + orgId: z.string(), }); export * from './schema-change-model'; @@ -5768,15 +5279,15 @@ const getOrganizationInvitationId = (keys: { email: string; code: string; }) => Buffer.from([keys.organizationId, keys.email, keys.code].join(':')).toString('hex'); -const OrganizationInvitationModel = zod +const OrganizationInvitationModel = z .object({ - organizationId: zod.string(), - code: zod.string(), - email: zod.string().email(), - createdAt: zod.number().transform(v => new Date(v).toISOString()), - expiresAt: zod.number().transform(v => new Date(v).toISOString()), - roleId: zod.string(), - assignedResources: zod.any(), + organizationId: z.string(), + code: z.string(), + email: z.string().email(), + createdAt: z.string().transform(v => new Date(v).toISOString()), + expiresAt: z.string().transform(v => new Date(v).toISOString()), + roleId: z.string(), + assignedResources: z.any(), }) .transform( invitation => @@ -5788,7 +5299,7 @@ const OrganizationInvitationModel = zod }) as OrganizationInvitation, ); -export const userFields = (user: TaggedTemplateLiteralInvocation) => sql` +export const userFields = (user: TaggedTemplateLiteralInvocation) => psql` ${user}"id" , ${user}"email" , to_json(${user}"created_at") AS "createdAt" @@ -5813,21 +5324,455 @@ export const userFields = (user: TaggedTemplateLiteralInvocation) => sql` ) AS "providers" `; -export const UserModel = zod.object({ - id: zod.string(), - email: zod.string(), - createdAt: zod.string(), - displayName: zod.string(), - fullName: zod.string(), - superTokensUserId: zod.string().nullable(), - isAdmin: zod +const organizationFields = (prefix: TaggedTemplateLiteralInvocation) => psql` + ${prefix}"id" + , ${prefix}"clean_id" AS "slug" + , ${prefix}"name" + , ${prefix}"limit_retention_days" AS "limitRetentionDays" + , ${prefix}"limit_operations_monthly" AS "limitOperationsMonthly" + , ${prefix}"plan_name" AS "billingPlan" + , ${prefix}"get_started_creating_project" AS "getStartedCreatingProject" + , ${prefix}"get_started_publishing_schema" AS "getStartedPublishingSchema" + , ${prefix}"get_started_checking_schema" AS "getStartedCheckingSchema" + , ${prefix}"get_started_inviting_members" AS "getStartedInvitingMembers" + , ${prefix}"get_started_reporting_operations" AS "getStartedReportingOperations" + , ${prefix}"get_started_usage_breaking" AS "getStartedUsageBreaking" + , ${prefix}"feature_flags" AS "featureFlags" + , ${prefix}"zendesk_organization_id" AS "zendeskId" + , ${prefix}"user_id" AS "ownerId" +`; + +const projectFields = (prefix: TaggedTemplateLiteralInvocation) => psql` + ${prefix}"id" + , ${prefix}"clean_id" AS "slug" + , ${prefix}"name" + , ${prefix}"org_id" AS "orgId" + , ${prefix}"type" + , to_json(${prefix}"created_at") AS "createdAt" + , ${prefix}"build_url" AS "buildUrl" + , ${prefix}"validation_url" AS "validationUrl" + , ${prefix}"git_repository" AS "gitRepository" + , ${prefix}"github_check_with_project_name" AS "useProjectNameInGithubCheck" + , ${prefix}"external_composition_enabled" AS "externalCompositionEnabled" + , ${prefix}"external_composition_endpoint" AS "externalCompositionEndpoint" + , ${prefix}"external_composition_secret" AS "externalCompositionEncryptedSecret" + , ${prefix}"native_federation" AS "nativeFederation" +`; + +const schemaPolicyFields = (prefix: TaggedTemplateLiteralInvocation) => psql` + ${prefix}"resource_type" AS "resourceType" + , ${prefix}"resource_id" AS "resourceId" + , ${prefix}"config" + , ${prefix}"allow_overriding" AS "allowOverrides" + , to_json(${prefix}"created_at") AS "createdAt" + , to_json(${prefix}"updated_at") AS "updatedAt" +`; + +const alertChannelFields = (prefix: TaggedTemplateLiteralInvocation = psql``) => psql` + ${prefix}"id" AS "id", + ${prefix}"name" AS "name", + ${prefix}"type" AS "type", + ${prefix}"project_id" AS "projectId", + to_json(${prefix}"created_at") AS "createdAt", + ${prefix}"slack_channel" AS "slackChannel", + ${prefix}"webhook_endpoint" AS "webhookEndpoint" +`; + +const alertFields = (prefix: TaggedTemplateLiteralInvocation = psql``) => psql` + ${prefix}"id" AS "id", + ${prefix}"type" AS "type", + to_json(${prefix}"created_at") AS "createdAt", + ${prefix}"alert_channel_id" AS "channelId", + ${prefix}"project_id" AS "projectId", + ${prefix}"target_id" AS "targetId" +`; + +const targetSettingsFields = (prefix: TaggedTemplateLiteralInvocation) => psql` + ${prefix}"validation_enabled" AS "validationEnabled" + , ${prefix}"validation_percentage" AS "validationPercentage" + , ${prefix}"validation_period" AS "validationPeriod" + , ${prefix}"validation_excluded_clients" AS "validationExcludedClients" + , ${prefix}"validation_excluded_app_deployments" AS "validationExcludedAppDeployments" + , ${prefix}"validation_request_count" AS "validationRequestCount" + , ${prefix}"validation_breaking_change_formula" AS "validationBreakingChangeFormula" + , ${prefix}"fail_diff_on_dangerous_change" AS "failDiffOnDangerousChange" + , ${prefix}"app_deployment_protection_enabled" AS "appDeploymentProtectionEnabled" + , ${prefix}"app_deployment_protection_min_days_inactive" AS "appDeploymentProtectionMinDaysInactive" + , ${prefix}"app_deployment_protection_min_days_since_creation" AS "appDeploymentProtectionMinDaysSinceCreation" + , ${prefix}"app_deployment_protection_max_traffic_percentage" AS "appDeploymentProtectionMaxTrafficPercentage" + , ${prefix}"app_deployment_protection_traffic_period_days" AS "appDeploymentProtectionTrafficPeriodDays" + , ${prefix}"app_deployment_protection_rule_logic" AS "appDeploymentProtectionRuleLogic" +`; + +const schemaLogFields = (prefix: TaggedTemplateLiteralInvocation) => psql` + ${prefix}"id" + , ${prefix}"author" + , ${prefix}"commit" + , ${prefix}"sdl" + , ${prefix}"created_at" AS "date" + , ${prefix}"target_id" AS "target" + , ${prefix}"metadata" + , lower(${prefix}"service_name") AS "service_name" + , ${prefix}"service_url" + , ${prefix}"action" +`; + +const cdnAccessTokenFields = (prefix: TaggedTemplateLiteralInvocation = psql``) => psql` + ${prefix}"id" AS "id" + , ${prefix}"target_id" AS "targetId" + , ${prefix}"s3_key" AS "s3Key" + , ${prefix}"first_characters" AS "firstCharacters" + , ${prefix}"last_characters" AS "lastCharacters" + , ${prefix}"alias" AS "alias" + , to_json(${prefix}"created_at") as "createdAt" +`; + +const oidcIntegrationFields = (prefix: TaggedTemplateLiteralInvocation = psql``) => psql` + ${prefix}"id" AS "id" + , ${prefix}"linked_organization_id" AS "linkedOrganizationId" + , ${prefix}"client_id" AS "clientId" + , ${prefix}"client_secret" AS "clientSecret" + , ${prefix}"oauth_api_url" AS "oauthApiUrl" + , ${prefix}"token_endpoint" AS "tokenEndpoint" + , ${prefix}"userinfo_endpoint" AS "userinfoEndpoint" + , ${prefix}"authorization_endpoint" AS "authorizationEndpoint" + , ${prefix}"additional_scopes" AS "additionalScopes" + , ${prefix}"oidc_user_join_only" AS "oidcUserJoinOnly" + , ${prefix}"oidc_user_access_only" AS "oidcUserAccessOnly" + , ${prefix}"default_role_id" AS "defaultRoleId" + , ${prefix}"default_assigned_resources" AS "defaultAssignedResources" + , ${prefix}"require_invitation" AS "requireInvitation" +`; + +const SchemaLogBase = z.object({ + id: z.string(), + date: z.number(), + target: z.string().uuid(), +}); + +const SchemaPushLogBase = SchemaLogBase.extend({ + author: z.string(), + commit: z.string(), + sdl: z.string(), + metadata: z.string().nullish().default(null), +}); + +const SinglePushSchemaLogModel = SchemaPushLogBase.extend({ + action: z.literal('PUSH'), + type: z.literal('SINGLE'), + service_name: z.string().nullable(), + service_url: z.string().nullable(), +}).transform(value => ({ + ...value, + kind: 'single' as const, +})); + +export type SinglePushSchemaLog = z.TypeOf; + +const CompositeSchemaTypeModel = z.union([z.literal('FEDERATION'), z.literal('STITCHING')]); + +const CompositePushSchemaLogModel = SchemaPushLogBase.extend({ + action: z.literal('PUSH'), + type: CompositeSchemaTypeModel, + service_name: z.string(), + service_url: z.string(), +}).transform(value => ({ + ...value, + kind: 'composite' as const, +})); + +export type CompositePushSchemaLog = z.TypeOf; + +const CompositeDeletedSchemaLogModel = SchemaLogBase.extend({ + action: z.literal('DELETE'), + service_name: z.string(), +}).transform(value => ({ + ...value, + kind: 'composite' as const, +})); + +export type CompositeDeletedSchemaLog = z.TypeOf; + +const SchemaPushLogModel = z.union([SinglePushSchemaLogModel, CompositePushSchemaLogModel]); +const SchemaLogModel = z.union([SchemaPushLogModel, CompositeDeletedSchemaLogModel]); + +const OrganizationModel = z + .object({ + id: z.string(), + slug: z.string(), + name: z.string(), + limitRetentionDays: z.number(), + limitOperationsMonthly: z.number(), + billingPlan: z.string(), + getStartedCreatingProject: z.boolean(), + getStartedPublishingSchema: z.boolean(), + getStartedCheckingSchema: z.boolean(), + getStartedInvitingMembers: z.boolean(), + getStartedReportingOperations: z.boolean(), + getStartedUsageBreaking: z.boolean(), + featureFlags: FeatureFlagsModel, + zendeskId: z.string().nullable(), + ownerId: z.string(), + }) + .transform(org => ({ + id: org.id, + slug: org.slug, + name: org.name, + billingPlan: org.billingPlan, + monthlyRateLimit: { + retentionInDays: org.limitRetentionDays, + operations: org.limitOperationsMonthly, + }, + getStarted: { + id: org.id, + creatingProject: org.getStartedCreatingProject, + publishingSchema: org.getStartedPublishingSchema, + checkingSchema: org.getStartedCheckingSchema, + invitingMembers: org.getStartedInvitingMembers, + reportingOperations: org.getStartedReportingOperations, + enablingUsageBasedBreakingChanges: org.getStartedUsageBreaking, + }, + featureFlags: org.featureFlags, + zendeskId: org.zendeskId, + ownerId: org.ownerId, + })); + +const ProjectModel = z + .object({ + id: z.string(), + slug: z.string(), + name: z.string(), + orgId: z.string(), + type: z.string(), + createdAt: z.string(), + buildUrl: z.string().nullable(), + validationUrl: z.string().nullable(), + gitRepository: z.string().nullable(), + useProjectNameInGithubCheck: z.boolean(), + externalCompositionEnabled: z.boolean(), + externalCompositionEndpoint: z.string().nullable(), + externalCompositionEncryptedSecret: z.string().nullable(), + nativeFederation: z.boolean().nullable(), + }) + .transform(project => ({ + id: project.id, + slug: project.slug, + orgId: project.orgId, + name: project.name, + type: project.type as ProjectType, + createdAt: project.createdAt, + buildUrl: project.buildUrl, + validationUrl: project.validationUrl, + gitRepository: project.gitRepository as `${string}/${string}` | null, + useProjectNameInGithubCheck: project.useProjectNameInGithubCheck === true, + externalComposition: { + enabled: project.externalCompositionEnabled, + endpoint: project.externalCompositionEndpoint, + encryptedSecret: project.externalCompositionEncryptedSecret, + }, + nativeFederation: project.nativeFederation === true, + })); + +const OrganizationBillingModel = z.object({ + organizationId: z.string(), + externalBillingReference: z.string(), + billingEmailAddress: z.string().nullable(), +}); + +const AlertChannelModel = z.object({ + id: z.string(), + name: z.string(), + type: z.enum(['MSTEAMS_WEBHOOK', 'SLACK', 'WEBHOOK']), + projectId: z.string(), + createdAt: z.string(), + slackChannel: z.string().nullable(), + webhookEndpoint: z.string().nullable(), +}); + +const AlertModel = z.object({ + id: z.string(), + type: z.enum(['SCHEMA_CHANGE_NOTIFICATIONS']), + createdAt: z.string(), + channelId: z.string(), + projectId: z.string(), + targetId: z.string(), +}); + +const OrganizationUserIdAndIdModel = z.object({ + id: z.string(), + user_id: z.string(), +}); + +const SchemaPolicyModel = z + .object({ + resourceType: z.enum(['ORGANIZATION', 'PROJECT']), + resourceId: z.string(), + config: z.any(), + allowOverrides: z.boolean(), + createdAt: z.string().transform(t => new Date(t)), + updatedAt: z.string().transform(t => new Date(t)), + }) + .transform(sp => ({ + id: `${sp.resourceType}_${sp.resourceId}`, + config: sp.config, + createdAt: sp.createdAt, + updatedAt: sp.updatedAt, + resource: sp.resourceType, + resourceId: sp.resourceId, + allowOverrides: sp.allowOverrides, + })); + +const MemberModel = z + .object({ + id: z.string(), + isOwner: z.boolean(), + scopes: z.array(z.string()).nullable(), + organizationId: z.string(), + oidcIntegrationId: z.string().nullable(), + connectedToZendesk: z.boolean().nullable(), + roleId: z.string(), + roleName: z.string(), + roleLocked: z.boolean(), + roleDescription: z.string(), + roleScopes: z.array(z.string()).nullable(), + // user fields are also present in the row + email: z.string(), + createdAt: z.string(), + displayName: z.string(), + fullName: z.string(), + superTokensUserId: z.string().nullable(), + isAdmin: z + .boolean() + .nullable() + .transform(value => value ?? false), + zendeskId: z.string().nullable(), + providers: z.array( + z + .string() + .nullable() + .transform(provider => { + if (provider === 'oidc') { + return 'OIDC' as const; + } + if (provider === 'github') { + return 'GITHUB' as const; + } + if (provider === 'google') { + return 'GOOGLE' as const; + } + return null; + }), + ), + }) + .transform(row => ({ + id: row.id, + isOwner: row.isOwner, + user: { + id: row.id, + email: row.email, + fullName: row.fullName, + displayName: row.displayName, + providers: row.providers.filter((p): p is NonNullable => p != null), + superTokensUserId: row.superTokensUserId, + isAdmin: row.isAdmin, + zendeskId: row.zendeskId, + }, + scopes: (row.scopes as Member['scopes']) || [], + organization: row.organizationId, + oidcIntegrationId: row.oidcIntegrationId ?? null, + connectedToZendesk: row.connectedToZendesk ?? false, + role: { + id: row.roleId, + name: row.roleName, + locked: row.roleLocked, + description: row.roleDescription, + scopes: (row.roleScopes as Member['scopes']) ?? [], + organizationId: row.organizationId, + membersCount: undefined as number | undefined, + }, + })); + +const OrganizationMemberAccessModel = z.object({ + organization_id: z.string(), + user_id: z.string(), + scopes: z.array(z.string()).nullable(), +}); + +const TargetSettingsModel = z + .object({ + validationEnabled: z.boolean(), + validationPercentage: z.number(), + validationPeriod: z.number(), + validationExcludedClients: z.array(z.string()).nullable(), + validationExcludedAppDeployments: z.array(z.string()).nullable(), + validationRequestCount: z.number().nullable(), + validationBreakingChangeFormula: z.string().nullable(), + failDiffOnDangerousChange: z.boolean(), + targets: z.array(z.string()).nullable(), + appDeploymentProtectionEnabled: z.boolean(), + appDeploymentProtectionMinDaysInactive: z.number(), + appDeploymentProtectionMinDaysSinceCreation: z.number(), + appDeploymentProtectionMaxTrafficPercentage: z.coerce.number(), + appDeploymentProtectionTrafficPeriodDays: z.number(), + appDeploymentProtectionRuleLogic: z.enum(['AND', 'OR']), + }) + .transform(row => ({ + failDiffOnDangerousChange: row.failDiffOnDangerousChange, + validation: { + isEnabled: row.validationEnabled, + percentage: row.validationPercentage, + period: row.validationPeriod, + requestCount: row.validationRequestCount ?? 1, + breakingChangeFormula: (row.validationBreakingChangeFormula ?? 'PERCENTAGE') as + | 'PERCENTAGE' + | 'REQUEST_COUNT', + targets: Array.isArray(row.targets) ? row.targets : [], + excludedClients: Array.isArray(row.validationExcludedClients) + ? row.validationExcludedClients + : [], + excludedAppDeployments: Array.isArray(row.validationExcludedAppDeployments) + ? row.validationExcludedAppDeployments + : [], + }, + appDeploymentProtection: { + isEnabled: row.appDeploymentProtectionEnabled, + minDaysInactive: row.appDeploymentProtectionMinDaysInactive, + minDaysSinceCreation: row.appDeploymentProtectionMinDaysSinceCreation, + maxTrafficPercentage: row.appDeploymentProtectionMaxTrafficPercentage, + trafficPeriodDays: row.appDeploymentProtectionTrafficPeriodDays, + ruleLogic: row.appDeploymentProtectionRuleLogic, + }, + })); + +const TargetIdModel = z.object({ + id: z.string(), +}); + +const OrganizationTargetPairModel = z.object({ + organization: z.string(), + target: z.string(), +}); + +const OrganizationStatModel = z.object({ + id: z.string(), + total: z.number(), +}); + +export const UserModel = z.object({ + id: z.string(), + email: z.string(), + createdAt: z.string(), + displayName: z.string(), + fullName: z.string(), + superTokensUserId: z.string().nullable(), + isAdmin: z .boolean() .nullable() .transform(value => value ?? false), - oidcIntegrationId: zod.string().nullable(), - zendeskId: zod.string().nullable(), - providers: zod.array( - zod + oidcIntegrationId: z.string().nullable(), + zendeskId: z.string().nullable(), + providers: z.array( + z .string() .nullable() .transform(provider => { @@ -5845,4 +5790,4 @@ export const UserModel = zod.object({ ), }); -type UserType = zod.TypeOf; +type UserType = z.TypeOf; diff --git a/packages/services/storage/src/schema-change-model.ts b/packages/services/storage/src/schema-change-model.ts index e6b92b6eb..3906438a9 100644 --- a/packages/services/storage/src/schema-change-model.ts +++ b/packages/services/storage/src/schema-change-model.ts @@ -2,7 +2,6 @@ import crypto from 'node:crypto'; import stableJSONStringify from 'fast-json-stable-stringify'; import { Kind } from 'graphql'; -import { SerializableValue } from 'slonik'; import { z } from 'zod'; import { ChangeType, @@ -89,6 +88,7 @@ import { UnionMemberAddedChange, UnionMemberRemovedChange, } from '@graphql-inspector/core'; +import { SerializableValue } from '@hive/postgres'; import { RegistryServiceUrlChangeSerializableChange, schemaChangeFromSerializableChange, diff --git a/packages/services/storage/src/shared.ts b/packages/services/storage/src/shared.ts deleted file mode 100644 index 2ed9d235e..000000000 --- a/packages/services/storage/src/shared.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Slonik 23.8.X requires an index signature in types (which is weird) - */ -export type Slonik = { - [K in keyof T]: T[K]; -} & { - [key: string]: null; -}; diff --git a/packages/services/storage/src/tokens.ts b/packages/services/storage/src/tokens.ts index cf5309d6a..b191b2177 100644 --- a/packages/services/storage/src/tokens.ts +++ b/packages/services/storage/src/tokens.ts @@ -1,27 +1,43 @@ -import { Interceptor, sql } from 'slonik'; -import { getPool, toDate, tokens } from './db'; -import type { Slonik } from './shared'; +import { z } from 'zod'; +import { createPostgresDatabasePool, psql, toDate, type Interceptor } from '@hive/postgres'; -function transformToken(row: tokens) { - return { - token: row.token, - tokenAlias: row.token_alias, - name: row.name, - date: row.created_at as unknown as string, - lastUsedAt: row.last_used_at as unknown as string | null, - organization: row.organization_id, - project: row.project_id, - target: row.target_id, - scopes: row.scopes || [], - }; -} +const TokenModel = z.object({ + token: z.string(), + tokenAlias: z.string(), + name: z.string(), + date: z.string(), + lastUsedAt: z.string().nullable(), + organization: z.string(), + project: z.string(), + target: z.string(), + scopes: z + .array(z.string()) + .nullable() + .transform(value => value ?? []), +}); + +const tokenFields = psql` + "token" + , "token_alias" AS "tokenAlias" + , "name" + , to_json("created_at") AS "date" + , "last_used_at" AS "lastUsedAt" + , "organization_id" AS "organization" + , "project_id" AS "project" + , "target_id" AS "target" + , "scopes" +`; export async function createTokenStorage( connection: string, maximumPoolSize: number, additionalInterceptors: Interceptor[] = [], ) { - const pool = await getPool(connection, maximumPoolSize, additionalInterceptors); + const pool = await createPostgresDatabasePool({ + connectionParameters: connection, + maximumPoolSize, + additionalInterceptors, + }); return { destroy() { @@ -29,37 +45,37 @@ export async function createTokenStorage( }, async isReady() { try { - await pool.exists(sql`SELECT 1`); + await pool.exists(psql`SELECT 1`); return true; } catch { return false; } }, async getTokens({ target }: { target: string }) { - const result = await pool.query>( - sql` - SELECT * + return await pool + .any( + psql` + SELECT ${tokenFields} FROM tokens WHERE target_id = ${target} AND deleted_at IS NULL ORDER BY created_at DESC `, - ); - - return result.rows.map(transformToken); + ) + .then(z.array(TokenModel).parse); }, async getToken({ token }: { token: string }) { - const row = await pool.maybeOne>( - sql` - SELECT * + return await pool + .maybeOne( + psql` + SELECT ${tokenFields} FROM tokens WHERE token = ${token} AND deleted_at IS NULL LIMIT 1 `, - ); - - return row ? transformToken(row) : null; + ) + .then(TokenModel.parse); }, async createToken({ token, @@ -78,28 +94,28 @@ export async function createTokenStorage( organization: string; scopes: readonly string[]; }) { - const row = await pool.one>( - sql` + return await pool + .one( + psql` INSERT INTO tokens (name, token, token_alias, target_id, project_id, organization_id, scopes) VALUES - (${name}, ${token}, ${tokenAlias}, ${target}, ${project}, ${organization}, ${sql.array( + (${name}, ${token}, ${tokenAlias}, ${target}, ${project}, ${organization}, ${psql.array( scopes, 'text', )}) - RETURNING * + RETURNING ${tokenFields} `, - ); - - return transformToken(row); + ) + .then(TokenModel.parse); }, async deleteToken(params: { targetId: string; token: string; postDeletionTransaction: () => Promise; }) { - return await pool.transaction(async t => { - const deleted = await t.maybeOneFirst(sql` + return await pool.transaction('deleteToken', async t => { + const deleted = await t.maybeOneFirst(psql` UPDATE "tokens" SET @@ -114,18 +130,18 @@ export async function createTokenStorage( await params.postDeletionTransaction(); } - return deleted ?? false; + return !!deleted; }); }, async touchTokens({ tokens }: { tokens: Array<{ token: string; date: Date }> }) { - await pool.query(sql` + await pool.query(psql` UPDATE tokens as t SET last_used_at = c.last_used_at FROM ( VALUES - (${sql.join( - tokens.map(t => sql`${t.token}, ${toDate(t.date)}`), - sql`), (`, + (${psql.join( + tokens.map(t => psql`${t.token}, ${toDate(t.date)}`), + psql`), (`, )}) ) as c(token, last_used_at) WHERE c.token = t.token; diff --git a/packages/services/tokens/package.json b/packages/services/tokens/package.json index 7d72892ab..6a6a61f25 100644 --- a/packages/services/tokens/package.json +++ b/packages/services/tokens/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@hive/postgres": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", "@sentry/node": "7.120.2", diff --git a/packages/services/tokens/src/multi-tier-storage.ts b/packages/services/tokens/src/multi-tier-storage.ts index d40a95896..0d9ba6e7e 100644 --- a/packages/services/tokens/src/multi-tier-storage.ts +++ b/packages/services/tokens/src/multi-tier-storage.ts @@ -2,7 +2,8 @@ import type { FastifyBaseLogger } from 'fastify'; import type { Redis } from 'ioredis'; import { LRUCache } from 'lru-cache'; import ms from 'ms'; -import { createConnectionString, createTokenStorage, Interceptor } from '@hive/storage'; +import { createConnectionString, type PostgresConnectionParamaters } from '@hive/postgres'; +import { createTokenStorage, type Interceptor } from '@hive/storage'; import { captureException, captureMessage } from '@sentry/node'; import { atomic, until, useActionTracker } from './helpers'; import { recordCacheFill, recordCacheRead } from './metrics'; @@ -44,7 +45,7 @@ const cacheConfig = { } as const; export async function createStorage( - config: Parameters[0], + config: PostgresConnectionParamaters, redis: Redis, serverLogger: FastifyBaseLogger, additionalInterceptors: Interceptor[], diff --git a/packages/services/usage/package.json b/packages/services/usage/package.json index c510361ff..9f2948db9 100644 --- a/packages/services/usage/package.json +++ b/packages/services/usage/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@hive/api": "workspace:*", + "@hive/postgres": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", "@hive/tokens": "workspace:*", diff --git a/packages/services/usage/src/authn.ts b/packages/services/usage/src/authn.ts index 3a35be5e1..4274fce68 100644 --- a/packages/services/usage/src/authn.ts +++ b/packages/services/usage/src/authn.ts @@ -1,17 +1,17 @@ import type Redis from 'ioredis'; -import type { DatabasePool } from 'slonik'; import { AuthN } from '@hive/api/modules/auth/lib/authz'; import { OrganizationAccessTokenStrategy } from '@hive/api/modules/auth/lib/organization-access-token-strategy'; import { OrganizationAccessTokenValidationCache } from '@hive/api/modules/auth/providers/organization-access-token-validation-cache'; import { OrganizationAccessTokensCache } from '@hive/api/modules/organization/providers/organization-access-tokens-cache'; import { Logger } from '@hive/api/modules/shared/providers/logger'; import { PrometheusConfig } from '@hive/api/modules/shared/providers/prometheus-config'; +import type { PostgresDatabasePool } from '@hive/postgres'; /** * Creates an authentication provider for organization access tokens. */ export function createAuthN(args: { - pgPool: DatabasePool; + pgPool: PostgresDatabasePool; redis: Redis; isPrometheusEnabled: boolean; }): AuthN { diff --git a/packages/services/usage/src/index.ts b/packages/services/usage/src/index.ts index be617075f..713ea428d 100644 --- a/packages/services/usage/src/index.ts +++ b/packages/services/usage/src/index.ts @@ -4,6 +4,7 @@ import Redis from 'ioredis'; import { PrometheusConfig } from '@hive/api/modules/shared/providers/prometheus-config'; import { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache'; import { TargetsBySlugCache } from '@hive/api/modules/target/providers/targets-by-slug-cache'; +import { createPostgresDatabasePool } from '@hive/postgres'; import { configureTracing, createServer, @@ -13,8 +14,6 @@ import { startMetrics, TracingInstance, } from '@hive/service-common'; -import { createConnectionString } from '@hive/storage'; -import { getPool } from '@hive/storage/db/pool'; import * as Sentry from '@sentry/node'; import { createAuthN } from './authn'; import { env } from './environment'; @@ -72,11 +71,11 @@ async function main() { }, }); - const pgPool = await getPool( - createConnectionString(env.postgres), - 5, - tracing ? [tracing.instrumentSlonik()] : [], - ); + const pgPool = await createPostgresDatabasePool({ + connectionParameters: env.postgres, + maximumPoolSize: 5, + additionalInterceptors: tracing ? [tracing.instrumentSlonik()] : undefined, + }); const authN = createAuthN({ pgPool, diff --git a/packages/services/workflows/package.json b/packages/services/workflows/package.json index 2e503ed74..6b84d6d8d 100644 --- a/packages/services/workflows/package.json +++ b/packages/services/workflows/package.json @@ -13,6 +13,7 @@ "@graphql-inspector/core": "7.1.2", "@graphql-inspector/patch": "0.1.3", "@graphql-yoga/redis-event-target": "3.0.3", + "@hive/postgres": "workspace:*", "@hive/pubsub": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", @@ -30,7 +31,6 @@ "mjml": "4.14.0", "nodemailer": "8.0.4", "sendmail": "1.6.1", - "slonik": "30.4.4", "zod": "3.25.76" } } diff --git a/packages/services/workflows/src/context.ts b/packages/services/workflows/src/context.ts index 944ea2b87..cfc676545 100644 --- a/packages/services/workflows/src/context.ts +++ b/packages/services/workflows/src/context.ts @@ -1,5 +1,5 @@ -import type { DatabasePool } from 'slonik'; import type { Logger } from '@graphql-hive/logger'; +import { PostgresDatabasePool } from '@hive/postgres'; import type { HivePubSub } from '@hive/pubsub'; import type { EmailProvider } from './lib/emails/providers.js'; import type { SchemaProvider } from './lib/schema/provider.js'; @@ -9,7 +9,7 @@ export type Context = { logger: Logger; email: EmailProvider; schema: SchemaProvider; - pg: DatabasePool; + pg: PostgresDatabasePool; requestBroker: RequestBroker | null; pubSub: HivePubSub; }; diff --git a/packages/services/workflows/src/environment.ts b/packages/services/workflows/src/environment.ts index 44e9f4cb7..32ee8ee8b 100644 --- a/packages/services/workflows/src/environment.ts +++ b/packages/services/workflows/src/environment.ts @@ -1,6 +1,6 @@ import zod from 'zod'; +import { PostgresConnectionParamaters } from '@hive/postgres'; import { OpenTelemetryConfigurationModel } from '@hive/service-common'; -import { createConnectionString } from '@hive/storage'; import { RequestBroker } from './lib/webhooks/send-webhook.js'; const isNumberString = (input: unknown) => zod.string().regex(/^\d+$/).safeParse(input).success; @@ -231,14 +231,14 @@ export const env = { } : null, postgres: { - connectionString: createConnectionString({ + connectionString: { ssl: postgres.POSTGRES_SSL === '1', host: postgres.POSTGRES_HOST, db: postgres.POSTGRES_DB, password: postgres.POSTGRES_PASSWORD, port: postgres.POSTGRES_PORT, user: postgres.POSTGRES_USER, - }), + } satisfies PostgresConnectionParamaters, }, requestBroker: requestBroker.REQUEST_BROKER === '1' diff --git a/packages/services/workflows/src/index.ts b/packages/services/workflows/src/index.ts index d1fb2ae6e..305817bed 100644 --- a/packages/services/workflows/src/index.ts +++ b/packages/services/workflows/src/index.ts @@ -1,6 +1,6 @@ import { run } from 'graphile-worker'; -import { createPool } from 'slonik'; import { Logger } from '@graphql-hive/logger'; +import { createPostgresDatabasePool } from '@hive/postgres'; import { bridgeGraphileLogger, createHivePubSub } from '@hive/pubsub'; import { createServer, @@ -52,7 +52,9 @@ const crontab = ` 0 3 * * * purgeExpiredDedupeKeys `; -const pg = await createPool(env.postgres.connectionString); +const pg = await createPostgresDatabasePool({ + connectionParameters: env.postgres.connectionString, +}); const logger = new Logger({ level: env.log.level }); logger.info({ pid: process.pid }, 'starting workflow service'); @@ -142,7 +144,7 @@ const shutdownMetrics = env.prometheus const runner = await run({ logger: bridgeGraphileLogger(logger), crontab, - pgPool: pg.pool, + pgPool: pg.getRawPgPool(), taskList: Object.fromEntries(modules.map(module => module.task(context))), noHandleSignals: true, events: createTaskEventEmitter(), diff --git a/packages/services/workflows/src/lib/expired-schema-checks.ts b/packages/services/workflows/src/lib/expired-schema-checks.ts index b65a97b6c..6889c7e3d 100644 --- a/packages/services/workflows/src/lib/expired-schema-checks.ts +++ b/packages/services/workflows/src/lib/expired-schema-checks.ts @@ -1,5 +1,5 @@ -import { DatabasePool, sql } from 'slonik'; import { z } from 'zod'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; const SchemaCheckModel = z.object({ schemaCheckIds: z.array(z.string()), @@ -9,10 +9,13 @@ const SchemaCheckModel = z.object({ contractIds: z.array(z.string()), }); -export async function purgeExpiredSchemaChecks(args: { pool: DatabasePool; expiresAt: Date }) { - return await args.pool.transaction(async pool => { +export async function purgeExpiredSchemaChecks(args: { + pool: PostgresDatabasePool; + expiresAt: Date; +}) { + return await args.pool.transaction('purgeExpiredSchemaChecks', async pool => { const date = args.expiresAt.toISOString(); - const rawData = await pool.maybeOne(sql`/* findSchemaChecksToPurge */ + const rawData = await pool.maybeOne(psql`/* findSchemaChecksToPurge */ WITH "filtered_schema_checks" AS ( SELECT * FROM "schema_checks" @@ -70,22 +73,24 @@ export async function purgeExpiredSchemaChecks(args: { pool: DatabasePool; expir let deletedSchemaChangeApprovalCount = 0; let deletedContractSchemaChangeApprovalCount = 0; - await pool.any(sql`/* purgeExpiredSchemaChecks */ + await pool.any(psql`/* purgeExpiredSchemaChecks */ DELETE FROM "schema_checks" WHERE - "id" = ANY(${sql.array(data.schemaCheckIds, 'uuid')}) + "id" = ANY(${psql.array(data.schemaCheckIds, 'uuid')}) `); if (data.sdlStoreIds.length) { - deletedSdlStoreCount = await pool.oneFirst(sql`/* purgeExpiredSdlStore */ + deletedSdlStoreCount = await pool + .oneFirst( + psql`/* purgeExpiredSdlStore */ WITH "deleted" AS ( DELETE FROM "sdl_store" WHERE "id" = ANY( - ${sql.array(data.sdlStoreIds, 'text')} + ${psql.array(data.sdlStoreIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -109,22 +114,25 @@ export async function purgeExpiredSchemaChecks(args: { pool: DatabasePool; expir RETURNING "id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } if (data.targetIds.length && data.contextIds.length) { - deletedSchemaChangeApprovalCount = - await pool.oneFirst(sql`/* purgeExpiredSchemaChangeApprovals */ + deletedSchemaChangeApprovalCount = await pool + .oneFirst( + psql`/* purgeExpiredSchemaChangeApprovals */ WITH "deleted" AS ( DELETE FROM "schema_change_approvals" WHERE "target_id" = ANY( - ${sql.array(data.targetIds, 'uuid')} + ${psql.array(data.targetIds, 'uuid')} ) AND "context_id" = ANY( - ${sql.array(data.contextIds, 'text')} + ${psql.array(data.contextIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -137,22 +145,25 @@ export async function purgeExpiredSchemaChecks(args: { pool: DatabasePool; expir RETURNING "target_id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } if (data.contractIds.length && data.contextIds.length) { - deletedContractSchemaChangeApprovalCount = - await pool.oneFirst(sql`/* purgeExpiredContractSchemaChangeApprovals */ + deletedContractSchemaChangeApprovalCount = await pool + .oneFirst( + psql`/* purgeExpiredContractSchemaChangeApprovals */ WITH "deleted" AS ( DELETE FROM "contract_schema_change_approvals" WHERE "contract_id" = ANY( - ${sql.array(data.contractIds, 'uuid')} + ${psql.array(data.contractIds, 'uuid')} ) AND "context_id" = ANY( - ${sql.array(data.contextIds, 'text')} + ${psql.array(data.contextIds, 'text')} ) AND NOT EXISTS ( SELECT @@ -168,7 +179,9 @@ export async function purgeExpiredSchemaChecks(args: { pool: DatabasePool; expir RETURNING "contract_id" ) SELECT COUNT(*) FROM "deleted" - `); + `, + ) + .then(z.number().parse); } return { diff --git a/packages/services/workflows/src/lib/schema/provider.ts b/packages/services/workflows/src/lib/schema/provider.ts index ecc27cca6..32dedd7aa 100644 --- a/packages/services/workflows/src/lib/schema/provider.ts +++ b/packages/services/workflows/src/lib/schema/provider.ts @@ -1,11 +1,11 @@ import { DocumentNode, GraphQLError, parse, print, SourceLocation } from 'graphql'; -import { DatabasePool, sql } from 'slonik'; import { z } from 'zod'; import type { Logger } from '@graphql-hive/logger'; import type { Change } from '@graphql-inspector/core'; import { errors, patch } from '@graphql-inspector/patch'; import type { Project, SchemaObject } from '@hive/api'; import type { ComposeAndValidateResult } from '@hive/api/shared/entities'; +import { PostgresDatabasePool, psql } from '@hive/postgres'; import type { ContractsInputType, SchemaBuilderApi } from '@hive/schema'; import { decodeCreatedAtAndUUIDIdBasedCursor, HiveSchemaChangeModel } from '@hive/storage'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; @@ -208,11 +208,11 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { timestamp: string; reason: string | null; status: 'ERROR' | 'SUCCESS'; - pool: DatabasePool; + pool: PostgresDatabasePool; }) { const { pool, ...state } = args; - await pool.query( - sql`/* updateSchemaProposalComposition */ + await pool.query( + psql`/* updateSchemaProposalComposition */ UPDATE schema_proposals SET composition_status = ${state.status} @@ -222,24 +222,25 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { ); }, - async latestComposableSchemas(args: { targetId: string; pool: DatabasePool }) { - const latestVersion = await args.pool.maybeOne<{ id: string }>( - sql`/* findLatestComposableSchemaVersion */ + async latestComposableSchemas(args: { targetId: string; pool: PostgresDatabasePool }) { + const latestVersionId = await args.pool + .maybeOneFirst( + psql`/* findLatestComposableSchemaVersion */ SELECT sv.id FROM schema_versions as sv WHERE sv.target_id = ${args.targetId} AND sv.is_composable IS TRUE ORDER BY sv.created_at DESC LIMIT 1 `, - ); + ) + .then(z.string().nullable().parse); - if (!latestVersion) { + if (!latestVersionId) { return []; } - const version = latestVersion.id; - const result = await args.pool.query( - sql`/* getSchemasOfVersion */ + const result = await args.pool.any( + psql`/* getSchemasOfVersion */ SELECT sl.id , sl.sdl @@ -250,28 +251,30 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) LEFT JOIN projects as p ON (p.id = sl.project_id) WHERE - svl.version_id = ${version} + svl.version_id = ${latestVersionId} AND sl.action = 'PUSH' AND p.type != 'CUSTOM' ORDER BY sl.created_at DESC `, ); - return result.rows.map(row => SchemaModel.parse(row)); + return result.map(row => SchemaModel.parse(row)); }, - async getBaseSchema(args: { targetId: string; pool: DatabasePool }) { - const data = await args.pool.maybeOne>( - sql`/* getBaseSchema */ SELECT base_schema FROM targets WHERE id=${args.targetId}`, - ); - return data?.base_schema ?? null; + async getBaseSchema(args: { targetId: string; pool: PostgresDatabasePool }) { + const baseSchema = await args.pool + .maybeOneFirst( + psql`/* getBaseSchema */ SELECT base_schema FROM targets WHERE id=${args.targetId}`, + ) + .then(z.string().nullable().parse); + return baseSchema; }, async proposedSchemas(args: { targetId: string; proposalId: string; cursor?: string | null; - pool: DatabasePool; + pool: PostgresDatabasePool; }) { const now = new Date().toISOString(); let cursor: { @@ -294,7 +297,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { // collect changes in paginated requests to avoid stalling the db let i = 0; do { - const result = await args.pool.query(sql` + const result = await args.pool.any(psql` SELECT c."id" , c."service_name" as "serviceName" @@ -308,7 +311,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { FROM schema_checks ${ cursor - ? sql` + ? psql` WHERE "schema_proposal_id" = ${args.proposalId} AND ( ( @@ -318,7 +321,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { OR "created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } GROUP BY "service", "schema_proposal_id" ) as cc @@ -330,7 +333,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { AND c."schema_proposal_id" = ${args.proposalId} ${ cursor - ? sql` + ? psql` AND ( ( c."created_at" = ${cursor.createdAt} @@ -339,7 +342,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { OR c."created_at" < ${cursor.createdAt} ) ` - : sql`` + : psql`` } ORDER BY c."created_at" DESC @@ -347,7 +350,7 @@ export function schemaProvider(providerConfig: SchemaProviderConfig) { LIMIT 20 `); - const changes = result.rows.map(row => { + const changes = result.map(row => { const value = SchemaProposalChangesModel.parse(row); return { ...value, diff --git a/packages/services/workflows/src/tasks/purge-expired-dedupe-keys.ts b/packages/services/workflows/src/tasks/purge-expired-dedupe-keys.ts index 054ffefe2..eb1bda0b0 100644 --- a/packages/services/workflows/src/tasks/purge-expired-dedupe-keys.ts +++ b/packages/services/workflows/src/tasks/purge-expired-dedupe-keys.ts @@ -1,5 +1,5 @@ -import { sql } from 'slonik'; import { z } from 'zod'; +import { psql } from '@hive/postgres'; import { defineTask, implementTask } from '../kit.js'; export const PurgeExpiredDedupeKeysTask = defineTask({ @@ -9,7 +9,7 @@ export const PurgeExpiredDedupeKeysTask = defineTask({ export const task = implementTask(PurgeExpiredDedupeKeysTask, async args => { args.logger.debug('purging expired postgraphile task dedupe keys'); - const result = await args.context.pg.oneFirst(sql` + const result = await args.context.pg.oneFirst(psql` WITH "deleted" AS ( DELETE FROM "graphile_worker_deduplication" WHERE "expires_at" < NOW() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb92921ad..5b0968e84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,6 +342,9 @@ importers: '@hive/commerce': specifier: workspace:* version: link:../packages/services/commerce + '@hive/postgres': + specifier: workspace:* + version: link:../packages/internal/postgres '@hive/schema': specifier: workspace:* version: link:../packages/services/schema @@ -408,9 +411,6 @@ importers: set-cookie-parser: specifier: 2.7.1 version: 2.7.1 - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) strip-ansi: specifier: 7.1.2 version: 7.1.2 @@ -430,6 +430,19 @@ importers: specifier: 1.2.0 version: 1.2.0 + packages/internal/postgres: + dependencies: + slonik: + specifier: 30.4.4 + version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) + slonik-interceptor-query-logging: + specifier: 46.4.0 + version: 46.4.0(slonik@30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)) + devDependencies: + '@hive/service-common': + specifier: workspace:* + version: link:../../services/service-common + packages/libraries/apollo: dependencies: '@graphql-hive/core': @@ -996,6 +1009,9 @@ importers: '@graphql-hive/core': specifier: workspace:* version: link:../libraries/core/dist + '@hive/postgres': + specifier: workspace:* + version: link:../internal/postgres '@hive/service-common': specifier: workspace:* version: link:../services/service-common @@ -1035,9 +1051,6 @@ importers: pg-promise: specifier: 11.10.2 version: 11.10.2(pg-query-stream@4.7.0(pg@8.13.1)) - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) tslib: specifier: 2.8.1 version: 2.8.1 @@ -1084,6 +1097,9 @@ importers: '@hive/cdn-script': specifier: workspace:* version: link:../cdn-worker + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/pubsub': specifier: workspace:* version: link:../../libraries/pubsub @@ -1225,9 +1241,6 @@ importers: redlock: specifier: 5.0.0-beta.2 version: 5.0.0-beta.2 - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) stripe: specifier: 17.5.0 version: 17.5.0 @@ -1327,6 +1340,9 @@ importers: '@hive/api': specifier: workspace:* version: link:../api + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1586,6 +1602,9 @@ importers: '@hive/cdn-script': specifier: workspace:* version: link:../cdn-worker + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/pubsub': specifier: workspace:* version: link:../../libraries/pubsub @@ -1731,9 +1750,6 @@ importers: prom-client: specifier: 15.1.3 version: 15.1.3 - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) zod: specifier: 3.25.76 version: 3.25.76 @@ -1743,6 +1759,9 @@ importers: '@graphql-inspector/core': specifier: 7.1.2 version: 7.1.2(graphql@16.9.0) + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1776,15 +1795,9 @@ importers: pg-promise: specifier: 11.10.2 version: 11.10.2(pg-query-stream@4.7.0(pg@8.13.1)) - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) - slonik-interceptor-query-logging: - specifier: 46.4.0 - version: 46.4.0(slonik@30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)) slonik-utilities: specifier: 1.9.4 - version: 1.9.4(slonik@30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)) + version: 1.9.4 tslib: specifier: 2.8.1 version: 2.8.1 @@ -1797,6 +1810,9 @@ importers: packages/services/tokens: devDependencies: + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1851,6 +1867,9 @@ importers: '@hive/api': specifier: workspace:* version: link:../api + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1974,6 +1993,9 @@ importers: '@graphql-yoga/redis-event-target': specifier: 3.0.3 version: 3.0.3(ioredis@5.8.2) + '@hive/postgres': + specifier: workspace:* + version: link:../../internal/postgres '@hive/pubsub': specifier: workspace:* version: link:../../libraries/pubsub @@ -2025,9 +2047,6 @@ importers: sendmail: specifier: 1.6.1 version: 1.6.1 - slonik: - specifier: 30.4.4 - version: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) zod: specifier: 3.25.76 version: 3.25.76 @@ -16111,11 +16130,6 @@ packages: pg-copy-streams@6.0.4: resolution: {integrity: sha512-FH6q2nFo0n2cFacLyIKorjDz8AOYtxrAANx1XMvYbKWNM2geY731gZstuP4mXMlqO6urRl9oIscFxf3GMIg3Ng==} - pg-cursor@2.15.3: - resolution: {integrity: sha512-eHw63TsiGtFEfAd7tOTZ+TLy+i/2ePKS20H84qCQ+aQ60pve05Okon9tKMC+YN3j6XyeFoHnaim7Lt9WVafQsA==} - peerDependencies: - pg: ^8 - pg-cursor@2.18.0: resolution: {integrity: sha512-WkMubzXP+FWDIC6XfA9pZwJHO0rmUwbNXUNFfBshp9amnCraQslVLYqEuWA+7qemtyz+v3zybcvcX//Dq5WpxQ==} peerDependencies: @@ -38508,10 +38522,6 @@ snapshots: dependencies: obuf: 1.1.2 - pg-cursor@2.15.3(pg@8.13.1): - dependencies: - pg: 8.13.1 - pg-cursor@2.18.0(pg@8.13.1): dependencies: pg: 8.13.1 @@ -39554,7 +39564,7 @@ snapshots: boolean: 3.2.0 fast-json-stringify: 2.7.13 fast-printf: 1.6.9 - globalthis: 1.0.3 + globalthis: 1.0.4 safe-stable-stringify: 2.5.0 semver-compare: 1.0.0 @@ -39969,7 +39979,7 @@ snapshots: serialize-error: 8.1.0 slonik: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) - slonik-utilities@1.9.4(slonik@30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9)): + slonik-utilities@1.9.4: dependencies: core-js: 3.25.5 delay: 4.4.1 @@ -39977,7 +39987,6 @@ snapshots: lodash: 4.17.23 roarr: 7.14.3 serialize-error: 5.0.0 - slonik: 30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9) slonik@30.4.4(patch_hash=195b140c0181c27a85a6026c0058087a419e38f6c5d89f5f2c608e39f5bf23e9): dependencies: @@ -39992,7 +40001,7 @@ snapshots: pg: 8.13.1 pg-copy-streams: 6.0.4 pg-copy-streams-binary: 2.2.0 - pg-cursor: 2.15.3(pg@8.13.1) + pg-cursor: 2.18.0(pg@8.13.1) pg-protocol: 1.7.0 pg-types: 4.0.1 postgres-array: 3.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9026ba15e..9aa9daf32 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - packages/internal/* - packages/services/demo/* - packages/services/* - packages/migrations diff --git a/tsconfig.json b/tsconfig.json index f9d907c49..03d328b2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "moduleResolution": "node", "strict": true, "paths": { + "@hive/postgres": ["./packages/internal/postgres/src/index.ts"], "@hive/api": ["./packages/services/api/src/index.ts"], "@hive/api/*": ["./packages/services/api/src/*"], "@hive/service-common": ["./packages/services/service-common/src/index.ts"],