@openlup/platform 0.0.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/LICENSE +204 -0
- package/README.md +471 -1
- package/dist/bff/dispatch.d.ts +27 -0
- package/dist/bff/dispatch.js +47 -0
- package/dist/bff/envelope.d.ts +46 -0
- package/dist/bff/envelope.js +80 -0
- package/dist/bff/health.d.ts +24 -0
- package/dist/bff/health.js +19 -0
- package/dist/bff/index.d.ts +6 -0
- package/dist/bff/index.js +6 -0
- package/dist/bff/lazyRoute.d.ts +4 -0
- package/dist/bff/lazyRoute.js +13 -0
- package/dist/bff/requestContext.d.ts +17 -0
- package/dist/bff/requestContext.js +26 -0
- package/dist/bff/routerRewrite.d.ts +2 -0
- package/dist/bff/routerRewrite.js +31 -0
- package/dist/blob/binding.d.ts +16 -0
- package/dist/blob/binding.js +37 -0
- package/dist/blob/filesystem.d.ts +8 -0
- package/dist/blob/filesystem.js +71 -0
- package/dist/blob/index.d.ts +6 -0
- package/dist/blob/index.js +6 -0
- package/dist/blob/s3.d.ts +32 -0
- package/dist/blob/s3.js +56 -0
- package/dist/blob/s3Client.d.ts +25 -0
- package/dist/blob/s3Client.js +76 -0
- package/dist/blob/supabase.d.ts +40 -0
- package/dist/blob/supabase.js +59 -0
- package/dist/blob/vercel.d.ts +63 -0
- package/dist/blob/vercel.js +69 -0
- package/dist/composition/composeBundle.d.ts +47 -0
- package/dist/composition/composeBundle.js +45 -0
- package/dist/composition/httpBinding.d.ts +8 -0
- package/dist/composition/httpBinding.js +18 -0
- package/dist/composition/index.d.ts +3 -0
- package/dist/composition/index.js +3 -0
- package/dist/composition/nodeHost.d.ts +103 -0
- package/dist/composition/nodeHost.js +145 -0
- package/dist/data/binding.d.ts +34 -0
- package/dist/data/binding.js +92 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +5 -0
- package/dist/data/postgresGateway.d.ts +42 -0
- package/dist/data/postgresGateway.js +135 -0
- package/dist/data/queryBuilder.d.ts +54 -0
- package/dist/data/queryBuilder.js +240 -0
- package/dist/data/queryBuilderSql.d.ts +76 -0
- package/dist/data/queryBuilderSql.js +145 -0
- package/dist/data/supabaseClientFactory.d.ts +34 -0
- package/dist/data/supabaseClientFactory.js +56 -0
- package/dist/data/supabaseGateway.d.ts +22 -0
- package/dist/data/supabaseGateway.js +49 -0
- package/dist/http/deadline.d.ts +20 -0
- package/dist/http/deadline.js +93 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +5 -0
- package/dist/http/requestAdapter.d.ts +38 -0
- package/dist/http/requestAdapter.js +203 -0
- package/dist/http/runtime.d.ts +83 -0
- package/dist/http/runtime.js +199 -0
- package/dist/http/security.d.ts +44 -0
- package/dist/http/security.js +114 -0
- package/dist/http/staticAssets.d.ts +3 -0
- package/dist/http/staticAssets.js +42 -0
- package/dist/http/types.d.ts +23 -0
- package/dist/http/types.js +6 -0
- package/dist/identity/adminAuthBinding.d.ts +28 -0
- package/dist/identity/adminAuthBinding.js +66 -0
- package/dist/identity/adminAuthorization.d.ts +34 -0
- package/dist/identity/adminAuthorization.js +72 -0
- package/dist/identity/adminContracts.d.ts +70 -0
- package/dist/identity/adminContracts.js +43 -0
- package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
- package/dist/identity/adminMagicLinkHandler.js +94 -0
- package/dist/identity/adminMeHandler.d.ts +15 -0
- package/dist/identity/adminMeHandler.js +49 -0
- package/dist/identity/adminPreflight.d.ts +16 -0
- package/dist/identity/adminPreflight.js +38 -0
- package/dist/identity/adminUserActionPorts.d.ts +31 -0
- package/dist/identity/adminUserActionPorts.js +51 -0
- package/dist/identity/adminUserHandlers.d.ts +38 -0
- package/dist/identity/adminUserHandlers.js +140 -0
- package/dist/identity/authVerifyUrl.d.ts +6 -0
- package/dist/identity/authVerifyUrl.js +54 -0
- package/dist/identity/contracts/contracts.d.ts +8 -0
- package/dist/identity/contracts/contracts.js +2 -0
- package/dist/identity/contracts/emailTrust.d.ts +3 -0
- package/dist/identity/contracts/emailTrust.js +4 -0
- package/dist/identity/contracts/index.d.ts +8 -0
- package/dist/identity/contracts/index.js +8 -0
- package/dist/identity/contracts/oidc.d.ts +3 -0
- package/dist/identity/contracts/oidc.js +9 -0
- package/dist/identity/contracts/operatorAuth.d.ts +35 -0
- package/dist/identity/contracts/operatorAuth.js +53 -0
- package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
- package/dist/identity/contracts/operatorInvitations.js +1 -0
- package/dist/identity/contracts/ports.d.ts +22 -0
- package/dist/identity/contracts/ports.js +1 -0
- package/dist/identity/contracts/privacy.d.ts +62 -0
- package/dist/identity/contracts/privacy.js +99 -0
- package/dist/identity/contracts/providerList.d.ts +3 -0
- package/dist/identity/contracts/providerList.js +11 -0
- package/dist/identity/contracts/types.d.ts +16 -0
- package/dist/identity/contracts/types.js +4 -0
- package/dist/identity/contribution.d.ts +8 -0
- package/dist/identity/contribution.js +22 -0
- package/dist/identity/customerAuth.d.ts +8 -0
- package/dist/identity/customerAuth.js +1 -0
- package/dist/identity/customerAuthorization.d.ts +5 -0
- package/dist/identity/customerAuthorization.js +22 -0
- package/dist/identity/customerContracts.d.ts +42 -0
- package/dist/identity/customerContracts.js +43 -0
- package/dist/identity/customerIdentityBinding.d.ts +44 -0
- package/dist/identity/customerIdentityBinding.js +60 -0
- package/dist/identity/customerIdentityVerifier.d.ts +8 -0
- package/dist/identity/customerIdentityVerifier.js +101 -0
- package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
- package/dist/identity/customerMagicLinkHandler.js +115 -0
- package/dist/identity/customerMeHandler.d.ts +11 -0
- package/dist/identity/customerMeHandler.js +46 -0
- package/dist/identity/customerPorts.d.ts +4 -0
- package/dist/identity/customerPorts.js +1 -0
- package/dist/identity/customerSessionBinding.d.ts +38 -0
- package/dist/identity/customerSessionBinding.js +80 -0
- package/dist/identity/customerSessionIssuer.d.ts +10 -0
- package/dist/identity/customerSessionIssuer.js +57 -0
- package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
- package/dist/identity/customerVerifyOtpHandler.js +41 -0
- package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
- package/dist/identity/directOperatorInvitationStore.js +193 -0
- package/dist/identity/emailTrustPolicy.d.ts +14 -0
- package/dist/identity/emailTrustPolicy.js +16 -0
- package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
- package/dist/identity/identityPersonalDataContributor.js +46 -0
- package/dist/identity/identityReadiness.d.ts +10 -0
- package/dist/identity/identityReadiness.js +25 -0
- package/dist/identity/index.d.ts +67 -0
- package/dist/identity/index.js +66 -0
- package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
- package/dist/identity/inviteAdminUserUseCase.js +75 -0
- package/dist/identity/jwtLibrary.d.ts +54 -0
- package/dist/identity/jwtLibrary.js +1 -0
- package/dist/identity/machineActorWriteFence.d.ts +4 -0
- package/dist/identity/machineActorWriteFence.js +13 -0
- package/dist/identity/magicLinkPorts.d.ts +28 -0
- package/dist/identity/magicLinkPorts.js +1 -0
- package/dist/identity/managedAccountLinkStore.d.ts +2 -0
- package/dist/identity/managedAccountLinkStore.js +93 -0
- package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
- package/dist/identity/managedAdminAuthVerifier.js +27 -0
- package/dist/identity/managedAdminUserGateways.d.ts +123 -0
- package/dist/identity/managedAdminUserGateways.js +127 -0
- package/dist/identity/managedClient.d.ts +90 -0
- package/dist/identity/managedClient.js +10 -0
- package/dist/identity/managedClientAuthLink.d.ts +3 -0
- package/dist/identity/managedClientAuthLink.js +115 -0
- package/dist/identity/managedCustomerMe.d.ts +13 -0
- package/dist/identity/managedCustomerMe.js +30 -0
- package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
- package/dist/identity/managedEmailTrustPolicy.js +105 -0
- package/dist/identity/managedIdentityVerifier.d.ts +2 -0
- package/dist/identity/managedIdentityVerifier.js +16 -0
- package/dist/identity/managedPrivacy.d.ts +4 -0
- package/dist/identity/managedPrivacy.js +39 -0
- package/dist/identity/messages.d.ts +29 -0
- package/dist/identity/messages.js +39 -0
- package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
- package/dist/identity/oidcIdentityVerifier.js +309 -0
- package/dist/identity/oidcPrincipal.d.ts +2 -0
- package/dist/identity/oidcPrincipal.js +10 -0
- package/dist/identity/operatorAdminAuth.d.ts +6 -0
- package/dist/identity/operatorAdminAuth.js +13 -0
- package/dist/identity/operatorAuthHandlers.d.ts +18 -0
- package/dist/identity/operatorAuthHandlers.js +71 -0
- package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
- package/dist/identity/operatorIdentityVerifier.js +19 -0
- package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
- package/dist/identity/operatorInvitationLifecycle.js +15 -0
- package/dist/identity/operatorInvitationPorts.d.ts +31 -0
- package/dist/identity/operatorInvitationPorts.js +1 -0
- package/dist/identity/operatorPorts.d.ts +37 -0
- package/dist/identity/operatorPorts.js +1 -0
- package/dist/identity/operatorSession.d.ts +13 -0
- package/dist/identity/operatorSession.js +31 -0
- package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
- package/dist/identity/operatorTokenAdminAuth.js +50 -0
- package/dist/identity/personalDataRegistry.d.ts +4 -0
- package/dist/identity/personalDataRegistry.js +11 -0
- package/dist/identity/platformAdminAuth.d.ts +15 -0
- package/dist/identity/platformAdminAuth.js +1 -0
- package/dist/identity/ports.d.ts +127 -0
- package/dist/identity/ports.js +38 -0
- package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
- package/dist/identity/postgresAccountLinkStore.js +52 -0
- package/dist/identity/postgresCustomerMe.d.ts +3 -0
- package/dist/identity/postgresCustomerMe.js +21 -0
- package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
- package/dist/identity/postgresCustomerRecovery.js +57 -0
- package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
- package/dist/identity/postgresOperatorIdentity.js +137 -0
- package/dist/identity/postgresPrivacy.d.ts +7 -0
- package/dist/identity/postgresPrivacy.js +126 -0
- package/dist/identity/privacyHandlers.d.ts +10 -0
- package/dist/identity/privacyHandlers.js +35 -0
- package/dist/identity/privacyPorts.d.ts +72 -0
- package/dist/identity/privacyPorts.js +8 -0
- package/dist/identity/privacyUseCases.d.ts +5 -0
- package/dist/identity/privacyUseCases.js +55 -0
- package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
- package/dist/identity/reconcileAccountForPrincipal.js +66 -0
- package/dist/identity/reconcileAccountHandler.d.ts +11 -0
- package/dist/identity/reconcileAccountHandler.js +30 -0
- package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
- package/dist/identity/removeAdminUserUseCase.js +60 -0
- package/dist/identity/response.d.ts +3 -0
- package/dist/identity/response.js +2 -0
- package/dist/identity/sql.d.ts +44 -0
- package/dist/identity/sql.js +10 -0
- package/dist/job-ledger/index.d.ts +5 -0
- package/dist/job-ledger/index.js +3 -0
- package/dist/job-ledger/ports.d.ts +10 -0
- package/dist/job-ledger/ports.js +7 -0
- package/dist/job-ledger/postgres.d.ts +10 -0
- package/dist/job-ledger/postgres.js +101 -0
- package/dist/job-ledger/sql.d.ts +7 -0
- package/dist/job-ledger/sql.js +10 -0
- package/dist/job-ledger/supabase.d.ts +77 -0
- package/dist/job-ledger/supabase.js +202 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +1 -0
- package/dist/kernel/platformKernel.d.ts +34 -0
- package/dist/kernel/platformKernel.js +115 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +1 -0
- package/dist/migrations/postgresRunner.d.ts +50 -0
- package/dist/migrations/postgresRunner.js +176 -0
- package/dist/scheduler/advisoryLock.d.ts +44 -0
- package/dist/scheduler/advisoryLock.js +114 -0
- package/dist/scheduler/cronGuard.d.ts +10 -0
- package/dist/scheduler/cronGuard.js +19 -0
- package/dist/scheduler/cronSecret.d.ts +5 -0
- package/dist/scheduler/cronSecret.js +23 -0
- package/dist/scheduler/handlers.d.ts +9 -0
- package/dist/scheduler/handlers.js +10 -0
- package/dist/scheduler/httpEntry.d.ts +16 -0
- package/dist/scheduler/httpEntry.js +31 -0
- package/dist/scheduler/index.d.ts +9 -0
- package/dist/scheduler/index.js +9 -0
- package/dist/scheduler/jobRegistry.d.ts +21 -0
- package/dist/scheduler/jobRegistry.js +39 -0
- package/dist/scheduler/jobs.d.ts +48 -0
- package/dist/scheduler/jobs.js +92 -0
- package/dist/scheduler/nodeScheduler.d.ts +42 -0
- package/dist/scheduler/nodeScheduler.js +73 -0
- package/dist/scheduler/vercelScheduler.d.ts +29 -0
- package/dist/scheduler/vercelScheduler.js +51 -0
- package/package.json +116 -3
- package/release-gates.json +143 -0
- package/sql/20260809150000_job_run_ledger.sql +301 -0
- package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
- package/sql/20260810180001_self_host_actor_auth.sql +40 -0
- package/sql/20260810204057_customer_identity_read.sql +31 -0
- package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
- package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
- package/sql/20261018120000_identity_operator_invitations.sql +42 -0
- package/sql/20261018180000_identity_operator_challenges.sql +56 -0
- package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
- package/src/bff/dispatch.ts +81 -0
- package/src/bff/envelope.ts +142 -0
- package/src/bff/health.ts +37 -0
- package/src/bff/index.ts +30 -0
- package/src/bff/lazyRoute.ts +28 -0
- package/src/bff/requestContext.ts +48 -0
- package/src/bff/routerRewrite.ts +38 -0
- package/src/blob/binding.ts +55 -0
- package/src/blob/filesystem.ts +86 -0
- package/src/blob/index.ts +17 -0
- package/src/blob/s3.ts +87 -0
- package/src/blob/s3Client.ts +91 -0
- package/src/blob/supabase.ts +88 -0
- package/src/blob/vercel.ts +127 -0
- package/src/composition/composeBundle.ts +109 -0
- package/src/composition/httpBinding.ts +30 -0
- package/src/composition/index.ts +17 -0
- package/src/composition/nodeHost.ts +289 -0
- package/src/data/binding.ts +157 -0
- package/src/data/index.ts +33 -0
- package/src/data/postgresGateway.ts +186 -0
- package/src/data/queryBuilder.ts +300 -0
- package/src/data/queryBuilderSql.ts +183 -0
- package/src/data/supabaseClientFactory.ts +92 -0
- package/src/data/supabaseGateway.ts +76 -0
- package/src/http/deadline.ts +117 -0
- package/src/http/index.ts +36 -0
- package/src/http/requestAdapter.ts +215 -0
- package/src/http/runtime.ts +319 -0
- package/src/http/security.ts +155 -0
- package/src/http/staticAssets.ts +42 -0
- package/src/http/types.ts +31 -0
- package/src/identity/adminAuthBinding.ts +76 -0
- package/src/identity/adminAuthorization.ts +106 -0
- package/src/identity/adminContracts.ts +52 -0
- package/src/identity/adminMagicLinkHandler.ts +132 -0
- package/src/identity/adminMeHandler.ts +64 -0
- package/src/identity/adminPreflight.ts +46 -0
- package/src/identity/adminUserActionPorts.ts +78 -0
- package/src/identity/adminUserHandlers.ts +174 -0
- package/src/identity/authVerifyUrl.ts +56 -0
- package/src/identity/contracts/contracts.ts +15 -0
- package/src/identity/contracts/emailTrust.ts +5 -0
- package/src/identity/contracts/index.ts +8 -0
- package/src/identity/contracts/oidc.ts +10 -0
- package/src/identity/contracts/operatorAuth.ts +61 -0
- package/src/identity/contracts/operatorInvitations.ts +32 -0
- package/src/identity/contracts/ports.ts +25 -0
- package/src/identity/contracts/privacy.ts +86 -0
- package/src/identity/contracts/providerList.ts +13 -0
- package/src/identity/contracts/types.ts +25 -0
- package/src/identity/contribution.ts +29 -0
- package/src/identity/customerAuth.ts +3 -0
- package/src/identity/customerAuthorization.ts +38 -0
- package/src/identity/customerContracts.ts +52 -0
- package/src/identity/customerIdentityBinding.ts +125 -0
- package/src/identity/customerIdentityVerifier.ts +141 -0
- package/src/identity/customerMagicLinkHandler.ts +171 -0
- package/src/identity/customerMeHandler.ts +64 -0
- package/src/identity/customerPorts.ts +2 -0
- package/src/identity/customerSessionBinding.ts +114 -0
- package/src/identity/customerSessionIssuer.ts +71 -0
- package/src/identity/customerVerifyOtpHandler.ts +44 -0
- package/src/identity/directOperatorInvitationStore.ts +178 -0
- package/src/identity/emailTrustPolicy.ts +16 -0
- package/src/identity/identityPersonalDataContributor.ts +48 -0
- package/src/identity/identityReadiness.ts +26 -0
- package/src/identity/index.ts +69 -0
- package/src/identity/inviteAdminUserUseCase.ts +134 -0
- package/src/identity/jwtLibrary.ts +24 -0
- package/src/identity/machineActorWriteFence.ts +19 -0
- package/src/identity/magicLinkPorts.ts +7 -0
- package/src/identity/managedAccountLinkStore.ts +105 -0
- package/src/identity/managedAdminAuthVerifier.ts +40 -0
- package/src/identity/managedAdminUserGateways.ts +164 -0
- package/src/identity/managedClient.ts +58 -0
- package/src/identity/managedClientAuthLink.ts +120 -0
- package/src/identity/managedCustomerMe.ts +52 -0
- package/src/identity/managedEmailTrustPolicy.ts +90 -0
- package/src/identity/managedIdentityVerifier.ts +20 -0
- package/src/identity/managedPrivacy.ts +37 -0
- package/src/identity/messages.ts +41 -0
- package/src/identity/oidcIdentityVerifier.ts +229 -0
- package/src/identity/oidcPrincipal.ts +10 -0
- package/src/identity/operatorAdminAuth.ts +14 -0
- package/src/identity/operatorAuthHandlers.ts +52 -0
- package/src/identity/operatorIdentityVerifier.ts +19 -0
- package/src/identity/operatorInvitationLifecycle.ts +14 -0
- package/src/identity/operatorInvitationPorts.ts +11 -0
- package/src/identity/operatorPorts.ts +11 -0
- package/src/identity/operatorSession.ts +30 -0
- package/src/identity/operatorTokenAdminAuth.ts +79 -0
- package/src/identity/personalDataRegistry.ts +8 -0
- package/src/identity/platformAdminAuth.ts +7 -0
- package/src/identity/ports.ts +158 -0
- package/src/identity/postgresAccountLinkStore.ts +49 -0
- package/src/identity/postgresCustomerMe.ts +33 -0
- package/src/identity/postgresCustomerRecovery.ts +72 -0
- package/src/identity/postgresOperatorIdentity.ts +123 -0
- package/src/identity/postgresPrivacy.ts +117 -0
- package/src/identity/privacyHandlers.ts +33 -0
- package/src/identity/privacyPorts.ts +52 -0
- package/src/identity/privacyUseCases.ts +48 -0
- package/src/identity/reconcileAccountForPrincipal.ts +99 -0
- package/src/identity/reconcileAccountHandler.ts +36 -0
- package/src/identity/removeAdminUserUseCase.ts +85 -0
- package/src/identity/response.ts +3 -0
- package/src/identity/sql.ts +10 -0
- package/src/job-ledger/index.ts +25 -0
- package/src/job-ledger/ports.ts +27 -0
- package/src/job-ledger/postgres.ts +121 -0
- package/src/job-ledger/sql.ts +12 -0
- package/src/job-ledger/supabase.ts +294 -0
- package/src/kernel/index.ts +17 -0
- package/src/kernel/platformKernel.ts +153 -0
- package/src/migrations/index.ts +10 -0
- package/src/migrations/postgresRunner.ts +255 -0
- package/src/scheduler/advisoryLock.ts +145 -0
- package/src/scheduler/cronGuard.ts +30 -0
- package/src/scheduler/cronSecret.ts +26 -0
- package/src/scheduler/handlers.ts +24 -0
- package/src/scheduler/httpEntry.ts +49 -0
- package/src/scheduler/index.ts +47 -0
- package/src/scheduler/jobRegistry.ts +57 -0
- package/src/scheduler/jobs.ts +119 -0
- package/src/scheduler/nodeScheduler.ts +123 -0
- package/src/scheduler/vercelScheduler.ts +71 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// DataGatewayPort bindings for bundle composition.
|
|
2
|
+
//
|
|
3
|
+
// Maps a bundle's declared `data` capability to a concrete DataGatewayPort. The supabase gateway is
|
|
4
|
+
// constructed eagerly (pure factory; no client/network until a port method runs work), so the
|
|
5
|
+
// default vercel-supabase path binds the slot with no secrets needed: env is read with
|
|
6
|
+
// empty-string fallbacks and validated lazily inside asActor/asService. The application supplies
|
|
7
|
+
// the supabase-js `createClient` once, when it creates its bindings.
|
|
8
|
+
//
|
|
9
|
+
// The `postgres` capability (node-postgres) uses the existing direct-PG DataGatewayPort. Its
|
|
10
|
+
// PgGatewayClient exposes the same narrow RPC surface used by Supabase-backed domain ports.
|
|
11
|
+
import { getBundleDescriptor, resolveBundleId, } from "../kernel/index.js";
|
|
12
|
+
import { createSupabaseDataGateway } from "./supabaseGateway.js";
|
|
13
|
+
import { createPostgresDataGateway, resolvePostgresDataGatewayEnv, } from "./postgresGateway.js";
|
|
14
|
+
/** Create one set of data bindings; the bundle data port cache belongs to this set. */
|
|
15
|
+
export function createDataBindings(deps) {
|
|
16
|
+
let cachedBundleDataPort = null;
|
|
17
|
+
const createPostgresGateway = deps.createPostgresGateway ?? ((env) => createPostgresDataGateway(env));
|
|
18
|
+
/** Resolve the DataGatewayPort for a bundle's declared data capability kind. */
|
|
19
|
+
function bindDataPort(dataKind, env, requestOptions = {}) {
|
|
20
|
+
switch (dataKind) {
|
|
21
|
+
case "supabase":
|
|
22
|
+
// Construct from env with empty-string fallbacks; the actual supabase client is created
|
|
23
|
+
// lazily when asActor/asService runs work, so the slot binds even before secrets are present
|
|
24
|
+
// (same as the blob/transactional/analytics slots — keeps composeBundle({}) non-null).
|
|
25
|
+
return createSupabaseDataGateway({
|
|
26
|
+
url: env.SUPABASE_URL ?? env.VITE_SUPABASE_URL ?? "",
|
|
27
|
+
anonKey: env.VITE_SUPABASE_PUBLISHABLE_KEY ??
|
|
28
|
+
env.VITE_SUPABASE_ANON_KEY ??
|
|
29
|
+
env.SUPABASE_ANON_KEY ??
|
|
30
|
+
"",
|
|
31
|
+
serviceRoleKey: env.SUPABASE_SERVICE_ROLE_KEY ?? "",
|
|
32
|
+
}, { ...requestOptions, createClientImpl: requestOptions.createClientImpl ?? deps.createSupabaseClient });
|
|
33
|
+
case "postgres":
|
|
34
|
+
// Direct-PG gateway (pg.Pool, tx-per-request RLS via SET LOCAL). Pure factory — no pool
|
|
35
|
+
// until asActor/asService runs work, so the slot binds even before DATABASE_URL is present
|
|
36
|
+
// (matches the supabase case's lazy null-is-no-op convention; keeps composeBundle non-null).
|
|
37
|
+
return createPostgresGateway(resolvePostgresDataGatewayEnv(env));
|
|
38
|
+
default:
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Bind one request to actor-only data access.
|
|
44
|
+
*
|
|
45
|
+
* HTTP-backed bundles receive a fresh gateway so the raw bearer resolver cannot
|
|
46
|
+
* cross request boundaries. The direct database bundle reuses the existing
|
|
47
|
+
* cached port (and therefore its lazy pool); only this actor-only facade is new.
|
|
48
|
+
*/
|
|
49
|
+
function bindRequestActorDataPort(accessToken, env, requestOptions = {}) {
|
|
50
|
+
const bundleId = resolveBundleId(env);
|
|
51
|
+
const dataKind = getBundleDescriptor(bundleId).capabilities.data;
|
|
52
|
+
const port = dataKind === "postgres"
|
|
53
|
+
? bindBundleDataPort(env)
|
|
54
|
+
: bindDataPort(dataKind, env, {
|
|
55
|
+
...requestOptions,
|
|
56
|
+
resolveAccessToken: () => accessToken,
|
|
57
|
+
});
|
|
58
|
+
if (!port)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
asActor: (claims, work) => port.asActor(claims, work),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Resolve and bind the active bundle's data gateway without composing the whole runtime. */
|
|
65
|
+
function bindBundleDataPort(env) {
|
|
66
|
+
const bundleId = resolveBundleId(env);
|
|
67
|
+
const descriptor = getBundleDescriptor(bundleId);
|
|
68
|
+
const cacheKey = bundleDataPortCacheKey(bundleId, descriptor.capabilities.data, env);
|
|
69
|
+
if (cachedBundleDataPort?.cacheKey === cacheKey) {
|
|
70
|
+
return cachedBundleDataPort.port;
|
|
71
|
+
}
|
|
72
|
+
const port = bindDataPort(descriptor.capabilities.data, env);
|
|
73
|
+
cachedBundleDataPort = { cacheKey, port };
|
|
74
|
+
return port;
|
|
75
|
+
}
|
|
76
|
+
return { bindDataPort, bindRequestActorDataPort, bindBundleDataPort };
|
|
77
|
+
}
|
|
78
|
+
function bundleDataPortCacheKey(bundleId, dataKind, env) {
|
|
79
|
+
const dataConfig = dataKind === "postgres"
|
|
80
|
+
? [env.DATABASE_URL ?? ""]
|
|
81
|
+
: dataKind === "supabase"
|
|
82
|
+
? [
|
|
83
|
+
env.SUPABASE_URL ?? env.VITE_SUPABASE_URL ?? "",
|
|
84
|
+
env.VITE_SUPABASE_PUBLISHABLE_KEY ??
|
|
85
|
+
env.VITE_SUPABASE_ANON_KEY ??
|
|
86
|
+
env.SUPABASE_ANON_KEY ??
|
|
87
|
+
"",
|
|
88
|
+
env.SUPABASE_SERVICE_ROLE_KEY ?? "",
|
|
89
|
+
]
|
|
90
|
+
: [];
|
|
91
|
+
return JSON.stringify([bundleId, dataKind, ...dataConfig]);
|
|
92
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createPostgresDataGateway, createPostgresGatewayTypeOverrides, createPostgresRoleFreeTransactionLane, postgresTimestampTzToRfc3339, resolvePostgresDataGatewayEnv, type PostgresDataGatewayEnv, type PostgresDataGatewayOptions, type PostgresRoleFreeTransactionLane, } from "./postgresGateway.js";
|
|
2
|
+
export { PgGatewayClient, PgQueryBuilder, type PgQueryExecutor, type PgShimError, type PgShimResult, } from "./queryBuilder.js";
|
|
3
|
+
export { createActorClient, createServiceClient, readSupabaseActorDataGatewayEnv, readSupabaseDataGatewayEnv, type SupabaseCreateClient, type SupabaseDataGatewayEnv, } from "./supabaseClientFactory.js";
|
|
4
|
+
export { createSupabaseDataGateway, type SupabaseDataGatewayOptions } from "./supabaseGateway.js";
|
|
5
|
+
export { createDataBindings, type ActorOnlyDataPort, type DataBindingDependencies, type DataBindings, type DataPortBindingOptions, } from "./binding.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createPostgresDataGateway, createPostgresGatewayTypeOverrides, createPostgresRoleFreeTransactionLane, postgresTimestampTzToRfc3339, resolvePostgresDataGatewayEnv, } from "./postgresGateway.js";
|
|
2
|
+
export { PgGatewayClient, PgQueryBuilder, } from "./queryBuilder.js";
|
|
3
|
+
export { createActorClient, createServiceClient, readSupabaseActorDataGatewayEnv, readSupabaseDataGatewayEnv, } from "./supabaseClientFactory.js";
|
|
4
|
+
export { createSupabaseDataGateway } from "./supabaseGateway.js";
|
|
5
|
+
export { createDataBindings, } from "./binding.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { DataGatewayPort } from "@openlup/core/platform-runtime";
|
|
3
|
+
export interface PostgresDataGatewayEnv {
|
|
4
|
+
/** Postgres connection string (DATABASE_URL). Empty until a node-postgres deploy supplies it. */
|
|
5
|
+
connectionString: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PostgresDataGatewayOptions {
|
|
8
|
+
/** Injection seam: tests pass a stub pool; production lazily builds a real pg.Pool from env. */
|
|
9
|
+
poolFactory?: (env: PostgresDataGatewayEnv) => Pool | Promise<Pool>;
|
|
10
|
+
}
|
|
11
|
+
/** Convert node-postgres timestamptz text to lossless RFC 3339. */
|
|
12
|
+
export declare function postgresTimestampTzToRfc3339(value: string): string;
|
|
13
|
+
/** Keep PostgREST-compatible, lossless timestamp strings at the gateway edge. */
|
|
14
|
+
export declare function createPostgresGatewayTypeOverrides(TypeOverridesCtor: typeof import("pg").TypeOverrides): import("pg").TypeOverrides;
|
|
15
|
+
/**
|
|
16
|
+
* Capability-local, role-free transaction lane.
|
|
17
|
+
*
|
|
18
|
+
* The platform manifest intentionally creates no service role. This is not an
|
|
19
|
+
* alternate DataGatewayPort access mode: a deployment hands one lane to exactly
|
|
20
|
+
* one adapter that calls its own rail RPCs, and names that lane after the
|
|
21
|
+
* capability, because the name is the access rule. Its lifecycle reuses the same
|
|
22
|
+
* pooled BEGIN/callback/COMMIT|ROLLBACK/release implementation as the public
|
|
23
|
+
* gateway below, but it never changes the connection role.
|
|
24
|
+
*/
|
|
25
|
+
export interface PostgresRoleFreeTransactionLane {
|
|
26
|
+
run<T>(work: (gateway: unknown) => Promise<T>): Promise<T>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a node-postgres-backed DataGatewayPort. Pure: NO pool/connection is created until a port
|
|
31
|
+
* method runs work (lazy, like the supabase adapter), so the slot binds even before DATABASE_URL is
|
|
32
|
+
* present (keeps composeBundle({}) non-null). The pool is created once and reused across calls.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createPostgresDataGateway(env: PostgresDataGatewayEnv, options?: PostgresDataGatewayOptions): DataGatewayPort;
|
|
35
|
+
/**
|
|
36
|
+
* Build one role-free, per-operation lane. Do not use this for generic service
|
|
37
|
+
* work: DataGatewayPort.asService remains the mandatory service_role boundary.
|
|
38
|
+
* Only a named owner-authority capability of the deployment may hold a lane.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPostgresRoleFreeTransactionLane(env: PostgresDataGatewayEnv, options?: PostgresDataGatewayOptions): PostgresRoleFreeTransactionLane;
|
|
41
|
+
/** Resolve the gateway env from process env (DATABASE_URL), mirroring the supabase binding shape. */
|
|
42
|
+
export declare function resolvePostgresDataGatewayEnv(env: Record<string, string | undefined>): PostgresDataGatewayEnv;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { PgGatewayClient } from "./queryBuilder.js";
|
|
2
|
+
/** Convert node-postgres timestamptz text to lossless RFC 3339. */
|
|
3
|
+
export function postgresTimestampTzToRfc3339(value) {
|
|
4
|
+
const parts = /^(\d{4,}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)([+-])(\d{2})(?::?(\d{2}))?$/.exec(value);
|
|
5
|
+
if (!parts)
|
|
6
|
+
return value;
|
|
7
|
+
const [, date, time, sign, offsetHours, offsetMinutes = "00"] = parts;
|
|
8
|
+
return `${date}T${time}${sign}${offsetHours}:${offsetMinutes}`;
|
|
9
|
+
}
|
|
10
|
+
/** Keep PostgREST-compatible, lossless timestamp strings at the gateway edge. */
|
|
11
|
+
export function createPostgresGatewayTypeOverrides(TypeOverridesCtor) {
|
|
12
|
+
const overrides = new TypeOverridesCtor();
|
|
13
|
+
// node-postgres otherwise turns timestamptz into a millisecond Date and drops
|
|
14
|
+
// PostgreSQL microseconds, making an immediate schedule round-trip look stale.
|
|
15
|
+
overrides.setTypeParser(1184, "text", postgresTimestampTzToRfc3339);
|
|
16
|
+
return overrides;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a node-postgres-backed DataGatewayPort. Pure: NO pool/connection is created until a port
|
|
20
|
+
* method runs work (lazy, like the supabase adapter), so the slot binds even before DATABASE_URL is
|
|
21
|
+
* present (keeps composeBundle({}) non-null). The pool is created once and reused across calls.
|
|
22
|
+
*/
|
|
23
|
+
export function createPostgresDataGateway(env, options = {}) {
|
|
24
|
+
const transactions = createPooledPostgresTransactions(env, options);
|
|
25
|
+
return {
|
|
26
|
+
async asService(work) {
|
|
27
|
+
return transactions.run(async (client) => {
|
|
28
|
+
await client.query("SET LOCAL ROLE service_role");
|
|
29
|
+
}, work);
|
|
30
|
+
},
|
|
31
|
+
async asActor(claims, work) {
|
|
32
|
+
return transactions.run(async (client) => {
|
|
33
|
+
const jwtClaims = buildJwtClaims(claims);
|
|
34
|
+
if (jwtClaims) {
|
|
35
|
+
// Parameterized: the claims JSON is bound as $1, never interpolated into the SET statement.
|
|
36
|
+
await client.query("SELECT set_config('request.jwt.claims', $1, true)", [jwtClaims]);
|
|
37
|
+
await client.query("SET LOCAL ROLE authenticated");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// No usable principal → drop to the `anon` role so RLS evaluates the request as
|
|
41
|
+
// unauthenticated. CRITICAL: the connection's own login role may be a superuser/owner
|
|
42
|
+
// (which BYPASSes RLS), so we MUST switch roles rather than leave the default — otherwise an
|
|
43
|
+
// unauthenticated actor would silently see everything. Fail-safe: anon is RLS-bound, never
|
|
44
|
+
// elevated. (No request.jwt.claims is set, so auth.uid() returns NULL.)
|
|
45
|
+
await client.query("SET LOCAL ROLE anon");
|
|
46
|
+
}
|
|
47
|
+
}, work);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build one role-free, per-operation lane. Do not use this for generic service
|
|
53
|
+
* work: DataGatewayPort.asService remains the mandatory service_role boundary.
|
|
54
|
+
* Only a named owner-authority capability of the deployment may hold a lane.
|
|
55
|
+
*/
|
|
56
|
+
export function createPostgresRoleFreeTransactionLane(env, options = {}) {
|
|
57
|
+
const transactions = createPooledPostgresTransactions(env, options);
|
|
58
|
+
return { run: (work) => transactions.run(async () => { }, work), close: transactions.close };
|
|
59
|
+
}
|
|
60
|
+
function createPooledPostgresTransactions(env, options) {
|
|
61
|
+
let poolPromise = null;
|
|
62
|
+
async function getPool() {
|
|
63
|
+
// Cache the in-flight promise (not the resolved Pool) so concurrent first calls share ONE pool
|
|
64
|
+
// rather than racing to build two. Promise.resolve normalizes the factory's Pool | Promise<Pool>.
|
|
65
|
+
if (!poolPromise) {
|
|
66
|
+
const factory = options.poolFactory ?? defaultPoolFactory;
|
|
67
|
+
poolPromise = Promise.resolve(factory(env));
|
|
68
|
+
}
|
|
69
|
+
return poolPromise;
|
|
70
|
+
}
|
|
71
|
+
async function withClient(setup, work) {
|
|
72
|
+
const activePool = await getPool();
|
|
73
|
+
const client = await activePool.connect();
|
|
74
|
+
try {
|
|
75
|
+
await client.query("BEGIN");
|
|
76
|
+
await setup(client);
|
|
77
|
+
const result = await work(new PgGatewayClient(client));
|
|
78
|
+
await client.query("COMMIT");
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
try {
|
|
83
|
+
await client.query("ROLLBACK");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore rollback failure; surface the original error
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
client.release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
run: withClient,
|
|
96
|
+
async close() {
|
|
97
|
+
if (poolPromise)
|
|
98
|
+
await (await poolPromise).end();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the request.jwt.claims JSON PostgREST would set. Returns null when there is no usable
|
|
104
|
+
* principal (no sub) so the caller stays anon rather than asserting an authenticated-but-empty role.
|
|
105
|
+
*/
|
|
106
|
+
function buildJwtClaims(claims) {
|
|
107
|
+
if (!claims || !claims.sub)
|
|
108
|
+
return null;
|
|
109
|
+
const role = claims.role && claims.role.trim() !== "" ? claims.role : "authenticated";
|
|
110
|
+
return JSON.stringify({ sub: claims.sub, role });
|
|
111
|
+
}
|
|
112
|
+
// Lazy default: only import `pg` and build the Pool when actually invoked, so typecheck/test paths
|
|
113
|
+
// that never run work do not load the driver. SET LOCAL keeps pooled-connection reuse safe.
|
|
114
|
+
async function defaultPoolFactory(env) {
|
|
115
|
+
const pg = await import("pg").catch(peerMissing);
|
|
116
|
+
const PoolCtor = (pg.default?.Pool ?? pg.Pool);
|
|
117
|
+
const TypeOverridesCtor = (pg.default?.TypeOverrides ?? pg.TypeOverrides);
|
|
118
|
+
return new PoolCtor({
|
|
119
|
+
connectionString: env.connectionString,
|
|
120
|
+
types: createPostgresGatewayTypeOverrides(TypeOverridesCtor),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Resolve the gateway env from process env (DATABASE_URL), mirroring the supabase binding shape. */
|
|
124
|
+
export function resolvePostgresDataGatewayEnv(env) {
|
|
125
|
+
return { connectionString: env.DATABASE_URL ?? "" };
|
|
126
|
+
}
|
|
127
|
+
/** Translate only absence of this optional peer; preserve broken SDKs and other failures. */
|
|
128
|
+
function peerMissing(cause) {
|
|
129
|
+
if (cause instanceof Error && "code" in cause
|
|
130
|
+
&& (cause.code === "ERR_MODULE_NOT_FOUND" || cause.code === "MODULE_NOT_FOUND")
|
|
131
|
+
&& /^Cannot find (?:package|module) ['"]pg['"]/.test(cause.message)) {
|
|
132
|
+
throw new Error("OPENLUP_E_PEER_MISSING: pg is required by the Postgres data gateway; install it alongside @openlup/platform", { cause });
|
|
133
|
+
}
|
|
134
|
+
throw cause;
|
|
135
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PoolClient } from "pg";
|
|
2
|
+
import { type PgQueryExecutor, type PgShimError, type PgShimResult } from "./queryBuilderSql.js";
|
|
3
|
+
export type { PgQueryExecutor, PgShimError, PgShimResult };
|
|
4
|
+
/**
|
|
5
|
+
* Builder over one pg client. Chainable + awaitable (PromiseLike). Every value flows through a param
|
|
6
|
+
* accumulator, so the rendered SQL contains only $N placeholders for values.
|
|
7
|
+
*/
|
|
8
|
+
export declare class PgQueryBuilder<T = unknown> implements PromiseLike<PgShimResult<T>> {
|
|
9
|
+
private readonly client;
|
|
10
|
+
private state;
|
|
11
|
+
constructor(client: PgQueryExecutor, table: string);
|
|
12
|
+
select(columns: string, options?: {
|
|
13
|
+
count?: string;
|
|
14
|
+
head?: boolean;
|
|
15
|
+
}): this;
|
|
16
|
+
insert(values: Record<string, unknown> | Array<Record<string, unknown>>): this;
|
|
17
|
+
update(values: Record<string, unknown>): this;
|
|
18
|
+
eq(column: string, value: unknown): this;
|
|
19
|
+
gt(column: string, value: unknown): this;
|
|
20
|
+
gte(column: string, value: unknown): this;
|
|
21
|
+
lte(column: string, value: unknown): this;
|
|
22
|
+
in(column: string, values: unknown[]): this;
|
|
23
|
+
is(column: string, value: unknown): this;
|
|
24
|
+
ilike(column: string, value: string): this;
|
|
25
|
+
order(column: string, options?: {
|
|
26
|
+
ascending?: boolean;
|
|
27
|
+
}): this;
|
|
28
|
+
limit(count: number): this;
|
|
29
|
+
range(from: number, to: number): Promise<PgShimResult<T>>;
|
|
30
|
+
maybeSingle(): Promise<PgShimResult<T>>;
|
|
31
|
+
single(): Promise<PgShimResult<T>>;
|
|
32
|
+
then<R1 = PgShimResult<T>, R2 = never>(onfulfilled?: ((value: PgShimResult<T>) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
|
|
33
|
+
private run;
|
|
34
|
+
private runExactCount;
|
|
35
|
+
private shape;
|
|
36
|
+
private compile;
|
|
37
|
+
private compileSelect;
|
|
38
|
+
private compileInsert;
|
|
39
|
+
private compileUpdate;
|
|
40
|
+
private renderWhere;
|
|
41
|
+
private renderOrder;
|
|
42
|
+
private renderLimitOffset;
|
|
43
|
+
}
|
|
44
|
+
/** supabase-js-style client over one acquired pg client: `.from(t)` + `.rpc(fn, args)`. */
|
|
45
|
+
export declare class PgGatewayClient {
|
|
46
|
+
private readonly client;
|
|
47
|
+
constructor(client: PgQueryExecutor);
|
|
48
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
49
|
+
rows: Record<string, unknown>[];
|
|
50
|
+
}>;
|
|
51
|
+
from<T = Record<string, unknown>>(table: string): PgQueryBuilder<T>;
|
|
52
|
+
rpc(functionName: string, args?: Record<string, unknown>): Promise<PgShimResult>;
|
|
53
|
+
}
|
|
54
|
+
export type { PoolClient };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Vanilla-Postgres query-builder shim.
|
|
2
|
+
//
|
|
3
|
+
// Option A (approved): a SCOPED, injection-safe builder that emulates ONLY the supabase-js methods
|
|
4
|
+
// the 6 existing gateway consumers actually call — never a full PostgREST reimplementation. It
|
|
5
|
+
// translates a builder chain into a single PARAMETERIZED `pg` query ($1,$2,…). Values are never
|
|
6
|
+
// string-interpolated and identifiers are allowlist-validated; both invariants live in the pure SQL
|
|
7
|
+
// layer (queryBuilderSql.ts), which this file composes.
|
|
8
|
+
//
|
|
9
|
+
// Result semantics mirror PostgREST for the methods used:
|
|
10
|
+
// - default (await builder) -> { data: Row[], error }
|
|
11
|
+
// - .maybeSingle() -> { data: Row | null, error } (0/1 row; >1 is an error)
|
|
12
|
+
// - .single() -> { data: Row, error } (exactly 1 row; else error)
|
|
13
|
+
// - .range(from,to)/.limit(n) -> LIMIT/OFFSET, returns { data: Row[], error }
|
|
14
|
+
// - .select(c, {count:"exact"[,head]}) -> { data, count } from a separate aggregate; head omits rows
|
|
15
|
+
// - .insert(v).select(c).maybeSingle() -> INSERT ... RETURNING c, single row
|
|
16
|
+
// - .update(v)... -> UPDATE ... [RETURNING c]
|
|
17
|
+
// - error.code carries the pg SQLSTATE (e.g. 23505 unique-violation) so consumers' code-based
|
|
18
|
+
// branches (idempotency replay) keep working unchanged.
|
|
19
|
+
//
|
|
20
|
+
// The shim is constructed over a SINGLE acquired pg client (one per asActor/asService tx in the
|
|
21
|
+
// DataGatewayPort), so every query here runs inside that request's transaction with its RLS GUCs set.
|
|
22
|
+
import { ParamAccumulator, joinParts, quoteIdent, rangeNotSatisfiable, renderColumns, renderCountQuery, renderFilter, renderInsertStatement, toShimError, unwrapRpcRows, } from "./queryBuilderSql.js";
|
|
23
|
+
/**
|
|
24
|
+
* Builder over one pg client. Chainable + awaitable (PromiseLike). Every value flows through a param
|
|
25
|
+
* accumulator, so the rendered SQL contains only $N placeholders for values.
|
|
26
|
+
*/
|
|
27
|
+
export class PgQueryBuilder {
|
|
28
|
+
client;
|
|
29
|
+
state;
|
|
30
|
+
constructor(client, table) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
this.state = {
|
|
33
|
+
table,
|
|
34
|
+
mode: "select",
|
|
35
|
+
columns: "*",
|
|
36
|
+
filters: [],
|
|
37
|
+
orders: [],
|
|
38
|
+
limit: null,
|
|
39
|
+
offset: null,
|
|
40
|
+
insertRows: null,
|
|
41
|
+
updateValues: null,
|
|
42
|
+
exactCount: false,
|
|
43
|
+
head: false,
|
|
44
|
+
rangeRequested: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
select(columns, options) {
|
|
48
|
+
this.state.columns = columns;
|
|
49
|
+
this.state.exactCount = options?.count === "exact";
|
|
50
|
+
this.state.head = options?.head === true;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
insert(values) {
|
|
54
|
+
this.state.mode = "insert";
|
|
55
|
+
this.state.insertRows = Array.isArray(values) ? values : [values];
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
update(values) {
|
|
59
|
+
this.state.mode = "update";
|
|
60
|
+
this.state.updateValues = values;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
eq(column, value) {
|
|
64
|
+
this.state.filters.push({ kind: "eq", column, value });
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
gt(column, value) {
|
|
68
|
+
this.state.filters.push({ kind: "gt", column, value });
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
gte(column, value) {
|
|
72
|
+
this.state.filters.push({ kind: "gte", column, value });
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
lte(column, value) {
|
|
76
|
+
this.state.filters.push({ kind: "lte", column, value });
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
in(column, values) {
|
|
80
|
+
this.state.filters.push({ kind: "in", column, values });
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
is(column, value) {
|
|
84
|
+
this.state.filters.push({ kind: "is", column, value });
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
ilike(column, value) {
|
|
88
|
+
this.state.filters.push({ kind: "ilike", column, value });
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
order(column, options) {
|
|
92
|
+
this.state.orders.push({ column, ascending: options?.ascending !== false });
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
limit(count) {
|
|
96
|
+
this.state.limit = count;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
range(from, to) {
|
|
100
|
+
// PostgREST range is inclusive on both ends → LIMIT (to-from+1) OFFSET from.
|
|
101
|
+
this.state.offset = from;
|
|
102
|
+
this.state.limit = to - from + 1;
|
|
103
|
+
this.state.rangeRequested = true;
|
|
104
|
+
return this.run("many");
|
|
105
|
+
}
|
|
106
|
+
async maybeSingle() {
|
|
107
|
+
return this.run("maybe");
|
|
108
|
+
}
|
|
109
|
+
async single() {
|
|
110
|
+
return this.run("single");
|
|
111
|
+
}
|
|
112
|
+
then(onfulfilled, onrejected) {
|
|
113
|
+
return this.run("many").then(onfulfilled, onrejected);
|
|
114
|
+
}
|
|
115
|
+
async run(cardinality) {
|
|
116
|
+
try {
|
|
117
|
+
const count = this.state.exactCount ? await this.runExactCount() : null;
|
|
118
|
+
// A page past the end is a REFUSAL upstream, not an empty answer — but only once the total is
|
|
119
|
+
// known, which is why this sits behind the count and not behind `.range()` itself.
|
|
120
|
+
const unsatisfiable = this.state.rangeRequested ? rangeNotSatisfiable(this.state.offset, count) : null;
|
|
121
|
+
if (unsatisfiable)
|
|
122
|
+
return { data: [], error: unsatisfiable, count: null };
|
|
123
|
+
// head:true is a HEAD request: no body, so the client contract reports data:null and no row query.
|
|
124
|
+
if (this.state.head)
|
|
125
|
+
return { data: null, error: null, count };
|
|
126
|
+
const { text, values } = this.compile();
|
|
127
|
+
const { rows } = await this.client.query(text, values);
|
|
128
|
+
// The client contract reports count: null unless a count was requested; `shape` must not invent one.
|
|
129
|
+
return { ...this.shape(rows, cardinality), count };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return { data: (cardinality === "many" ? [] : null), error: toShimError(error), count: null };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async runExactCount() {
|
|
136
|
+
const params = new ParamAccumulator();
|
|
137
|
+
const { rows } = await this.client.query(renderCountQuery(this.state.table, this.state.filters, params), params.values);
|
|
138
|
+
return Number(rows[0]?.exact_count ?? 0);
|
|
139
|
+
}
|
|
140
|
+
shape(rows, cardinality) {
|
|
141
|
+
if (cardinality === "many") {
|
|
142
|
+
return { data: rows, error: null };
|
|
143
|
+
}
|
|
144
|
+
if (rows.length > 1) {
|
|
145
|
+
// PostgREST returns 406 (PGRST116) when maybeSingle/single match multiple rows.
|
|
146
|
+
return { data: null, error: { code: "PGRST116", message: "multiple rows returned" } };
|
|
147
|
+
}
|
|
148
|
+
if (cardinality === "single" && rows.length === 0) {
|
|
149
|
+
return { data: null, error: { code: "PGRST116", message: "no rows returned" } };
|
|
150
|
+
}
|
|
151
|
+
return { data: (rows[0] ?? null), error: null };
|
|
152
|
+
}
|
|
153
|
+
compile() {
|
|
154
|
+
const params = new ParamAccumulator();
|
|
155
|
+
if (this.state.mode === "insert")
|
|
156
|
+
return this.compileInsert(params);
|
|
157
|
+
if (this.state.mode === "update")
|
|
158
|
+
return this.compileUpdate(params);
|
|
159
|
+
return this.compileSelect(params);
|
|
160
|
+
}
|
|
161
|
+
compileSelect(params) {
|
|
162
|
+
const parts = [`SELECT ${renderColumns(this.state.columns)} FROM ${quoteIdent(this.state.table)}`];
|
|
163
|
+
parts.push(this.renderWhere(params));
|
|
164
|
+
parts.push(this.renderOrder());
|
|
165
|
+
parts.push(this.renderLimitOffset(params));
|
|
166
|
+
return { text: joinParts(parts), values: params.values };
|
|
167
|
+
}
|
|
168
|
+
compileInsert(params) {
|
|
169
|
+
const text = renderInsertStatement(this.state.table, this.state.insertRows ?? [], this.state.columns, params);
|
|
170
|
+
return { text, values: params.values };
|
|
171
|
+
}
|
|
172
|
+
compileUpdate(params) {
|
|
173
|
+
const values = this.state.updateValues ?? {};
|
|
174
|
+
const assignments = Object.keys(values)
|
|
175
|
+
.map((c) => `${quoteIdent(c)} = ${params.add(values[c])}`)
|
|
176
|
+
.join(", ");
|
|
177
|
+
if (assignments === "")
|
|
178
|
+
throw new Error("pg-shim: update requires at least one column");
|
|
179
|
+
const parts = [`UPDATE ${quoteIdent(this.state.table)} SET ${assignments}`];
|
|
180
|
+
parts.push(this.renderWhere(params));
|
|
181
|
+
const returning = this.state.columns && this.state.columns !== "*"
|
|
182
|
+
? `RETURNING ${renderColumns(this.state.columns)}`
|
|
183
|
+
: "";
|
|
184
|
+
if (returning)
|
|
185
|
+
parts.push(returning);
|
|
186
|
+
return { text: joinParts(parts), values: params.values };
|
|
187
|
+
}
|
|
188
|
+
renderWhere(params) {
|
|
189
|
+
if (this.state.filters.length === 0)
|
|
190
|
+
return "";
|
|
191
|
+
const clauses = this.state.filters.map((f) => renderFilter(f, params));
|
|
192
|
+
return `WHERE ${clauses.join(" AND ")}`;
|
|
193
|
+
}
|
|
194
|
+
renderOrder() {
|
|
195
|
+
if (this.state.orders.length === 0)
|
|
196
|
+
return "";
|
|
197
|
+
const cols = this.state.orders
|
|
198
|
+
.map((o) => `${quoteIdent(o.column)} ${o.ascending ? "ASC" : "DESC"}`)
|
|
199
|
+
.join(", ");
|
|
200
|
+
return `ORDER BY ${cols}`;
|
|
201
|
+
}
|
|
202
|
+
renderLimitOffset(params) {
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (this.state.limit !== null)
|
|
205
|
+
parts.push(`LIMIT ${params.add(this.state.limit)}`);
|
|
206
|
+
if (this.state.offset !== null)
|
|
207
|
+
parts.push(`OFFSET ${params.add(this.state.offset)}`);
|
|
208
|
+
return parts.join(" ");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** supabase-js-style client over one acquired pg client: `.from(t)` + `.rpc(fn, args)`. */
|
|
212
|
+
export class PgGatewayClient {
|
|
213
|
+
client;
|
|
214
|
+
constructor(client) {
|
|
215
|
+
this.client = client;
|
|
216
|
+
}
|
|
217
|
+
query(text, values) { return this.client.query(text, values); }
|
|
218
|
+
from(table) {
|
|
219
|
+
return new PgQueryBuilder(this.client, table);
|
|
220
|
+
}
|
|
221
|
+
async rpc(functionName, args = {}) {
|
|
222
|
+
try {
|
|
223
|
+
const name = quoteIdent(functionName);
|
|
224
|
+
const params = new ParamAccumulator();
|
|
225
|
+
// Named-arg invocation: every existing RPC uses `p_*` named params. `name => $n` binds by name
|
|
226
|
+
// so arg order is irrelevant and values are fully parameterized.
|
|
227
|
+
const namedArgs = Object.keys(args)
|
|
228
|
+
.map((key) => `${quoteIdent(key)} => ${params.add(args[key])}`)
|
|
229
|
+
.join(", ");
|
|
230
|
+
const text = `SELECT * FROM ${name}(${namedArgs})`;
|
|
231
|
+
const { rows } = await this.client.query(text, params.values);
|
|
232
|
+
// RPCs returning a scalar set-returning function expose the value under the function name;
|
|
233
|
+
// a single-column single-row result unwraps to that value (matches supabase-js rpc shape).
|
|
234
|
+
return { data: unwrapRpcRows(rows, functionName), error: null };
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return { data: null, error: toShimError(error) };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** PostgREST-shaped error the consumers narrow on (`.code`, `.message`, `.details`, `.hint`). */
|
|
2
|
+
export interface PgShimError {
|
|
3
|
+
code?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
details?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PgShimResult<T = unknown> {
|
|
9
|
+
data: T;
|
|
10
|
+
error: PgShimError | null;
|
|
11
|
+
count?: number | null;
|
|
12
|
+
}
|
|
13
|
+
/** Minimal query surface we need from a pg client (Pool acquires a PoolClient per request). */
|
|
14
|
+
export interface PgQueryExecutor {
|
|
15
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
16
|
+
rows: Record<string, unknown>[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export type Filter = {
|
|
20
|
+
kind: "eq" | "gte" | "lte" | "gt";
|
|
21
|
+
column: string;
|
|
22
|
+
value: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "in";
|
|
25
|
+
column: string;
|
|
26
|
+
values: unknown[];
|
|
27
|
+
} | {
|
|
28
|
+
kind: "ilike";
|
|
29
|
+
column: string;
|
|
30
|
+
value: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "is";
|
|
33
|
+
column: string;
|
|
34
|
+
value: unknown;
|
|
35
|
+
};
|
|
36
|
+
export interface OrderBy {
|
|
37
|
+
column: string;
|
|
38
|
+
ascending: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function quoteIdent(raw: string): string;
|
|
41
|
+
export declare function renderColumns(columns: string): string;
|
|
42
|
+
export declare function renderFilter(filter: Filter, params: ParamAccumulator): string;
|
|
43
|
+
/**
|
|
44
|
+
* PostgREST `count=exact` rendered as a SEPARATE aggregate over the same FROM/WHERE — deliberately
|
|
45
|
+
* NOT a `COUNT(*) OVER()` folded into the row query. PostgREST's exact count ignores limit/offset,
|
|
46
|
+
* and a window function emits no row at all for an empty page, so an offset past the end would
|
|
47
|
+
* report 0 instead of the true total. Keeping it separate also leaves the row projection
|
|
48
|
+
* byte-identical: there is no synthetic column to strip before cardinality shaping.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderCountQuery(table: string, filters: readonly Filter[], params: ParamAccumulator): string;
|
|
51
|
+
/**
|
|
52
|
+
* PostgREST refuses an out-of-bounds page instead of answering an empty one -- but ONLY when it
|
|
53
|
+
* knows the total, i.e. when an exact count was requested; without one it answers 200 and `[]`,
|
|
54
|
+
* which is what this shim already did. The boundary is strict: offset === total is a legal empty
|
|
55
|
+
* page, offset === total + 1 is not. Measured against PostgREST 14.10 before this was written,
|
|
56
|
+
* including the `details` sentence, which carries the two numbers a caller pages on.
|
|
57
|
+
*/
|
|
58
|
+
export declare function rangeNotSatisfiable(offset: number | null, total: number | null): PgShimError | null;
|
|
59
|
+
/**
|
|
60
|
+
* INSERT ... VALUES ... RETURNING, with the column list taken from the FIRST row and every later
|
|
61
|
+
* row required to carry the same key set. Deriving the columns from row 0 alone and binding the
|
|
62
|
+
* rest positionally is how a batch used to lose a column silently: an extra key vanished, a missing
|
|
63
|
+
* one bound NULL over a DEFAULT. PostgREST answers PGRST102 "All object keys must match" to exactly
|
|
64
|
+
* this payload -- and keeps answering it under `Prefer: missing=default`, measured -- so the shim
|
|
65
|
+
* refuses too. Refusal, not union: parity is the point, and this builder exposes no Prefer header
|
|
66
|
+
* through which a caller could ask for anything else.
|
|
67
|
+
*/
|
|
68
|
+
export declare function renderInsertStatement(table: string, rows: readonly Record<string, unknown>[], projection: string, params: ParamAccumulator): string;
|
|
69
|
+
export declare function unwrapRpcRows(rows: Record<string, unknown>[], functionName: string): unknown;
|
|
70
|
+
/** Accumulates values and hands back $1,$2,… placeholders — the single injection-safety chokepoint. */
|
|
71
|
+
export declare class ParamAccumulator {
|
|
72
|
+
readonly values: unknown[];
|
|
73
|
+
add(value: unknown): string;
|
|
74
|
+
}
|
|
75
|
+
export declare function joinParts(parts: string[]): string;
|
|
76
|
+
export declare function toShimError(error: unknown): PgShimError;
|