@rebasepro/server-postgres 0.25.0 → 0.26.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/dist/{BranchService-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/backup/backup-cli.d.ts +40 -3
- package/dist/backup/backup-service.d.ts +11 -0
- package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
- package/dist/backup-cli-PoRG-zEq.js.map +1 -0
- package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
- package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-argv.d.ts +15 -0
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli-errors.d.ts +14 -0
- package/dist/cli-flags.d.ts +51 -0
- package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
- package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
- package/dist/cli.js +121 -271
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
- package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
- package/dist/connection-czvI7TZL.js.map +1 -0
- package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
- package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
- package/dist/db-command-line.d.ts +14 -0
- package/dist/db-subcommands.d.ts +11 -0
- package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
- package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
- package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
- package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
- package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
- package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
- package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
- package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
- package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
- package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
- package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
- package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
- package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
- package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
- package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
- package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.es.js +288 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
- package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
- package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
- package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
- package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
- package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
- package/dist/pool-disconnects.d.ts +28 -0
- package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
- package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
- package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
- package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
- package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
- package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/doctor.d.ts +20 -2
- package/dist/schema/ensure-collection-policies.d.ts +4 -1
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +21 -0
- package/dist/schema/introspect-db.js +5 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-policy-stamps.d.ts +23 -0
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/push-policy-plan.d.ts +5 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/BranchService.d.ts +20 -2
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/pg-notify-listener.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
- package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
- package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
- package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
- package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-iXXZeVYD.js.map +0 -1
- package/dist/backup-cli-B2CAn9fR.js +0 -647
- package/dist/backup-cli-B2CAn9fR.js.map +0 -1
- package/dist/backup-service-Bb39plam.js.map +0 -1
- package/dist/cli-console-DQ_K-sdM.js.map +0 -1
- package/dist/cli-errors-DwQnNATs.js.map +0 -1
- package/dist/cli-flags-D-b0qL9T.js +0 -206
- package/dist/cli-flags-D-b0qL9T.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/connection-C938nR3E.js.map +0 -1
- package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
- package/dist/doctor-DvJH-IQG.js.map +0 -1
- package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
- package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
- package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
- package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
- package/dist/websocket-MC0zTBar.js.map +0 -1
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { i as createPostgresDatabaseConnection } from "./connection-
|
|
4
|
-
import { _ as visibleColumnProjection, a as buildSearchColumnSpec,
|
|
5
|
-
import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName,
|
|
6
|
-
import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-
|
|
7
|
-
import { t as BranchService } from "./BranchService-
|
|
8
|
-
import { a as ensureAppRoleGrants, d as
|
|
9
|
-
import { u as resolveColumnName } from "./column-plan-helpers-
|
|
10
|
-
import {
|
|
11
|
-
import { n as planSchema
|
|
12
|
-
import { t as generateSchema } from "./generate-drizzle-schema-logic-
|
|
13
|
-
import { r as renderPgType } from "./render-ddl-
|
|
14
|
-
import { u as typesAgree } from "./deferred-index-build-
|
|
15
|
-
import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-
|
|
16
|
-
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
3
|
+
import { i as createPostgresDatabaseConnection } from "./connection-czvI7TZL.js";
|
|
4
|
+
import { _ as visibleColumnProjection, a as buildSearchColumnSpec, r as SEARCH_UNACCENT_FN, s as hiddenColumnsOption } from "./search-column-BM-GV6vH.js";
|
|
5
|
+
import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName, r as deepestErrorMessage, rt as extractCauseMessage, u as parseHostInfo } from "./cli-errors-DsYyNYzd.js";
|
|
6
|
+
import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-DhiUTlVx.js";
|
|
7
|
+
import { t as BranchService } from "./BranchService-CWBnDhcv.js";
|
|
8
|
+
import { a as ensureAppRoleGrants, c as revokeUserRoleSql, d as warnOnAnonymousGrants, f as warnOnLegacyRlsFunctions, i as ensureAppRole, n as applyAuthContext, o as findExposedInternalTables, p as warnOnRoleSchemaCollision, r as detectConnectionPosture, t as REBASE_USER_ROLE$1, u as validatePolicyPgRoles } from "./rls-enforcement-BOHuHVlc.js";
|
|
9
|
+
import { u as resolveColumnName } from "./column-plan-helpers-IG6uFb1s.js";
|
|
10
|
+
import { t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
|
|
11
|
+
import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
|
|
12
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-CPzCRD-8.js";
|
|
13
|
+
import { r as renderPgType } from "./render-ddl-4v88y-Gv.js";
|
|
14
|
+
import { u as typesAgree } from "./deferred-index-build-CqaOv1Xx.js";
|
|
15
|
+
import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-EsgqDkx0.js";
|
|
16
|
+
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-CFIRq8ni.js";
|
|
17
|
+
import { a as mergeDatabaseBranch, i as diffDatabaseBranch } from "./branch-diff-CxE4VlxT.js";
|
|
18
|
+
import { t as DatabasePoolManager } from "./databasePoolManager-3stMNFL-.js";
|
|
19
|
+
import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-4nA-tfwi.js";
|
|
20
|
+
import { a as buildBaasSchema, c as introspectSchema, i as warnOnGeneratedSchemaDrift, l as readRlsStatus, n as diffGeneratedSchemaAgainstCatalogue, o as readCatalogueSchema, s as buildCollectionsFromSchema } from "./generated-schema-diff-eZCklRZQ.js";
|
|
20
21
|
import { Client, DatabaseError, Pool } from "pg";
|
|
21
22
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
22
23
|
import { ApiError, assertQueryFieldsReadable, createDdlBootstrapper, createEmailService, logger, rawQueryLoggingEnabled, resolveBatchRefs, rowETag, splitFieldOps } from "@rebasepro/server";
|
|
23
24
|
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, is, isNotNull, isNull, isTable, lt, lte, not, notInArray, or, relations, sql } from "drizzle-orm";
|
|
24
|
-
import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean,
|
|
25
|
-
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf } from "@rebasepro/types";
|
|
26
|
-
import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys,
|
|
27
|
-
import { camelCase,
|
|
25
|
+
import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean, getTableConfig, index, integer, jsonb, pgSchema, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
|
|
26
|
+
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf, roleNameProblem } from "@rebasepro/types";
|
|
27
|
+
import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, hasFieldAccessRules, includableRelationNames, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseIdValues as parseIdValues$1, parseOrderBySpecStrict, queryableFieldNames, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveTenantWrite, restoresSoftDeletedRow, revokeInternalTableSql, softDeleteFieldOf, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, updateDateAutoValues, updateUserAutoValues, withRelationReferencesOnly } from "@rebasepro/common";
|
|
28
|
+
import { camelCase, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
|
|
28
29
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29
30
|
import { WebSocket } from "ws";
|
|
30
31
|
import { EventEmitter } from "events";
|
|
@@ -4404,6 +4405,38 @@ function isJunctionBackedRelation(relation) {
|
|
|
4404
4405
|
return relation.shared;
|
|
4405
4406
|
}
|
|
4406
4407
|
/**
|
|
4408
|
+
* Reject a create or update whose body names a relation Rebase does not write
|
|
4409
|
+
* — a multi-hop `via` (`writable: false`) — before anything is read or written.
|
|
4410
|
+
*
|
|
4411
|
+
* A single-hop `via` is written through this door as it shipped (to-one: the
|
|
4412
|
+
* target's key; to-many: one junction's rows). A longer chain used to get as
|
|
4413
|
+
* far as the writer: a to-many one failed inside the transaction as a 500
|
|
4414
|
+
* RELATION_MISCONFIGURED telling the caller to regenerate a schema that was
|
|
4415
|
+
* fine, and a to-one one re-pointed the target at the first step's column.
|
|
4416
|
+
*
|
|
4417
|
+
* Matched the way the write path matches: a key that is a relation's own name
|
|
4418
|
+
* (the to-many split), or a `relation` property that resolves to one (the
|
|
4419
|
+
* to-one transformer). An `undefined` value is no value — the transformer skips
|
|
4420
|
+
* it — so it is not a write; `null` is, since it would unlink.
|
|
4421
|
+
*/
|
|
4422
|
+
function assertNoReadOnlyRelationValues(values, collection) {
|
|
4423
|
+
const properties = collection.properties ?? {};
|
|
4424
|
+
let relations;
|
|
4425
|
+
const named = [];
|
|
4426
|
+
for (const [key, value] of Object.entries(values)) {
|
|
4427
|
+
if (value === void 0) continue;
|
|
4428
|
+
relations ??= resolveCollectionRelations(collection);
|
|
4429
|
+
const relation = relations[key] ?? (properties[key]?.type === "relation" ? findRelation(relations, key) : void 0);
|
|
4430
|
+
if (relation && !relation.writable) named.push(key);
|
|
4431
|
+
}
|
|
4432
|
+
if (named.length === 0) return;
|
|
4433
|
+
const list = named.map((key) => `'${key}'`).join(", ");
|
|
4434
|
+
throw ApiError.badRequest(`${list} on '${collection.slug}' ${named.length > 1 ? "are multi-hop `via` relations" : "is a multi-hop `via` relation"}, which is read-only: only single-hop via relations are writable — a to-one chain of one step, or a to-many chain through one junction table. Write the rows the chain joins directly.`, "RELATION_NOT_WRITABLE", {
|
|
4435
|
+
collection: collection.slug,
|
|
4436
|
+
fields: named
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
/**
|
|
4407
4440
|
* Reject a nested write whose final segment is a to-one relation.
|
|
4408
4441
|
*
|
|
4409
4442
|
* There is no column on the target row that records a to-one parent — the
|
|
@@ -4415,8 +4448,8 @@ function isJunctionBackedRelation(relation) {
|
|
|
4415
4448
|
*/
|
|
4416
4449
|
function assertWritableThrough(hop, path) {
|
|
4417
4450
|
const { relation } = hop;
|
|
4418
|
-
if (relation.writable && relation.cardinality === "many") return;
|
|
4419
|
-
if (
|
|
4451
|
+
if (relation.kind !== "via" && relation.writable && relation.cardinality === "many") return;
|
|
4452
|
+
if (relation.kind === "via") throw ApiError.badRequest(`"${path}" ends in '${hop.relationKey}', a \`via\` relation, which a nested path does not write through — write the row at "${hop.targetCollection.slug}" directly` + (relation.writable ? `, and link it by setting '${hop.relationKey}' in an update of the '${hop.parentCollection.slug}' row.` : "."), "RELATION_NOT_WRITABLE");
|
|
4420
4453
|
throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
|
|
4421
4454
|
}
|
|
4422
4455
|
//#endregion
|
|
@@ -5339,6 +5372,38 @@ var FetchService = class FetchService {
|
|
|
5339
5372
|
return values;
|
|
5340
5373
|
}
|
|
5341
5374
|
/**
|
|
5375
|
+
* Every row of a collection's table, as stored, a page at a time, in key
|
|
5376
|
+
* order — the driver's `scanStoredRows`.
|
|
5377
|
+
*
|
|
5378
|
+
* Deliberately none of the read pipeline: no `beforeQuery` (a sweep has no
|
|
5379
|
+
* caller to scope to, and the documented hook narrows a caller-less read to
|
|
5380
|
+
* the rows owned by nobody), no soft-delete filter, no `afterRead`, no
|
|
5381
|
+
* field stripping. The columns come back under their table names; a sweep
|
|
5382
|
+
* reads values, not keys. Paged by the key rather than an offset, so a row
|
|
5383
|
+
* deleted during the scan never shifts another past the next page.
|
|
5384
|
+
*/
|
|
5385
|
+
async *scanStoredRows(collectionPath, pageSize = 500) {
|
|
5386
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
5387
|
+
const table = getTableForCollection(collection, this.registry);
|
|
5388
|
+
const keys = primaryKeyColumns(table, requirePrimaryKeys(collection, this.registry), collectionPath);
|
|
5389
|
+
const keyNames = keys.map((column) => {
|
|
5390
|
+
const entry = Object.entries(getTableColumns(table)).find(([, candidate]) => candidate === column);
|
|
5391
|
+
if (!entry) throw new Error(`Key column '${column.name}' not found in table for collection '${collectionPath}'`);
|
|
5392
|
+
return entry[0];
|
|
5393
|
+
});
|
|
5394
|
+
const limit = Math.max(1, pageSize);
|
|
5395
|
+
let after;
|
|
5396
|
+
for (;;) {
|
|
5397
|
+
const past = after ? or(...keys.map((column, i) => and(...keys.slice(0, i).map((earlier, j) => eq(earlier, after[j])), gt(column, after[i])))) : void 0;
|
|
5398
|
+
const query = this.db.select().from(table);
|
|
5399
|
+
const rows = await (past ? query.where(past) : query).orderBy(...keys.map((column) => asc(column))).limit(limit);
|
|
5400
|
+
if (rows.length > 0) yield rows;
|
|
5401
|
+
if (rows.length < limit) return;
|
|
5402
|
+
const last = rows[rows.length - 1];
|
|
5403
|
+
after = keyNames.map((name) => last[name]);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
/**
|
|
5342
5407
|
* Fetch a collection of rows
|
|
5343
5408
|
*/
|
|
5344
5409
|
async fetchCollection(collectionPath, options = {}) {
|
|
@@ -6804,6 +6869,7 @@ var PersistService = class {
|
|
|
6804
6869
|
}
|
|
6805
6870
|
}
|
|
6806
6871
|
const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
|
|
6872
|
+
assertNoReadOnlyRelationValues(effectiveValues, collection);
|
|
6807
6873
|
const table = getTableForCollection(collection, this.registry);
|
|
6808
6874
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6809
6875
|
idInfoArray.map((info) => info.fieldName);
|
|
@@ -8900,6 +8966,15 @@ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
|
8900
8966
|
/** Guards the identifier interpolated into `LISTEN`. */
|
|
8901
8967
|
var SAFE_CHANNEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8902
8968
|
/**
|
|
8969
|
+
* Why a connect failed, in one line: the message, and the SQLSTATE or socket
|
|
8970
|
+
* code when the message does not already carry it.
|
|
8971
|
+
*/
|
|
8972
|
+
function connectFailureReason(err) {
|
|
8973
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8974
|
+
const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : void 0;
|
|
8975
|
+
return code && !message.includes(code) ? `${message} [${code}]` : message;
|
|
8976
|
+
}
|
|
8977
|
+
/**
|
|
8903
8978
|
* A half-open connection — one whose packets stop arriving, with no FIN and no
|
|
8904
8979
|
* RST, which is what an idle-flow timeout on a load balancer, a NAT, a dead
|
|
8905
8980
|
* middlebox or a failed-over network looks like — fires no `error` and no
|
|
@@ -8923,6 +8998,8 @@ var PgNotifyListener = class {
|
|
|
8923
8998
|
heartbeatInFlight = false;
|
|
8924
8999
|
downSince;
|
|
8925
9000
|
lastHeartbeatAt;
|
|
9001
|
+
/** Whether this outage's "could not be opened" has been said. Cleared once it listens again. */
|
|
9002
|
+
outageWarned = false;
|
|
8926
9003
|
constructor(options) {
|
|
8927
9004
|
this.options = options;
|
|
8928
9005
|
if (!SAFE_CHANNEL.test(options.channel)) throw new Error(`Unsafe NOTIFY channel name "${options.channel}" — expected a plain SQL identifier.`);
|
|
@@ -9014,6 +9091,7 @@ var PgNotifyListener = class {
|
|
|
9014
9091
|
this.client = client;
|
|
9015
9092
|
pending = void 0;
|
|
9016
9093
|
this.downSince = void 0;
|
|
9094
|
+
this.outageWarned = false;
|
|
9017
9095
|
this.lastHeartbeatAt = Date.now();
|
|
9018
9096
|
this.startHeartbeat(client);
|
|
9019
9097
|
logger.debug(`📡 ${logLabel} Listening on channel "${channel}".`);
|
|
@@ -9022,7 +9100,12 @@ var PgNotifyListener = class {
|
|
|
9022
9100
|
await pending.end();
|
|
9023
9101
|
} catch {}
|
|
9024
9102
|
if (first && !this.options.retryInitialConnect) throw err;
|
|
9025
|
-
|
|
9103
|
+
const retry = `${logLabel} LISTEN connection could not be opened (${connectFailureReason(err)}); retrying every ${Math.round((this.options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS) / 1e3)} s.`;
|
|
9104
|
+
if (this.outageWarned) logger.debug(retry);
|
|
9105
|
+
else {
|
|
9106
|
+
this.outageWarned = true;
|
|
9107
|
+
logger.warn(`⚠️ ${retry}`);
|
|
9108
|
+
}
|
|
9026
9109
|
this.scheduleReconnect();
|
|
9027
9110
|
return;
|
|
9028
9111
|
}
|
|
@@ -9460,10 +9543,29 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9460
9543
|
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
9461
9544
|
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
9462
9545
|
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
9463
|
-
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
9546
|
+
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService),
|
|
9547
|
+
diffBranch: async (name, options) => this.nameDiffCollections(await diffDatabaseBranch(this.admin, name, options)),
|
|
9548
|
+
mergeBranch: (name, options) => mergeDatabaseBranch(this.admin, name, options)
|
|
9464
9549
|
} : {}
|
|
9465
9550
|
};
|
|
9466
9551
|
}
|
|
9552
|
+
/** The diff with each table named by the collection it backs, where one does. */
|
|
9553
|
+
nameDiffCollections(diff) {
|
|
9554
|
+
const collections = this.registry?.getCollections() ?? [];
|
|
9555
|
+
const slugOf = (schema, table) => collections.find((collection) => {
|
|
9556
|
+
return ("schema" in collection && typeof collection.schema === "string" ? collection.schema : "public") === schema && getTableName$1(collection) === table;
|
|
9557
|
+
})?.slug;
|
|
9558
|
+
return {
|
|
9559
|
+
...diff,
|
|
9560
|
+
data: diff.data.map((entry) => {
|
|
9561
|
+
const collection = slugOf(entry.schema, entry.table);
|
|
9562
|
+
return collection ? {
|
|
9563
|
+
...entry,
|
|
9564
|
+
collection
|
|
9565
|
+
} : entry;
|
|
9566
|
+
})
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9467
9569
|
/**
|
|
9468
9570
|
* `SQLAdmin.listen`: a `LISTEN` on a connection of its own, through the
|
|
9469
9571
|
* listener CDC and the channel bus use, which reconnects and heartbeats.
|
|
@@ -9558,6 +9660,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9558
9660
|
const client = this.client;
|
|
9559
9661
|
return {
|
|
9560
9662
|
user: this.user,
|
|
9663
|
+
...this.user?.credential ? { credential: this.user.credential } : {},
|
|
9561
9664
|
driver: this,
|
|
9562
9665
|
data: this.data,
|
|
9563
9666
|
get client() {
|
|
@@ -9727,6 +9830,15 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9727
9830
|
return rows;
|
|
9728
9831
|
}
|
|
9729
9832
|
/**
|
|
9833
|
+
* Every stored row of a collection, past every read hook, for a whole-store
|
|
9834
|
+
* sweep — see `DataDriver.scanStoredRows`. Only this unscoped driver has
|
|
9835
|
+
* it: {@link AuthenticatedPostgresBackendDriver} reads as a caller, and a
|
|
9836
|
+
* caller's reads are scoped.
|
|
9837
|
+
*/
|
|
9838
|
+
scanStoredRows(props) {
|
|
9839
|
+
return this.dataService.getFetchService().scanStoredRows(props.path, props.pageSize);
|
|
9840
|
+
}
|
|
9841
|
+
/**
|
|
9730
9842
|
* Listen to a collection: the rows now, then again after every change.
|
|
9731
9843
|
*
|
|
9732
9844
|
* The realtime service runs every read, the first one included, as
|
|
@@ -11066,6 +11178,7 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
11066
11178
|
const client = this.delegate.client;
|
|
11067
11179
|
writeScope?.setSettledContext({
|
|
11068
11180
|
user: this.user,
|
|
11181
|
+
...this.user?.credential ? { credential: this.user.credential } : {},
|
|
11069
11182
|
driver: this,
|
|
11070
11183
|
data: this.data,
|
|
11071
11184
|
get client() {
|
|
@@ -13026,9 +13139,13 @@ var ChannelPresenceStore = class {
|
|
|
13026
13139
|
* `tablesReady` is now set from a probe of what exists, not from having been
|
|
13027
13140
|
* the instance that created it.
|
|
13028
13141
|
*/
|
|
13029
|
-
async ensureTables() {
|
|
13030
|
-
if (this.tablesReady) return;
|
|
13142
|
+
async ensureTables(options = {}) {
|
|
13143
|
+
if (this.tablesReady) return true;
|
|
13031
13144
|
const ddl = drizzleDdlBootstrapper(this.db, "channel-presence");
|
|
13145
|
+
if (options.provision === false) {
|
|
13146
|
+
this.tablesReady = await ddl.isReadable("rebase.channel_presence");
|
|
13147
|
+
return this.tablesReady;
|
|
13148
|
+
}
|
|
13032
13149
|
await ddl.ensureObject("rebase schema", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
13033
13150
|
await ddl.ensureObject("channel_presence table", `
|
|
13034
13151
|
CREATE TABLE IF NOT EXISTS rebase.channel_presence (
|
|
@@ -13052,6 +13169,7 @@ var ChannelPresenceStore = class {
|
|
|
13052
13169
|
await ddl.step("channel_presence revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence"))));
|
|
13053
13170
|
this.tablesReady = true;
|
|
13054
13171
|
}
|
|
13172
|
+
return this.tablesReady;
|
|
13055
13173
|
}
|
|
13056
13174
|
/** Record (or refresh) a client's presence. */
|
|
13057
13175
|
async track(channel, clientId, state) {
|
|
@@ -13734,6 +13852,14 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
13734
13852
|
refetchQueue = /* @__PURE__ */ new Set();
|
|
13735
13853
|
/** Group refetches running now — at most {@link maxConcurrentRefetches}. */
|
|
13736
13854
|
refetchesRunning = 0;
|
|
13855
|
+
/**
|
|
13856
|
+
* What a group refetch runs inside — see {@link setScopeRunner}. A refetch
|
|
13857
|
+
* runs on a timer armed by whatever announced the change; one armed by a
|
|
13858
|
+
* change-capture notification inherits the LISTEN connection's context,
|
|
13859
|
+
* which is no backend's, so a hook the read reached saw the process
|
|
13860
|
+
* default as `rebase`.
|
|
13861
|
+
*/
|
|
13862
|
+
runInScope = (fn) => fn();
|
|
13737
13863
|
/** See {@link refetchConcurrencyFor}. */
|
|
13738
13864
|
maxConcurrentRefetches;
|
|
13739
13865
|
/**
|
|
@@ -14002,6 +14128,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14002
14128
|
if (group.pending !== "all") group.pending.delete(key);
|
|
14003
14129
|
if (group.members.size > 0) return;
|
|
14004
14130
|
if (group.timer) clearTimeout(group.timer);
|
|
14131
|
+
if (group.recountTimer) clearTimeout(group.recountTimer);
|
|
14005
14132
|
this.refetchQueue.delete(group);
|
|
14006
14133
|
this.groups.delete(group.key);
|
|
14007
14134
|
const [index, address] = group.type === "collection" ? [this.collectionGroupsByPath, group.path] : [this.singleGroupsByAddress, `${group.path}\u0000${group.id}`];
|
|
@@ -14075,7 +14202,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14075
14202
|
this.refetchQueue.delete(group);
|
|
14076
14203
|
if (this.groups.get(group.key) !== group) continue;
|
|
14077
14204
|
this.refetchesRunning++;
|
|
14078
|
-
this.refetchGroup(group).finally(() => {
|
|
14205
|
+
this.runInScope(() => this.refetchGroup(group)).finally(() => {
|
|
14079
14206
|
this.refetchesRunning--;
|
|
14080
14207
|
this.drainRefetchQueue();
|
|
14081
14208
|
});
|
|
@@ -14849,6 +14976,17 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14849
14976
|
});
|
|
14850
14977
|
}
|
|
14851
14978
|
/**
|
|
14979
|
+
* Refetch `group` — and so count it again — at `at`, when the total its
|
|
14980
|
+
* last frame reused expires. One pending at a time per group.
|
|
14981
|
+
*/
|
|
14982
|
+
scheduleRecount(group, at) {
|
|
14983
|
+
if (group.recountTimer) return;
|
|
14984
|
+
group.recountTimer = setTimeout(() => {
|
|
14985
|
+
group.recountTimer = void 0;
|
|
14986
|
+
if (this.groups.get(group.key) === group) this.scheduleGroupRefetch(group);
|
|
14987
|
+
}, Math.max(0, at - Date.now()) + 1);
|
|
14988
|
+
}
|
|
14989
|
+
/**
|
|
14852
14990
|
* The `meta` a `collection_update` frame carries beside its rows, counted
|
|
14853
14991
|
* through `service` — the subscriber's own transaction, read only, with
|
|
14854
14992
|
* its auth context and `beforeQuery` narrowing already bound.
|
|
@@ -14877,6 +15015,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14877
15015
|
else if (reusable && this.recountIntervalMs !== void 0 && reusable.total >= this.recountMinRows && Date.now() - reusable.at < this.recountIntervalMs) {
|
|
14878
15016
|
total = reusable.total;
|
|
14879
15017
|
totalAsOf = new Date(reusable.at).toISOString();
|
|
15018
|
+
if (group) this.scheduleRecount(group, reusable.at + this.recountIntervalMs);
|
|
14880
15019
|
} else {
|
|
14881
15020
|
try {
|
|
14882
15021
|
total = await service.count(notifyPath, {
|
|
@@ -15455,7 +15594,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15455
15594
|
* what it was before this existed, whereas refusing to boot takes the whole
|
|
15456
15595
|
* backend down for it.
|
|
15457
15596
|
*/
|
|
15458
|
-
async configureChannelBus(bus) {
|
|
15597
|
+
async configureChannelBus(bus, options = {}) {
|
|
15459
15598
|
if (bus.kind === "memory") {
|
|
15460
15599
|
this.bus = bus;
|
|
15461
15600
|
return;
|
|
@@ -15471,7 +15610,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15471
15610
|
this.bus = bus;
|
|
15472
15611
|
try {
|
|
15473
15612
|
const store = new ChannelPresenceStore(this.db, this.instanceId);
|
|
15474
|
-
await store.ensureTables();
|
|
15613
|
+
if (!await store.ensureTables({ provision: options.provision }) && options.provision === false) throw new Error("rebase.channel_presence does not exist, and this backend changes nothing in its database. A provisioning run with the same channel bus configured creates it.");
|
|
15475
15614
|
this.presenceStore = store;
|
|
15476
15615
|
this.ensurePresenceSweep();
|
|
15477
15616
|
} catch (error) {
|
|
@@ -15802,6 +15941,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15802
15941
|
for (const group of this.groups.values()) {
|
|
15803
15942
|
if (group.timer) clearTimeout(group.timer);
|
|
15804
15943
|
group.timer = void 0;
|
|
15944
|
+
if (group.recountTimer) clearTimeout(group.recountTimer);
|
|
15945
|
+
group.recountTimer = void 0;
|
|
15805
15946
|
}
|
|
15806
15947
|
this.unscheduledListGroups.clear();
|
|
15807
15948
|
this.listPathChangedAt.clear();
|
|
@@ -15861,6 +16002,10 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15861
16002
|
isCdcActive() {
|
|
15862
16003
|
return this.cdcActive && this.cdcListener?.connected === true;
|
|
15863
16004
|
}
|
|
16005
|
+
/** See `RealtimeProvider.setScopeRunner`. */
|
|
16006
|
+
setScopeRunner(run) {
|
|
16007
|
+
this.runInScope = run;
|
|
16008
|
+
}
|
|
15864
16009
|
/** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
|
|
15865
16010
|
health() {
|
|
15866
16011
|
const listeners = [];
|
|
@@ -16395,7 +16540,7 @@ function columnNames(table) {
|
|
|
16395
16540
|
}
|
|
16396
16541
|
return names;
|
|
16397
16542
|
}
|
|
16398
|
-
var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
16543
|
+
var quote$1 = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
16399
16544
|
/** `on.from` / `on.to` accept a single column or a composite tuple. */
|
|
16400
16545
|
var asColumns = (value) => Array.isArray(value) ? value : [value];
|
|
16401
16546
|
/**
|
|
@@ -16417,7 +16562,7 @@ function compositeKeyDefect(relation, source, target, registry) {
|
|
|
16417
16562
|
const keys = getPrimaryKeys(keyed, registry);
|
|
16418
16563
|
if (keys.length < 2) return void 0;
|
|
16419
16564
|
return {
|
|
16420
|
-
problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
|
|
16565
|
+
problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote$1(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
|
|
16421
16566
|
fix
|
|
16422
16567
|
};
|
|
16423
16568
|
};
|
|
@@ -16546,7 +16691,7 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16546
16691
|
} : {
|
|
16547
16692
|
...at,
|
|
16548
16693
|
problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
|
|
16549
|
-
fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
|
|
16694
|
+
fix: `add the column, or set \`localKey\` to one of: ${quote$1(sourceColumns)}`
|
|
16550
16695
|
});
|
|
16551
16696
|
}
|
|
16552
16697
|
break;
|
|
@@ -16560,13 +16705,13 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16560
16705
|
} : {
|
|
16561
16706
|
...at,
|
|
16562
16707
|
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
16563
|
-
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
16708
|
+
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote$1(targetColumns)}`
|
|
16564
16709
|
});
|
|
16565
16710
|
}
|
|
16566
16711
|
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
16567
16712
|
...at,
|
|
16568
16713
|
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
16569
|
-
fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote(sourceColumns)}`
|
|
16714
|
+
fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote$1(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote$1(sourceColumns)}`
|
|
16570
16715
|
});
|
|
16571
16716
|
break;
|
|
16572
16717
|
case "manyToMany": {
|
|
@@ -16593,7 +16738,7 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16593
16738
|
} : {
|
|
16594
16739
|
...at,
|
|
16595
16740
|
problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
|
|
16596
|
-
fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
16741
|
+
fix: `set \`through.${label}\` to one of: ${quote$1(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
16597
16742
|
});
|
|
16598
16743
|
}
|
|
16599
16744
|
break;
|
|
@@ -16625,12 +16770,12 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16625
16770
|
for (const column of asColumns(step.on.from)) if (!prevColumns.has(column)) defects.push({
|
|
16626
16771
|
...at,
|
|
16627
16772
|
problem: `step ${i + 1} joins \`${prevName}.${column}\` → \`${step.table}\`, but \`${column}\` is not a column on \`${prevName}\``,
|
|
16628
|
-
fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote(prevColumns)}`
|
|
16773
|
+
fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote$1(prevColumns)}`
|
|
16629
16774
|
});
|
|
16630
16775
|
for (const column of asColumns(step.on.to)) if (!stepColumns.has(column)) defects.push({
|
|
16631
16776
|
...at,
|
|
16632
16777
|
problem: `step ${i + 1} joins into \`${step.table}.${column}\`, but \`${column}\` is not a column on \`${step.table}\``,
|
|
16633
|
-
fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote(stepColumns)}`
|
|
16778
|
+
fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote$1(stepColumns)}`
|
|
16634
16779
|
});
|
|
16635
16780
|
if (asColumns(step.on.from).length !== asColumns(step.on.to).length) defects.push({
|
|
16636
16781
|
...at,
|
|
@@ -17122,14 +17267,26 @@ var UserService = class {
|
|
|
17122
17267
|
return result.rows[0].roles ?? [];
|
|
17123
17268
|
}
|
|
17124
17269
|
/**
|
|
17125
|
-
* Set roles for a user (replaces existing roles)
|
|
17270
|
+
* Set roles for a user (replaces existing roles).
|
|
17271
|
+
*
|
|
17272
|
+
* Each role is its own bound parameter. The array used to be spelled as a
|
|
17273
|
+
* literal, `{${roles.join(",")}}`, so the database parsed what the caller
|
|
17274
|
+
* had only checked as strings: `"support,admin"` was stored as two roles,
|
|
17275
|
+
* one of them `admin`, and `"NULL"` as a SQL NULL. A name that would be read
|
|
17276
|
+
* back as other roles is refused (`roleNameProblem`) — RLS splits the
|
|
17277
|
+
* comma-joined `app.user_roles` the same way.
|
|
17126
17278
|
*/
|
|
17127
17279
|
async setUserRoles(uid, roleIds) {
|
|
17280
|
+
const problems = roleIds.flatMap((role) => {
|
|
17281
|
+
const problem = roleNameProblem(role);
|
|
17282
|
+
return problem ? [`${JSON.stringify(role)} ${problem}`] : [];
|
|
17283
|
+
});
|
|
17284
|
+
if (problems.length > 0) throw ApiError.badRequest(`Not valid role names: ${problems.join("; ")}`, "INVALID_ROLE");
|
|
17128
17285
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
17129
|
-
const rolesArray = `{${
|
|
17286
|
+
const rolesArray = roleIds.length === 0 ? sql`'{}'::text[]` : sql`ARRAY[${sql.join(roleIds.map((role) => sql`${role}`), sql`, `)}]::text[]`;
|
|
17130
17287
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
17131
17288
|
UPDATE ${sql.raw(usersTableName)}
|
|
17132
|
-
SET roles = ${rolesArray}
|
|
17289
|
+
SET roles = ${rolesArray}, updated_at = NOW()
|
|
17133
17290
|
WHERE id = ${uid}
|
|
17134
17291
|
`));
|
|
17135
17292
|
}
|
|
@@ -18151,6 +18308,59 @@ var MfaService = class {
|
|
|
18151
18308
|
}
|
|
18152
18309
|
};
|
|
18153
18310
|
//#endregion
|
|
18311
|
+
//#region src/auth/verify-auth-tables.ts
|
|
18312
|
+
/**
|
|
18313
|
+
* Whether the auth tables a backend reads are there, on a backend that may not
|
|
18314
|
+
* create them.
|
|
18315
|
+
*
|
|
18316
|
+
* A driver opened without provisioning skips `ensureAuthTablesExist` — the
|
|
18317
|
+
* tables are another process's to make — and before this nothing took its
|
|
18318
|
+
* place. A database that lacked `rebase.refresh_tokens`, or had a users table
|
|
18319
|
+
* from a release before `disabled_at`, booted green and then failed every
|
|
18320
|
+
* sign-in with a 500 whose cause was one missing relation. Auth cannot run on
|
|
18321
|
+
* part of its schema, so this is a refusal at boot, naming each table.
|
|
18322
|
+
*
|
|
18323
|
+
* Asked by reading, not by the catalogue: a `SELECT` of every column the auth
|
|
18324
|
+
* services name, `WHERE false`. That answers "missing", "out of date" and "this
|
|
18325
|
+
* role may not read it" in one statement per table, with the database's own
|
|
18326
|
+
* words for whichever it is.
|
|
18327
|
+
*/
|
|
18328
|
+
var quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
18329
|
+
/** What a Drizzle table definition says auth will query. */
|
|
18330
|
+
function expectationOf(table) {
|
|
18331
|
+
const config = getTableConfig(table);
|
|
18332
|
+
return {
|
|
18333
|
+
table: config.schema ? `${quote(config.schema)}.${quote(config.name)}` : quote(config.name),
|
|
18334
|
+
columns: Object.values(getTableColumns(table)).map((column) => column.name)
|
|
18335
|
+
};
|
|
18336
|
+
}
|
|
18337
|
+
/**
|
|
18338
|
+
* The users table, which is the project's own when it declares an auth
|
|
18339
|
+
* collection: only the columns the auth bootstrap guarantees on it are
|
|
18340
|
+
* expected, never the collection's other fields — those are the collection
|
|
18341
|
+
* schema's business, checked where collections are.
|
|
18342
|
+
*/
|
|
18343
|
+
function usersExpectation(qualifiedTable) {
|
|
18344
|
+
return {
|
|
18345
|
+
table: qualifiedTable,
|
|
18346
|
+
columns: ["id", ...AUTH_USERS_COLUMNS.map((spec) => spec.column)]
|
|
18347
|
+
};
|
|
18348
|
+
}
|
|
18349
|
+
/** Each table that cannot be read as expected, with the database's reason. */
|
|
18350
|
+
async function findUnusableAuthTables(db, expectations) {
|
|
18351
|
+
const problems = [];
|
|
18352
|
+
for (const { table, columns } of expectations) try {
|
|
18353
|
+
await db.execute(sql.raw(`SELECT ${columns.map(quote).join(", ")} FROM ${table} WHERE false`));
|
|
18354
|
+
} catch (err) {
|
|
18355
|
+
problems.push(`${table}: ${deepestErrorMessage(err)}`);
|
|
18356
|
+
}
|
|
18357
|
+
return problems;
|
|
18358
|
+
}
|
|
18359
|
+
/** The refusal, phrased as what to run. */
|
|
18360
|
+
function unusableAuthTablesError(problems) {
|
|
18361
|
+
return /* @__PURE__ */ new Error("Auth cannot run on this database, and this backend changes nothing in it (provisioning is off):\n" + problems.map((problem) => ` - ${problem}`).join("\n") + "\nCreate them with a provisioning run against this database (REBASE_PROVISION_ONLY=true, without REBASE_PROVISION=false), or apply the SQL such a run recorded (REBASE_PROVISION_SQL_OUT).");
|
|
18362
|
+
}
|
|
18363
|
+
//#endregion
|
|
18154
18364
|
//#region src/history/ensure-history-table.ts
|
|
18155
18365
|
/** How a history entry gets written from inside the write it records. */
|
|
18156
18366
|
var RECORD_HISTORY_FUNCTION = "rebase.record_history";
|
|
@@ -18554,6 +18764,28 @@ var HistoryService = class {
|
|
|
18554
18764
|
`)) ?? null;
|
|
18555
18765
|
}
|
|
18556
18766
|
/**
|
|
18767
|
+
* Every entry's `values` and `previous_values`, of every row, a page at a
|
|
18768
|
+
* time in id order — for a whole-store sweep. `rebase storage orphans`
|
|
18769
|
+
* reads it because a history revert restores the values an entry holds,
|
|
18770
|
+
* so a file only an entry names is still in use.
|
|
18771
|
+
*/
|
|
18772
|
+
async *scanStoredValues(pageSize = 1e3) {
|
|
18773
|
+
const limit = Math.max(1, pageSize);
|
|
18774
|
+
let after;
|
|
18775
|
+
for (;;) {
|
|
18776
|
+
const rows = sqlRows(await this.db.execute(sql`
|
|
18777
|
+
SELECT id, "values", previous_values
|
|
18778
|
+
FROM rebase.entity_history
|
|
18779
|
+
${after === void 0 ? sql`` : sql`WHERE id > ${after}`}
|
|
18780
|
+
ORDER BY id
|
|
18781
|
+
LIMIT ${limit}
|
|
18782
|
+
`));
|
|
18783
|
+
if (rows.length > 0) yield rows.map((row) => [row.values, row.previous_values]);
|
|
18784
|
+
if (rows.length < limit) return;
|
|
18785
|
+
after = rows[rows.length - 1].id;
|
|
18786
|
+
}
|
|
18787
|
+
}
|
|
18788
|
+
/**
|
|
18557
18789
|
* Each entry's `format`. A table without the column holds only entries
|
|
18558
18790
|
* that store the whole previous row, which is what `1` says.
|
|
18559
18791
|
*/
|
|
@@ -18758,991 +18990,6 @@ function patchPgNumericToNumber(tables) {
|
|
|
18758
18990
|
if (patchedCount > 0) logger.debug(`[PgNumeric] ${patchedCount} numeric column(s) will be served as numbers`);
|
|
18759
18991
|
}
|
|
18760
18992
|
//#endregion
|
|
18761
|
-
//#region src/schema/introspect-runtime.ts
|
|
18762
|
-
/**
|
|
18763
|
-
* Read the RLS posture of each table in a schema.
|
|
18764
|
-
*
|
|
18765
|
-
* This is what decides whether baas mode may serve a table. A table with RLS
|
|
18766
|
-
* disabled has no authorization model: since every authenticated request runs
|
|
18767
|
-
* as `rebase_user`, and that role is granted DML on the schema, serving such a
|
|
18768
|
-
* table hands every row to every logged-in user.
|
|
18769
|
-
*/
|
|
18770
|
-
async function readRlsStatus(client, pgSchema) {
|
|
18771
|
-
const { rows } = await client.query(`SELECT c.relname AS table,
|
|
18772
|
-
c.relrowsecurity AS rls_enabled,
|
|
18773
|
-
(SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
|
|
18774
|
-
FROM pg_class c
|
|
18775
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
18776
|
-
WHERE n.nspname = $1 AND c.relkind = 'r'`, [pgSchema]);
|
|
18777
|
-
return new Map(rows.map((r) => [r.table, {
|
|
18778
|
-
table: r.table,
|
|
18779
|
-
rlsEnabled: r.rls_enabled === true,
|
|
18780
|
-
policyCount: Number(r.policy_count ?? 0)
|
|
18781
|
-
}]));
|
|
18782
|
-
}
|
|
18783
|
-
/**
|
|
18784
|
-
* Read tables, columns, enums, primary keys and foreign keys for a schema.
|
|
18785
|
-
* Mirrors the queries in introspect-db.ts.
|
|
18786
|
-
*/
|
|
18787
|
-
async function introspectSchema(client, pgSchema) {
|
|
18788
|
-
const { rows: tables } = await client.query(`SELECT table_name
|
|
18789
|
-
FROM information_schema.tables
|
|
18790
|
-
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
18791
|
-
AND table_name NOT LIKE 'drizzle_%'
|
|
18792
|
-
AND table_name NOT LIKE 'rebase_%'
|
|
18793
|
-
ORDER BY table_name`, [pgSchema]);
|
|
18794
|
-
const { rows: columns } = await client.query(`SELECT
|
|
18795
|
-
c.table_name,
|
|
18796
|
-
c.column_name,
|
|
18797
|
-
c.data_type,
|
|
18798
|
-
c.udt_name,
|
|
18799
|
-
c.is_nullable,
|
|
18800
|
-
c.column_default,
|
|
18801
|
-
-- A stored generated column must be declared as one, or drizzle
|
|
18802
|
-
-- writes DEFAULT into it on every INSERT. Postgres tolerates that
|
|
18803
|
-
-- today; the generated module has always declared them properly,
|
|
18804
|
-
-- and the runtime tables are built to match it.
|
|
18805
|
-
c.is_generated,
|
|
18806
|
-
c.generation_expression,
|
|
18807
|
-
(SELECT a.atttypmod FROM pg_attribute a
|
|
18808
|
-
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
18809
|
-
WHERE pc.relname = c.table_name
|
|
18810
|
-
AND a.attname = c.column_name
|
|
18811
|
-
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
18812
|
-
FROM information_schema.columns c
|
|
18813
|
-
WHERE c.table_schema = $1`, [pgSchema]);
|
|
18814
|
-
const { rows: enumValues } = await client.query(`SELECT t.typname AS enum_name,
|
|
18815
|
-
e.enumlabel AS enum_value,
|
|
18816
|
-
e.enumsortorder AS sort_order
|
|
18817
|
-
FROM pg_type t
|
|
18818
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
18819
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
18820
|
-
WHERE n.nspname = $1
|
|
18821
|
-
ORDER BY t.typname, e.enumsortorder`, [pgSchema]);
|
|
18822
|
-
const { rows: pks } = await client.query(`SELECT t.relname as table_name, a.attname as column_name
|
|
18823
|
-
FROM pg_index i
|
|
18824
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
18825
|
-
AND a.attnum = ANY(i.indkey)
|
|
18826
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
18827
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
18828
|
-
WHERE i.indisprimary AND n.nspname = $1`, [pgSchema]);
|
|
18829
|
-
const { rows: fks } = await client.query(`SELECT
|
|
18830
|
-
tc.table_name,
|
|
18831
|
-
kcu.column_name,
|
|
18832
|
-
ccu.table_name AS foreign_table_name,
|
|
18833
|
-
ccu.column_name AS foreign_column_name
|
|
18834
|
-
FROM information_schema.table_constraints AS tc
|
|
18835
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
18836
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
18837
|
-
AND tc.table_schema = kcu.table_schema
|
|
18838
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
18839
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
18840
|
-
AND ccu.table_schema = tc.table_schema
|
|
18841
|
-
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`, [pgSchema]);
|
|
18842
|
-
const tablesMap = buildTablesMap(tables, columns, pks, fks);
|
|
18843
|
-
return {
|
|
18844
|
-
tablesMap,
|
|
18845
|
-
enumMap: buildEnumMap(enumValues),
|
|
18846
|
-
joinTables: identifyJoinTables(tablesMap)
|
|
18847
|
-
};
|
|
18848
|
-
}
|
|
18849
|
-
/** Derive the `isId` flavour for a primary-key column. */
|
|
18850
|
-
function idKindFor(col, propType) {
|
|
18851
|
-
if (col.data_type.toLowerCase() === "uuid") return "uuid";
|
|
18852
|
-
if (propType === "number") return "increment";
|
|
18853
|
-
return true;
|
|
18854
|
-
}
|
|
18855
|
-
function buildProperties(meta, enumMap) {
|
|
18856
|
-
const properties = {};
|
|
18857
|
-
const takenKeys = /* @__PURE__ */ new Set();
|
|
18858
|
-
for (const col of meta.columns) {
|
|
18859
|
-
const isPk = meta.pks.includes(col.column_name);
|
|
18860
|
-
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !isPk) continue;
|
|
18861
|
-
const enumValues = enumMap.get(col.udt_name);
|
|
18862
|
-
const isEnum = col.data_type === "USER-DEFINED" && enumValues !== void 0;
|
|
18863
|
-
const isVector = col.udt_name === "vector";
|
|
18864
|
-
const propType = isEnum ? "string" : isVector ? "vector" : mapPgType(col.data_type);
|
|
18865
|
-
const property = {
|
|
18866
|
-
name: humanize(col.column_name),
|
|
18867
|
-
columnName: col.column_name,
|
|
18868
|
-
type: propType
|
|
18869
|
-
};
|
|
18870
|
-
const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);
|
|
18871
|
-
takenKeys.add(key);
|
|
18872
|
-
if (isPk) property.isId = idKindFor(col, propType);
|
|
18873
|
-
else if (col.is_nullable === "NO" && col.column_default === null) property.validation = { required: true };
|
|
18874
|
-
if (isEnum && enumValues) property.enum = enumValues.map((value) => ({
|
|
18875
|
-
id: value,
|
|
18876
|
-
label: humanize(value)
|
|
18877
|
-
}));
|
|
18878
|
-
properties[key] = property;
|
|
18879
|
-
}
|
|
18880
|
-
return properties;
|
|
18881
|
-
}
|
|
18882
|
-
/**
|
|
18883
|
-
* Owning relations, derived from this table's foreign keys — the same shape
|
|
18884
|
-
* `generateCollectionFile` writes into a collection file: a `relation` property
|
|
18885
|
-
* whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
|
|
18886
|
-
*
|
|
18887
|
-
* The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
|
|
18888
|
-
* relations from `property.relation` and `resolveRelation` requires `target` to
|
|
18889
|
-
* be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
|
|
18890
|
-
* property with the slug as a bare string, which satisfies neither. Nothing
|
|
18891
|
-
* threw — the resolver simply skipped every such property and reported that the
|
|
18892
|
-
* collection had no relations. So an introspected BaaS collection had its FK
|
|
18893
|
-
* columns removed from `properties` (they "surface as relations") and then no
|
|
18894
|
-
* resolvable relation to surface as, which is why writing the FK column
|
|
18895
|
-
* directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
|
|
18896
|
-
* learns that column from the resolved relation's `localKey`.
|
|
18897
|
-
*
|
|
18898
|
-
* The thunk closes over the collections being built in this same pass rather
|
|
18899
|
-
* than importing a module, which is what a runtime introspection has instead of
|
|
18900
|
-
* generated files. It is called lazily, after the map is fully populated, so a
|
|
18901
|
-
* table may reference one introspected later.
|
|
18902
|
-
*/
|
|
18903
|
-
function buildRelations(meta, slugByTable, collectionBySlug) {
|
|
18904
|
-
const relations = {};
|
|
18905
|
-
for (const fk of meta.fks) {
|
|
18906
|
-
const targetSlug = slugByTable.get(fk.foreign_table_name);
|
|
18907
|
-
if (!targetSlug) continue;
|
|
18908
|
-
let key = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
18909
|
-
if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
|
|
18910
|
-
relations[key] = {
|
|
18911
|
-
name: humanize(key),
|
|
18912
|
-
type: "relation",
|
|
18913
|
-
relation: {
|
|
18914
|
-
kind: "belongsTo",
|
|
18915
|
-
target: () => collectionBySlug.get(targetSlug),
|
|
18916
|
-
localKey: fk.column_name
|
|
18917
|
-
}
|
|
18918
|
-
};
|
|
18919
|
-
}
|
|
18920
|
-
return relations;
|
|
18921
|
-
}
|
|
18922
|
-
/**
|
|
18923
|
-
* Turn an introspected schema into collections.
|
|
18924
|
-
*
|
|
18925
|
-
* Join tables are skipped: they carry no identity of their own and exist to
|
|
18926
|
-
* express a many-to-many edge between two other tables.
|
|
18927
|
-
*/
|
|
18928
|
-
function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }, pgSchema) {
|
|
18929
|
-
const slugByTable = /* @__PURE__ */ new Map();
|
|
18930
|
-
for (const tableName of tablesMap.keys()) if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
|
|
18931
|
-
const collections = [];
|
|
18932
|
-
const collectionBySlug = /* @__PURE__ */ new Map();
|
|
18933
|
-
for (const [tableName, meta] of tablesMap) {
|
|
18934
|
-
if (joinTables.has(tableName)) continue;
|
|
18935
|
-
const collectionName = humanize(tableName);
|
|
18936
|
-
const collection = {
|
|
18937
|
-
name: collectionName,
|
|
18938
|
-
singularName: singularize(collectionName),
|
|
18939
|
-
slug: tableName,
|
|
18940
|
-
table: tableName,
|
|
18941
|
-
schema: pgSchema,
|
|
18942
|
-
icon: getIconForTable(tableName),
|
|
18943
|
-
properties: {
|
|
18944
|
-
...buildProperties(meta, enumMap),
|
|
18945
|
-
...buildRelations(meta, slugByTable, collectionBySlug)
|
|
18946
|
-
}
|
|
18947
|
-
};
|
|
18948
|
-
collections.push(collection);
|
|
18949
|
-
collectionBySlug.set(tableName, collection);
|
|
18950
|
-
}
|
|
18951
|
-
return collections;
|
|
18952
|
-
}
|
|
18953
|
-
//#endregion
|
|
18954
|
-
//#region src/schema/dynamic-tables.ts
|
|
18955
|
-
/**
|
|
18956
|
-
* Build drizzle tables at runtime from an introspected schema.
|
|
18957
|
-
*
|
|
18958
|
-
* **Every** data source's tables are built here, from `information_schema`,
|
|
18959
|
-
* after boot-ensure has run — a project with declared collections included. The
|
|
18960
|
-
* committed `schema.generated.ts` is a build artefact for Atlas, `db push`,
|
|
18961
|
-
* `eject` and user code; the runtime does not read it for its tables, because a
|
|
18962
|
-
* file that is one commit behind the database used to change what the server
|
|
18963
|
-
* served (a renamed foreign-key column 500'd every request, a lost relation
|
|
18964
|
-
* silently emptied a list). See `catalogue-schema.ts` for the caller.
|
|
18965
|
-
*
|
|
18966
|
-
* These are handed to drizzle as its schema, which keeps the relational query
|
|
18967
|
-
* path (`db.query.*`) working; without them FetchService would fall back to
|
|
18968
|
-
* plain selects and lose relation loading.
|
|
18969
|
-
*
|
|
18970
|
-
* Two things a caller has to supply for the tables to match what the generated
|
|
18971
|
-
* module used to produce, both about *names*: the Drizzle object is keyed by the
|
|
18972
|
-
* wire name of a column and not by the column (`authorId`, not `author_id`), and
|
|
18973
|
-
* a relation is keyed by the collection's own relation key rather than by a
|
|
18974
|
-
* guess made from the foreign key. Both come from the collections — see
|
|
18975
|
-
* `columnKeysFromCollections` and `buildDrizzleRelationsFromCollections`.
|
|
18976
|
-
*/
|
|
18977
|
-
var columnNameAsKey = (_table, column) => column;
|
|
18978
|
-
/** drizzle ships no bytea builder; binary must round-trip as a Buffer. */
|
|
18979
|
-
var bytea = customType({ dataType: () => "bytea" });
|
|
18980
|
-
/**
|
|
18981
|
-
* A full-text search column, declared with its real SQL type.
|
|
18982
|
-
*
|
|
18983
|
-
* Not cosmetic: `isSearchIndexColumn` asks the column for its type and drops
|
|
18984
|
-
* every `tsvector` from the projection, so a search column that fell through to
|
|
18985
|
-
* `text` (the old default for an unrecognised type) would be selected, returned
|
|
18986
|
-
* to callers, and rendered in the admin as a wall of lexemes. drizzle ships no
|
|
18987
|
-
* builder for these two.
|
|
18988
|
-
*/
|
|
18989
|
-
var tsvector = customType({ dataType: () => "tsvector" });
|
|
18990
|
-
var tsquery = customType({ dataType: () => "tsquery" });
|
|
18991
|
-
/**
|
|
18992
|
-
* Postgres stores a column's type modifier (varchar length, vector dimensions)
|
|
18993
|
-
* in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);
|
|
18994
|
-
* for pgvector it is the dimension count as-is. -1 means unspecified.
|
|
18995
|
-
*/
|
|
18996
|
-
function varlenLength(col) {
|
|
18997
|
-
return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : void 0;
|
|
18998
|
-
}
|
|
18999
|
-
/**
|
|
19000
|
-
* Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the
|
|
19001
|
-
* concrete underlying type, which is exact where `data_type` reports umbrella
|
|
19002
|
-
* values like "ARRAY" or "USER-DEFINED".
|
|
19003
|
-
*
|
|
19004
|
-
* Unknown types fall back to `text`: the driver still reads and writes them,
|
|
19005
|
-
* with the value passing through as a string, which beats dropping the column.
|
|
19006
|
-
*/
|
|
19007
|
-
function scalarBuilder(udtName, name, col) {
|
|
19008
|
-
switch (udtName) {
|
|
19009
|
-
case "uuid": return uuid(name);
|
|
19010
|
-
case "bool": return boolean(name);
|
|
19011
|
-
case "int2": return smallint(name);
|
|
19012
|
-
case "int4": return integer(name);
|
|
19013
|
-
case "int8": return bigint(name, { mode: "number" });
|
|
19014
|
-
case "float4": return real(name);
|
|
19015
|
-
case "float8": return doublePrecision(name);
|
|
19016
|
-
case "numeric":
|
|
19017
|
-
case "money": return numeric(name);
|
|
19018
|
-
case "json": return json(name);
|
|
19019
|
-
case "jsonb": return jsonb(name);
|
|
19020
|
-
case "date": return date(name);
|
|
19021
|
-
case "time": return time(name);
|
|
19022
|
-
case "timetz": return time(name, { withTimezone: true });
|
|
19023
|
-
case "timestamp": return timestamp(name, { mode: "string" });
|
|
19024
|
-
case "timestamptz": return timestamp(name, {
|
|
19025
|
-
withTimezone: true,
|
|
19026
|
-
mode: "string"
|
|
19027
|
-
});
|
|
19028
|
-
case "interval": return interval(name);
|
|
19029
|
-
case "bytea": return bytea(name);
|
|
19030
|
-
case "inet": return inet(name);
|
|
19031
|
-
case "cidr": return cidr(name);
|
|
19032
|
-
case "macaddr": return macaddr(name);
|
|
19033
|
-
case "macaddr8": return macaddr8(name);
|
|
19034
|
-
case "point": return point(name);
|
|
19035
|
-
case "line": return line(name);
|
|
19036
|
-
case "geometry": return geometry(name);
|
|
19037
|
-
case "vector": {
|
|
19038
|
-
const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : void 0;
|
|
19039
|
-
return dimensions ? vector(name, { dimensions }) : text(name);
|
|
19040
|
-
}
|
|
19041
|
-
case "bpchar": {
|
|
19042
|
-
const length = varlenLength(col);
|
|
19043
|
-
return length ? char(name, { length }) : char(name);
|
|
19044
|
-
}
|
|
19045
|
-
case "varchar": {
|
|
19046
|
-
const length = varlenLength(col);
|
|
19047
|
-
return length ? varchar(name, { length }) : text(name);
|
|
19048
|
-
}
|
|
19049
|
-
case "tsvector": return tsvector(name);
|
|
19050
|
-
case "tsquery": return tsquery(name);
|
|
19051
|
-
default: return text(name);
|
|
19052
|
-
}
|
|
19053
|
-
}
|
|
19054
|
-
/** See {@link FluentColumnBuilder}. */
|
|
19055
|
-
var fluent = (builder) => builder;
|
|
19056
|
-
function columnBuilderFor(col) {
|
|
19057
|
-
if (col.udt_name.startsWith("_")) return fluent(scalarBuilder(col.udt_name.slice(1), col.column_name, col)).array();
|
|
19058
|
-
return scalarBuilder(col.udt_name, col.column_name, col);
|
|
19059
|
-
}
|
|
19060
|
-
/**
|
|
19061
|
-
* Build one drizzle table per introspected table, keyed by table name.
|
|
19062
|
-
*
|
|
19063
|
-
* @param columnKey how a column is named on the wire — see {@link ColumnKeyResolver}.
|
|
19064
|
-
*/
|
|
19065
|
-
function buildDrizzleTablesFromSchema(tablesMap, pgSchemaName = "public", columnKey = columnNameAsKey) {
|
|
19066
|
-
const schema = pgSchemaName === "public" ? null : pgSchema(pgSchemaName);
|
|
19067
|
-
const createTable = schema ? schema.table.bind(schema) : pgTable;
|
|
19068
|
-
const tables = {};
|
|
19069
|
-
for (const [tableName, meta] of tablesMap) {
|
|
19070
|
-
const columns = {};
|
|
19071
|
-
const keyOf = /* @__PURE__ */ new Map();
|
|
19072
|
-
for (const col of meta.columns) {
|
|
19073
|
-
let builder = columnBuilderFor(col);
|
|
19074
|
-
if (col.is_nullable === "NO") builder = fluent(builder).notNull();
|
|
19075
|
-
if (meta.pks.length === 1 && meta.pks[0] === col.column_name) builder = fluent(builder).primaryKey();
|
|
19076
|
-
if (col.is_generated === "ALWAYS") builder = fluent(builder).generatedAlwaysAs(sql.raw(col.generation_expression ?? ""));
|
|
19077
|
-
const key = columnKey(tableName, col.column_name);
|
|
19078
|
-
keyOf.set(col.column_name, key);
|
|
19079
|
-
columns[key] = builder;
|
|
19080
|
-
}
|
|
19081
|
-
tables[tableName] = createTable(tableName, columns, meta.pks.length > 1 ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[keyOf.get(pk) ?? pk]) })] : void 0);
|
|
19082
|
-
}
|
|
19083
|
-
return tables;
|
|
19084
|
-
}
|
|
19085
|
-
/**
|
|
19086
|
-
* Build drizzle `relations()` for the foreign keys, so the relational query
|
|
19087
|
-
* path can actually load them.
|
|
19088
|
-
*
|
|
19089
|
-
* FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation
|
|
19090
|
-
* property on the collection. Tables alone don't satisfy that — without these,
|
|
19091
|
-
* `?include=author` silently returns the raw `author_id` and no author.
|
|
19092
|
-
*
|
|
19093
|
-
* The keys here must match `buildRelations` in introspect-runtime.ts, which is
|
|
19094
|
-
* what names the collection's relation properties.
|
|
19095
|
-
*/
|
|
19096
|
-
function buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey = columnNameAsKey) {
|
|
19097
|
-
/** Owning side, per table: the `one()` relations from its foreign keys. */
|
|
19098
|
-
const owning = /* @__PURE__ */ new Map();
|
|
19099
|
-
/** Inverse side, per referenced table: the matching `many()` back-references. */
|
|
19100
|
-
const inverse = /* @__PURE__ */ new Map();
|
|
19101
|
-
for (const [tableName, meta] of tablesMap) {
|
|
19102
|
-
if (!tables[tableName]) continue;
|
|
19103
|
-
const columnNames = new Set(meta.columns.map((c) => columnKey(tableName, c.column_name)));
|
|
19104
|
-
for (const fk of meta.fks) {
|
|
19105
|
-
if (!tables[fk.foreign_table_name]) continue;
|
|
19106
|
-
let key = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
19107
|
-
if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
|
|
19108
|
-
if (columnNames.has(key)) continue;
|
|
19109
|
-
const relationName = `${tableName}_${fk.column_name}`;
|
|
19110
|
-
owning.set(tableName, [...owning.get(tableName) ?? [], {
|
|
19111
|
-
key,
|
|
19112
|
-
targetTable: fk.foreign_table_name,
|
|
19113
|
-
fkColumn: columnKey(tableName, fk.column_name),
|
|
19114
|
-
targetColumn: columnKey(fk.foreign_table_name, fk.foreign_column_name),
|
|
19115
|
-
relationName
|
|
19116
|
-
}]);
|
|
19117
|
-
const backKey = tableName;
|
|
19118
|
-
if (new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => columnKey(fk.foreign_table_name, c.column_name))).has(backKey)) continue;
|
|
19119
|
-
inverse.set(fk.foreign_table_name, [...inverse.get(fk.foreign_table_name) ?? [], {
|
|
19120
|
-
key: backKey,
|
|
19121
|
-
sourceTable: tableName,
|
|
19122
|
-
relationName
|
|
19123
|
-
}]);
|
|
19124
|
-
}
|
|
19125
|
-
}
|
|
19126
|
-
const built = {};
|
|
19127
|
-
for (const tableName of tablesMap.keys()) {
|
|
19128
|
-
const table = tables[tableName];
|
|
19129
|
-
const ones = owning.get(tableName) ?? [];
|
|
19130
|
-
const manys = inverse.get(tableName) ?? [];
|
|
19131
|
-
if (!table || ones.length === 0 && manys.length === 0) continue;
|
|
19132
|
-
built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
|
|
19133
|
-
const map = {};
|
|
19134
|
-
for (const rel of ones) map[rel.key] = one(tables[rel.targetTable], {
|
|
19135
|
-
fields: [getTableColumns(table)[rel.fkColumn]],
|
|
19136
|
-
references: [getTableColumns(tables[rel.targetTable])[rel.targetColumn]],
|
|
19137
|
-
relationName: rel.relationName
|
|
19138
|
-
});
|
|
19139
|
-
for (const rel of manys) {
|
|
19140
|
-
if (map[rel.key]) continue;
|
|
19141
|
-
map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });
|
|
19142
|
-
}
|
|
19143
|
-
return map;
|
|
19144
|
-
});
|
|
19145
|
-
}
|
|
19146
|
-
return built;
|
|
19147
|
-
}
|
|
19148
|
-
//#endregion
|
|
19149
|
-
//#region src/schema/config-relations.ts
|
|
19150
|
-
/**
|
|
19151
|
-
* Build drizzle `relations()` for declared collections, from the collections.
|
|
19152
|
-
*
|
|
19153
|
-
* The heuristic builder in `dynamic-tables.ts` names a relation after the
|
|
19154
|
-
* foreign-key column (`author_id` → `author`), which is the only evidence a
|
|
19155
|
-
* database offers and is right for BaaS mode. It is wrong for a project that
|
|
19156
|
-
* declares its collections: there the relation's name is whatever the author
|
|
19157
|
-
* called it, `include=writer` addresses that name, and a relation whose key was
|
|
19158
|
-
* guessed from the column simply does not answer to it — the read returns the
|
|
19159
|
-
* bare foreign key and no target, silently, which is how a demo lost every
|
|
19160
|
-
* relation in its list view while the detail view (a different code path) kept
|
|
19161
|
-
* working.
|
|
19162
|
-
*
|
|
19163
|
-
* So when there are collections, they are the authority for *which* relations
|
|
19164
|
-
* exist and what they are called; the live catalogue is the authority for the
|
|
19165
|
-
* columns underneath. This is the same pairing `generate-drizzle-schema-logic`
|
|
19166
|
-
* emits into `schema.generated.ts` — kind for kind, key for key, and paired by
|
|
19167
|
-
* the same {@link sharedRelationName} — only built against tables read back from
|
|
19168
|
-
* `information_schema` rather than against a file.
|
|
19169
|
-
*/
|
|
19170
|
-
/** A collection's table, with any schema prefix stripped — as the DDL creates it. */
|
|
19171
|
-
function bareTableName(name) {
|
|
19172
|
-
return name.includes(".") ? name.split(".").pop() : name;
|
|
19173
|
-
}
|
|
19174
|
-
/**
|
|
19175
|
-
* The field key a collection's rows are addressed by, as the generated module
|
|
19176
|
-
* spells it: the property marked `isId`, else `id`.
|
|
19177
|
-
*
|
|
19178
|
-
* Deliberately the config's answer rather than the catalogue's primary key —
|
|
19179
|
-
* `references: [users.id]` in the generated file is keyed the same way, and the
|
|
19180
|
-
* two have to name the same Drizzle key for a relation to compile at all.
|
|
19181
|
-
*/
|
|
19182
|
-
function primaryKeyFieldKey(collection) {
|
|
19183
|
-
for (const [key, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId)) return key;
|
|
19184
|
-
return "id";
|
|
19185
|
-
}
|
|
19186
|
-
/**
|
|
19187
|
-
* The field key on the source table that a `hasOne`/`hasMany` foreign key
|
|
19188
|
-
* holds: its `sourceKey` when the link names one, else the primary key.
|
|
19189
|
-
*/
|
|
19190
|
-
function sourceKeyFieldKey(relation, collection) {
|
|
19191
|
-
return relation.sourceKey ? fieldKeyForColumn(collection, relation.sourceKey) : primaryKeyFieldKey(collection);
|
|
19192
|
-
}
|
|
19193
|
-
/**
|
|
19194
|
-
* A column of a built table, by its Drizzle key or by its column name.
|
|
19195
|
-
*
|
|
19196
|
-
* The key is the answer in the ordinary case, and the name is the fallback for
|
|
19197
|
-
* the one place a caller holds a column and not a field: a junction table's two
|
|
19198
|
-
* foreign keys, which are keyed by column *unless* the project also declares a
|
|
19199
|
-
* collection over that table, in which case they are keyed by its property
|
|
19200
|
-
* names. Looking both up costs a scan of one table and removes the difference.
|
|
19201
|
-
*/
|
|
19202
|
-
function columnOf(table, key) {
|
|
19203
|
-
if (!table) return void 0;
|
|
19204
|
-
const direct = getTableColumns(table)[key];
|
|
19205
|
-
if (direct) return direct;
|
|
19206
|
-
for (const column of Object.values(getTableColumns(table))) if (column?.name === key) return column;
|
|
19207
|
-
}
|
|
19208
|
-
/**
|
|
19209
|
-
* Drizzle relations for every declared collection, keyed `<table>Relations`.
|
|
19210
|
-
*
|
|
19211
|
-
* @param collections every collection backed by a table in `tables`
|
|
19212
|
-
* @param tables catalogue-built tables, keyed by bare table name
|
|
19213
|
-
*/
|
|
19214
|
-
function buildDrizzleRelationsFromCollections(collections, tables) {
|
|
19215
|
-
const ones = /* @__PURE__ */ new Map();
|
|
19216
|
-
const manys = /* @__PURE__ */ new Map();
|
|
19217
|
-
/** `<relationName>::<kind>` already emitted on a table — the generator dedupes the same way. */
|
|
19218
|
-
const emitted = /* @__PURE__ */ new Map();
|
|
19219
|
-
const claim = (table, name, kind) => {
|
|
19220
|
-
const seen = emitted.get(table) ?? /* @__PURE__ */ new Set();
|
|
19221
|
-
emitted.set(table, seen);
|
|
19222
|
-
const key = `${name}::${kind}`;
|
|
19223
|
-
if (seen.has(key)) return false;
|
|
19224
|
-
seen.add(key);
|
|
19225
|
-
return true;
|
|
19226
|
-
};
|
|
19227
|
-
const addOne = (table, spec) => ones.set(table, [...ones.get(table) ?? [], spec]);
|
|
19228
|
-
const addMany = (table, spec) => manys.set(table, [...manys.get(table) ?? [], spec]);
|
|
19229
|
-
const byTable = /* @__PURE__ */ new Map();
|
|
19230
|
-
for (const collection of collections) {
|
|
19231
|
-
const table = bareTableName(getTableName$1(collection));
|
|
19232
|
-
if (table && tables[table]) byTable.set(table, collection);
|
|
19233
|
-
}
|
|
19234
|
-
for (const [tableName, collection] of byTable) {
|
|
19235
|
-
const table = tables[tableName];
|
|
19236
|
-
for (const [relationKey, relation] of Object.entries(resolveCollectionRelations(collection))) {
|
|
19237
|
-
let target;
|
|
19238
|
-
try {
|
|
19239
|
-
target = relation.target();
|
|
19240
|
-
} catch (err) {
|
|
19241
|
-
logger.debug(`[schema] Relation ${collection.slug}.${relationKey} has no resolvable target`, { error: err });
|
|
19242
|
-
continue;
|
|
19243
|
-
}
|
|
19244
|
-
const targetTable = tables[bareTableName(getTableName$1(target))];
|
|
19245
|
-
const name = sharedRelationName(relation, collection);
|
|
19246
|
-
switch (relation.kind) {
|
|
19247
|
-
case "belongsTo": {
|
|
19248
|
-
if (!targetTable) continue;
|
|
19249
|
-
if (!claim(tableName, name, relation.kind)) continue;
|
|
19250
|
-
const localKey = fieldKeyForColumn(collection, relation.localKey);
|
|
19251
|
-
const referenced = primaryKeyFieldKey(target);
|
|
19252
|
-
const field = columnOf(table, localKey);
|
|
19253
|
-
const reference = columnOf(targetTable, referenced);
|
|
19254
|
-
if (!field || !reference) continue;
|
|
19255
|
-
addOne(tableName, {
|
|
19256
|
-
key: relationKey,
|
|
19257
|
-
target: targetTable,
|
|
19258
|
-
fields: [field],
|
|
19259
|
-
references: [reference],
|
|
19260
|
-
relationName: name
|
|
19261
|
-
});
|
|
19262
|
-
break;
|
|
19263
|
-
}
|
|
19264
|
-
case "hasOne": {
|
|
19265
|
-
if (!targetTable) continue;
|
|
19266
|
-
if (!claim(tableName, name, relation.kind)) continue;
|
|
19267
|
-
const field = columnOf(table, sourceKeyFieldKey(relation, collection));
|
|
19268
|
-
const reference = columnOf(targetTable, fieldKeyForColumn(target, relation.foreignKeyOnTarget));
|
|
19269
|
-
if (!field || !reference) continue;
|
|
19270
|
-
addOne(tableName, {
|
|
19271
|
-
key: relationKey,
|
|
19272
|
-
target: targetTable,
|
|
19273
|
-
fields: [field],
|
|
19274
|
-
references: [reference],
|
|
19275
|
-
relationName: name
|
|
19276
|
-
});
|
|
19277
|
-
break;
|
|
19278
|
-
}
|
|
19279
|
-
case "hasMany":
|
|
19280
|
-
if (!targetTable) continue;
|
|
19281
|
-
if (!claim(tableName, name, relation.kind)) continue;
|
|
19282
|
-
addMany(tableName, {
|
|
19283
|
-
key: relationKey,
|
|
19284
|
-
target: targetTable,
|
|
19285
|
-
relationName: name
|
|
19286
|
-
});
|
|
19287
|
-
break;
|
|
19288
|
-
case "manyToMany": {
|
|
19289
|
-
const junctionName = bareTableName(relation.through.table);
|
|
19290
|
-
const junction = tables[junctionName];
|
|
19291
|
-
if (!junction) continue;
|
|
19292
|
-
if (!claim(tableName, name, relation.kind)) continue;
|
|
19293
|
-
addMany(tableName, {
|
|
19294
|
-
key: relationKey,
|
|
19295
|
-
target: junction,
|
|
19296
|
-
relationName: name
|
|
19297
|
-
});
|
|
19298
|
-
addJunctionSides(relation, collection, target, junctionName, tables, addOne, claim);
|
|
19299
|
-
break;
|
|
19300
|
-
}
|
|
19301
|
-
case "via": break;
|
|
19302
|
-
}
|
|
19303
|
-
}
|
|
19304
|
-
}
|
|
19305
|
-
for (const [tableName, collection] of byTable) {
|
|
19306
|
-
const table = tables[tableName];
|
|
19307
|
-
for (const [, relation] of Object.entries(resolveCollectionRelations(collection))) {
|
|
19308
|
-
if (relation.kind !== "hasMany" && relation.kind !== "hasOne") continue;
|
|
19309
|
-
let target;
|
|
19310
|
-
try {
|
|
19311
|
-
target = relation.target();
|
|
19312
|
-
} catch {
|
|
19313
|
-
continue;
|
|
19314
|
-
}
|
|
19315
|
-
const targetTableName = bareTableName(getTableName$1(target));
|
|
19316
|
-
const targetTable = tables[targetTableName];
|
|
19317
|
-
if (!targetTable) continue;
|
|
19318
|
-
const name = sharedRelationName(relation, collection);
|
|
19319
|
-
if (!claim(targetTableName, name, "belongsTo")) continue;
|
|
19320
|
-
const fkKey = fieldKeyForColumn(target, relation.foreignKeyOnTarget);
|
|
19321
|
-
const field = columnOf(targetTable, fkKey);
|
|
19322
|
-
const reference = columnOf(table, sourceKeyFieldKey(relation, collection));
|
|
19323
|
-
if (!field || !reference) continue;
|
|
19324
|
-
addOne(targetTableName, {
|
|
19325
|
-
key: `_synth_${targetTableName}_${fkKey}`,
|
|
19326
|
-
target: tables[tableName],
|
|
19327
|
-
fields: [field],
|
|
19328
|
-
references: [reference],
|
|
19329
|
-
relationName: name
|
|
19330
|
-
});
|
|
19331
|
-
}
|
|
19332
|
-
}
|
|
19333
|
-
const built = {};
|
|
19334
|
-
for (const tableName of /* @__PURE__ */ new Set([...ones.keys(), ...manys.keys()])) {
|
|
19335
|
-
const table = tables[tableName];
|
|
19336
|
-
if (!table) continue;
|
|
19337
|
-
const tableOnes = ones.get(tableName) ?? [];
|
|
19338
|
-
const tableManys = manys.get(tableName) ?? [];
|
|
19339
|
-
built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
|
|
19340
|
-
const map = {};
|
|
19341
|
-
for (const spec of tableOnes) map[spec.key] = one(spec.target, {
|
|
19342
|
-
fields: spec.fields,
|
|
19343
|
-
references: spec.references,
|
|
19344
|
-
relationName: spec.relationName
|
|
19345
|
-
});
|
|
19346
|
-
for (const spec of tableManys) {
|
|
19347
|
-
if (map[spec.key]) continue;
|
|
19348
|
-
map[spec.key] = many(spec.target, { relationName: spec.relationName });
|
|
19349
|
-
}
|
|
19350
|
-
return map;
|
|
19351
|
-
});
|
|
19352
|
-
}
|
|
19353
|
-
return built;
|
|
19354
|
-
}
|
|
19355
|
-
/**
|
|
19356
|
-
* The junction table's own two `one()` sides.
|
|
19357
|
-
*
|
|
19358
|
-
* A many-to-many is a `many(junction)` on each endpoint, so the junction has to
|
|
19359
|
-
* carry the matching `one()` back to each — without them drizzle can join a
|
|
19360
|
-
* post to its `posts_tags` rows and no further, and the tags come back as
|
|
19361
|
-
* junction rows. Keyed by the junction's own columns, which is how the
|
|
19362
|
-
* generated file keys them too.
|
|
19363
|
-
*/
|
|
19364
|
-
function addJunctionSides(relation, sourceCollection, targetCollection, junctionName, tables, addOne, claim) {
|
|
19365
|
-
const junction = tables[junctionName];
|
|
19366
|
-
const sourceTable = tables[bareTableName(getTableName$1(sourceCollection))];
|
|
19367
|
-
const targetTable = tables[bareTableName(getTableName$1(targetCollection))];
|
|
19368
|
-
if (!junction || !sourceTable || !targetTable) return;
|
|
19369
|
-
const sourceName = sharedRelationName(relation, sourceCollection);
|
|
19370
|
-
const inverse = Object.values(resolveCollectionRelations(targetCollection)).find((r) => r.kind === "manyToMany" && bareTableName(r.through.table) === junctionName);
|
|
19371
|
-
const targetName = inverse ? sharedRelationName(inverse, targetCollection) : `${junctionName}_${relation.through.targetColumn}`;
|
|
19372
|
-
const pairs = [[
|
|
19373
|
-
relation.through.sourceColumn,
|
|
19374
|
-
sourceTable,
|
|
19375
|
-
sourceName,
|
|
19376
|
-
sourceCollection,
|
|
19377
|
-
"source"
|
|
19378
|
-
], [
|
|
19379
|
-
relation.through.targetColumn,
|
|
19380
|
-
targetTable,
|
|
19381
|
-
targetName,
|
|
19382
|
-
targetCollection,
|
|
19383
|
-
"target"
|
|
19384
|
-
]];
|
|
19385
|
-
for (const [column, endpointTable, relationName, endpointCollection] of pairs) {
|
|
19386
|
-
if (!claim(junctionName, relationName, "junction")) continue;
|
|
19387
|
-
const field = columnOf(junction, column);
|
|
19388
|
-
const reference = columnOf(endpointTable, primaryKeyFieldKey(endpointCollection));
|
|
19389
|
-
if (!field || !reference) continue;
|
|
19390
|
-
addOne(junctionName, {
|
|
19391
|
-
key: column,
|
|
19392
|
-
target: endpointTable,
|
|
19393
|
-
fields: [field],
|
|
19394
|
-
references: [reference],
|
|
19395
|
-
relationName
|
|
19396
|
-
});
|
|
19397
|
-
}
|
|
19398
|
-
}
|
|
19399
|
-
//#endregion
|
|
19400
|
-
//#region src/schema/catalogue-schema.ts
|
|
19401
|
-
/** The Postgres schema a collection's table lives in. */
|
|
19402
|
-
function schemaOfCollection(collection, fallback) {
|
|
19403
|
-
const declared = collection.schema;
|
|
19404
|
-
return typeof declared === "string" && declared.length > 0 ? declared : fallback;
|
|
19405
|
-
}
|
|
19406
|
-
/**
|
|
19407
|
-
* How a column of each table is keyed in the Drizzle object.
|
|
19408
|
-
*
|
|
19409
|
-
* The wire name, which is the property key — `authorId`, never `author_id` —
|
|
19410
|
-
* because that is what every read and write path in this driver indexes tables
|
|
19411
|
-
* and rows by, and what the generated module has always emitted. Getting this
|
|
19412
|
-
* wrong does not throw: drizzle leaves an unknown key out of the statement and
|
|
19413
|
-
* the write reports success having stored nothing.
|
|
19414
|
-
*
|
|
19415
|
-
* Two exceptions, both columns no property declares:
|
|
19416
|
-
*
|
|
19417
|
-
* - the generated search columns keep their raw names, because `search_vector`
|
|
19418
|
-
* is how `searchColumnNames` excludes them from responses and how the
|
|
19419
|
-
* condition builder finds the one to match against;
|
|
19420
|
-
* - a table no collection claims (a junction) is keyed by its **key** columns'
|
|
19421
|
-
* names, which is also what the generated file does for them — but a
|
|
19422
|
-
* junction that declares `through.properties` keys those payload columns by
|
|
19423
|
-
* their property key, for the same reason a collection does. `joinedAt` with
|
|
19424
|
-
* `columnName: "joined_at"` has to be one name on both sides or a `_pivot`
|
|
19425
|
-
* write is silently dropped by drizzle and the read serves a key the caller
|
|
19426
|
-
* never declared.
|
|
19427
|
-
*/
|
|
19428
|
-
function columnKeysFromCollections(collections) {
|
|
19429
|
-
const byTable = /* @__PURE__ */ new Map();
|
|
19430
|
-
const rawColumns = /* @__PURE__ */ new Map();
|
|
19431
|
-
for (const collection of collections) {
|
|
19432
|
-
const table = bareTableName(getTableName$1(collection));
|
|
19433
|
-
if (!table) continue;
|
|
19434
|
-
byTable.set(table, collection);
|
|
19435
|
-
rawColumns.set(table, new Set(searchColumnNames(collection)));
|
|
19436
|
-
}
|
|
19437
|
-
const junctionByTable = /* @__PURE__ */ new Map();
|
|
19438
|
-
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
19439
|
-
if (Object.keys(spec.properties).length === 0) continue;
|
|
19440
|
-
junctionByTable.set(bareTableName(spec.table), getJunctionCollectionConfig(spec));
|
|
19441
|
-
}
|
|
19442
|
-
return (tableName, columnName) => {
|
|
19443
|
-
const collection = byTable.get(tableName);
|
|
19444
|
-
if (!collection) {
|
|
19445
|
-
const junction = junctionByTable.get(tableName);
|
|
19446
|
-
return junction ? fieldKeyForColumn(junction, columnName) : columnName;
|
|
19447
|
-
}
|
|
19448
|
-
if (rawColumns.get(tableName)?.has(columnName)) return columnName;
|
|
19449
|
-
return fieldKeyForColumn(collection, columnName);
|
|
19450
|
-
};
|
|
19451
|
-
}
|
|
19452
|
-
/**
|
|
19453
|
-
* Read the tables this data source serves out of `information_schema`.
|
|
19454
|
-
*
|
|
19455
|
-
* Restricted to the collections handed in, because the driver is given every
|
|
19456
|
-
* collection the project declares while each database holds only its own: an
|
|
19457
|
-
* unfiltered read had a second source (`database("analytics")`) reporting the
|
|
19458
|
-
* default's tables as its own. Junction tables come too — they back no
|
|
19459
|
-
* collection and a many-to-many is unreadable without them.
|
|
19460
|
-
*
|
|
19461
|
-
* Every schema the collections name is read, not just `public`. The users
|
|
19462
|
-
* collection lives in `rebase`, and a run restricted to `public` would leave the
|
|
19463
|
-
* registry with no table for it at all.
|
|
19464
|
-
*/
|
|
19465
|
-
async function readCatalogueSchema(options) {
|
|
19466
|
-
const { client, collections, defaultSchema } = options;
|
|
19467
|
-
/** schema → the tables wanted from it. */
|
|
19468
|
-
const wanted = /* @__PURE__ */ new Map();
|
|
19469
|
-
const want = (schema, table) => {
|
|
19470
|
-
const set = wanted.get(schema) ?? /* @__PURE__ */ new Set();
|
|
19471
|
-
wanted.set(schema, set);
|
|
19472
|
-
set.add(table);
|
|
19473
|
-
};
|
|
19474
|
-
for (const collection of collections) {
|
|
19475
|
-
const table = bareTableName(getTableName$1(collection));
|
|
19476
|
-
if (table) want(schemaOfCollection(collection, defaultSchema), table);
|
|
19477
|
-
}
|
|
19478
|
-
for (const spec of resolveJunctionSpecs(collections).values()) want(spec.schema, bareTableName(spec.table));
|
|
19479
|
-
const columnKey = columnKeysFromCollections(collections);
|
|
19480
|
-
const tables = {};
|
|
19481
|
-
const meta = /* @__PURE__ */ new Map();
|
|
19482
|
-
const missing = [];
|
|
19483
|
-
for (const [schema, names] of wanted) {
|
|
19484
|
-
const live = await introspectSchema(client, schema);
|
|
19485
|
-
const present = /* @__PURE__ */ new Map();
|
|
19486
|
-
for (const name of names) {
|
|
19487
|
-
const found = live.tablesMap.get(name);
|
|
19488
|
-
if (!found) {
|
|
19489
|
-
missing.push(schema === defaultSchema ? name : `${schema}.${name}`);
|
|
19490
|
-
continue;
|
|
19491
|
-
}
|
|
19492
|
-
if (tables[name]) continue;
|
|
19493
|
-
present.set(name, found);
|
|
19494
|
-
meta.set(name, found);
|
|
19495
|
-
}
|
|
19496
|
-
Object.assign(tables, buildDrizzleTablesFromSchema(present, schema, columnKey));
|
|
19497
|
-
}
|
|
19498
|
-
return {
|
|
19499
|
-
tables,
|
|
19500
|
-
relations: buildDrizzleRelationsFromCollections(collections, tables),
|
|
19501
|
-
meta,
|
|
19502
|
-
missing
|
|
19503
|
-
};
|
|
19504
|
-
}
|
|
19505
|
-
/**
|
|
19506
|
-
* The BaaS variant: no collections are declared, so every table in one schema is
|
|
19507
|
-
* served and both the collections and the relations are derived from the
|
|
19508
|
-
* foreign keys.
|
|
19509
|
-
*
|
|
19510
|
-
* Kept apart from {@link readCatalogueSchema} because the two answer different
|
|
19511
|
-
* questions. Here the database is the entire specification; there it supplies
|
|
19512
|
-
* the columns for a specification the project already wrote down.
|
|
19513
|
-
*/
|
|
19514
|
-
function buildBaasSchema(tablesMap, pgSchemaName, collections) {
|
|
19515
|
-
const columnKey = columnKeysFromCollections(collections);
|
|
19516
|
-
const tables = buildDrizzleTablesFromSchema(tablesMap, pgSchemaName, columnKey);
|
|
19517
|
-
return {
|
|
19518
|
-
tables,
|
|
19519
|
-
relations: buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey)
|
|
19520
|
-
};
|
|
19521
|
-
}
|
|
19522
|
-
//#endregion
|
|
19523
|
-
//#region src/schema/generated-schema-diff.ts
|
|
19524
|
-
/**
|
|
19525
|
-
* Compare the committed `schema.generated.ts` against the live catalogue, and
|
|
19526
|
-
* say so — once per difference, at boot, as a warning and nothing more.
|
|
19527
|
-
*
|
|
19528
|
-
* The runtime stopped reading that file for its tables (see
|
|
19529
|
-
* `catalogue-schema.ts`), which removes an entire class of production failure
|
|
19530
|
-
* and creates a smaller one: a stale file is now *invisible*. It is still real —
|
|
19531
|
-
* it is what Atlas plans migrations from, what `db push` diffs, what `eject`
|
|
19532
|
-
* writes out and what user code imports — so a project whose file no longer
|
|
19533
|
-
* describes its database will hit it later, in a tool, with no clue that boot
|
|
19534
|
-
* already knew.
|
|
19535
|
-
*
|
|
19536
|
-
* Hence this: a guard rather than a doc. It never fails boot and never changes
|
|
19537
|
-
* what is served — the previous arrangement, where the file could do both,
|
|
19538
|
-
* is exactly what was removed.
|
|
19539
|
-
*
|
|
19540
|
-
* `generated-schema-staleness.ts` answers a narrower question (does the file
|
|
19541
|
-
* name every table the collections do) from the file's *source text*. This one
|
|
19542
|
-
* compares the loaded module against the database, column by column.
|
|
19543
|
-
*/
|
|
19544
|
-
/**
|
|
19545
|
-
* The families a boot warning is worth raising over.
|
|
19546
|
-
*
|
|
19547
|
-
* Deliberately coarse. `varchar(120)` and `text` are the same family because the
|
|
19548
|
-
* runtime treats them identically and a warning about the difference would fire
|
|
19549
|
-
* on every boot of a correct project — the widths still reach Postgres, which
|
|
19550
|
-
* enforces them. What this is looking for is the kind of drift that changes what
|
|
19551
|
-
* a value *is*: a number that became text, a timestamp that became a date, a
|
|
19552
|
-
* column that stopped existing.
|
|
19553
|
-
*/
|
|
19554
|
-
function typeFamily(sqlType) {
|
|
19555
|
-
if (!sqlType) return "unknown";
|
|
19556
|
-
let type = sqlType.trim().toLowerCase();
|
|
19557
|
-
const isArray = type.endsWith("[]") || type.startsWith("_");
|
|
19558
|
-
type = type.replace(/\[\]$/, "").replace(/^_/, "");
|
|
19559
|
-
type = type.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
19560
|
-
const family = FAMILIES[type] ?? type;
|
|
19561
|
-
return isArray ? `${family}[]` : family;
|
|
19562
|
-
}
|
|
19563
|
-
var FAMILIES = {
|
|
19564
|
-
text: "text",
|
|
19565
|
-
varchar: "text",
|
|
19566
|
-
"character varying": "text",
|
|
19567
|
-
bpchar: "text",
|
|
19568
|
-
char: "text",
|
|
19569
|
-
character: "text",
|
|
19570
|
-
citext: "text",
|
|
19571
|
-
name: "text",
|
|
19572
|
-
int2: "integer",
|
|
19573
|
-
smallint: "integer",
|
|
19574
|
-
smallserial: "integer",
|
|
19575
|
-
int4: "integer",
|
|
19576
|
-
int: "integer",
|
|
19577
|
-
integer: "integer",
|
|
19578
|
-
serial: "integer",
|
|
19579
|
-
int8: "bigint",
|
|
19580
|
-
bigint: "bigint",
|
|
19581
|
-
bigserial: "bigint",
|
|
19582
|
-
float4: "float",
|
|
19583
|
-
real: "float",
|
|
19584
|
-
float8: "float",
|
|
19585
|
-
"double precision": "float",
|
|
19586
|
-
numeric: "numeric",
|
|
19587
|
-
decimal: "numeric",
|
|
19588
|
-
money: "numeric",
|
|
19589
|
-
bool: "boolean",
|
|
19590
|
-
boolean: "boolean",
|
|
19591
|
-
uuid: "uuid",
|
|
19592
|
-
json: "json",
|
|
19593
|
-
jsonb: "jsonb",
|
|
19594
|
-
date: "date",
|
|
19595
|
-
time: "time",
|
|
19596
|
-
"time without time zone": "time",
|
|
19597
|
-
timetz: "time",
|
|
19598
|
-
"time with time zone": "time",
|
|
19599
|
-
timestamp: "timestamp",
|
|
19600
|
-
"timestamp without time zone": "timestamp",
|
|
19601
|
-
timestamptz: "timestamptz",
|
|
19602
|
-
"timestamp with time zone": "timestamptz",
|
|
19603
|
-
interval: "interval",
|
|
19604
|
-
bytea: "bytea",
|
|
19605
|
-
tsvector: "tsvector",
|
|
19606
|
-
tsquery: "tsquery",
|
|
19607
|
-
vector: "vector"
|
|
19608
|
-
};
|
|
19609
|
-
function catalogueColumns(meta) {
|
|
19610
|
-
const columns = /* @__PURE__ */ new Map();
|
|
19611
|
-
for (const col of meta.columns) columns.set(col.column_name, {
|
|
19612
|
-
name: col.column_name,
|
|
19613
|
-
family: typeFamily(col.udt_name || col.data_type),
|
|
19614
|
-
notNull: col.is_nullable === "NO"
|
|
19615
|
-
});
|
|
19616
|
-
return columns;
|
|
19617
|
-
}
|
|
19618
|
-
/**
|
|
19619
|
-
* Every way the generated module and the database disagree, for the tables both
|
|
19620
|
-
* describe.
|
|
19621
|
-
*
|
|
19622
|
-
* One-directional in one respect only: a table the module has and the database
|
|
19623
|
-
* does not is reported (that is the stale-file case), while a table the database
|
|
19624
|
-
* has and the module does not is left alone — a database may hold anything, and
|
|
19625
|
-
* the collections-vs-file question is `generated-schema-staleness`'s.
|
|
19626
|
-
*/
|
|
19627
|
-
function diffGeneratedSchemaAgainstCatalogue(options) {
|
|
19628
|
-
const { generated, catalogue, collections } = options;
|
|
19629
|
-
if (!generated) return [];
|
|
19630
|
-
const collectionByTable = /* @__PURE__ */ new Map();
|
|
19631
|
-
for (const collection of collections) {
|
|
19632
|
-
const table = getTableName$1(collection);
|
|
19633
|
-
if (table) collectionByTable.set(bareTableName(table), collection);
|
|
19634
|
-
}
|
|
19635
|
-
const differences = [];
|
|
19636
|
-
for (const value of Object.values(generated)) {
|
|
19637
|
-
if (!isTable(value)) continue;
|
|
19638
|
-
const table = getTableName(value);
|
|
19639
|
-
const collection = collectionByTable.get(table);
|
|
19640
|
-
const at = {
|
|
19641
|
-
table,
|
|
19642
|
-
collection: collection?.slug
|
|
19643
|
-
};
|
|
19644
|
-
const meta = catalogue.get(table);
|
|
19645
|
-
if (!meta) {
|
|
19646
|
-
if (collection) differences.push({
|
|
19647
|
-
...at,
|
|
19648
|
-
kind: "missing-table"
|
|
19649
|
-
});
|
|
19650
|
-
continue;
|
|
19651
|
-
}
|
|
19652
|
-
const live = catalogueColumns(meta);
|
|
19653
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19654
|
-
const authOwned = isAuthCollection(collection) ? new Set(AUTH_USERS_COLUMNS.map((c) => c.column)) : void 0;
|
|
19655
|
-
for (const [key, column] of Object.entries(getTableColumns(value))) {
|
|
19656
|
-
const name = column.name ?? key;
|
|
19657
|
-
seen.add(name);
|
|
19658
|
-
const property = collection ? fieldKeyForColumn(collection, name) : key;
|
|
19659
|
-
const generatedFamily = typeFamily(column.getSQLType?.());
|
|
19660
|
-
const generatedNotNull = Boolean(column.notNull);
|
|
19661
|
-
const found = live.get(name);
|
|
19662
|
-
if (!found) {
|
|
19663
|
-
differences.push({
|
|
19664
|
-
...at,
|
|
19665
|
-
kind: "missing-column",
|
|
19666
|
-
column: name,
|
|
19667
|
-
property,
|
|
19668
|
-
generated: generatedFamily
|
|
19669
|
-
});
|
|
19670
|
-
continue;
|
|
19671
|
-
}
|
|
19672
|
-
if (found.family !== generatedFamily) differences.push({
|
|
19673
|
-
...at,
|
|
19674
|
-
kind: "type",
|
|
19675
|
-
column: name,
|
|
19676
|
-
property,
|
|
19677
|
-
generated: generatedFamily,
|
|
19678
|
-
live: found.family
|
|
19679
|
-
});
|
|
19680
|
-
if (found.notNull !== generatedNotNull && !authOwned?.has(name)) differences.push({
|
|
19681
|
-
...at,
|
|
19682
|
-
kind: "nullability",
|
|
19683
|
-
column: name,
|
|
19684
|
-
property,
|
|
19685
|
-
generated: generatedNotNull ? "NOT NULL" : "nullable",
|
|
19686
|
-
live: found.notNull ? "NOT NULL" : "nullable"
|
|
19687
|
-
});
|
|
19688
|
-
}
|
|
19689
|
-
for (const [name, column] of live) {
|
|
19690
|
-
if (seen.has(name)) continue;
|
|
19691
|
-
if (authOwned?.has(name)) continue;
|
|
19692
|
-
differences.push({
|
|
19693
|
-
...at,
|
|
19694
|
-
kind: "extra-column",
|
|
19695
|
-
column: name,
|
|
19696
|
-
property: collection ? fieldKeyForColumn(collection, name) : name,
|
|
19697
|
-
live: column.family
|
|
19698
|
-
});
|
|
19699
|
-
}
|
|
19700
|
-
}
|
|
19701
|
-
return differences;
|
|
19702
|
-
}
|
|
19703
|
-
/** One line, in the terms the reader has: collection, property, column. */
|
|
19704
|
-
function describeSchemaDifference(difference) {
|
|
19705
|
-
const where = difference.collection ? `${difference.collection} (table "${difference.table}")` : `table "${difference.table}"`;
|
|
19706
|
-
const field = difference.column ? `${difference.property && difference.property !== difference.column ? `${difference.property} → "${difference.column}"` : `"${difference.column}"`}` : void 0;
|
|
19707
|
-
switch (difference.kind) {
|
|
19708
|
-
case "missing-table": return `${where} is in schema.generated.ts but not in the database`;
|
|
19709
|
-
case "missing-column": return `${where}: ${field} is in schema.generated.ts but not in the database`;
|
|
19710
|
-
case "extra-column": return `${where}: ${field} is in the database but not in schema.generated.ts (${difference.live})`;
|
|
19711
|
-
case "type": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
|
|
19712
|
-
case "nullability": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
|
|
19713
|
-
}
|
|
19714
|
-
}
|
|
19715
|
-
/**
|
|
19716
|
-
* Report the differences, one warning each, and never throw.
|
|
19717
|
-
*
|
|
19718
|
-
* Per difference rather than one summary block on purpose: these are read out of
|
|
19719
|
-
* a log aggregator as often as a terminal, and a single multi-line message
|
|
19720
|
-
* cannot be filtered down to the one collection somebody is looking at.
|
|
19721
|
-
*
|
|
19722
|
-
* The fix it names is `rebase db push`. `rebase dev` rewrites
|
|
19723
|
-
* schema.generated.ts from the collections on every change, and boot applies
|
|
19724
|
-
* only additive changes, so what is still different at boot is the database
|
|
19725
|
-
* behind the file: a rename, a dropped column, a narrowed type, a NOT NULL.
|
|
19726
|
-
* `rebase schema generate` rewrites the file and leaves the database as it is,
|
|
19727
|
-
* so it is named only for collections that changed since the file was written.
|
|
19728
|
-
*/
|
|
19729
|
-
function warnOnGeneratedSchemaDrift(differences) {
|
|
19730
|
-
if (differences.length === 0) return;
|
|
19731
|
-
logger.warn(`📄 [schema] schema.generated.ts and the database disagree (${differences.length} difference${differences.length === 1 ? "" : "s"}). Boot applies only additive changes; these are the ones it left for you. Run \`rebase db push\` to apply them (it shows the plan and asks before anything destructive), or \`rebase db generate\` for a migration. If the collections changed since schema.generated.ts was written, run \`rebase schema generate\` first.`);
|
|
19732
|
-
for (const difference of differences) logger.warn(` • ${describeSchemaDifference(difference)}`, {
|
|
19733
|
-
schemaDrift: {
|
|
19734
|
-
kind: difference.kind,
|
|
19735
|
-
table: difference.table,
|
|
19736
|
-
collection: difference.collection,
|
|
19737
|
-
column: difference.column,
|
|
19738
|
-
property: difference.property,
|
|
19739
|
-
generated: difference.generated,
|
|
19740
|
-
live: difference.live
|
|
19741
|
-
},
|
|
19742
|
-
fix: "rebase db push"
|
|
19743
|
-
});
|
|
19744
|
-
}
|
|
19745
|
-
//#endregion
|
|
19746
18993
|
//#region src/PostgresBootstrapper.ts
|
|
19747
18994
|
/**
|
|
19748
18995
|
* PostgresBootstrapper
|
|
@@ -20164,7 +19411,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20164
19411
|
const { readUrl, directUrl } = resolveSourceUrls(pgConfig, sourceKey, {});
|
|
20165
19412
|
let readDb;
|
|
20166
19413
|
if (readUrl && readUrl !== pgConfig.connectionString) try {
|
|
20167
|
-
const { createReadReplicaConnection } = await import("./connection-
|
|
19414
|
+
const { createReadReplicaConnection } = await import("./connection-czvI7TZL.js").then((n) => n.n);
|
|
20168
19415
|
readDb = createReadReplicaConnection(readUrl, mergedSchema).db;
|
|
20169
19416
|
logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
20170
19417
|
} catch (err) {
|
|
@@ -20175,6 +19422,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20175
19422
|
driver.listenConnectionString = directUrl;
|
|
20176
19423
|
realtimeService.setDataDriver(driver);
|
|
20177
19424
|
let rlsEnforcement;
|
|
19425
|
+
let exposedInternalTables;
|
|
20178
19426
|
{
|
|
20179
19427
|
const runSql = async (text) => {
|
|
20180
19428
|
return (await schemaAwareDb.execute(sql.raw(text))).rows ?? [];
|
|
@@ -20192,6 +19440,10 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20192
19440
|
if (!provision) {
|
|
20193
19441
|
const roleExists = (await runSql(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE$1}'`)).length > 0;
|
|
20194
19442
|
switched = roleExists || bypassesNow;
|
|
19443
|
+
if (roleExists) {
|
|
19444
|
+
exposedInternalTables = await findExposedInternalTables(runSql, managedSchemas);
|
|
19445
|
+
if (exposedInternalTables.length > 0) logger.warn(`⚠️ RLS enforcement: the request role "${REBASE_USER_ROLE$1}" can reach framework tables no user should: ${exposedInternalTables.join(", ")}. A provisioning boot revokes this; this driver was opened without provisioning, so nothing does.`);
|
|
19446
|
+
}
|
|
20195
19447
|
if (!roleExists && bypassesNow) logger.warn(`⚠️ RLS enforcement: connection role "${posture.role}" bypasses row-level security and the restricted role "${REBASE_USER_ROLE$1}" does not exist yet — this driver was opened without provisioning, so it was not created. Requests made as a user will fail until a server boot (or \`rebase db push\`) creates it; requests as the connection itself are unaffected.`);
|
|
20196
19448
|
} else try {
|
|
20197
19449
|
await ensureAppRole(runSql, managedSchemas);
|
|
@@ -20231,7 +19483,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20231
19483
|
if (!realtimeOff && (isChannelBusInstance(busSetting) || busSetting.type !== "memory")) await realtimeService.configureChannelBus(createChannelBus(busSetting, {
|
|
20232
19484
|
db: schemaAwareDb,
|
|
20233
19485
|
directUrl
|
|
20234
|
-
}));
|
|
19486
|
+
}), { provision });
|
|
20235
19487
|
} catch (err) {
|
|
20236
19488
|
logger.warn("⚠️ [ChannelBus] Could not configure the channel bus — channel broadcast and presence stay per-instance", { error: err });
|
|
20237
19489
|
}
|
|
@@ -20277,7 +19529,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20277
19529
|
};
|
|
20278
19530
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.` + (realtimeSubscribes ? "" : " This process installs the capture but does not consume it."));
|
|
20279
19531
|
} catch (err) {
|
|
20280
|
-
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", {
|
|
19532
|
+
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { detail: err instanceof Error ? err.message : String(err) });
|
|
20281
19533
|
else logger.info("ℹ️ [CDC] Database-level change capture unavailable (likely insufficient privileges to create triggers, or the LISTEN connection was refused) — using app-level realtime. Set REALTIME_CDC=off to silence this.", { detail: err instanceof Error ? err.message : String(err) });
|
|
20282
19534
|
}
|
|
20283
19535
|
}
|
|
@@ -20293,7 +19545,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20293
19545
|
if (!cdcEnabled && directUrl && realtimeSubscribes) try {
|
|
20294
19546
|
await realtimeService.startListening(directUrl);
|
|
20295
19547
|
} catch (err) {
|
|
20296
|
-
logger.warn("⚠️ Cross-instance realtime could not be started", {
|
|
19548
|
+
logger.warn("⚠️ Cross-instance realtime could not be started", { detail: err instanceof Error ? err.message : String(err) });
|
|
20297
19549
|
}
|
|
20298
19550
|
else if (!cdcEnabled && !realtimeSubscribes && !realtimeOff) realtimeService.startPublishing();
|
|
20299
19551
|
try {
|
|
@@ -20366,6 +19618,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
20366
19618
|
poolManager,
|
|
20367
19619
|
provisioned: provision,
|
|
20368
19620
|
rlsEnforcement,
|
|
19621
|
+
exposedInternalTables,
|
|
20369
19622
|
provisionCdcForTables
|
|
20370
19623
|
};
|
|
20371
19624
|
return {
|
|
@@ -20436,7 +19689,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20436
19689
|
const openIndexBuildSession = (driverResult) => async () => {
|
|
20437
19690
|
const db = requireProvisioningDb(driverResult);
|
|
20438
19691
|
const pool = "$client" in db ? db.$client : void 0;
|
|
20439
|
-
const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-
|
|
19692
|
+
const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
20440
19693
|
if (isPgPool(pool)) return poolIndexBuildSession(pool)();
|
|
20441
19694
|
const queryable = provisioningQueryable(driverResult);
|
|
20442
19695
|
return {
|
|
@@ -20462,7 +19715,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20462
19715
|
*/
|
|
20463
19716
|
const provisionRlsRuntime = async (driverResult) => {
|
|
20464
19717
|
const db = requireProvisioningDb(driverResult);
|
|
20465
|
-
const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./
|
|
19718
|
+
const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
|
|
20466
19719
|
const run = async (text) => sqlRows(await db.execute(sql.raw(text)));
|
|
20467
19720
|
if (await rlsRuntimeIsCurrent(run).catch(() => false)) return;
|
|
20468
19721
|
await db.transaction(async (tx) => {
|
|
@@ -20495,6 +19748,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20495
19748
|
const init = config;
|
|
20496
19749
|
const opened = await openPostgresDriver(effectiveConfig(init.dataSourceKey), init);
|
|
20497
19750
|
if (opened.internals.rlsEnforcement) logger.info(opened.internals.rlsEnforcement);
|
|
19751
|
+
const exposed = opened.internals.exposedInternalTables ?? [];
|
|
19752
|
+
if (exposed.length > 0) throw new Error(`The request role "${REBASE_USER_ROLE$1}" can read or write framework tables that hold sessions, key hashes and job payloads: ${exposed.join(", ")}.\nA provisioning boot revokes this, and this backend changes nothing in its database (provisioning is off). Revoke it as part of your change process:
|
|
19753
|
+
` + exposed.map((table) => ` REVOKE ALL ON ${table.split(".").map((part) => `"${part}"`).join(".")} FROM ${REBASE_USER_ROLE$1};`).join("\n"));
|
|
20498
19754
|
return opened;
|
|
20499
19755
|
},
|
|
20500
19756
|
async initializeAuth(config, driverResult) {
|
|
@@ -20505,7 +19761,23 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20505
19761
|
const registry = internals.registry;
|
|
20506
19762
|
const authCollection = authConfig.collection;
|
|
20507
19763
|
if (internals.provisioned !== false) await ensureAuthTablesExist(db, authCollection);
|
|
20508
|
-
else
|
|
19764
|
+
else {
|
|
19765
|
+
await assertAuthSchemaCompatible(db, resolveAuthSchema(authCollection));
|
|
19766
|
+
const usersSchema = authCollection ? "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "public" : "rebase";
|
|
19767
|
+
const usersTableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : "users";
|
|
19768
|
+
const expected = createAuthSchema(usersSchema === "public" ? "rebase" : usersSchema);
|
|
19769
|
+
const problems = await findUnusableAuthTables(db, [usersExpectation(usersSchema === "public" ? `"${usersTableName}"` : `"${usersSchema}"."${usersTableName}"`), ...[
|
|
19770
|
+
expected.refreshTokens,
|
|
19771
|
+
expected.passwordResetTokens,
|
|
19772
|
+
expected.appConfig,
|
|
19773
|
+
expected.userIdentities,
|
|
19774
|
+
expected.mfaFactors,
|
|
19775
|
+
expected.mfaChallenges,
|
|
19776
|
+
expected.recoveryCodes,
|
|
19777
|
+
expected.magicLinkTokens
|
|
19778
|
+
].map(expectationOf)]);
|
|
19779
|
+
if (problems.length > 0) throw unusableAuthTablesError(problems);
|
|
19780
|
+
}
|
|
20509
19781
|
if (authCollection && internals.provisionCdcForTables) {
|
|
20510
19782
|
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
20511
19783
|
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
@@ -20556,11 +19828,11 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20556
19828
|
unavailable: historyUnavailableReason(state, { requireTransaction: false })
|
|
20557
19829
|
}) };
|
|
20558
19830
|
}
|
|
20559
|
-
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-
|
|
19831
|
+
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
|
|
20560
19832
|
const historyTable = await ensureHistoryTableExists(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
|
|
20561
19833
|
const historyIndexes = await ensureHistoryIndexes(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
|
|
20562
19834
|
if (historyIndexes.length > 0) {
|
|
20563
|
-
const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-
|
|
19835
|
+
const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
20564
19836
|
const indexOptions = {
|
|
20565
19837
|
log: (message) => logger.info(`[history] ${message}`),
|
|
20566
19838
|
lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS
|
|
@@ -20616,9 +19888,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20616
19888
|
* proved it can bootstrap with.
|
|
20617
19889
|
*/
|
|
20618
19890
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
20619
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
19891
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CFIRq8ni.js").then((n) => n.n);
|
|
20620
19892
|
const queryable = provisioningQueryable(driverResult);
|
|
20621
|
-
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-
|
|
19893
|
+
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
|
|
20622
19894
|
const provisionOnly = ["1", "true"].includes((process.env.REBASE_PROVISION_ONLY ?? "").trim().toLowerCase());
|
|
20623
19895
|
const plan = await ensureCollectionTables(queryable, collections, log, {
|
|
20624
19896
|
lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS,
|
|
@@ -20630,7 +19902,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20630
19902
|
pendingIndexBuild?.cancel();
|
|
20631
19903
|
pendingIndexBuild = void 0;
|
|
20632
19904
|
if (!provisionOnly && plan.deferredIndexes.length > 0) {
|
|
20633
|
-
const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-
|
|
19905
|
+
const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
20634
19906
|
logger.info(`[schema] ${plan.deferredIndexes.length} index(es) are missing, or were left INVALID, on tables that already exist. They are built in the background once the server is serving, one at a time, without blocking reads or writes.`);
|
|
20635
19907
|
pendingIndexBuild = scheduleDeferredIndexBuild(openIndexBuildSession(driverResult), plan.deferredIndexes, {
|
|
20636
19908
|
log: (message) => logger.info(`[schema] ${message}`),
|
|
@@ -20670,7 +19942,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20670
19942
|
await provisionRlsRuntime(driverResult);
|
|
20671
19943
|
},
|
|
20672
19944
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
20673
|
-
const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-
|
|
19945
|
+
const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-DLWgabQ2.js");
|
|
20674
19946
|
const queryable = provisioningQueryable(driverResult);
|
|
20675
19947
|
const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
|
|
20676
19948
|
if (!rows[0]?.present) await provisionRlsRuntime(driverResult);
|
|
@@ -20695,7 +19967,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20695
19967
|
for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE$1}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
|
|
20696
19968
|
if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE$1} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
|
|
20697
19969
|
try {
|
|
20698
|
-
const { dropLegacyAuthSchema } = await import("./
|
|
19970
|
+
const { dropLegacyAuthSchema } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
|
|
20699
19971
|
await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
|
|
20700
19972
|
info: (m) => logger.info(m),
|
|
20701
19973
|
warn: (m) => logger.warn(m)
|
|
@@ -20715,14 +19987,14 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20715
19987
|
async readCollectionsSchemaVersion(driverResult) {
|
|
20716
19988
|
const db = provisioningDb(driverResult);
|
|
20717
19989
|
if (!db) return null;
|
|
20718
|
-
const { readCollectionsSchemaVersion } = await import("./collections-schema-version-
|
|
19990
|
+
const { readCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
|
|
20719
19991
|
return readCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA);
|
|
20720
19992
|
},
|
|
20721
19993
|
/** Record what this process just applied. Only the provisioning process calls this. */
|
|
20722
19994
|
async stampCollectionsSchemaVersion(version, driverResult) {
|
|
20723
19995
|
const db = provisioningDb(driverResult);
|
|
20724
19996
|
if (!db) return;
|
|
20725
|
-
const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-
|
|
19997
|
+
const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
|
|
20726
19998
|
await stampCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA, version);
|
|
20727
19999
|
},
|
|
20728
20000
|
/**
|
|
@@ -20786,7 +20058,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20786
20058
|
return driverResult.internals.driver.admin;
|
|
20787
20059
|
},
|
|
20788
20060
|
async initializeWebsockets(server, realtimeService, driver, config, adapter, limits) {
|
|
20789
|
-
const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-
|
|
20061
|
+
const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-CKYc3V4N.js").then((n) => n.c);
|
|
20790
20062
|
if (!isSocketRealtimeService(realtimeService)) throw new Error(`The realtime service handed to the Postgres socket cannot serve it — it has no ${socketRealtimeMissing(realtimeService).join(", ")}. Every socket would fail on its first frame.`);
|
|
20791
20063
|
const realtimeConfig = effectiveConfig().realtime;
|
|
20792
20064
|
const maxSocketsPerUser = realtimeConfig?.maxSocketsPerUser !== void 0 ? parseMaxSocketsPerUser(realtimeConfig.maxSocketsPerUser, "realtime.maxSocketsPerUser") : void 0;
|
|
@@ -20860,6 +20132,7 @@ async function openRebase(options) {
|
|
|
20860
20132
|
const hint = extractPgError(error)?.hint;
|
|
20861
20133
|
throw new Error(`Cannot connect to PostgreSQL at ${parseHostInfo(connectionString ?? "")}: ${deepestErrorMessage(error) ?? "the connection failed"}.${hint ? ` ${hint}` : ""}`, { cause: error });
|
|
20862
20134
|
}
|
|
20135
|
+
const ownKey = options.dataSourceKey ?? DEFAULT_DATA_SOURCE_KEY;
|
|
20863
20136
|
let opened;
|
|
20864
20137
|
try {
|
|
20865
20138
|
opened = await openPostgresDriver({
|
|
@@ -20867,7 +20140,7 @@ async function openRebase(options) {
|
|
|
20867
20140
|
connectionString
|
|
20868
20141
|
}, {
|
|
20869
20142
|
collections: options.collections,
|
|
20870
|
-
dataSourceKey:
|
|
20143
|
+
dataSourceKey: ownKey,
|
|
20871
20144
|
introspectCollections: options.collections.length === 0,
|
|
20872
20145
|
provision: false,
|
|
20873
20146
|
realtime: false,
|
|
@@ -20882,6 +20155,7 @@ async function openRebase(options) {
|
|
|
20882
20155
|
}
|
|
20883
20156
|
const driver = opened.internals.driver;
|
|
20884
20157
|
const registry = opened.internals.registry;
|
|
20158
|
+
if (options.callbacks) registry.setGlobalCallbacks(options.callbacks);
|
|
20885
20159
|
if (opened.internals.rlsEnforcement) logger.debug(opened.internals.rlsEnforcement);
|
|
20886
20160
|
const collections = opened.collections ?? options.collections;
|
|
20887
20161
|
if (collections.some((collection) => collection.history)) try {
|
|
@@ -20906,10 +20180,13 @@ async function openRebase(options) {
|
|
|
20906
20180
|
collections,
|
|
20907
20181
|
async sql(query, sqlOptions) {
|
|
20908
20182
|
if (!isSQLAdmin(admin)) throw new Error("This driver cannot run raw SQL.");
|
|
20183
|
+
if (sqlOptions?.database !== void 0) {
|
|
20184
|
+
const asked = resourceKeyOf(sqlOptions.database);
|
|
20185
|
+
if (asked !== ownKey) throw new Error(`openRebase opened the "${ownKey}" data source only, so it cannot run SQL on "${asked}". Open that one with its own openRebase({ databaseUrl, dataSourceKey: "${asked}" }).`);
|
|
20186
|
+
}
|
|
20909
20187
|
return admin.executeSql(query, sqlOptions && {
|
|
20910
20188
|
role: sqlOptions.role,
|
|
20911
|
-
params: sqlOptions.params
|
|
20912
|
-
...sqlOptions.database !== void 0 ? { database: resourceKeyOf(sqlOptions.database) } : {}
|
|
20189
|
+
params: sqlOptions.params
|
|
20913
20190
|
});
|
|
20914
20191
|
},
|
|
20915
20192
|
async withAuth(user) {
|
|
@@ -20925,4 +20202,4 @@ async function openRebase(options) {
|
|
|
20925
20202
|
//#endregion
|
|
20926
20203
|
export { AuthenticatedPostgresBackendDriver as $, budgetSendBuffers as A, mfaFactors as B, PG_NOTIFY_MAX_PAYLOAD_BYTES as C, MemoryChannelBus as D, parseChannelBusPayload as E, createAuthSchema as F, recoveryCodesRelations as G, passwordResetTokens as H, magicLinkTokens as I, userIdentities as J, refreshTokens as K, magicLinkTokensRelations as L, terminateIfBacklogged as M, assertReadRequestReadable as N, frameByteLength as O, appConfig as P, usersSchema as Q, mfaChallenges as R, DEFAULT_BATCH_WINDOW_MS as S, parseChannelBusFrame as T, passwordResetTokensRelations as U, mfaFactorsRelations as V, recoveryCodes as W, users as X, userIdentitiesRelations as Y, usersRelations as Z, parseRealtimeTuning as _, listedColumnKeys as _t, openPostgresDriver as a, isRoleSwitchingOptedOut as at, resolveChannelBusSetting as b, resolveSourceUrls as c, SqlResultTooLargeError as ct, DEFAULT_RECOUNT_MIN_ROWS as d, DrizzleConditionBuilder as dt, CONNECTION_OWNER as et, PostgresRealtimeProvider as f, PostgresConditionBuilder as ft, parseMaxSubscriptionsPerSocket as g, joinColumn as gt, parseMaxSocketsPerUser as h, getUnknownFilterFieldsMode as ht, isScaffoldedLocalDatabase as i, effectiveSqlRole as it, reapSilentSockets as j, isChannelBusInstance$1 as k, PostgresCollectionRegistry as l, isNestedPath as lt, packEntityChanges as m, escapeLikePattern as mt, createPostgresBootstrapper as n, RoleSwitchUnavailableError as nt, postgresDriverConfigFromEnv as o, sqlRoleRanAs as ot, RealtimeService as p, configureUnknownFilterFields as pt, refreshTokensRelations as q, describeSchemaDriftCause as r, branchParentDatabase as rt, resolveDriftCheckName as s, SQL_RESULT_BYTE_BUDGET as st, openRebase as t, PostgresBackendDriver as tt, DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET as u, resolveNestedPath as ut, refetchConcurrencyFor as v, PostgresChannelBus as w, CHANNEL_BUS_NOTIFY_CHANNEL as x, createChannelBus as y, mfaChallengesRelations as z };
|
|
20927
20204
|
|
|
20928
|
-
//# sourceMappingURL=open-rebase-
|
|
20205
|
+
//# sourceMappingURL=open-rebase-C8jQ6s5a.js.map
|