@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g56af4a4

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.
Files changed (162) hide show
  1. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  2. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +83 -2
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +6 -0
  6. package/dist/backup/backup-cli.d.ts +14 -0
  7. package/dist/backup/backup-service.d.ts +2 -2
  8. package/dist/backup/pg-binary.d.ts +5 -0
  9. package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
  10. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  11. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  12. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  13. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  14. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  15. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  16. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  17. package/dist/cli-errors.d.ts +51 -5
  18. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  19. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  20. package/dist/cli-helpers.d.ts +40 -0
  21. package/dist/cli-output.d.ts +16 -28
  22. package/dist/cli-push.d.ts +87 -0
  23. package/dist/cli.js +563 -182
  24. package/dist/cli.js.map +1 -1
  25. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  26. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  27. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  28. package/dist/connection-BPoNILnD.js.map +1 -0
  29. package/dist/connection.d.ts +9 -1
  30. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  31. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  32. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  33. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  34. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  35. package/dist/doctor-C4WiftHm.js.map +1 -0
  36. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  37. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  38. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  39. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  40. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
  41. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  42. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  43. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  44. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  45. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  46. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  47. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  48. package/dist/generate-files-CdEckX6c.js +199 -0
  49. package/dist/generate-files-CdEckX6c.js.map +1 -0
  50. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  51. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  52. package/dist/generated-sql.d.ts +8 -2
  53. package/dist/history/HistoryService.d.ts +38 -6
  54. package/dist/history/ensure-history-table.d.ts +42 -4
  55. package/dist/index.es.js +2922 -7475
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  58. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  59. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  60. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  61. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  62. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  63. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  64. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  65. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  66. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  67. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  68. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  69. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  70. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  71. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  72. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  73. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  74. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  75. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  76. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  77. package/dist/schema/auth-schema.d.ts +2 -2
  78. package/dist/schema/boot-ddl.d.ts +47 -0
  79. package/dist/schema/collection-index.d.ts +45 -0
  80. package/dist/schema/column-plan-helpers.d.ts +5 -0
  81. package/dist/schema/deferred-index-build.d.ts +131 -0
  82. package/dist/schema/destructive-sql.d.ts +7 -0
  83. package/dist/schema/doctor-cli.js +110 -6
  84. package/dist/schema/doctor-cli.js.map +1 -1
  85. package/dist/schema/doctor-search-scale.d.ts +45 -0
  86. package/dist/schema/doctor.d.ts +20 -1
  87. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  88. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  89. package/dist/schema/enum-labels-kept.d.ts +26 -0
  90. package/dist/schema/generate-drizzle-schema.js +1 -1
  91. package/dist/schema/generate-files.d.ts +44 -0
  92. package/dist/schema/generate-next-step.d.ts +0 -13
  93. package/dist/schema/generate-postgres-ddl.js +10 -81
  94. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  95. package/dist/schema/introspect-db-logic.d.ts +12 -0
  96. package/dist/schema/introspect-db-queries.d.ts +11 -1
  97. package/dist/schema/introspect-db-structure.d.ts +10 -0
  98. package/dist/schema/introspect-db.js +4 -5
  99. package/dist/schema/introspect-db.js.map +1 -1
  100. package/dist/schema/migration-post-process.d.ts +34 -0
  101. package/dist/schema/plan/diff-plan.d.ts +126 -0
  102. package/dist/schema/plan/types.d.ts +8 -0
  103. package/dist/schema/push-index-prebuild.d.ts +25 -0
  104. package/dist/schema/push-legacy-renames.d.ts +20 -0
  105. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  106. package/dist/security/rls-enforcement.d.ts +32 -4
  107. package/dist/services/FetchService.d.ts +58 -8
  108. package/dist/services/PersistService.d.ts +15 -0
  109. package/dist/services/RelationService.d.ts +88 -3
  110. package/dist/services/cdc/CdcListener.d.ts +17 -6
  111. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  112. package/dist/services/channel-history.d.ts +15 -4
  113. package/dist/services/collection-helpers.d.ts +15 -0
  114. package/dist/services/field-viewer.d.ts +15 -1
  115. package/dist/services/pg-notify-listener.d.ts +7 -0
  116. package/dist/services/realtimeService.d.ts +314 -14
  117. package/dist/services/row-pipeline.d.ts +16 -9
  118. package/dist/services/socket-liveness.d.ts +42 -2
  119. package/dist/services/sql-script.d.ts +207 -3
  120. package/dist/services/vector-iterative-scan.d.ts +40 -0
  121. package/dist/services/write-depth.d.ts +6 -0
  122. package/dist/services/write-transaction-scope.d.ts +9 -0
  123. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  124. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  125. package/dist/socket-opt-ins.d.ts +64 -0
  126. package/dist/source-DBDbBTL4.js +811 -0
  127. package/dist/source-DBDbBTL4.js.map +1 -0
  128. package/dist/tsx-loader.d.ts +14 -0
  129. package/dist/utils/drizzle-conditions.d.ts +21 -1
  130. package/dist/utils/error-cause.d.ts +7 -0
  131. package/dist/utils/pg-error-utils.d.ts +2 -4
  132. package/dist/utils/result-rows.d.ts +10 -0
  133. package/dist/websocket.d.ts +13 -0
  134. package/package.json +7 -7
  135. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  136. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  137. package/dist/cli-errors-DsA-K9uP.js +0 -893
  138. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  139. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  140. package/dist/cli-output-CNdMql-L.js +0 -47
  141. package/dist/cli-output-CNdMql-L.js.map +0 -1
  142. package/dist/collection-index-DdnsxiJ_.js +0 -286
  143. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  144. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  145. package/dist/doctor-C-sYWbmt.js.map +0 -1
  146. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  147. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  148. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  149. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  150. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  151. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  152. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  153. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  154. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  155. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  156. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  157. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  158. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  159. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  160. package/dist/source-Br7L7GOI.js +0 -410
  161. package/dist/source-Br7L7GOI.js.map +0 -1
  162. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -1 +0,0 @@
1
- {"version":3,"file":"collection-index-DdnsxiJ_.js","names":[],"sources":["../src/schema/collection-index.ts"],"sourcesContent":["/**\n * The one place a collection's `indexes:` block becomes `CREATE INDEX`.\n *\n * Like `search-column.ts` and `vector-index.ts`, this module exists so the DDL\n * generator and the boot-time ensure render the *same* specification rather\n * than describing the same index twice, differently.\n *\n * ## Why every form here is core Postgres\n *\n * `rebase db push` runs `atlas schema apply`, which materialises the desired\n * state in a bare scratch database to plan against. `--exclude` does not\n * suppress that replay, and `CREATE EXTENSION` cannot go in the file. So an\n * index wanting `gin_trgm_ops` or `vector_cosine_ops` would parse, plan, and\n * then fail against a database the author has never heard of. Those are\n * refused here instead, and redirected to the feature that owns them: trigram\n * search is `search:`, ANN is a `vector` property.\n *\n * Verified against atlas v1.2.3 and Postgres 18 before this was written: plain,\n * composite with `DESC NULLS LAST`, partial, unique, covering `INCLUDE`, `GIN`\n * and expression indexes all parse, apply, and re-plan clean. The Atlas\n * limitation that forced the search carve-out is that it will not parse a file\n * containing a function *definition* — a function *call* inside an index is\n * fine. That is why this module needs no carve-out and search did.\n *\n * ## Why the name carries a hash\n *\n * `CREATE INDEX IF NOT EXISTS` is a **name** check, not a definition check. A\n * readable name means a changed declaration keeps the old index and reports\n * success, forever. Hashing the index's *semantics* into its name makes a\n * redefinition a different object: the new one is built before the old one is\n * dropped, there is never a window with no index, and drift detection reduces\n * to a set difference over names.\n */\nimport type { CollectionConfig, CollectionIndex, IndexPredicate, Property, ResolvedRelation } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { sha1Hex, truncateToBytes } from \"@rebasepro/utils\";\n\n/** Resolve a property key to its column name. Injected to avoid an import cycle. */\nexport type ResolveColumnName = (propName: string, prop?: Property | null) => string;\n\nexport type IndexMethod = \"btree\" | \"gin\" | \"brin\";\n\n/** A key column, with the ordering Postgres will actually apply. */\nexport interface ResolvedIndexKey {\n column: string;\n /** Always concrete. `btree` defaults ascending; unordered methods report `asc`. */\n direction: \"asc\" | \"desc\";\n /** Postgres's own default: `last` under `asc`, `first` under `desc`. */\n nulls: \"first\" | \"last\";\n}\n\n/** A predicate resolved onto column names, ready to render and to hash. */\nexport type ResolvedPredicate =\n | { column: string; op: \"=\" | \"!=\" | \"<\" | \"<=\" | \">\" | \">=\"; value: string | number | boolean }\n | { column: string; op: \"is null\" | \"is not null\" }\n | { column: string; op: \"in\"; value: readonly (string | number)[] }\n | { and: readonly ResolvedPredicate[] };\n\n/** One index, fully resolved. The only shape the renderers accept. */\nexport interface CollectionIndexSpec {\n schema: string;\n table: string;\n method: IndexMethod;\n unique: boolean;\n keys: ResolvedIndexKey[];\n include: string[];\n predicate: ResolvedPredicate | null;\n /** The author's one-line justification. Never enters the name. */\n reason: string;\n /** Derived by {@link deriveIndexName}. Frozen — see the module comment. */\n indexName: string;\n}\n\n/**\n * A declaration that cannot become an index.\n *\n * Thrown at build time, naming the collection and the array position, because\n * the alternative is a `CREATE INDEX` that fails during a push with a Postgres\n * error mentioning a column the author never wrote.\n */\nexport class CollectionIndexConfigError extends Error {\n readonly collectionSlug: string;\n readonly position: number;\n\n constructor(collectionSlug: string, position: number, message: string) {\n super(`${collectionSlug}.indexes[${position}]: ${message}`);\n this.name = \"CollectionIndexConfigError\";\n this.collectionSlug = collectionSlug;\n this.position = position;\n }\n}\n\n/** Postgres allows 32 key columns. See the doc comment on `on`. */\nexport const MAX_INDEX_KEYS = 5;\n\n/**\n * `_ix`/`_ux` plus `_` plus 7 hex — the part of the name that must always\n * survive truncation, and therefore is never inside the truncated portion.\n */\nconst NAME_SUFFIX_BYTES = 11;\n\n/**\n * Every Rebase-managed index name, and nothing else.\n *\n * The terminal `_ix_`/`_ux_` plus exactly seven lowercase hex characters is\n * what separates this scheme from every other producer in the codebase —\n * `_fkey`, `_gin`, `_trgm`, `_pkey`, `_key`, the vector distances, and the\n * `idx_` prefix auth uses. `_idx` was rejected as a tail because it is already\n * taken for real: `users_email_verification_token_idx` is byte-for-byte what a\n * naive `<table>_<column>_idx` derives on an auth-enabled `users` collection.\n *\n * Load-bearing for safety, not just tidiness. An index that does NOT match this\n * belongs to somebody else — a hand-written one, or one an introspected\n * database arrived with — and is excluded from the Atlas diff so the push\n * cannot drop it.\n */\nexport const isRebaseIndexName = (name: string): boolean => /_(?:ix|ux)_[0-9a-f]{7}$/.test(name);\n\nconst isOrderedMethod = (method: IndexMethod): boolean => method === \"btree\";\n\n/**\n * The parts of an index that decide what it *is*.\n *\n * A semantic projection, not the rendered statement — the same arrangement as\n * `getPolicyNameHash`, and for the same reason. A change to how this file\n * formats SQL (eliding a default `USING btree`, quoting differently, emitting\n * `NULLS LAST` explicitly) must not silently rename every index in every\n * deployed database. Hashing generator output would make every cosmetic edit a\n * fleet-wide DROP + CREATE.\n *\n * `reason` is deliberately absent: rewording a comment must not rebuild an\n * index. `nulls` is the *effective* placement, so writing Postgres's own\n * default down is a no-op rather than a redefinition.\n *\n * `v` is the only escape hatch, and it is expensive on purpose: bumping it\n * renames every index in the field.\n */\nexport const indexFingerprint = (spec: Omit<CollectionIndexSpec, \"indexName\">): string => sha1Hex(JSON.stringify({\n v: 1,\n s: spec.schema,\n t: spec.table,\n m: spec.method,\n u: spec.unique,\n k: spec.keys.map(k => [k.column, k.direction, k.nulls]),\n i: spec.include,\n w: spec.predicate\n})).substring(0, 7);\n\n/**\n * `<table>_<columns>_ix_<hash>`, or `_ux_` when unique.\n *\n * Truncation eats the readable head and never the hash. `toPostgresIdentifier`\n * truncates the whole string at 63 bytes, which on a hashed name would cut off\n * the one part that makes it unique — the failure already frozen into\n * `contracts/derived-names.txt`, where a foreign key is recorded with its\n * `_fkey` suffix truncated away, so a second foreign key on that table would\n * derive a byte-identical name.\n */\nexport const deriveIndexName = (spec: Omit<CollectionIndexSpec, \"indexName\">): string => {\n // Built suffix-first, so the two parts that carry meaning — the `_ix`/`_ux`\n // tag that {@link isRebaseIndexName} matches on, and the fingerprint — are\n // never in the string being truncated. Composing the whole name and then\n // trimming it to 63 loses both, silently: an 80-byte table name yields\n // `xxxx…xxx_610bb9e` with the tag gone, so the index stops being\n // recognisable as Rebase's and `db push` treats it as foreign forever.\n const tag = spec.unique ? \"ux\" : \"ix\";\n const suffix = `_${tag}_${indexFingerprint(spec)}`;\n const readable = `${spec.table}_${spec.keys.map(k => k.column).join(\"_\")}`;\n return `${truncateToBytes(readable, 63 - NAME_SUFFIX_BYTES)}${suffix}`;\n};\n\nconst quoteLiteral = (value: string | number | boolean): string => {\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"boolean\") return value ? \"TRUE\" : \"FALSE\";\n return `'${value.replace(/'/g, \"''\")}'`;\n};\n\n/** Render a resolved predicate as the body of a `WHERE` clause. */\nexport const renderPredicate = (predicate: ResolvedPredicate): string => {\n if (\"and\" in predicate) {\n // Always parenthesised. Postgres would apply the same precedence\n // without it, but the rendered SQL is read by people diffing a plan.\n return predicate.and.map(renderPredicate).join(\" AND \");\n }\n switch (predicate.op) {\n case \"is null\":\n case \"is not null\":\n return `\"${predicate.column}\" ${predicate.op.toUpperCase()}`;\n case \"in\":\n return `\"${predicate.column}\" IN (${predicate.value.map(quoteLiteral).join(\", \")})`;\n default:\n return `\"${predicate.column}\" ${predicate.op} ${quoteLiteral(predicate.value)}`;\n }\n};\n\n/**\n * The `CREATE INDEX` for one spec.\n *\n * `concurrently` is a parameter rather than a string replacement on the way\n * out. `search-column.ts` and `vector-index.ts` both reach for\n * `.replace(\"CREATE INDEX IF NOT EXISTS\", …)` instead, which silently does\n * nothing for a UNIQUE index — the rendered text is `CREATE UNIQUE INDEX …`\n * and the pattern never matches.\n */\nexport const collectionIndexStatement = (\n spec: CollectionIndexSpec,\n options: { concurrently?: boolean; ifNotExists?: boolean } = {}\n): string => {\n const unique = spec.unique ? \"UNIQUE \" : \"\";\n const concurrently = options.concurrently ? \"CONCURRENTLY \" : \"\";\n const ifNotExists = options.ifNotExists ? \"IF NOT EXISTS \" : \"\";\n const using = spec.method === \"btree\" ? \"\" : ` USING ${spec.method}`;\n\n const keys = spec.keys.map(k => {\n if (!isOrderedMethod(spec.method)) return `\"${k.column}\"`;\n const direction = k.direction === \"desc\" ? \" DESC\" : \"\";\n // Emitted only when it is not what the direction already implies, so\n // the rendered SQL matches what `pg_get_indexdef` reads back and a\n // re-plan finds no difference.\n const impliedNulls = k.direction === \"desc\" ? \"first\" : \"last\";\n const nulls = k.nulls === impliedNulls ? \"\" : ` NULLS ${k.nulls.toUpperCase()}`;\n return `\"${k.column}\"${direction}${nulls}`;\n }).join(\", \");\n\n const include = spec.include.length > 0\n ? ` INCLUDE (${spec.include.map(c => `\"${c}\"`).join(\", \")})`\n : \"\";\n const where = spec.predicate ? ` WHERE ${renderPredicate(spec.predicate)}` : \"\";\n\n return `CREATE ${unique}INDEX ${concurrently}${ifNotExists}\"${spec.indexName}\" ` +\n `ON \"${spec.schema}\".\"${spec.table}\"${using} (${keys})${include}${where};`;\n};\n\nexport const collectionIndexStatements = (\n specs: readonly CollectionIndexSpec[],\n options: { concurrently?: boolean; ifNotExists?: boolean } = {}\n): string[] => specs.map(spec => collectionIndexStatement(spec, options));\n\nconst relationOf = (\n collection: CollectionConfig,\n propKey: string\n): ResolvedRelation | undefined => resolveCollectionRelations(collection)[propKey];\n\n/**\n * The column a property key indexes.\n *\n * A `belongsTo` resolves to its `localKey` — `primaryCategory` becomes\n * `primary_category_id` — which is the case an index is most often wanted for\n * and the case where the property key and the column differ. Everything else\n * goes through `resolveColumnName`.\n *\n * The other relation kinds have no local column at all: the foreign key lives\n * on the target's table, or in a junction. Indexing them here is refused\n * rather than resolved to a column that does not exist.\n */\nexport const resolveIndexableColumn = (\n collection: CollectionConfig,\n propKey: string,\n resolveColumnName: ResolveColumnName,\n fail: (message: string) => never\n): string => {\n const relation = relationOf(collection, propKey);\n if (relation) {\n if (relation.kind === \"belongsTo\") return relation.localKey;\n fail(\n `\"${propKey}\" is a ${relation.kind} relation, which has no column on this table — ` +\n `the foreign key lives on \"${relation.targetSlug}\". Declare the index there.`\n );\n }\n\n const property = collection.properties?.[propKey] as Property | undefined;\n if (!property) {\n fail(`\"${propKey}\" is not a property of this collection.`);\n }\n return resolveColumnName(propKey, property);\n};\n\nconst resolvePredicate = (\n collection: CollectionConfig,\n predicate: IndexPredicate,\n resolveColumnName: ResolveColumnName,\n fail: (message: string) => never\n): ResolvedPredicate => {\n if (\"and\" in predicate) {\n return { and: predicate.and.map(p => resolvePredicate(collection, p, resolveColumnName, fail)) };\n }\n const column = resolveIndexableColumn(collection, predicate.prop, resolveColumnName, fail);\n switch (predicate.op) {\n case \"is null\":\n case \"is not null\":\n return { column, op: predicate.op };\n case \"in\": {\n const seen = new Set(predicate.value);\n if (seen.size !== predicate.value.length) {\n fail(`the \\`in\\` list for \"${predicate.prop}\" repeats a value, which changes nothing.`);\n }\n return { column, op: \"in\", value: [...predicate.value] };\n }\n default:\n return { column, op: predicate.op, value: predicate.value };\n }\n};\n\n/** The primary key columns of a collection, for the \"you already have this\" refusal. */\nconst primaryKeyColumns = (collection: CollectionConfig, resolveColumnName: ResolveColumnName): string[] =>\n Object.entries(collection.properties ?? {})\n .filter(([, prop]) => prop && typeof prop === \"object\" && \"isId\" in prop && Boolean((prop as { isId?: unknown }).isId))\n .map(([key, prop]) => resolveColumnName(key, prop as Property));\n\n/**\n * Every index one collection declares, resolved and named.\n *\n * Throws {@link CollectionIndexConfigError} rather than dropping a bad entry:\n * an index that silently does not exist is the failure mode this whole feature\n * is here to remove.\n */\nexport const buildCollectionIndexSpecs = (\n collection: CollectionConfig,\n resolveColumnName: ResolveColumnName\n): CollectionIndexSpec[] => {\n if (!isPostgresCollectionConfig(collection)) return [];\n const declared = collection.indexes;\n if (!declared || declared.length === 0) return [];\n\n const slug = collection.slug ?? getTableName(collection);\n const schema = collection.schema ?? \"public\";\n const table = getTableName(collection);\n const pk = primaryKeyColumns(collection, resolveColumnName).sort().join(\",\");\n\n const specs: CollectionIndexSpec[] = [];\n const byName = new Map<string, number>();\n\n declared.forEach((index: CollectionIndex, position: number) => {\n const fail = (message: string): never => {\n throw new CollectionIndexConfigError(slug, position, message);\n };\n\n if (typeof index.reason !== \"string\" || index.reason.trim() === \"\") {\n fail(\"`reason` is required — see the doc comment. An index nobody can justify is one nobody can delete.\");\n }\n if (!Array.isArray(index.on) || index.on.length === 0) {\n fail(\"`on` must name at least one property.\");\n }\n if (index.on.length > MAX_INDEX_KEYS) {\n fail(`\\`on\\` has ${index.on.length} keys; the limit is ${MAX_INDEX_KEYS}. Payload columns belong in \\`include\\`.`);\n }\n\n const method: IndexMethod = index.using ?? \"btree\";\n const unique = method === \"btree\" && Boolean((index as { unique?: boolean }).unique);\n if (!isOrderedMethod(method)) {\n for (const key of index.on) {\n if (typeof key !== \"string\" && (\"direction\" in key || \"nulls\" in key)) {\n fail(`access method \"${method}\" does not support ASC/DESC or NULLS options.`);\n }\n }\n }\n\n const keys: ResolvedIndexKey[] = index.on.map(key => {\n const propKey = typeof key === \"string\" ? key : key.prop;\n const column = resolveIndexableColumn(collection, propKey, resolveColumnName, fail);\n const direction = (typeof key === \"string\" ? undefined : (key as { direction?: \"asc\" | \"desc\" }).direction) ?? \"asc\";\n const nulls = (typeof key === \"string\" ? undefined : (key as { nulls?: \"first\" | \"last\" }).nulls)\n ?? (direction === \"desc\" ? \"first\" : \"last\");\n return { column, direction, nulls };\n });\n\n const duplicateKey = keys.map(k => k.column).find((c, i, all) => all.indexOf(c) !== i);\n if (duplicateKey) fail(`\"${duplicateKey}\" appears twice in \\`on\\`.`);\n\n if (keys.map(k => k.column).sort().join(\",\") === pk && pk !== \"\") {\n fail(`this is the primary key — \"${table}_pkey\" already indexes exactly these columns.`);\n }\n\n const include = ((index as { include?: readonly string[] }).include ?? [])\n .map(propKey => resolveIndexableColumn(collection, propKey, resolveColumnName, fail));\n const overlap = include.find(c => keys.some(k => k.column === c));\n if (overlap) fail(`\"${overlap}\" is in both \\`on\\` and \\`include\\`; Postgres rejects the overlap.`);\n\n if (unique && keys.length === 1) {\n const propKey = typeof index.on[0] === \"string\" ? index.on[0] as string : (index.on[0] as { prop: string }).prop;\n const property = collection.properties?.[propKey] as { validation?: { unique?: boolean } } | undefined;\n if (property?.validation?.unique) {\n fail(\n `\"${propKey}\" already declares \\`validation.unique\\`, which compiles to an inline UNIQUE. ` +\n `Two declarations of one guarantee — remove one.`\n );\n }\n }\n\n const predicate = index.where\n ? resolvePredicate(collection, index.where, resolveColumnName, fail)\n : null;\n\n const withoutName = { schema, table, method, unique, keys, include, predicate, reason: index.reason };\n const indexName = deriveIndexName(withoutName);\n\n const clash = byName.get(indexName);\n if (clash !== undefined) {\n fail(`derives the same name as indexes[${clash}] — they are the same index declared twice.`);\n }\n byName.set(indexName, position);\n\n specs.push({ ...withoutName, indexName });\n });\n\n return specs;\n};\n\n/**\n * Every declared index across a set of collections, in a stable order.\n *\n * Sorted because the result reaches `schema.sql`, which `doctor` string-\n * compares against a regenerated copy — `generatePostgresDdl` does not sort its\n * collections, so leaving this in declaration order would make the artifact\n * depend on the order files happened to load in.\n */\nexport const buildCollectionIndexPlan = (\n collections: readonly CollectionConfig[],\n resolveColumnName: ResolveColumnName\n): CollectionIndexSpec[] =>\n sortIndexSpecs(collections.flatMap(collection => buildCollectionIndexSpecs(collection, resolveColumnName)));\n\n/**\n * The stable order every consumer of a set of index specs uses.\n *\n * Exported because the boot-time diff reads the specs off the schema plan\n * rather than rebuilding them, and it has to reach the same order the CLI's\n * exclude list and `schema.sql` do — a second sort written out beside this one\n * is a second order the moment either is edited.\n */\nexport const sortIndexSpecs = (specs: readonly CollectionIndexSpec[]): CollectionIndexSpec[] =>\n [...specs].sort((a, b) =>\n a.schema.localeCompare(b.schema) ||\n a.table.localeCompare(b.table) ||\n a.indexName.localeCompare(b.indexName));\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"databasePoolManager-Bj5FbeAs.js","names":[],"sources":["../src/connection.ts","../src/databasePoolManager.ts"],"sourcesContent":["import { Pool, PoolConfig } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Configuration for the Postgres connection pool.\n *\n * Sensible defaults are provided for production Cloud Run / single-instance\n * deployments. Override via environment variables or explicit config.\n */\nexport interface PostgresPoolConfig {\n /** Maximum number of connections in the pool (default: 20) */\n max?: number;\n /** Close idle connections after this many ms (default: 30 000) */\n idleTimeoutMillis?: number;\n /** Abort connection attempts after this many ms (default: 10 000) */\n connectionTimeoutMillis?: number;\n /** Per-query timeout in ms (default: 30 000) */\n queryTimeout?: number;\n /** Per-statement timeout in ms (default: 30 000) */\n statementTimeout?: number;\n /** Enable TCP keep-alive (default: true) */\n keepAlive?: boolean;\n /**\n * `search_path` pinned on every connection (default: `\"public\"`).\n *\n * Pass `false` to send no `search_path` at all and inherit whatever the\n * server/role defaults to. See {@link pinSearchPath} for why the default\n * is not \"inherit\".\n */\n searchPath?: string | false;\n}\n\n/**\n * Ceiling on pool size, from the environment.\n *\n * Exists for the managed development database. PGlite is a single session\n * behind a multiplexing socket server, and two pooled clients holding\n * overlapping transactions deadlock there — which a request-per-transaction\n * server produces the moment two requests overlap. One connection turns that\n * deadlock into ordinary queueing.\n *\n * A ceiling rather than a value, so it constrains every pool in the process\n * without any of them having to know why. Ignored when unset or unparseable,\n * because a malformed limit must not silently serialize a production server.\n */\nexport function poolMaxCeiling(env: NodeJS.ProcessEnv = process.env): number | null {\n const raw = env.REBASE_DB_POOL_MAX;\n if (typeof raw !== \"string\") return null;\n\n // Plain decimal digits only. `Number` would also accept `1e3`, `0x10` and\n // ` 4 ` — none of which anyone writes on purpose in a config value, and all\n // of which would mean this silently did something other than it appears to.\n if (!/^[0-9]+$/.test(raw.trim())) return null;\n\n const parsed = Number(raw.trim());\n\n return parsed >= 1 ? parsed : null;\n}\n\n/** Apply {@link poolMaxCeiling} to a requested pool size. */\nexport function cappedPoolMax(requested: number, env: NodeJS.ProcessEnv = process.env): number {\n const ceiling = poolMaxCeiling(env);\n\n return ceiling === null ? requested : Math.min(requested, ceiling);\n}\n\nconst DEFAULT_POOL: Required<PostgresPoolConfig> = {\n max: 20,\n searchPath: \"public\",\n idleTimeoutMillis: 30_000,\n connectionTimeoutMillis: 10_000,\n // The client-side read timeout MUST be comfortably above the server-side\n // statement_timeout. When the client timer fires first, node-postgres\n // abandons the in-flight statement but keeps the connection — inside a\n // transaction that leaves the tx open (and any pending ROLLBACK is\n // spliced out of the client queue before it ever reaches the wire), so\n // the pooled connection is returned still in-transaction with its RLS\n // GUCs set. The server abort (SQLSTATE 57014) is the clean path; the\n // client timeout is only a backstop for a dead network.\n queryTimeout: 60_000,\n statementTimeout: 30_000,\n keepAlive: true\n};\n\n/** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */\nconst TX_IDLE = \"I\";\n\n/**\n * Pin `search_path` into a connection string, so unqualified SQL resolves to a\n * schema this framework chose rather than to one Postgres inferred.\n *\n * Postgres defaults `search_path` to `\"$user\", public`: the *first* candidate\n * is a schema named after the connecting role. Rebase creates a schema called\n * `rebase` (auth, history, api keys), and every template, compose file and\n * deployment doc names the database role `rebase` too — so `$user` resolves to\n * a schema that exists, and every unqualified statement lands there instead of\n * in `public`. The generated Drizzle schema emits bare `pgTable(\"posts\", …)`\n * for any collection without an explicit `schema`, which makes the *runtime's*\n * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the\n * Studio SQL editor and any hand-written migration are unqualified too. The\n * result is collection tables created in, and served from, `rebase`.\n *\n * Drizzle cannot express the fix on its side: `pgSchema(\"public\")` throws by\n * design (\"just use pgTable() instead\"), so there is no way to emit a\n * public-qualified table from the generator. The pin has to live on the\n * connection.\n *\n * Precedence is deliberate and verified against node-postgres: `options` in\n * the connection string wins over the `options` field passed to `Pool`, so\n * rewriting the URL — rather than setting the field — is what makes this\n * authoritative. Two escape hatches survive it:\n *\n * - an `options` that already mentions `search_path` is left untouched, so a\n * deployment that deliberately pins something else keeps it;\n * - `searchPath: false` (or an unparseable, non-URL connection string) sends\n * nothing and inherits the server default.\n *\n * Anything else in `options` (a `statement_timeout`, say) is preserved and the\n * `search_path` flag is appended to it.\n */\nexport function pinSearchPath(connectionString: string, searchPath: string | false = \"public\"): string {\n if (searchPath === false) return connectionString;\n\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n // Key/value DSNs and anything else we cannot parse are returned as\n // given: a connection that works unpinned beats one we corrupted.\n return connectionString;\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return connectionString;\n\n const existing = url.searchParams.get(\"options\");\n if (existing && /(^|\\s)-c\\s*search_path\\s*=/.test(existing)) return connectionString;\n\n const flag = `-c search_path=${searchPath}`;\n url.searchParams.set(\"options\", existing ? `${existing} ${flag}` : flag);\n // Re-serialize by hand. `URLSearchParams` writes a space as `+`, which\n // node-postgres happens to decode but libpq does not — and this same string\n // is handed to `pg_dump`/`psql` for backups. Percent-encoding is the form\n // both agree on, and is what the scaffolded `.env` already ships.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n return url.toString();\n}\n\n/**\n * Destroy pool clients that are released while still inside a transaction.\n *\n * pg-pool returns a client to the idle list whenever `release()` is called\n * without an error — even if the connection is still mid-transaction (status\n * `T`/`E`). That happens in practice: drizzle's pool transaction releases in\n * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails\n * (e.g. it was queued behind a statement that hit the client-side\n * query_timeout), the client goes back dirty. The next checkout then runs\n * its statements inside the zombie transaction — with the previous request's\n * `app.*` RLS GUCs still applied, which turns unrelated queries into\n * RLS-scoped ones (observed in production as registration failing with\n * SQLSTATE 42501 under a leaked anonymous context).\n *\n * pg-pool emits `release` before it consults its private `_expired` set, so\n * marking the client expired here makes `_release()` destroy it instead of\n * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are\n * private APIs — feature-detect and fall back to loud logging so an upstream\n * change degrades to observability, never to silent corruption.\n */\nexport function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void {\n pool.on(\"release\", (err: Error | undefined, client: unknown) => {\n if (err) return; // errored clients are already destroyed by pg-pool\n const txStatus = (client as { _txStatus?: string | null })?._txStatus;\n if (typeof txStatus !== \"string\" || txStatus === TX_IDLE) return;\n\n // pg-pool keeps expired clients in a WeakSet (a plain object works too\n // if upstream ever changes it — duck-type on add/has).\n const expired = (pool as unknown as { _expired?: { add(c: object): unknown; has(c: object): boolean } })._expired;\n if (expired && typeof expired.add === \"function\" && typeof expired.has === \"function\" && client && typeof client === \"object\") {\n expired.add(client);\n logger.error(\n `[${label}] Client released back to the pool while still in a transaction ` +\n `(status '${txStatus}') — destroying it so the open transaction and its ` +\n `session state (RLS GUCs) cannot leak into the next request.`\n );\n } else {\n logger.error(\n `[${label}] Client released mid-transaction (status '${txStatus}') but the ` +\n `pool's internal expiry set is unavailable (pg-pool internals changed?). ` +\n `The connection may leak its open transaction into subsequent requests.`\n );\n }\n });\n}\n\n/**\n * Create a Drizzle-backed Postgres connection with a production-grade\n * connection pool.\n *\n * @param connectionString Postgres connection URL\n * @param schema Optional Drizzle schema for the relational API\n * @param poolConfig Optional pool tuning (merged over defaults)\n *\n * @returns `{ db, pool, connectionString }` — the `pool` is exposed so\n * callers can register shutdown hooks (`pool.end()`) or monitor\n * pool metrics.\n */\nexport function createPostgresDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = { ...DEFAULT_POOL,\n...poolConfig };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n // ── Pool event logging ────────────────────────────────────────────────\n // Uses console.* because the structured logger lives in server\n // (a separate package). The caller can replace these with the structured\n // logger if desired via pool.on() after creation.\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-pool] Unexpected pool error\", { detail: err.message });\n if (err.message.includes(\"ETIMEDOUT\")) {\n logger.warn(\"[pg-pool] Connection timeout detected — pool will auto-retry\");\n }\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-pool\");\n\n // Create drizzle instance — pass schema when available to enable db.query relational API\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a direct (non-pooled) connection for operations that require\n * session-level features incompatible with PgBouncer transaction mode,\n * such as LISTEN/NOTIFY, prepared statements, or advisory locks.\n *\n * Uses a smaller pool since this is only for specific use cases.\n */\nexport function createDirectDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 5,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-direct-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-direct-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a read-only connection for routing read queries to replicas.\n * Uses a moderate pool size since reads are distributed across replicas.\n */\nexport function createReadReplicaConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 10,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-replica-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-replica-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n","import { Pool } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax } from \"./connection\";\n\nexport class DatabasePoolManager {\n private pools: Map<string, Pool> = new Map();\n private drizzleInstances: Map<string, NodePgDatabase> = new Map();\n public readonly defaultDatabaseName: string;\n private readonly rootConnectionString: string;\n\n constructor(adminConnectionString: string) {\n this.rootConnectionString = adminConnectionString;\n try {\n const url = new URL(adminConnectionString);\n this.defaultDatabaseName = url.pathname.slice(1);\n } catch (e) {\n throw new Error(`Invalid adminConnectionString provided: ${e}`);\n }\n }\n\n public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {\n const existing = this.drizzleInstances.get(databaseName);\n if (existing) {\n return existing;\n }\n\n const pool = this.getPool(databaseName);\n const db = drizzle(pool);\n this.drizzleInstances.set(databaseName, db);\n return db;\n }\n\n public getPool(databaseName: string): Pool {\n if (this.pools.has(databaseName)) {\n return this.pools.get(databaseName)!;\n }\n\n const url = new URL(this.rootConnectionString);\n url.pathname = `/${databaseName}`;\n\n const pool = new Pool({\n // Same pin as the primary pool: these are branch/multi-database\n // connections to the *same* server, so they inherit the same\n // `\"$user\"` hazard. See `pinSearchPath`.\n connectionString: pinSearchPath(url.toString()),\n // Capped by REBASE_DB_POOL_MAX, which the managed development\n // database sets to 1: PGlite multiplexes onto a single session and\n // overlapping transactions deadlock there.\n max: cappedPoolMax(10),\n idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup\n allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process\n });\n\n // Prevent idle client errors from crashing the Node.js process\n pool.on(\"error\", (err) => {\n logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });\n });\n guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);\n\n this.pools.set(databaseName, pool);\n return pool;\n }\n\n /**\n * Disconnect and remove the pool for a specific database.\n * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,\n * which need exclusive access to the target database.\n */\n public async disconnectDatabase(databaseName: string): Promise<void> {\n const pool = this.pools.get(databaseName);\n if (pool) {\n await pool.end();\n this.pools.delete(databaseName);\n this.drizzleInstances.delete(databaseName);\n }\n }\n\n /** Check if a pool exists for a given database name. */\n public hasPool(databaseName: string): boolean {\n return this.pools.has(databaseName);\n }\n\n /**\n * Does this server serve exactly one database?\n *\n * The managed development database is PGlite, and PGlite serves exactly\n * one. `CREATE DATABASE \"rb_feature\" TEMPLATE \"postgres\"` there writes a\n * `pg_database` catalogue row and nothing else: nothing errors, the row is\n * listable, `pg_database_size` answers — and connecting to the \"branch\"\n * lands you in the parent. Every write made in the belief that it is\n * sandboxed goes to the developer's real development database.\n *\n * The CLI already refuses `rebase db branch` on this ground, but it does it\n * by parsing argv, so Studio — which reaches `BranchService` over the\n * websocket — walked straight past it. The connection is the only thing\n * both callers share, so the answer belongs here.\n *\n * Measured, not assumed: `select version()` over the PGlite socket returns\n * `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`, which is how a wasm build\n * announces itself and what `dev-db/constraints.ts` documents. A real\n * Postgres never carries `(PGlite `.\n *\n * Cached, because a live connection cannot change engine underneath us, and\n * a `PostgresBackendDriver` — which builds a `BranchService` — is\n * constructed per transaction.\n */\n public servesOneDatabase(): Promise<boolean> {\n this.oneDatabaseProbe ??= this.probeOneDatabase();\n return this.oneDatabaseProbe;\n }\n\n private oneDatabaseProbe?: Promise<boolean>;\n\n private async probeOneDatabase(): Promise<boolean> {\n try {\n const result = await this.getPool(this.defaultDatabaseName)\n .query<{ version: string }>(\"SELECT version() AS version\");\n return /\\(PGlite\\b/i.test(result.rows[0]?.version ?? \"\");\n } catch (err) {\n // Fail open, and do not cache the failure. The cost of a wrong\n // \"yes\" is refusing branching on a real Postgres that momentarily\n // could not answer; the cost of a wrong \"no\" is one fake branch,\n // which is what this exists to prevent — but a server that cannot\n // run `select version()` is not one we can conclude anything about.\n this.oneDatabaseProbe = undefined;\n logger.warn(\"[DatabasePoolManager] Could not read the server version\", { error: err });\n return false;\n }\n }\n\n public async shutdown(): Promise<void> {\n const promises = [];\n for (const [dbName, pool] of this.pools.entries()) {\n logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);\n promises.push(pool.end());\n }\n await Promise.all(promises);\n this.pools.clear();\n this.drizzleInstances.clear();\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"doctor-C-sYWbmt.js","names":[],"sources":["../../codegen/dist/index.es.js","../src/schema/doctor.ts"],"sourcesContent":["import { getDeclaredSubcollections } from \"@rebasepro/types\";\nimport { effectiveAccess, fieldKeyForColumn, findRelation, isRelationRequired, resolveCollectionRelations, sortCollectionsBySlug } from \"@rebasepro/common\";\n//#region src/utils.ts\n/**\n* Utility functions for the SDK generator\n*/\n/**\n* Convert a slug/snake_case string to PascalCase\n* e.g. \"private_notes\" → \"PrivateNotes\"\n*\n* Capitals already inside a word are meaningful and are kept: lowercasing the\n* tail of every chunk turned \"TestEntities\" into \"Testentities\", which is what\n* ended up in the generated type names. SHOUTING_CASE is the one shape where\n* the tail is not meaningful, so it is folded down.\n*/\nfunction toPascalCase(str) {\n\treturn str.split(/[_\\-\\s]+/).filter(Boolean).map((word) => {\n\t\tconst rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);\n\t\treturn word.charAt(0).toUpperCase() + rest;\n\t}).join(\"\");\n}\n/**\n* Convert a slug/snake_case string to camelCase\n* e.g. \"private_notes\" → \"privateNotes\"\n*/\nfunction toCamelCase(str) {\n\tif (!/[_\\-\\s]/.test(str)) return str.charAt(0).toLowerCase() + str.slice(1);\n\tconst pascal = toPascalCase(str);\n\treturn pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n/**\n* Convert a slug to a safe JS identifier\n* e.g. \"private-notes\" → \"privateNotes\"\n*/\nfunction toSafeIdentifier(str) {\n\treturn toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, \"_\"));\n}\n/**\n* Indent a block of text by a given number of spaces\n*/\nfunction indent(text, spaces) {\n\tconst pad = \" \".repeat(spaces);\n\treturn text.split(\"\\n\").map((line) => line.trim() ? pad + line : line).join(\"\\n\");\n}\n//#endregion\n//#region src/generate-types.ts\n/**\n* A schema that cannot be expressed as a valid TypeScript file.\n*\n* Thrown rather than emitted. The generator used to concatenate whatever it was\n* given, so a slug that collided with another one, or that was not an\n* identifier, produced a file that either failed to compile or — worse —\n* compiled while quietly routing one collection to another's slug.\n*/\nvar CodegenError = class extends Error {\n\tconstructor(message) {\n\t\tsuper(message);\n\t\tthis.name = \"CodegenError\";\n\t}\n};\nvar IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n/**\n* A property name for the emitted TypeScript: verbatim when it is a valid\n* identifier, quoted otherwise.\n*\n* Every key that reaches the output goes through here. Column names are not\n* required to be identifiers — `\"order\"`, `\"user id\"`, a quoted Postgres\n* identifier — and the previous behaviour of camel-casing them into shape\n* renamed the column in the type while the wire kept the original, so the\n* generated `Row` described fields that did not exist.\n*/\nfunction emitKey(key) {\n\treturn IDENTIFIER.test(key) ? key : JSON.stringify(key);\n}\n/**\n* A string literal, escaped.\n*\n* `\"${value}\"` was the previous form. A value containing a quote closed the\n* literal early, which at best broke the file and at worst let a slug from a\n* remote contract inject top-level statements into a file the developer\n* compiles and bundles.\n*/\nfunction emitString(value) {\n\treturn JSON.stringify(value);\n}\n/** The `id`s of an enum declared as an array, an array of `{ id }`, or an object map. */\nfunction enumIds(raw) {\n\tif (Array.isArray(raw)) return raw.map((entry) => entry && typeof entry === \"object\" ? entry.id : entry);\n\tif (raw && typeof raw === \"object\") return Object.keys(raw);\n\treturn [];\n}\nfunction propertyToTypeScriptType(prop) {\n\tswitch (prop.type) {\n\t\tcase \"string\": {\n\t\t\tconst sp = prop;\n\t\t\tif (sp.enum) {\n\t\t\t\tconst ids = enumIds(sp.enum);\n\t\t\t\tif (ids.length === 0) return \"string\";\n\t\t\t\treturn ids.map((v) => emitString(String(v))).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\t\t}\n\t\tcase \"number\": {\n\t\t\tconst np = prop;\n\t\t\tif (np.enum) {\n\t\t\t\tconst ids = enumIds(np.enum);\n\t\t\t\tconst numbers = ids.map(Number);\n\t\t\t\tif (ids.length === 0 || numbers.some((n) => !Number.isFinite(n))) return \"number\";\n\t\t\t\treturn numbers.map((n) => String(n)).join(\" | \");\n\t\t\t}\n\t\t\treturn \"number\";\n\t\t}\n\t\tcase \"boolean\": return \"boolean\";\n\t\tcase \"date\": return \"string\";\n\t\tcase \"geopoint\": return \"{ latitude: number; longitude: number; }\";\n\t\tcase \"reference\": return \"string | number\";\n\t\tcase \"relation\": return \"string | number\";\n\t\tcase \"map\": {\n\t\t\tconst mapProp = prop;\n\t\t\tif (mapProp.properties) return `{ ${Object.entries(mapProp.properties).map(([k, v]) => {\n\t\t\t\tconst child = v;\n\t\t\t\tconst optional = !child.validation?.required;\n\t\t\t\tconst type = propertyToTypeScriptType(child);\n\t\t\t\treturn `${emitKey(k)}${optional ? \"?\" : \"\"}: ${optional ? `${type} | null` : type};`;\n\t\t\t}).join(\" \")} }`;\n\t\t\treturn \"Record<string, unknown>\";\n\t\t}\n\t\tcase \"array\": {\n\t\t\tconst arrProp = prop;\n\t\t\tif (arrProp.of) return `Array<${propertyToTypeScriptType(arrProp.of)}>`;\n\t\t\treturn \"Array<unknown>\";\n\t\t}\n\t\tcase \"vector\": return \"number[]\";\n\t\tcase \"binary\": return \"string\";\n\t\tdefault: return \"unknown\";\n\t}\n}\n/**\n* Unwrap a relation target that arrived as a module namespace rather than the\n* collection itself — `target: () => import(\"./authors\")` is a common slip.\n*/\nfunction resolveTargetCollection(relation) {\n\ttry {\n\t\tlet target = relation.target();\n\t\tif (target && (target.default || target.__esModule)) target = target.default ?? target;\n\t\treturn target;\n\t} catch {\n\t\treturn;\n\t}\n}\n/** The TypeScript type of a foreign key: whatever the target's primary key is. */\nfunction foreignKeyType(relation) {\n\tconst target = resolveTargetCollection(relation);\n\tif (!target?.properties) return \"string | number\";\n\tconst idProp = Object.entries(target.properties).find(([_, p]) => p.isId);\n\tif (!idProp) return \"string | number\";\n\treturn idProp[1].type === \"number\" ? \"number\" : \"string\";\n}\n/**\n* The property's `isId`, on the two property kinds that declare one.\n*\n* `Property` is a union and only `StringProperty` and `NumberProperty` carry\n* `isId`, so it cannot be read off the union directly — which is what\n* `(prop as unknown as Record<string, unknown>).isId` was for, twice. The union\n* discriminates on `type`, so narrowing gets there with no claim at all, and\n* with the declared type (`boolean | \"manual\" | \"uuid\" | string`) intact\n* instead of flattened to `unknown`.\n*/\nfunction declaredIsId(prop) {\n\treturn prop.type === \"string\" || prop.type === \"number\" ? prop.isId : void 0;\n}\n/** Whether a property is the collection's primary key. */\nfunction isPrimaryKey(prop) {\n\treturn Boolean(declaredIsId(prop));\n}\n/**\n* Whether the server assigns this primary key, so a write does not have to.\n* `true` and `\"manual\"` both mean the caller supplies it.\n*/\nfunction isAutoAssignedId(prop) {\n\tconst isId = declaredIsId(prop);\n\treturn Boolean(isId) && isId !== \"manual\" && isId !== true;\n}\n/**\n* The type an *included* relation arrives as: the target's own row, inlined.\n*\n* This is what the read pipeline actually serves — `toRestRow` puts the\n* target's flat columns where the relation was, and the SDK and the HTTP API\n* both go through it. It is deliberately *not* a `{ __type: \"relation\" }`\n* envelope: that shape is the admin's view-model and never reaches a\n* developer's `find()`.\n*\n* Falls back to an open record when the target is not part of this generation\n* run, since there is no `Row` to point at.\n*/\nfunction includedRelationType(relation, accessors) {\n\tconst slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;\n\tconst accessor = slug ? accessors.get(slug) : void 0;\n\tconst rowType = accessor ? `Database[${emitString(accessor)}][\"Row\"]` : \"Record<string, unknown>\";\n\tconst pivot = relation.kind === \"manyToMany\" ? pivotType(relation.through.properties, \"read\") : void 0;\n\tconst elementType = pivot ? `${rowType} & { _pivot?: ${pivot} }` : rowType;\n\treturn relation.cardinality === \"many\" ? `Array<${elementType}>` : elementType;\n}\n/**\n* Map every slug to the property name it is reachable under on `client.data`.\n*\n* The accessor is a safe identifier because `client.data.myNotes` is the point\n* of generating this at all, and `collectionsDictionary` maps it back to the\n* slug the wire uses. Two slugs that safe down to the same identifier cannot\n* both have it: the interface would not compile, and the dictionary — an object\n* literal — would silently keep only the last, routing one collection's reads\n* to the other's table. There is no defensible way to pick, so this refuses.\n*/\nfunction buildAccessors(collections) {\n\tconst accessors = /* @__PURE__ */ new Map();\n\tconst bySafeName = /* @__PURE__ */ new Map();\n\tfor (const collection of collections) {\n\t\tconst slug = collection.slug;\n\t\tif (typeof slug !== \"string\" || slug.length === 0) throw new CodegenError(\"A collection has no slug, so it has no name to generate a type for. Every collection needs a unique `slug`.\");\n\t\tconst safe = toSafeIdentifier(slug);\n\t\tif (safe.length === 0) throw new CodegenError(`The slug ${emitString(slug)} has no characters that can form a property name, so it cannot be reached as \\`client.data.<name>\\`. Use a slug containing letters, digits, underscores or dashes.`);\n\t\tconst existing = bySafeName.get(safe);\n\t\tif (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor \"${safe}\", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);\n\t\tbySafeName.set(safe, slug);\n\t\taccessors.set(slug, safe);\n\t}\n\treturn accessors;\n}\n/**\n* A column's type on a write: its own type, or `null` too when it is not\n* required.\n*\n* `Row` has typed a non-required column `T | null` for a while; its write twins\n* did not, so the one way to clear a value — `update(id, { publishedAt: null })`,\n* `{ authorId: null }` to unassign, the edit-form round trip of a row that came\n* back with nulls in it — was a compile error the server would have accepted.\n* A primary key is never null, required or not.\n*/\nfunction writableType(prop, tsType) {\n\treturn prop.validation?.required || isPrimaryKey(prop) ? tsType : `${tsType} | null`;\n}\n/** One emitted `key: type;` line, already indented. */\nfunction line(key, type, optional) {\n\treturn ` ${emitKey(key)}${optional ? \"?\" : \"\"}: ${type};`;\n}\n/**\n* The keys nobody can reach take off the API surface — in *both* directions.\n*\n* `excludeFromApi` means one thing: the API surface does not mention this\n* property. `Row` already honoured that; `Insert` and `Update` deliberately did\n* not, on the reading that the column is stripped from responses rather than\n* from writes. That left the generated types as the one place a password hash\n* was still named, and it invited a client to send one. The server refuses\n* such a field on a write (`VALIDATION_EXCLUDED_FIELDS`), so nothing generated\n* advertises it.\n*\n* Read through `effectiveAccess`, so the flag and its longhand\n* `access: { read: [], write: [] }` produce the same file. A *role* rule is\n* deliberately not honoured here and `Row` is unchanged by one: a generated type\n* is one shape for every caller, and there is no `Row` that is right for both a\n* reader who holds `hr` and one who does not. The server is the enforcement\n* point; the types describe the surface a caller may name.\n*\n* Keyed by the property name *and* by its column name, the same pair the\n* server's `stripUnreadable` deletes, so a foreign key or a relation addressed\n* under the column name cannot put the property back.\n*/\nfunction excludedApiKeys(properties) {\n\tconst excluded = /* @__PURE__ */ new Set();\n\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\tconst prop = rawProp;\n\t\tconst access = effectiveAccess(prop);\n\t\tif (access?.read?.length !== 0 || access?.write?.length !== 0) continue;\n\t\texcluded.add(key);\n\t\tif (prop.columnName) excluded.add(prop.columnName);\n\t}\n\treturn excluded;\n}\n/**\n* The keys nobody may write: `access: { write: [] }`, readable or not.\n*\n* Off `Insert` and `Update` for the same reason `excludedApiKeys` takes a\n* hidden property off all three — except that a field everyone can read but\n* nobody can write stays on `Row`. The server refuses one on any write with\n* `VALIDATION_EXCLUDED_FIELDS`, for every caller, `admin` included, so a\n* generated `create({ computed })` compiled and could only ever be a 400.\n*\n* A write rule naming *roles* is not this: some caller can write it, and one\n* generated shape cannot say which.\n*/\nfunction unwritableKeys(properties) {\n\tconst unwritable = /* @__PURE__ */ new Set();\n\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\tconst prop = rawProp;\n\t\tif (effectiveAccess(prop)?.write?.length !== 0) continue;\n\t\tunwritable.add(key);\n\t\tif (prop.columnName) unwritable.add(prop.columnName);\n\t}\n\treturn unwritable;\n}\nfunction generateTypedefs(input) {\n\tconst collections = sortCollectionsBySlug(input);\n\tconst accessors = buildAccessors(collections);\n\tconst lines = [\n\t\t\"/**\",\n\t\t\" * This file was auto-generated by Rebase.\",\n\t\t\" * Do not make direct changes to the file.\",\n\t\t\" */\",\n\t\t\"\",\n\t\t\"export interface Database {\"\n\t];\n\tfor (const collection of collections) {\n\t\tconst properties = collection.properties ?? {};\n\t\tlet resolvedRelations = {};\n\t\ttry {\n\t\t\tresolvedRelations = resolveCollectionRelations(collection);\n\t\t} catch (e) {\n\t\t\tconsole.warn(`[rebase] Could not resolve the relations of \"${collection.slug}\", so its generated type has no relation fields and none of their foreign-key columns. This is usually a circular import in the collection files — make sure the target is \\`() => otherCollection\\` and not evaluated at module load.\\n ${e instanceof Error ? e.message : String(e)}`);\n\t\t}\n\t\tconst declared = getDeclaredSubcollections(collection);\n\t\tlet subcollectionCount = 0;\n\t\tif (declared) try {\n\t\t\tsubcollectionCount = declared().length;\n\t\t} catch {\n\t\t\tsubcollectionCount = -1;\n\t\t}\n\t\tif (subcollectionCount !== 0) console.warn(`[rebase] \"${collection.slug}\" declares ${subcollectionCount < 0 ? \"\" : `${subcollectionCount} `}subcollection(s), which are not part of the generated Database: they are reached over a nested path (\\`data/${collection.slug}/<id>/<relation>\\`), not as a top-level accessor. Register a subcollection as a collection of its own if you want a typed accessor for it.`);\n\t\tlines.push(` ${emitKey(accessors.get(collection.slug))}: {`);\n\t\tlines.push(\" Row: {\");\n\t\tconst emittedKeys = /* @__PURE__ */ new Set();\n\t\tconst excluded = excludedApiKeys(properties);\n\t\tfor (const key of excluded) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (excluded.has(key)) continue;\n\t\t\tconst tsType = propertyToTypeScriptType(prop);\n\t\t\tconst isRequired = Boolean(prop.validation?.required) || isPrimaryKey(prop);\n\t\t\tlines.push(line(key, isRequired ? tsType : `${tsType} | null`, !isRequired));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tfor (const [relKey, relation] of Object.entries(resolvedRelations)) if (relation.kind === \"belongsTo\" && relation.localKey) {\n\t\t\tconst fkKey = fieldKeyForColumn(collection, relation.localKey);\n\t\t\tif (emittedKeys.has(fkKey)) continue;\n\t\t\tconst fkType = foreignKeyType(relation);\n\t\t\tconst shadowedByInclude = relKey === fkKey;\n\t\t\tconst tsType = shadowedByInclude ? `${fkType} | ${includedRelationType(relation, accessors)}` : fkType;\n\t\t\tconst isRequired = isRelationRequired(collection, relation) && !shadowedByInclude;\n\t\t\tlines.push(line(fkKey, isRequired ? tsType : `${tsType} | null`, !isRequired));\n\t\t\temittedKeys.add(fkKey);\n\t\t}\n\t\tfor (const [key, relation] of Object.entries(resolvedRelations)) {\n\t\t\tif (emittedKeys.has(key)) continue;\n\t\t\tlines.push(line(key, includedRelationType(relation, accessors), true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tif (rawProp.type !== \"relation\") continue;\n\t\t\tif (emittedKeys.has(key)) continue;\n\t\t\tlines.push(line(key, \"Record<string, unknown>\", true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tlines.push(\" };\");\n\t\tlines.push(\" Insert: {\");\n\t\tconst notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);\n\t\temittedKeys.clear();\n\t\tfor (const key of notWritable) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (notWritable.has(key)) continue;\n\t\t\tconst tsType = propertyToTypeScriptType(prop);\n\t\t\tconst isOptional = !prop.validation?.required || isAutoAssignedId(prop);\n\t\t\tlines.push(line(key, writableType(prop, tsType), isOptional));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\temitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);\n\t\tlines.push(\" };\");\n\t\tlines.push(\" Update: {\");\n\t\temittedKeys.clear();\n\t\tfor (const key of notWritable) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (isPrimaryKey(prop)) continue;\n\t\t\tif (notWritable.has(key)) continue;\n\t\t\tlines.push(line(key, writableType(prop, propertyToTypeScriptType(prop)), true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\temitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);\n\t\tlines.push(\" };\");\n\t\tif (Object.keys(resolvedRelations).length === 0) lines.push(\" Relations: Record<never, never>;\");\n\t\telse lines.push(\" Relations: {\");\n\t\tfor (const [key, relation] of Object.entries(resolvedRelations)) {\n\t\t\tconst slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;\n\t\t\tconst accessor = slug ? accessors.get(slug) : void 0;\n\t\t\tlines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : \"never\"};`);\n\t\t}\n\t\tif (Object.keys(resolvedRelations).length > 0) lines.push(\" };\");\n\t\tlines.push(` Slug: ${emitString(collection.slug)};`);\n\t\tlines.push(\" };\");\n\t}\n\tlines.push(\"}\");\n\tlines.push(\"\");\n\tlines.push(\"export type CollectionName = keyof Database;\");\n\tlines.push(\"\");\n\tlines.push(\"export const collectionsDictionary = {\");\n\tfor (const collection of collections) lines.push(` ${emitKey(accessors.get(collection.slug))}: ${emitString(collection.slug)},`);\n\tlines.push(\"} as const;\");\n\tlines.push(\"\");\n\tlines.push(\"export type CollectionsDictionary = typeof collectionsDictionary;\");\n\tlines.push(\"\");\n\tlines.push(...includeHelperLines());\n\treturn lines.join(\"\\n\");\n}\n/**\n* The `include` type machinery, emitted into the generated file.\n*\n* It lives here rather than in `@rebasepro/types` because it is the *graph*\n* that makes it work, and the graph only exists once a project's collections\n* have been generated. `IncludeSpec` in `@rebasepro/types` is deliberately\n* unconstrained — a hand-written row type has no relations in it to check\n* against — and these narrow it for a project that does have them.\n*\n* Two things a caller gets:\n*\n* - `IncludeFor<\"posts\">` — an include whose keys are relations that exist,\n* recursively, so `include: { comments: { include: { authr: true } } }` is a\n* compile error rather than a 400 at runtime.\n* - `RowWith<\"posts\", I>` — the row a read with that include returns, where\n* every included relation is **present** rather than optional. `Row` types a\n* relation as optional because it is absent from every read that did not ask\n* for it; once a read has asked, `row.author.name` should not need a `?.`.\n*\n* The depth bound matches the server's (`MAX_INCLUDE_DEPTH`), and is spelled as\n* a decrementing tuple because TypeScript has no arithmetic — `Prev[3]` is `2`.\n* Without a bound, a self-referencing relation makes the type infinite and the\n* compiler gives up with \"type instantiation is excessively deep\".\n*/\nfunction includeHelperLines() {\n\treturn [\n\t\t\"/** Which collection each of `A`'s relations reaches. */\",\n\t\t\"export type RelationsOf<A extends CollectionName> =\",\n\t\t\" Database[A] extends { Relations: infer R } ? R : Record<string, never>;\",\n\t\t\"\",\n\t\t\"/** The names of `A`'s relations. */\",\n\t\t\"export type RelationKeys<A extends CollectionName> = keyof RelationsOf<A> & string;\",\n\t\t\"\",\n\t\t\"/** The collection a relation reaches, or `never` when it left this project. */\",\n\t\t\"export type RelationTarget<A extends CollectionName, K extends RelationKeys<A>> =\",\n\t\t\" RelationsOf<A>[K] extends CollectionName ? RelationsOf<A>[K] : never;\",\n\t\t\"\",\n\t\t\"/** Counts `IncludeFor` down; TypeScript has no arithmetic. */\",\n\t\t\"type Prev = [never, 0, 1, 2, 3];\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * Per-relation options, minus `include` — which `IncludeFor` supplies at\",\n\t\t\" * the next depth so the nested keys are checked against the *target's*\",\n\t\t\" * relations rather than this collection's.\",\n\t\t\" */\",\n\t\t\"export interface IncludeOptionsFor<A extends CollectionName, K extends RelationKeys<A>, D extends number> {\",\n\t\t\" limit?: number;\",\n\t\t\" where?: Record<string, unknown>;\",\n\t\t\" logical?: unknown;\",\n\t\t\" orderBy?: unknown;\",\n\t\t\" fields?: string[];\",\n\t\t\" include?: RelationTarget<A, K> extends CollectionName\",\n\t\t\" ? IncludeFor<RelationTarget<A, K>, Prev[D]>\",\n\t\t\" : never;\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * An `include` for collection `A`: its relation names, dotted paths, or the\",\n\t\t\" * parametrised tree — with every key checked against the relations that\",\n\t\t\" * actually exist, at every level.\",\n\t\t\" */\",\n\t\t\"export type IncludeFor<A extends CollectionName, D extends number = 3> =\",\n\t\t\" D extends 0\",\n\t\t\" ? never\",\n\t\t\" : | readonly (RelationKeys<A> | \\\"*\\\")[]\",\n\t\t\" | { [K in RelationKeys<A>]?: true | IncludeOptionsFor<A, K, D> };\",\n\t\t\"\",\n\t\t\"/** The relation names an include asks for at the top level. */\",\n\t\t\"type IncludedKeys<A extends CollectionName, I> =\",\n\t\t\" I extends readonly (infer K)[]\",\n\t\t\" ? Extract<K, RelationKeys<A>>\",\n\t\t\" : Extract<keyof I, RelationKeys<A>>;\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * The row a read with include `I` returns: `Row`, with every included\",\n\t\t\" * relation made **required**.\",\n\t\t\" *\",\n\t\t\" * `Row` types a relation as optional because it is absent from every read\",\n\t\t\" * that did not name it. Once a read has named it, it is there — and having\",\n\t\t\" * to write `row.author?.name` after asking for the author is the type\",\n\t\t\" * describing a possibility the query already ruled out.\",\n\t\t\" */\",\n\t\t\"export type RowWith<A extends CollectionName, I> =\",\n\t\t\" Database[A][\\\"Row\\\"] & Required<Pick<Database[A][\\\"Row\\\"], IncludedKeys<A, I> & keyof Database[A][\\\"Row\\\"]>>;\",\n\t\t\"\"\n\t];\n}\n/**\n* The two ways a write can name a `belongsTo` target, both of which the server\n* accepts: the foreign key under its own wire name (`{ authorId: 5 }`, which\n* passes through to the `author_id` column untouched) and the relation\n* *property* (`{ author: 5 }`, which the write transformer maps onto that\n* column).\n*\n* Only the first was generated, so the documented and idiomatic write shape was\n* a type error.\n*\n* The second form is emitted under the **property key**, not the resolved\n* relation name, because that is what the transformer keys off: it looks the\n* payload key up in `properties` and only treats it as a relation if what it\n* finds there is one. A relation whose `relationName` differs from its property\n* key is reachable as the property and not as the name, so emitting the name\n* would have offered a key that writes to a column that does not exist.\n*/\nfunction emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {\n\tconst emit = (key, relation) => {\n\t\tif (emittedKeys.has(key)) return;\n\t\tconst required = isRelationRequired(collection, relation);\n\t\tconst type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;\n\t\tlines.push(line(key, type, allOptional || !required));\n\t\temittedKeys.add(key);\n\t};\n\tfor (const relation of Object.values(resolvedRelations)) if (relation.kind === \"belongsTo\" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);\n\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\tif (rawProp.type !== \"relation\") continue;\n\t\tconst relation = findRelation(resolvedRelations, key);\n\t\tif (relation?.kind === \"belongsTo\" && relation.localKey) emit(key, relation);\n\t}\n\tfor (const [key, relation] of Object.entries(resolvedRelations)) {\n\t\tif (relation.cardinality !== \"many\" || !relation.writable) continue;\n\t\tif (emittedKeys.has(key)) continue;\n\t\tconst fk = foreignKeyType(relation);\n\t\tconst pivot = relation.kind === \"manyToMany\" ? pivotType(relation.through.properties, \"write\") : void 0;\n\t\tconst element = `${fk} | { id: ${fk}${pivot ? `; _pivot?: ${pivot}` : \"\"} }`;\n\t\tlines.push(line(key, `Array<${element}>`, true));\n\t\temittedKeys.add(key);\n\t}\n}\n/**\n* The `_pivot` a many-to-many link carries: the junction's payload columns,\n* typed as a row's columns are on a read and as a write's on a write. `undefined`\n* when the junction declares none, so there is no `_pivot` to name.\n*/\nfunction pivotType(pivotProperties, direction) {\n\tconst entries = Object.entries(pivotProperties);\n\tif (entries.length === 0) return void 0;\n\treturn `{ ${entries.map(([key, rawProp]) => {\n\t\tconst prop = rawProp;\n\t\tconst tsType = propertyToTypeScriptType(prop);\n\t\tconst required = Boolean(prop.validation?.required);\n\t\tconst type = direction === \"write\" ? writableType(prop, tsType) : required ? tsType : `${tsType} | null`;\n\t\treturn `${emitKey(key)}${required && direction === \"read\" ? \"\" : \"?\"}: ${type};`;\n\t}).join(\" \")} }`;\n}\n//#endregion\n//#region src/index.ts\nfunction generateSDK(collections, options = {}) {\n\tconst files = [];\n\tfiles.push({\n\t\tpath: \"database.types.ts\",\n\t\tcontent: generateTypedefs(collections)\n\t});\n\tif (options.includeReadme !== false) files.push({\n\t\tpath: \"README.md\",\n\t\tcontent: `# Rebase SDK\n\n> Auto-generated by \\`rebase generate-sdk\\`. Do not edit manually.\n\n## Usage\n\n1. Install the client package:\n \\`\\`\\`bash\n npm install @rebasepro/client\n \\`\\`\\`\n\n2. Initialize with your generated types:\n \\`\\`\\`typescript\n import { createRebaseClient } from '@rebasepro/client';\n import { collectionsDictionary, type Database } from './database.types';\n\n const rebase = createRebaseClient<Database>({\n baseUrl: 'http://localhost:3001',\n // Maps each accessor back to the slug the wire uses. Without it a\n // hyphenated slug is not resolvable from the property name alone.\n collections: collectionsDictionary,\n });\n\n // Property access is the typed surface: rows, filters and sorts are all\n // checked against the generated Database.\n const { data: users } = await rebase.data.users.find();\n console.log(users[0].email); // flat access — no .values wrapper\n \\`\\`\\`\n\n## Field names are the ones the API serves\n\nThe generated \\`Row\\` uses each field's **wire** name — the key it arrives under in\nJSON — and nothing here renames anything.\n\n- **A declared property is its key in the collection.** A property keyed\n \\`createdAt\\` is \\`row.createdAt\\`, whatever \\`columnName\\` says. A column name is\n the name of a different thing: where the value lives, not what the API calls it.\n- **A foreign key derived from a relation is camelCase**, because that is what\n the wire carries. A \\`belongsTo\\` named \\`author\\` gives you \\`row.authorId\\`, not\n the column spelling.\n- **A collection accessor is camelCase too** (\\`my-notes\\` → \\`rebase.data.myNotes\\`),\n which is what \\`collectionsDictionary\\` maps back to the slug.\n\n\\`where\\` and \\`orderBy\\` are keyed off the same type, so what compiles is what the\nbackend answers to.\n\n## \\`Row\\` vs \\`Insert\\` vs \\`Update\\`\n\n| Type | What it describes |\n|---|---|\n| \\`Row\\` | What a read serves. Nullable columns are \\`T \\\\| null\\`; relations appear only when \\`include\\` names them. |\n| \\`Insert\\` | What \\`create()\\` accepts. Server-assigned ids are optional; a \\`belongsTo\\` target may be named either way (\\`{ author: 5 }\\` or \\`{ authorId: 5 }\\`). |\n| \\`Update\\` | What \\`update()\\` accepts. Everything optional, and the primary key is not settable. |\n\nA property marked \\`excludeFromApi\\` is absent from all three: the API surface\ndoes not mention it, in either direction. The server holds the same line — a\nread never serves the column and a write naming it is refused — so this is a\nguarantee rather than a description, and nothing generated names a password\nhash.\n\nIf you need an untyped escape hatch, \\`rebase.data.collection(slug)\\` still works —\nbut it is generic over \\`Record<string, unknown>\\` and gives up everything above.\n`\n\t});\n\treturn files;\n}\n//#endregion\nexport { CodegenError, generateSDK, generateTypedefs, indent, toCamelCase, toPascalCase, toSafeIdentifier };\n\n//# sourceMappingURL=index.es.js.map","/**\n * Rebase Schema Doctor — Three-way schema drift detection.\n *\n * Compares:\n * 1. Collection definitions → Generated Drizzle schema (staleness check)\n * 2. Collection definitions → Live PostgreSQL database (structural drift)\n *\n * Run via: rebase doctor\n */\nimport { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport chalk from \"chalk\";\nimport { computeSchemaVersion, CollectionConfig, isPostgresCollectionConfig, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty, type ResolvedManyToMany, type ResolvedBelongsTo, isManyToMany } from \"@rebasepro/types\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { compareGeneratedDeclarations, describeDeclarationDifferences } from \"./generated-schema-staleness\";\nimport { columnPgType, planSchema } from \"./plan/plan-schema\";\nimport { renderPgType } from \"./plan/render-ddl\";\nimport type { PgType, SchemaPlan, TablePlan } from \"./plan/types\";\nimport { generateTypedefs } from \"@rebasepro/codegen\";\nimport { getTableName, resolveCollectionRelations, findRelation, relationalCollections, resolveJunctionSpecs } from \"@rebasepro/common\";\nimport { toPostgresIdentifier, toSnakeCase } from \"@rebasepro/utils\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\n// The report is CLI output, not application logging — see cli-output.ts.\nimport { out, outError } from \"../cli-output\";\nimport { deepestErrorMessage, diagnoseDbError, parseHostInfo } from \"../cli-errors\";\n\n/**\n * Resolve the SQL column name for a property.\n * Uses the explicit `columnName` when set (e.g. from introspection),\n * falling back to `toSnakeCase(propName)` for manually-authored collections.\n */\nconst resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport type IssueSeverity = \"error\" | \"warning\" | \"info\";\n\nexport interface DoctorIssue {\n severity: IssueSeverity;\n category: \"missing_table\" | \"missing_column\" | \"type_mismatch\" | \"missing_constraint\" | \"primary_key_mismatch\" | \"schema_stale\" | \"missing_enum\" | \"enum_value_mismatch\" | \"missing_foreign_key\" | \"orphaned_junction\" | \"sdk_stale\" | \"sdk_not_generated\" | \"sdk_ungeneratable\";\n table?: string;\n column?: string;\n expected?: string;\n actual?: string;\n message: string;\n fix: string;\n}\n\nexport interface DoctorPhase {\n passed: boolean;\n issues: DoctorIssue[];\n /**\n * Why this phase never ran, when it did not.\n *\n * A check that did not happen is a third state, not a passing one. While a\n * skipped phase initialised to `{ passed: true, issues: [] }` it rendered as\n * `✅ Collections → Database: In sync` and counted towards\n * `✓ All schemas are in sync!` — so a project whose connection string was\n * spelled `POSTGRES_URL`, or a CI job that never exported one, got two green\n * ticks and exit 0 against a database with no tables in it.\n */\n skipped?: string;\n /**\n * Why this phase had nothing to compare against, when it had nothing.\n *\n * Distinct from `skipped`, which means \"the check could not run, and that\n * is probably worth fixing\". This one means \"the artifact is optional and\n * you have not asked for it\": no drift is possible, so the run is still a\n * clean bill of health.\n *\n * It exists because the alternative was a contradiction. The typed-SDK\n * phase returned `{ passed: true }` when `generated/sdk/database.types.ts`\n * did not exist, so a fresh project's report read\n * `✅ Collections → SDK Types: In sync` directly above\n * `ℹ Typed SDK not generated (optional).` — one line calling a file\n * synchronised and the next saying it is absent. \"In sync\" is a claim about\n * a comparison, and no comparison happened.\n */\n notApplicable?: string;\n /**\n * The phase was skipped by a fault rather than by a choice.\n *\n * `skipped` covers both: \"no DATABASE_URL, so nothing to compare against\"\n * is a local situation a developer may be perfectly happy with, and \"the\n * DATABASE_URL you set refuses connections\" is not. The report renders them\n * the same way — a check that did not run is a check that did not run — but\n * the exit code must not, or `rebase doctor` in CI goes green against a\n * database it never reached.\n */\n blocked?: boolean;\n}\n\nexport interface DoctorReport {\n collectionsToSchema: DoctorPhase;\n collectionsToSdk: DoctorPhase;\n schemaToDatabase: DoctorPhase;\n summary: { passed: number; skipped: number; notApplicable: number; warnings: number; errors: number };\n}\n\n// ── Column type mapping ──────────────────────────────────────────────────\n\n/**\n * How `information_schema.columns.data_type` spells a planned column type.\n *\n * The catalogue's names are its own — `character varying`, not `varchar`;\n * `ARRAY` for every array whatever its element; `USER-DEFINED` for an enum, a\n * `vector`, and anything else identified by name rather than by shape. So the\n * translation has to exist somewhere. What must *not* exist twice is the step\n * before it: which column a property compiles to.\n *\n * This function was that second step — a fourth copy of the property switch,\n * kept \"in sync\" with the generators by a comment, and already out of step\n * (`geopoint` answered `null` here, so the one type that was genuinely\n * unpersistable was the one the doctor never checked). It reads the schema plan\n * now.\n */\nconst INFORMATION_SCHEMA_NAMES: Record<PgType[\"kind\"], string> = {\n text: \"text\",\n varchar: \"character varying\",\n char: \"character\",\n uuid: \"uuid\",\n // A `CREATE TYPE … AS ENUM` is a name, not a shape.\n enum: \"USER-DEFINED\",\n smallint: \"smallint\",\n integer: \"integer\",\n bigint: \"bigint\",\n // A serial is an integer with a sequence default; the catalogue reports the\n // underlying width and nothing about the sequence.\n smallserial: \"smallint\",\n serial: \"integer\",\n bigserial: \"bigint\",\n real: \"real\",\n doublePrecision: \"double precision\",\n numeric: \"numeric\",\n boolean: \"boolean\",\n timestamptz: \"timestamp with time zone\",\n date: \"date\",\n time: \"time without time zone\",\n json: \"json\",\n jsonb: \"jsonb\",\n // pgvector's type, like every extension type.\n vector: \"USER-DEFINED\",\n bytea: \"bytea\",\n tsvector: \"USER-DEFINED\",\n array: \"ARRAY\"\n};\n\n/**\n * The `data_type` the database should report for a property's column, or `null`\n * when there is nothing to compare.\n *\n * `null` covers the two honest cases: a `relation`, whose column is derived from\n * the relation rather than from the property, and a configuration the planner\n * refuses outright — which is a problem the doctor's other phases report with a\n * message, not a type mismatch to invent one for.\n */\nexport function getExpectedColumnType(prop: Property): string | null {\n // The FK column a relation owns is planned from the relation, and its type\n // is the *target's* key — which a bare property cannot resolve.\n if (prop.type === \"relation\") return null;\n\n // A one-property collection, because the planner asks the questions a\n // column needs in context: is this the primary key, what is the enum type\n // called, what does the link point at. A `reference` whose target is not\n // here resolves to `text`, exactly as it does in a real bundle.\n const collection = {\n slug: \"__doctor__\",\n table: \"__doctor__\",\n properties: { __column__: prop }\n } as unknown as CollectionConfig;\n\n try {\n const type = columnPgType(\"__column__\", prop, collection, () => undefined);\n return INFORMATION_SCHEMA_NAMES[type.kind];\n } catch {\n return null;\n }\n}\n\n// ── Collection loading ───────────────────────────────────────────────────\n\n/**\n * Re-exported so callers keep importing it from here, but the implementation is\n * now shared with the runtime and the generators — a doctor that disagreed with\n * the policy generator about which files are collections would compare the\n * wrong thing.\n */\nexport async function loadCollections(collectionsPath: string): Promise<CollectionConfig[]> {\n return loadCollectionsFromDirectory(collectionsPath);\n}\n\n// ── Phase 1: Collections ↔ Generated Schema ─────────────────────────────\n\nexport async function checkCollectionsVsSchema(\n collections: CollectionConfig[],\n schemaFilePath: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // Check if schema file exists\n if (!fs.existsSync(schemaFilePath)) {\n issues.push({\n severity: \"error\",\n category: \"schema_stale\",\n message: \"Generated schema file does not exist.\",\n fix: \"Run `rebase schema generate`\"\n });\n return { passed: false,\nissues };\n }\n\n // Re-generate schema in-memory and compare with file on disk. Only the\n // collections the generator itself will emit — a Firestore collection has\n // no table in the generated file and must not be reported as missing one.\n const postgresCollections = relationalCollections(collections);\n if (postgresCollections.length === 0) {\n return { passed: true,\nissues };\n }\n\n try {\n // Declaration by declaration, not byte by byte. The generated file is a\n // flat list of `export const`s, each the rendering of one piece of the\n // schema plan, so comparing them says *which* table or enum moved —\n // which is what the reader is about to go looking for — and ignores the\n // formatting and commentary the renderer is free to vary on its own.\n const differences = compareGeneratedDeclarations(\n generateSchema(postgresCollections),\n await fsPromises.readFile(schemaFilePath, \"utf-8\")\n );\n\n if (differences.length > 0) {\n issues.push({\n severity: \"warning\",\n category: \"schema_stale\",\n message: \"Generated schema is out of date — collection definitions have changed since last generation. \"\n + `Differs in: ${describeDeclarationDifferences(differences)}.`,\n fix: \"Run `rebase schema generate`\"\n });\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n issues.push({\n severity: \"warning\",\n category: \"schema_stale\",\n message: `Could not regenerate schema for comparison: ${message}`,\n fix: \"Run `rebase schema generate` to verify\"\n });\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\nexport async function checkCollectionsVsSdk(\n collections: CollectionConfig[],\n sdkFilePath: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // The typed SDK is opt-in — nothing in a scaffolded project imports it until\n // you choose to. A project that never generated one isn't drifting, so report\n // it as information rather than a warning; otherwise `doctor` can never come\n // back clean on a fresh project and users learn to ignore its output.\n if (!fs.existsSync(sdkFilePath)) {\n issues.push({\n severity: \"info\",\n category: \"sdk_not_generated\",\n message: \"Typed SDK not generated (optional).\",\n fix: \"Run `rebase generate-sdk` if you want typed collection access\"\n });\n return { passed: true,\nissues,\nnotApplicable: \"not generated (optional)\" };\n }\n\n try {\n const expectedSdk = generateTypedefs(collections);\n const actualSdk = await fsPromises.readFile(sdkFilePath, \"utf-8\");\n\n // Normalize whitespace for comparison\n const normalize = (s: string) =>\n s\n .replace(/\\/\\/.*$/gm, \"\") // strip single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // strip multi-line comments\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (normalize(expectedSdk) !== normalize(actualSdk)) {\n issues.push({\n severity: \"warning\",\n category: \"sdk_stale\",\n message: \"Generated SDK types are out of date — collection definitions have changed since last SDK generation.\",\n fix: \"Run `rebase generate-sdk`\"\n });\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n\n // A `CodegenError` is not \"the comparison did not run\" — it is the\n // schema saying it cannot produce a valid typed client at all, most\n // often two slugs collapsing onto one accessor. Reporting that as a\n // warning about staleness described the wrong problem and pointed at a\n // command that would fail the same way.\n if (err instanceof Error && err.name === \"CodegenError\") {\n issues.push({\n severity: \"error\",\n category: \"sdk_ungeneratable\",\n message: `The typed SDK cannot be generated from these collections: ${message}`,\n fix: \"Fix the collection definitions named above, then run `rebase generate-sdk`\"\n });\n } else {\n issues.push({\n severity: \"warning\",\n category: \"sdk_stale\",\n message: `Could not regenerate SDK types for comparison: ${message}`,\n fix: \"Run `rebase generate-sdk` to verify\"\n });\n }\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\n// ── Phase 2: Collections ↔ Database ──────────────────────────────────────\n\ninterface DbColumn {\n table_schema: string;\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n udt_name: string;\n}\n\n\ninterface DbEnumValue {\n enum_schema: string;\n enum_name: string;\n enum_value: string;\n}\n\n/**\n * Extensions the collections need, that the database does not have installed.\n *\n * Only `vector` today, and only because a `{ type: \"vector\" }` property plans a\n * column of a type that will not exist. Rebase installs it only where a project\n * has said it may (`database({ extensions: [\"vector\"] })`), so the absence is\n * often correct configuration and a wrong deployment — which is exactly why the\n * report has to name it rather than the first failing INSERT.\n *\n * Exported for its test.\n */\nexport function missingExtensionIssues(\n collections: CollectionConfig[],\n installed: ReadonlySet<string>\n): DoctorIssue[] {\n const usesVector = collections.some(collection =>\n Object.values(collection.properties ?? {})\n .some(property => (property as { type?: string } | null)?.type === \"vector\"));\n\n if (!usesVector || installed.has(\"vector\")) return [];\n\n return [{\n severity: \"error\",\n category: \"missing_table\",\n message: 'A collection declares a `{ type: \"vector\" }` property and the pgvector extension is not installed on this database.',\n fix: 'Declare it — `database({ extensions: [\"vector\"] })` in config/resources.ts — and use an image that ships the library '\n + \"(the scaffold's pgvector/pgvector:pg18; a stock postgres:18 does not). Or install it once by hand: CREATE EXTENSION vector;\"\n }];\n}\n\n/**\n * The stamp the runtime wrote, against the collections on disk.\n *\n * A stamp that disagrees means this database was provisioned from a different\n * set of collections than the ones in this checkout — a colleague's branch, an\n * older deploy, a tenant somebody else migrated. Everything else in this report\n * is then describing a schema somebody else created, which is worth knowing\n * before acting on any of it.\n *\n * A warning, not an error, and never a direction: the stamp is a hash, so it\n * can say the two disagree and never which is ahead. `null` — never stamped —\n * is not a finding: every database provisioned before the stamp existed reads\n * that way, and so does every fresh one.\n */\nexport async function schemaStampIssues(\n pool: { query<T>(text: string): Promise<{ rows: T[] }> },\n collections: CollectionConfig[]\n): Promise<DoctorIssue[]> {\n let stamped: string | null = null;\n try {\n // `to_regclass` so a missing schema or table is a null rather than a\n // thrown 42P01 — the commonest state on a fresh database is neither.\n const present = await pool.query<{ present: boolean }>(\n `SELECT to_regclass('\"rebase\".\"schema_meta\"') IS NOT NULL AS present`\n );\n if (!present.rows[0]?.present) return [];\n const row = await pool.query<{ value: string }>(\n `SELECT value FROM \"rebase\".\"schema_meta\" WHERE key = 'collections_schema_version' LIMIT 1`\n );\n stamped = row.rows[0]?.value ?? null;\n } catch {\n // The stamp is a diagnostic, not a gate. A database that will not\n // answer this question has bigger problems, and they are reported by\n // the checks around this one.\n return [];\n }\n\n if (!stamped) return [];\n const expected = computeSchemaVersion(collections);\n if (stamped === expected) return [];\n\n return [{\n severity: \"warning\",\n category: \"schema_stale\",\n message: `This database was provisioned from a different set of collections (stamp ${stamped.slice(0, 12)}…, this checkout computes ${expected.slice(0, 12)}…).`,\n fix: \"A hash says the two disagree, never which is ahead. Check you are pointed at the database you think you are; then `rebase db push` to bring it to these collections.\"\n }];\n}\n\n/** `\"public\".\"posts\"` — quoted, for a statement the reader will paste. */\nconst quotedTable = (schema: string, table: string): string => `\"${schema}\".\"${table}\"`;\n\n/**\n * What to do about a column whose type is not the one the collection plans.\n *\n * Both ways out, because the doctor cannot know which side is right: the\n * property moved, or the database did. Putting the property back always clears\n * the finding; converting the column clears it when every value converts.\n */\nexport function typeMismatchFix(\n schema: string,\n table: string,\n column: string,\n actual: string,\n planned: PgType | undefined\n): string {\n const back = `Either put the property back to the column's type (${actual}), or`;\n if (!planned) return `${back} run a migration that converts the column.`;\n const type = renderPgType(planned);\n return `${back} convert the column when you can watch it (every value must convert): `\n + `ALTER TABLE ${quotedTable(schema, table)} ALTER COLUMN \"${column}\" TYPE ${type} USING \"${column}\"::${type};`;\n}\n\n/**\n * The table's primary key against the one the collection reads it by.\n *\n * Rebase addresses a row by the key the plan names — the `isId` property, or\n * the implicit `id` — so a table keyed on other columns, or on none, serves\n * reads and writes against a key that does not hold. An error, because that is\n * what it is at runtime.\n */\nexport function primaryKeyIssues(tablePlan: TablePlan, dbPk: string[], displayName: string): DoctorIssue[] {\n const expected = tablePlan.primaryKey;\n const same = dbPk.length === expected.length && [...dbPk].sort().join(\"\\u0000\") === [...expected].sort().join(\"\\u0000\");\n if (same) return [];\n const table = quotedTable(tablePlan.schema, tablePlan.table);\n const keyList = expected.map(c => `\"${c}\"`).join(\", \");\n if (dbPk.length === 0) {\n return [{\n severity: \"error\",\n category: \"primary_key_mismatch\",\n table: displayName,\n expected: expected.join(\", \"),\n actual: \"(none)\",\n message: `Table \"${displayName}\" has no primary key; the collection reads its rows by ${keyList}.`,\n fix: `ALTER TABLE ${table} ADD PRIMARY KEY (${keyList});`\n }];\n }\n // Several `isId` properties are one composite key, so a table keyed on\n // several columns is matched by marking each of them — and only them.\n const only = expected.some(column => !dbPk.includes(column)) ? \"only \" : \"\";\n const fix = dbPk.length === 1\n ? `Mark ${only}the property for column \"${dbPk[0]}\" with \\`isId\\` — the table is keyed on it — or re-key the table on ${keyList}.`\n : `Mark ${only}the properties for columns ${dbPk.map(c => `\"${c}\"`).join(\", \")} with \\`isId\\` — together they are `\n + `the table's composite key — or re-key the table on ${keyList}.`;\n return [{\n severity: \"error\",\n category: \"primary_key_mismatch\",\n table: displayName,\n expected: expected.join(\", \"),\n actual: dbPk.join(\", \"),\n message: `Table \"${displayName}\" is keyed on (${dbPk.join(\", \")}), but the collection reads its rows by ${keyList}.`,\n fix\n }];\n}\n\n/**\n * A junction's two key columns: present, typed like the keys they point at,\n * and the table keyed on the pair. Order is not compared — boot and `db push`\n * have built the key in both orders, and either one serves the same rows.\n */\nexport function junctionKeyIssues(\n junctionPlan: TablePlan,\n dbColumns: DbColumn[],\n dbPk: string[],\n displayName: string\n): DoctorIssue[] {\n const issues: DoctorIssue[] = [];\n const byName = new Map(dbColumns.map(c => [c.column_name, c]));\n for (const column of junctionPlan.columns) {\n const dbCol = byName.get(column.column);\n if (!dbCol) {\n issues.push({\n severity: \"error\",\n category: \"missing_column\",\n table: displayName,\n column: column.column,\n message: `Junction table \"${displayName}\" has no \"${column.column}\" column.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n continue;\n }\n const expected = INFORMATION_SCHEMA_NAMES[column.type.kind];\n if (dbCol.data_type !== expected) {\n issues.push({\n severity: \"warning\",\n category: \"type_mismatch\",\n table: displayName,\n column: column.column,\n expected,\n actual: dbCol.data_type,\n message: `Junction column \"${column.column}\" in \"${displayName}\" is ${dbCol.data_type}, but the key it points at is ${expected}.`,\n fix: typeMismatchFix(junctionPlan.schema, junctionPlan.table, column.column, dbCol.data_type, column.type)\n });\n }\n }\n issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));\n return issues;\n}\n\nexport async function checkCollectionsVsDatabase(\n collections: CollectionConfig[],\n databaseUrl: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // Dynamic import to avoid loading pg when not needed\n const pgModule = await import(\"pg\");\n const { Pool } = pgModule.default ?? pgModule;\n const pool = new Pool({ connectionString: databaseUrl });\n\n // Determine all schemas defined by the collections, plus public and rebase\n const schemas = Array.from(new Set([\n \"public\",\n \"rebase\",\n ...relationalCollections(collections)\n .filter(isPostgresCollectionConfig)\n .map(c => c.schema)\n .filter((s): s is string => !!s)\n ]));\n\n try {\n // Fetch all tables in the defined schemas\n const tablesResult = await pool.query<{ table_schema: string; table_name: string }>(\n `SELECT table_schema, table_name \n FROM information_schema.tables \n WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,\n [schemas]\n );\n const existingTables = new Set(tablesResult.rows.map((r) =>\n r.table_schema === \"public\" ? r.table_name : `${r.table_schema}.${r.table_name}`\n ));\n\n // An extension the collections need and the database does not have.\n //\n // The symptom without this is `type \"vector\" does not exist` on the\n // first write to a table that looks perfectly well-formed in the\n // report: the column is planned, the extension is not, and the two\n // facts live in different places. Extensions are opt-in by design —\n // installing one needs an image that ships the library and a role\n // allowed to install it — so this reports, it does not install.\n const extensionsResult = await pool.query<{ extname: string }>(\"SELECT extname FROM pg_extension\");\n const installedExtensions = new Set(extensionsResult.rows.map(r => r.extname));\n issues.push(...missingExtensionIssues(collections, installedExtensions));\n\n // A database provisioned from a different set of collections than the\n // ones on disk.\n //\n // The runtime stamps what it applied; nothing compared it outside boot,\n // and a stamp that disagrees is exactly the state where the rest of\n // this report is describing a schema somebody else's deploy created.\n issues.push(...await schemaStampIssues(pool, collections));\n\n // Fetch all columns in the defined schemas\n const columnsResult = await pool.query<DbColumn>(\n `SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name\n FROM information_schema.columns\n WHERE table_schema = ANY($1)\n ORDER BY table_schema, table_name, ordinal_position`,\n [schemas]\n );\n const columnsByTable = new Map<string, DbColumn[]>();\n for (const row of columnsResult.rows) {\n const tableSchema = row.table_schema;\n const tableName = row.table_name;\n const key = tableSchema === \"public\" ? tableName : `${tableSchema}.${tableName}`;\n if (!columnsByTable.has(key)) {\n columnsByTable.set(key, []);\n }\n columnsByTable.get(key)!.push(row);\n }\n\n // Fetch enums, keyed `schema.name`. A type is found in the schema the\n // planner put it in, and nowhere else — a same-named type in another\n // schema is somebody else's, and the column does not use it.\n const enumsResult = await pool.query<DbEnumValue>(\n `SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value\n FROM pg_type t\n JOIN pg_namespace n ON n.oid = t.typnamespace\n JOIN pg_enum e ON t.oid = e.enumtypid\n ORDER BY n.nspname, t.typname, e.enumsortorder`\n );\n const enumsByName = new Map<string, string[]>();\n for (const row of enumsResult.rows) {\n const key = `${row.enum_schema}.${row.enum_name}`;\n if (!enumsByName.has(key)) {\n enumsByName.set(key, []);\n }\n enumsByName.get(key)!.push(row.enum_value);\n }\n\n // Every table's primary key, its columns in key order.\n const pksResult = await pool.query<{ table_schema: string; table_name: string; columns: string[] }>(\n `SELECT n.nspname AS table_schema, c.relname AS table_name,\n array_agg(a.attname::text ORDER BY array_position(i.indkey::int2[], a.attnum)) AS columns\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey::int2[])\n WHERE i.indisprimary AND n.nspname = ANY($1)\n GROUP BY n.nspname, c.relname`,\n [schemas]\n );\n const pksByTable = new Map<string, string[]>();\n for (const row of pksResult.rows) {\n pksByTable.set(row.table_schema === \"public\" ? row.table_name : `${row.table_schema}.${row.table_name}`, row.columns);\n }\n\n // Fetch foreign key constraints in the defined schemas\n const fksResult = await pool.query<{\n constraint_name: string;\n table_schema: string;\n table_name: string;\n column_name: string;\n foreign_table_schema: string;\n foreign_table_name: string;\n foreign_column_name: string;\n }>(\n `SELECT\n tc.constraint_name,\n tc.table_schema,\n tc.table_name,\n kcu.column_name,\n ccu.table_schema AS foreign_table_schema,\n ccu.table_name AS foreign_table_name,\n ccu.column_name AS foreign_column_name\n FROM information_schema.table_constraints AS tc\n JOIN information_schema.key_column_usage AS kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage AS ccu\n ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`,\n [schemas]\n );\n const fksByTable = new Map<string, typeof fksResult.rows>();\n for (const row of fksResult.rows) {\n const tableSchema = row.table_schema;\n const tableName = row.table_name;\n const key = tableSchema === \"public\" ? tableName : `${tableSchema}.${tableName}`;\n if (!fksByTable.has(key)) {\n fksByTable.set(key, []);\n }\n fksByTable.get(key)!.push(row);\n }\n\n // ── Compare each collection against the database ─────────────────\n\n const postgresCollections = relationalCollections(collections);\n const junctionSpecs = resolveJunctionSpecs(postgresCollections);\n\n // What push and boot build, read once. The key and the junction checks\n // compare against it rather than against a fifth reading of the\n // properties. A configuration the planner refuses has no plan; the\n // other phases name that problem, and these checks stand down.\n let plan: SchemaPlan | undefined;\n try {\n plan = planSchema(postgresCollections);\n } catch {\n plan = undefined;\n }\n const tablePlans = new Map<string, TablePlan>((plan?.tables ?? []).map(t => [t.qualified, t]));\n const checkedJunctions = new Set<string>();\n\n for (const collection of postgresCollections) {\n const tableName = getTableName(collection);\n const schemaName = (collection as { schema?: string }).schema || \"public\";\n const fullTableName = schemaName === \"public\" ? tableName : `${schemaName}.${tableName}`;\n\n // Check table existence\n if (!existingTables.has(fullTableName)) {\n issues.push({\n severity: \"error\",\n category: \"missing_table\",\n table: fullTableName,\n message: `Table \"${fullTableName}\" does not exist in the database.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n continue; // Skip column checks for missing tables\n }\n\n const dbColumns = columnsByTable.get(fullTableName) ?? [];\n const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));\n\n // The key. It used to be skipped outright — `id` sat in a list of\n // \"system columns\" — so a key whose type or whose columns had\n // drifted reported \"In sync\".\n const tablePlan = tablePlans.get(`${schemaName}.${tableName}`);\n if (tablePlan) {\n issues.push(...primaryKeyIssues(tablePlan, pksByTable.get(fullTableName) ?? [], fullTableName));\n }\n\n // Check properties → columns\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (prop.type === \"relation\") {\n // ResolvedRelation columns are derived from localKey\n const resolvedRelations = resolveCollectionRelations(collection);\n const relation = findRelation(resolvedRelations, (prop as RelationProperty).relation?.relationName ?? propName);\n if (relation?.kind === \"belongsTo\") {\n const fkColName = relation.localKey;\n if (!dbColumnMap.has(fkColName)) {\n issues.push({\n severity: \"error\",\n category: \"missing_column\",\n table: fullTableName,\n column: fkColName,\n message: `Foreign key column \"${fkColName}\" for relation \"${propName}\" is missing from table \"${fullTableName}\".`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n }\n\n // Check FK constraint exists\n const tableFks = fksByTable.get(fullTableName) ?? [];\n let targetTableName = \"unknown\";\n let targetSchemaName = \"public\";\n try {\n const targetColl = relation.target();\n targetTableName = getTableName(targetColl);\n targetSchemaName = (targetColl as { schema?: string }).schema || \"public\";\n } catch { /* ignore */ }\n\n const hasFk = tableFks.some((fk) =>\n fk.column_name === fkColName &&\n fk.foreign_table_name === targetTableName &&\n fk.foreign_table_schema === targetSchemaName\n );\n\n if (dbColumnMap.has(fkColName) && !hasFk) {\n issues.push({\n severity: \"warning\",\n category: \"missing_foreign_key\",\n table: fullTableName,\n column: fkColName,\n message: `Column \"${fkColName}\" exists but has no FOREIGN KEY constraint referencing \"${targetSchemaName === \"public\" ? targetTableName : `${targetSchemaName}.${targetTableName}`}\".`,\n fix: \"Run `rebase db push` or add the constraint manually\"\n });\n }\n }\n continue;\n }\n\n const colName = resolveColumnName(propName, prop);\n\n const dbCol = dbColumnMap.get(colName);\n if (!dbCol) {\n issues.push({\n severity: \"error\",\n category: \"missing_column\",\n table: fullTableName,\n column: colName,\n message: `Column \"${colName}\" is defined in collection \"${collection.slug}\" but missing from table \"${fullTableName}\".`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n continue;\n }\n\n // Type check\n const expectedType = getExpectedColumnType(prop);\n if (expectedType) {\n const actualType = dbCol.data_type;\n let isMismatch = actualType !== expectedType;\n if (prop.type === \"vector\" && dbCol.udt_name !== \"vector\") {\n isMismatch = true;\n }\n if (prop.type === \"array\") {\n const ap = prop as ArrayProperty;\n let expectedColType = ap.columnType;\n if (!expectedColType && ap.of && !Array.isArray(ap.of)) {\n const ofProp = ap.of as Property;\n if (ofProp.type === \"string\") expectedColType = \"text[]\";\n else if (ofProp.type === \"number\") expectedColType = ofProp.validation?.integer ? \"integer[]\" : \"numeric[]\";\n else if (ofProp.type === \"boolean\") expectedColType = \"boolean[]\";\n }\n if (expectedColType && expectedColType.endsWith(\"[]\")) {\n if (actualType !== \"ARRAY\") {\n isMismatch = true;\n } else {\n const expectedUdt = expectedColType === \"text[]\" ? \"_text\" :\n expectedColType === \"integer[]\" ? \"_int4\" :\n expectedColType === \"boolean[]\" ? \"_bool\" :\n expectedColType === \"numeric[]\" ? \"_numeric\" : \"\";\n if (expectedUdt && dbCol.udt_name !== expectedUdt) {\n isMismatch = true;\n }\n }\n }\n }\n if (isMismatch) {\n const planned = tablePlan?.columns.find(c => c.column === colName)?.type;\n issues.push({\n severity: \"warning\",\n category: \"type_mismatch\",\n table: fullTableName,\n column: colName,\n expected: prop.type === \"vector\" ? \"vector\" : expectedType,\n actual: dbCol.udt_name === \"vector\" ? \"vector\" : actualType,\n message: `Column \"${colName}\" in table \"${fullTableName}\": expected type \"${prop.type === \"vector\" ? \"vector\" : expectedType}\" but found \"${dbCol.udt_name === \"vector\" ? \"vector\" : actualType}\".`,\n fix: typeMismatchFix(schemaName, tableName, colName, actualType, planned)\n });\n }\n }\n\n // Enum value check\n if (prop.type === \"string\" && (prop as StringProperty).enum) {\n const enumValues = (prop as StringProperty).enum;\n if (enumValues) {\n // The planner's name: `<table>_<column>`, cut to the 63\n // bytes Postgres keeps, in the collection's schema.\n const enumName = toPostgresIdentifier(`${tableName}_${colName}`);\n const dbEnumValues = enumsByName.get(`${schemaName}.${enumName}`);\n if (!dbEnumValues) {\n issues.push({\n severity: \"warning\",\n category: \"missing_enum\",\n table: fullTableName,\n column: colName,\n expected: enumName,\n message: `Enum type \"${schemaName}\".\"${enumName}\" is defined in collection but not found in the database.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n } else {\n // Compare enum values\n const expectedValues = Array.isArray(enumValues)\n ? enumValues.map((v) => (typeof v === \"string\" ? v : String(v.id)))\n : Object.keys(enumValues);\n\n const missing = expectedValues.filter((v) => !dbEnumValues.includes(v));\n const extra = dbEnumValues.filter((v) => !expectedValues.includes(v));\n\n if (missing.length > 0 || extra.length > 0) {\n const parts: string[] = [];\n if (missing.length > 0) parts.push(`missing: ${missing.join(\", \")}`);\n if (extra.length > 0) parts.push(`extra in DB: ${extra.join(\", \")}`);\n issues.push({\n severity: \"warning\",\n category: \"enum_value_mismatch\",\n table: fullTableName,\n column: colName,\n expected: expectedValues.join(\", \"),\n actual: dbEnumValues.join(\", \"),\n message: `Enum values for \"${colName}\" in table \"${fullTableName}\" are out of sync (${parts.join(\"; \")}).`,\n fix: \"Run `rebase db push` to update the enum\"\n });\n }\n }\n }\n }\n }\n\n // Also check junction tables for many-to-many relations — where\n // `resolveJunctionSpecs` says they are, which is where boot and\n // `db push` create them: `public`, whatever schema the declaring\n // collection is in, under the name with any schema prefix removed.\n const resolvedRelations = resolveCollectionRelations(collection);\n for (const relation of Object.values(resolvedRelations)) {\n if (isManyToMany(relation)) {\n const junctionTable = relation.through.table.split(\".\").pop() ?? relation.through.table;\n const junctionSchema = junctionSpecs.get(junctionTable)?.schema ?? \"public\";\n const fullJunctionTable = junctionSchema === \"public\" ? junctionTable : `${junctionSchema}.${junctionTable}`;\n if (!existingTables.has(fullJunctionTable)) {\n issues.push({\n severity: \"error\",\n category: \"missing_table\",\n table: fullJunctionTable,\n message: `Junction table \"${fullJunctionTable}\" for many-to-many relation \"${relation.relationName}\" is missing.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n } else if (!checkedJunctions.has(fullJunctionTable)) {\n // Both sides declare the same junction; it is checked once.\n checkedJunctions.add(fullJunctionTable);\n const junctionPlan = tablePlans.get(`${junctionSchema}.${junctionTable}`);\n if (junctionPlan) {\n issues.push(...junctionKeyIssues(\n junctionPlan,\n columnsByTable.get(fullJunctionTable) ?? [],\n pksByTable.get(fullJunctionTable) ?? [],\n fullJunctionTable\n ));\n }\n }\n }\n }\n }\n } finally {\n await pool.end();\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\n// ── Report Rendering ─────────────────────────────────────────────────────\n\nexport function renderReport(report: DoctorReport): void {\n out();\n out(chalk.bold(\" 🩺 Rebase Schema Doctor\"));\n out(chalk.gray(\" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"));\n out();\n\n // Phase 1\n renderPhase(\"Collections → Generated Schema\", report.collectionsToSchema);\n\n // Phase 2\n renderPhase(\"Collections → Database\", report.schemaToDatabase);\n\n // Phase 3\n renderPhase(\"Collections → SDK Types\", report.collectionsToSdk);\n\n // Summary\n out(chalk.gray(\" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"));\n const { passed, skipped, notApplicable, warnings, errors } = report.summary;\n\n const parts: string[] = [];\n parts.push(chalk.green(`${passed} passed`));\n if (skipped > 0) parts.push(chalk.yellow(`${skipped} skipped`));\n if (notApplicable > 0) parts.push(chalk.gray(`${notApplicable} not applicable`));\n if (warnings > 0) parts.push(chalk.yellow(`${warnings} warnings`));\n if (errors > 0) parts.push(chalk.red(`${errors} errors`));\n\n out(` Summary: ${parts.join(\", \")}`);\n out();\n\n if (errors > 0) {\n out(chalk.red.bold(\" ✗ Schema drift detected. Run the suggested fixes above.\"));\n } else if (warnings > 0) {\n out(chalk.yellow.bold(\" ⚠ Minor issues detected. Consider running the suggested fixes.\"));\n } else if (skipped > 0) {\n // Never \"all schemas are in sync\" off the back of a check that did not\n // run: a clean bill of health has to come from measurement.\n out(chalk.yellow.bold(` ⚠ Nothing wrong in the checks that ran, but ${skipped} did not run — this is not a clean bill of health.`));\n } else {\n out(chalk.green.bold(\" ✓ All schemas are in sync!\"));\n }\n out();\n}\n\nfunction renderPhase(label: string, phase: DoctorPhase): void {\n // A phase that never ran gets its own marker and its reason. It is neither\n // \"In sync\" nor a drift report; conflating it with the former is what let\n // doctor certify a database it had not opened.\n if (phase.skipped) {\n out(` ${chalk.yellow(\"⏭\")} ${label}: ${chalk.yellow(`skipped (${phase.skipped})`)}`);\n out();\n return;\n }\n\n // Nothing to compare against. Its own marker, and only the remedy under it\n // — the phase header already carries the reason, so re-printing the note\n // would say the same thing twice. See DoctorPhase.notApplicable.\n if (phase.notApplicable) {\n out(` ${chalk.gray(\"➖\")} ${label}: ${chalk.gray(phase.notApplicable)}`);\n for (const issue of phase.issues.filter((i) => i.severity === \"info\")) {\n out(` ${chalk.gray(issue.fix)}`);\n }\n out();\n return;\n }\n\n const issues = phase.issues;\n const errorCount = issues.filter((i) => i.severity === \"error\").length;\n const warnCount = issues.filter((i) => i.severity === \"warning\").length;\n const infoIssues = issues.filter((i) => i.severity === \"info\");\n\n // Informational notes don't make a phase unhealthy, so key the header off\n // real problems rather than `phase.passed` alone.\n if (errorCount === 0 && warnCount === 0) {\n out(` ${chalk.green(\"✅\")} ${label}: ${chalk.green(\"In sync\")}`);\n } else {\n const parts: string[] = [];\n if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? \"s\" : \"\"}`);\n if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? \"s\" : \"\"}`);\n out(` ${chalk.yellow(\"⚠️\")} ${label}: ${chalk.yellow(parts.join(\", \"))}`);\n }\n out();\n\n // Notes render as a quiet one-liner, not a full drift box.\n for (const issue of infoIssues) {\n const fixPart = issue.fix ? chalk.gray(` — ${issue.fix}`) : \"\";\n out(` ${chalk.gray(\"ℹ\")} ${chalk.gray(issue.message)}${fixPart}`);\n out();\n }\n\n for (const issue of issues.filter((i) => i.severity !== \"info\")) {\n const severityIcon = issue.severity === \"error\" ? chalk.red(\"✗\") : chalk.yellow(\"⚠\");\n const categoryLabel = formatCategory(issue.category);\n out(` ${chalk.gray(\"┌─\")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray(\"─\".repeat(Math.max(0, 42 - categoryLabel.length)))}`);\n\n if (issue.table) {\n const colPart = issue.column ? ` │ Column: ${chalk.cyan(issue.column)}` : \"\";\n out(` ${chalk.gray(\"│\")} Table: ${chalk.cyan(issue.table)}${colPart}`);\n }\n\n if (issue.expected && issue.actual) {\n out(` ${chalk.gray(\"│\")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);\n }\n\n out(` ${chalk.gray(\"│\")} ${issue.message}`);\n out(` ${chalk.gray(\"│\")} Fix: ${chalk.blue(issue.fix)}`);\n out(` ${chalk.gray(\"└\" + \"─\".repeat(48))}`);\n out();\n }\n}\n\nfunction formatCategory(cat: DoctorIssue[\"category\"]): string {\n const labels: Record<DoctorIssue[\"category\"], string> = {\n missing_table: \"Missing Table\",\n missing_column: \"Missing Column\",\n type_mismatch: \"Type Mismatch\",\n missing_constraint: \"Missing Constraint\",\n primary_key_mismatch: \"Primary Key Mismatch\",\n schema_stale: \"Stale Schema\",\n missing_enum: \"Missing Enum\",\n enum_value_mismatch: \"Enum Value Mismatch\",\n missing_foreign_key: \"Missing Foreign Key\",\n orphaned_junction: \"Orphaned Junction Table\",\n sdk_stale: \"Stale SDK Types\",\n sdk_not_generated: \"SDK Types Not Generated\",\n sdk_ungeneratable: \"SDK Cannot Be Generated\"\n };\n return labels[cat];\n}\n\n// ── Main entry point ─────────────────────────────────────────────────────\n\nexport async function runDoctor(options: {\n collectionsPath: string;\n schemaPath: string;\n sdkPath: string;\n databaseUrl?: string;\n}): Promise<DoctorReport> {\n out();\n out(chalk.bold(\" 🩺 Loading collections...\"));\n const collections = await loadCollections(options.collectionsPath);\n if (collections.length === 0) {\n outError(chalk.red(\" ✗ No collections found.\"));\n process.exit(1);\n }\n out(chalk.gray(` Found ${collections.length} collection(s)`));\n out();\n\n // Phase 1: Collections ↔ Generated Schema\n out(chalk.gray(\" Checking Collections → Generated Schema...\"));\n const collectionsToSchema = await checkCollectionsVsSchema(collections, options.schemaPath);\n\n // Phase 2: Collections ↔ Database (only if we have a DATABASE_URL)\n let schemaToDatabase: DoctorPhase;\n if (options.databaseUrl) {\n out(chalk.gray(\" Checking Collections → Database...\"));\n try {\n schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);\n } catch (err: unknown) {\n // A database that cannot be reached used to end the whole run: the\n // error escaped `runDoctor` and the CLI's last-resort catch printed\n // a stack. So the two phases that need no database — the generated\n // schema and the SDK types, both of which are read off disk — never\n // ran, and the one command whose job is to say what is wrong said\n // only \"Doctor failed\" over a drizzle stack trace.\n const banner = diagnoseDbError(err, options.databaseUrl);\n if (banner) outError(banner);\n const reason = deepestErrorMessage(err)\n ?? (err instanceof Error ? err.message : String(err));\n schemaToDatabase = {\n passed: false,\n issues: [],\n skipped: `could not reach ${parseHostInfo(options.databaseUrl)} — ${reason}`,\n blocked: true\n };\n }\n } else {\n // Not `{ passed: true }`: see DoctorPhase.skipped.\n schemaToDatabase = { passed: false,\nissues: [],\nskipped: \"DATABASE_URL not set\" };\n out(chalk.yellow(\" ⚠ DATABASE_URL not set — skipping database comparison.\"));\n out(chalk.gray(\" Set DATABASE_URL in your .env to enable full drift detection.\"));\n }\n\n // Phase 3: Collections ↔ SDK Types\n out(chalk.gray(\" Checking Collections → SDK Types...\"));\n const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);\n\n const phases = [collectionsToSchema, schemaToDatabase, collectionsToSdk];\n const allIssues = phases.flatMap((p) => p.issues);\n const summary = {\n // A skipped phase is not a passing one, however few issues it collected.\n // Nor is one with nothing to compare against: \"passed\" would put a\n // never-generated SDK in the same column as a verified one.\n passed: phases.filter((p) => p.passed && !p.skipped && !p.notApplicable).length,\n skipped: phases.filter((p) => p.skipped).length,\n notApplicable: phases.filter((p) => p.notApplicable && !p.skipped).length,\n warnings: allIssues.filter((i) => i.severity === \"warning\").length,\n errors: allIssues.filter((i) => i.severity === \"error\").length\n };\n\n const report: DoctorReport = { collectionsToSchema,\ncollectionsToSdk,\nschemaToDatabase,\nsummary };\n renderReport(report);\n\n return report;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,SAAS,aAAa,KAAK;CAC1B,OAAO,IAAI,MAAM,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,SAAS;EAC1D,MAAM,OAAO,cAAc,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC;EAClF,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI;CACvC,CAAC,CAAC,CAAC,KAAK,EAAE;AACX;;;;;AAKA,SAAS,YAAY,KAAK;CACzB,IAAI,CAAC,UAAU,KAAK,GAAG,GAAG,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,MAAM,CAAC;CAC1E,MAAM,SAAS,aAAa,GAAG;CAC/B,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,OAAO,MAAM,CAAC;AACvD;;;;;AAKA,SAAS,iBAAiB,KAAK;CAC9B,OAAO,YAAY,IAAI,QAAQ,kBAAkB,GAAG,CAAC;AACtD;;;;;;;;;AAkBA,IAAI,eAAe,cAAc,MAAM;CACtC,YAAY,SAAS;EACpB,MAAM,OAAO;EACb,KAAK,OAAO;CACb;AACD;AACA,IAAI,aAAa;;;;;;;;;;;AAWjB,SAAS,QAAQ,KAAK;CACrB,OAAO,WAAW,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AACvD;;;;;;;;;AASA,SAAS,WAAW,OAAO;CAC1B,OAAO,KAAK,UAAU,KAAK;AAC5B;;AAEA,SAAS,QAAQ,KAAK;CACrB,IAAI,MAAM,QAAQ,GAAG,GAAG,OAAO,IAAI,KAAK,UAAU,SAAS,OAAO,UAAU,WAAW,MAAM,KAAK,KAAK;CACvG,IAAI,OAAO,OAAO,QAAQ,UAAU,OAAO,OAAO,KAAK,GAAG;CAC1D,OAAO,CAAC;AACT;AACA,SAAS,yBAAyB,MAAM;CACvC,QAAQ,KAAK,MAAb;EACC,KAAK,UAAU;GACd,MAAM,KAAK;GACX,IAAI,GAAG,MAAM;IACZ,MAAM,MAAM,QAAQ,GAAG,IAAI;IAC3B,IAAI,IAAI,WAAW,GAAG,OAAO;IAC7B,OAAO,IAAI,KAAK,MAAM,WAAW,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;GACxD;GACA,OAAO;EACR;EACA,KAAK,UAAU;GACd,MAAM,KAAK;GACX,IAAI,GAAG,MAAM;IACZ,MAAM,MAAM,QAAQ,GAAG,IAAI;IAC3B,MAAM,UAAU,IAAI,IAAI,MAAM;IAC9B,IAAI,IAAI,WAAW,KAAK,QAAQ,MAAM,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG,OAAO;IACzE,OAAO,QAAQ,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;GAChD;GACA,OAAO;EACR;EACA,KAAK,WAAW,OAAO;EACvB,KAAK,QAAQ,OAAO;EACpB,KAAK,YAAY,OAAO;EACxB,KAAK,aAAa,OAAO;EACzB,KAAK,YAAY,OAAO;EACxB,KAAK,OAAO;GACX,MAAM,UAAU;GAChB,IAAI,QAAQ,YAAY,OAAO,KAAK,OAAO,QAAQ,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,OAAO;IACtF,MAAM,QAAQ;IACd,MAAM,WAAW,CAAC,MAAM,YAAY;IACpC,MAAM,OAAO,yBAAyB,KAAK;IAC3C,OAAO,GAAG,QAAQ,CAAC,IAAI,WAAW,MAAM,GAAG,IAAI,WAAW,GAAG,KAAK,WAAW,KAAK;GACnF,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;GACb,OAAO;EACR;EACA,KAAK,SAAS;GACb,MAAM,UAAU;GAChB,IAAI,QAAQ,IAAI,OAAO,SAAS,yBAAyB,QAAQ,EAAE,EAAE;GACrE,OAAO;EACR;EACA,KAAK,UAAU,OAAO;EACtB,KAAK,UAAU,OAAO;EACtB,SAAS,OAAO;CACjB;AACD;;;;;AAKA,SAAS,wBAAwB,UAAU;CAC1C,IAAI;EACH,IAAI,SAAS,SAAS,OAAO;EAC7B,IAAI,WAAW,OAAO,WAAW,OAAO,aAAa,SAAS,OAAO,WAAW;EAChF,OAAO;CACR,QAAQ;EACP;CACD;AACD;;AAEA,SAAS,eAAe,UAAU;CACjC,MAAM,SAAS,wBAAwB,QAAQ;CAC/C,IAAI,CAAC,QAAQ,YAAY,OAAO;CAChC,MAAM,SAAS,OAAO,QAAQ,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,IAAI;CACxE,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,OAAO,EAAE,CAAC,SAAS,WAAW,WAAW;AACjD;;;;;;;;;;;AAWA,SAAS,aAAa,MAAM;CAC3B,OAAO,KAAK,SAAS,YAAY,KAAK,SAAS,WAAW,KAAK,OAAO,KAAK;AAC5E;;AAEA,SAAS,aAAa,MAAM;CAC3B,OAAO,QAAQ,aAAa,IAAI,CAAC;AAClC;;;;;AAKA,SAAS,iBAAiB,MAAM;CAC/B,MAAM,OAAO,aAAa,IAAI;CAC9B,OAAO,QAAQ,IAAI,KAAK,SAAS,YAAY,SAAS;AACvD;;;;;;;;;;;;;AAaA,SAAS,qBAAqB,UAAU,WAAW;CAClD,MAAM,OAAO,wBAAwB,QAAQ,CAAC,EAAE,QAAQ,SAAS;CACjE,MAAM,WAAW,OAAO,UAAU,IAAI,IAAI,IAAI,KAAK;CACnD,MAAM,UAAU,WAAW,YAAY,WAAW,QAAQ,EAAE,YAAY;CACxE,MAAM,QAAQ,SAAS,SAAS,eAAe,UAAU,SAAS,QAAQ,YAAY,MAAM,IAAI,KAAK;CACrG,MAAM,cAAc,QAAQ,GAAG,QAAQ,gBAAgB,MAAM,MAAM;CACnE,OAAO,SAAS,gBAAgB,SAAS,SAAS,YAAY,KAAK;AACpE;;;;;;;;;;;AAWA,SAAS,eAAe,aAAa;CACpC,MAAM,4BAA4B,IAAI,IAAI;CAC1C,MAAM,6BAA6B,IAAI,IAAI;CAC3C,KAAK,MAAM,cAAc,aAAa;EACrC,MAAM,OAAO,WAAW;EACxB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG,MAAM,IAAI,aAAa,6GAA6G;EACvL,MAAM,OAAO,iBAAiB,IAAI;EAClC,IAAI,KAAK,WAAW,GAAG,MAAM,IAAI,aAAa,YAAY,WAAW,IAAI,EAAE,mKAAmK;EAC9O,MAAM,WAAW,WAAW,IAAI,IAAI;EACpC,IAAI,aAAa,KAAK,GAAG,MAAM,IAAI,aAAa,mBAAmB,WAAW,QAAQ,EAAE,OAAO,WAAW,IAAI,EAAE,+BAA+B,KAAK,uJAAuJ;EAC3S,WAAW,IAAI,MAAM,IAAI;EACzB,UAAU,IAAI,MAAM,IAAI;CACzB;CACA,OAAO;AACR;;;;;;;;;;;AAWA,SAAS,aAAa,MAAM,QAAQ;CACnC,OAAO,KAAK,YAAY,YAAY,aAAa,IAAI,IAAI,SAAS,GAAG,OAAO;AAC7E;;AAEA,SAAS,KAAK,KAAK,MAAM,UAAU;CAClC,OAAO,SAAS,QAAQ,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI,KAAK;AAC7D;;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,gBAAgB,YAAY;CACpC,MAAM,2BAA2B,IAAI,IAAI;CACzC,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;EACxD,MAAM,OAAO;EACb,MAAM,SAAS,gBAAgB,IAAI;EACnC,IAAI,QAAQ,MAAM,WAAW,KAAK,QAAQ,OAAO,WAAW,GAAG;EAC/D,SAAS,IAAI,GAAG;EAChB,IAAI,KAAK,YAAY,SAAS,IAAI,KAAK,UAAU;CAClD;CACA,OAAO;AACR;;;;;;;;;;;;;AAaA,SAAS,eAAe,YAAY;CACnC,MAAM,6BAA6B,IAAI,IAAI;CAC3C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;EACxD,MAAM,OAAO;EACb,IAAI,gBAAgB,IAAI,CAAC,EAAE,OAAO,WAAW,GAAG;EAChD,WAAW,IAAI,GAAG;EAClB,IAAI,KAAK,YAAY,WAAW,IAAI,KAAK,UAAU;CACpD;CACA,OAAO;AACR;AACA,SAAS,iBAAiB,OAAO;CAChC,MAAM,cAAc,sBAAsB,KAAK;CAC/C,MAAM,YAAY,eAAe,WAAW;CAC5C,MAAM,QAAQ;EACb;EACA;EACA;EACA;EACA;EACA;CACD;CACA,KAAK,MAAM,cAAc,aAAa;EACrC,MAAM,aAAa,WAAW,cAAc,CAAC;EAC7C,IAAI,oBAAoB,CAAC;EACzB,IAAI;GACH,oBAAoB,2BAA2B,UAAU;EAC1D,SAAS,GAAG;GACX,QAAQ,KAAK,gDAAgD,WAAW,KAAK,4OAA4O,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAAG;EACtW;EACA,MAAM,WAAW,0BAA0B,UAAU;EACrD,IAAI,qBAAqB;EACzB,IAAI,UAAU,IAAI;GACjB,qBAAqB,SAAS,CAAC,CAAC;EACjC,QAAQ;GACP,qBAAqB;EACtB;EACA,IAAI,uBAAuB,GAAG,QAAQ,KAAK,aAAa,WAAW,KAAK,aAAa,qBAAqB,IAAI,KAAK,GAAG,mBAAmB,GAAG,8GAA8G,WAAW,KAAK,2IAA2I;EACrZ,MAAM,KAAK,KAAK,QAAQ,UAAU,IAAI,WAAW,IAAI,CAAC,EAAE,IAAI;EAC5D,MAAM,KAAK,YAAY;EACvB,MAAM,8BAA8B,IAAI,IAAI;EAC5C,MAAM,WAAW,gBAAgB,UAAU;EAC3C,KAAK,MAAM,OAAO,UAAU,YAAY,IAAI,GAAG;EAC/C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,SAAS,IAAI,GAAG,GAAG;GACvB,MAAM,SAAS,yBAAyB,IAAI;GAC5C,MAAM,aAAa,QAAQ,KAAK,YAAY,QAAQ,KAAK,aAAa,IAAI;GAC1E,MAAM,KAAK,KAAK,KAAK,aAAa,SAAS,GAAG,OAAO,UAAU,CAAC,UAAU,CAAC;GAC3E,YAAY,IAAI,GAAG;EACpB;EACA,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,iBAAiB,GAAG,IAAI,SAAS,SAAS,eAAe,SAAS,UAAU;GAC3H,MAAM,QAAQ,kBAAkB,YAAY,SAAS,QAAQ;GAC7D,IAAI,YAAY,IAAI,KAAK,GAAG;GAC5B,MAAM,SAAS,eAAe,QAAQ;GACtC,MAAM,oBAAoB,WAAW;GACrC,MAAM,SAAS,oBAAoB,GAAG,OAAO,KAAK,qBAAqB,UAAU,SAAS,MAAM;GAChG,MAAM,aAAa,mBAAmB,YAAY,QAAQ,KAAK,CAAC;GAChE,MAAM,KAAK,KAAK,OAAO,aAAa,SAAS,GAAG,OAAO,UAAU,CAAC,UAAU,CAAC;GAC7E,YAAY,IAAI,KAAK;EACtB;EACA,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,GAAG;GAChE,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,KAAK,KAAK,KAAK,qBAAqB,UAAU,SAAS,GAAG,IAAI,CAAC;GACrE,YAAY,IAAI,GAAG;EACpB;EACA,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,IAAI,QAAQ,SAAS,YAAY;GACjC,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,KAAK,KAAK,KAAK,2BAA2B,IAAI,CAAC;GACrD,YAAY,IAAI,GAAG;EACpB;EACA,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,eAAe;EAC1B,MAAM,8BAA8B,IAAI,IAAI,CAAC,GAAG,UAAU,GAAG,eAAe,UAAU,CAAC,CAAC;EACxF,YAAY,MAAM;EAClB,KAAK,MAAM,OAAO,aAAa,YAAY,IAAI,GAAG;EAClD,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,SAAS,yBAAyB,IAAI;GAC5C,MAAM,aAAa,CAAC,KAAK,YAAY,YAAY,iBAAiB,IAAI;GACtE,MAAM,KAAK,KAAK,KAAK,aAAa,MAAM,MAAM,GAAG,UAAU,CAAC;GAC5D,YAAY,IAAI,GAAG;EACpB;EACA,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,KAAK;EAC1F,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,eAAe;EAC1B,YAAY,MAAM;EAClB,KAAK,MAAM,OAAO,aAAa,YAAY,IAAI,GAAG;EAClD,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,aAAa,IAAI,GAAG;GACxB,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,KAAK,KAAK,KAAK,aAAa,MAAM,yBAAyB,IAAI,CAAC,GAAG,IAAI,CAAC;GAC9E,YAAY,IAAI,GAAG;EACpB;EACA,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,IAAI;EACzF,MAAM,KAAK,QAAQ;EACnB,IAAI,OAAO,KAAK,iBAAiB,CAAC,CAAC,WAAW,GAAG,MAAM,KAAK,sCAAsC;OAC7F,MAAM,KAAK,kBAAkB;EAClC,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,GAAG;GAChE,MAAM,OAAO,wBAAwB,QAAQ,CAAC,EAAE,QAAQ,SAAS;GACjE,MAAM,WAAW,OAAO,UAAU,IAAI,IAAI,IAAI,KAAK;GACnD,MAAM,KAAK,SAAS,QAAQ,GAAG,EAAE,IAAI,WAAW,WAAW,QAAQ,IAAI,QAAQ,EAAE;EAClF;EACA,IAAI,OAAO,KAAK,iBAAiB,CAAC,CAAC,SAAS,GAAG,MAAM,KAAK,QAAQ;EAClE,MAAM,KAAK,aAAa,WAAW,WAAW,IAAI,EAAE,EAAE;EACtD,MAAM,KAAK,MAAM;CAClB;CACA,MAAM,KAAK,GAAG;CACd,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,8CAA8C;CACzD,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,wCAAwC;CACnD,KAAK,MAAM,cAAc,aAAa,MAAM,KAAK,KAAK,QAAQ,UAAU,IAAI,WAAW,IAAI,CAAC,EAAE,IAAI,WAAW,WAAW,IAAI,EAAE,EAAE;CAChI,MAAM,KAAK,aAAa;CACxB,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,mEAAmE;CAC9E,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,GAAG,mBAAmB,CAAC;CAClC,OAAO,MAAM,KAAK,IAAI;AACvB;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,qBAAqB;CAC7B,OAAO;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;AACD;;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,aAAa;CAC1G,MAAM,QAAQ,KAAK,aAAa;EAC/B,IAAI,YAAY,IAAI,GAAG,GAAG;EAC1B,MAAM,WAAW,mBAAmB,YAAY,QAAQ;EACxD,MAAM,OAAO,WAAW,eAAe,QAAQ,IAAI,GAAG,eAAe,QAAQ,EAAE;EAC/E,MAAM,KAAK,KAAK,KAAK,MAAM,eAAe,CAAC,QAAQ,CAAC;EACpD,YAAY,IAAI,GAAG;CACpB;CACA,KAAK,MAAM,YAAY,OAAO,OAAO,iBAAiB,GAAG,IAAI,SAAS,SAAS,eAAe,SAAS,UAAU,KAAK,kBAAkB,YAAY,SAAS,QAAQ,GAAG,QAAQ;CAChL,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;EACxD,IAAI,QAAQ,SAAS,YAAY;EACjC,MAAM,WAAW,aAAa,mBAAmB,GAAG;EACpD,IAAI,UAAU,SAAS,eAAe,SAAS,UAAU,KAAK,KAAK,QAAQ;CAC5E;CACA,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,GAAG;EAChE,IAAI,SAAS,gBAAgB,UAAU,CAAC,SAAS,UAAU;EAC3D,IAAI,YAAY,IAAI,GAAG,GAAG;EAC1B,MAAM,KAAK,eAAe,QAAQ;EAClC,MAAM,QAAQ,SAAS,SAAS,eAAe,UAAU,SAAS,QAAQ,YAAY,OAAO,IAAI,KAAK;EACtG,MAAM,UAAU,GAAG,GAAG,WAAW,KAAK,QAAQ,cAAc,UAAU,GAAG;EACzE,MAAM,KAAK,KAAK,KAAK,SAAS,QAAQ,IAAI,IAAI,CAAC;EAC/C,YAAY,IAAI,GAAG;CACpB;AACD;;;;;;AAMA,SAAS,UAAU,iBAAiB,WAAW;CAC9C,MAAM,UAAU,OAAO,QAAQ,eAAe;CAC9C,IAAI,QAAQ,WAAW,GAAG,OAAO,KAAK;CACtC,OAAO,KAAK,QAAQ,KAAK,CAAC,KAAK,aAAa;EAC3C,MAAM,OAAO;EACb,MAAM,SAAS,yBAAyB,IAAI;EAC5C,MAAM,WAAW,QAAQ,KAAK,YAAY,QAAQ;EAClD,MAAM,OAAO,cAAc,UAAU,aAAa,MAAM,MAAM,IAAI,WAAW,SAAS,GAAG,OAAO;EAChG,OAAO,GAAG,QAAQ,GAAG,IAAI,YAAY,cAAc,SAAS,KAAK,IAAI,IAAI,KAAK;CAC/E,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7gBA,IAAM,qBAAqB,UAAkB,SAAmC;CAC5E,IAAI,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe,UAC3D,OAAO,KAAK;CAEhB,OAAO,YAAY,QAAQ;AAC/B;;;;;;;;;;;;;;;;AAqFA,IAAM,2BAA2D;CAC7D,MAAM;CACN,SAAS;CACT,MAAM;CACN,MAAM;CAEN,MAAM;CACN,UAAU;CACV,SAAS;CACT,QAAQ;CAGR,aAAa;CACb,QAAQ;CACR,WAAW;CACX,MAAM;CACN,iBAAiB;CACjB,SAAS;CACT,SAAS;CACT,aAAa;CACb,MAAM;CACN,MAAM;CACN,MAAM;CACN,OAAO;CAEP,QAAQ;CACR,OAAO;CACP,UAAU;CACV,OAAO;AACX;;;;;;;;;;AAWA,SAAgB,sBAAsB,MAA+B;CAGjE,IAAI,KAAK,SAAS,YAAY,OAAO;CAMrC,MAAM,aAAa;EACf,MAAM;EACN,OAAO;EACP,YAAY,EAAE,YAAY,KAAK;CACnC;CAEA,IAAI;EAEA,OAAO,yBADM,aAAa,cAAc,MAAM,kBAAkB,KAAA,CAChC,CAAA,CAAK;CACzC,QAAQ;EACJ,OAAO;CACX;AACJ;;;;;;;AAUA,eAAsB,gBAAgB,iBAAsD;CACxF,OAAO,6BAA6B,eAAe;AACvD;AAIA,eAAsB,yBAClB,aACA,gBACoB;CACpB,MAAM,SAAwB,CAAC;CAG/B,IAAI,CAAC,KAAG,WAAW,cAAc,GAAG;EAChC,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;EACD,OAAO;GAAE,QAAQ;GACzB;EAAO;CACH;CAKA,MAAM,sBAAsB,sBAAsB,WAAW;CAC7D,IAAI,oBAAoB,WAAW,GAC/B,OAAO;EAAE,QAAQ;EACzB;CAAO;CAGH,IAAI;EAMA,MAAM,cAAc,6BAChB,eAAe,mBAAmB,GAClC,MAAM,SAAW,SAAS,gBAAgB,OAAO,CACrD;EAEA,IAAI,YAAY,SAAS,GACrB,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,4GACY,+BAA+B,WAAW,EAAE;GACjE,KAAK;EACT,CAAC;CAET,SAAS,KAAc;EACnB,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAC/D,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,+CAA+C;GACxD,KAAK;EACT,CAAC;CACL;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;AAEA,eAAsB,sBAClB,aACA,aACoB;CACpB,MAAM,SAAwB,CAAC;CAM/B,IAAI,CAAC,KAAG,WAAW,WAAW,GAAG;EAC7B,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;EACD,OAAO;GAAE,QAAQ;GACzB;GACA,eAAe;EAA2B;CACtC;CAEA,IAAI;EACA,MAAM,cAAc,iBAAiB,WAAW;EAChD,MAAM,YAAY,MAAM,SAAW,SAAS,aAAa,OAAO;EAGhE,MAAM,aAAa,MACf,EACK,QAAQ,aAAa,EAAE,CAAC,CACxB,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;EAEd,IAAI,UAAU,WAAW,MAAM,UAAU,SAAS,GAC9C,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;CAET,SAAS,KAAc;EACnB,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAO/D,IAAI,eAAe,SAAS,IAAI,SAAS,gBACrC,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,6DAA6D;GACtE,KAAK;EACT,CAAC;OAED,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,kDAAkD;GAC3D,KAAK;EACT,CAAC;CAET;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;;;;;;;;;;;;AA+BA,SAAgB,uBACZ,aACA,WACa;CAKb,IAAI,CAJe,YAAY,MAAK,eAChC,OAAO,OAAO,WAAW,cAAc,CAAC,CAAC,CAAC,CACrC,MAAK,aAAa,UAAuC,SAAS,QAAQ,CAE9E,KAAc,UAAU,IAAI,QAAQ,GAAG,OAAO,CAAC;CAEpD,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,SAAS;EACT,KAAK;CAET,CAAC;AACL;;;;;;;;;;;;;;;AAgBA,eAAsB,kBAClB,MACA,aACsB;CACtB,IAAI,UAAyB;CAC7B,IAAI;EAMA,IAAI,EAAC,MAHiB,KAAK,MACvB,qEACJ,EAAA,CACa,KAAK,EAAE,EAAE,SAAS,OAAO,CAAC;EAIvC,WAAU,MAHQ,KAAK,MACnB,2FACJ,EAAA,CACc,KAAK,EAAE,EAAE,SAAS;CACpC,QAAQ;EAIJ,OAAO,CAAC;CACZ;CAEA,IAAI,CAAC,SAAS,OAAO,CAAC;CACtB,MAAM,WAAW,qBAAqB,WAAW;CACjD,IAAI,YAAY,UAAU,OAAO,CAAC;CAElC,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,SAAS,4EAA4E,QAAQ,MAAM,GAAG,EAAE,EAAE,4BAA4B,SAAS,MAAM,GAAG,EAAE,EAAE;EAC5J,KAAK;CACT,CAAC;AACL;;AAGA,IAAM,eAAe,QAAgB,UAA0B,IAAI,OAAO,KAAK,MAAM;;;;;;;;AASrF,SAAgB,gBACZ,QACA,OACA,QACA,QACA,SACM;CACN,MAAM,OAAO,sDAAsD,OAAO;CAC1E,IAAI,CAAC,SAAS,OAAO,GAAG,KAAK;CAC7B,MAAM,OAAO,aAAa,OAAO;CACjC,OAAO,GAAG,KAAK,oFACM,YAAY,QAAQ,KAAK,EAAE,iBAAiB,OAAO,SAAS,KAAK,UAAU,OAAO,KAAK,KAAK;AACrH;;;;;;;;;AAUA,SAAgB,iBAAiB,WAAsB,MAAgB,aAAoC;CACvG,MAAM,WAAW,UAAU;CAE3B,IADa,KAAK,WAAW,SAAS,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAQ,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAQ,GAC5G,OAAO,CAAC;CAClB,MAAM,QAAQ,YAAY,UAAU,QAAQ,UAAU,KAAK;CAC3D,MAAM,UAAU,SAAS,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI;CACrD,IAAI,KAAK,WAAW,GAChB,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,OAAO;EACP,UAAU,SAAS,KAAK,IAAI;EAC5B,QAAQ;EACR,SAAS,UAAU,YAAY,yDAAyD,QAAQ;EAChG,KAAK,eAAe,MAAM,oBAAoB,QAAQ;CAC1D,CAAC;CAIL,MAAM,OAAO,SAAS,MAAK,WAAU,CAAC,KAAK,SAAS,MAAM,CAAC,IAAI,UAAU;CACzE,MAAM,MAAM,KAAK,WAAW,IACtB,QAAQ,KAAK,2BAA2B,KAAK,GAAG,sEAAsE,QAAQ,KAC9H,QAAQ,KAAK,6BAA6B,KAAK,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,wFACnB,QAAQ;CACxE,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,OAAO;EACP,UAAU,SAAS,KAAK,IAAI;EAC5B,QAAQ,KAAK,KAAK,IAAI;EACtB,SAAS,UAAU,YAAY,iBAAiB,KAAK,KAAK,IAAI,EAAE,0CAA0C,QAAQ;EAClH;CACJ,CAAC;AACL;;;;;;AAOA,SAAgB,kBACZ,cACA,WACA,MACA,aACa;CACb,MAAM,SAAwB,CAAC;CAC/B,MAAM,SAAS,IAAI,IAAI,UAAU,KAAI,MAAK,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;CAC7D,KAAK,MAAM,UAAU,aAAa,SAAS;EACvC,MAAM,QAAQ,OAAO,IAAI,OAAO,MAAM;EACtC,IAAI,CAAC,OAAO;GACR,OAAO,KAAK;IACR,UAAU;IACV,UAAU;IACV,OAAO;IACP,QAAQ,OAAO;IACf,SAAS,mBAAmB,YAAY,YAAY,OAAO,OAAO;IAClE,KAAK;GACT,CAAC;GACD;EACJ;EACA,MAAM,WAAW,yBAAyB,OAAO,KAAK;EACtD,IAAI,MAAM,cAAc,UACpB,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,OAAO;GACP,QAAQ,OAAO;GACf;GACA,QAAQ,MAAM;GACd,SAAS,oBAAoB,OAAO,OAAO,QAAQ,YAAY,OAAO,MAAM,UAAU,gCAAgC,SAAS;GAC/H,KAAK,gBAAgB,aAAa,QAAQ,aAAa,OAAO,OAAO,QAAQ,MAAM,WAAW,OAAO,IAAI;EAC7G,CAAC;CAET;CACA,OAAO,KAAK,GAAG,iBAAiB,cAAc,MAAM,WAAW,CAAC;CAChE,OAAO;AACX;AAEA,eAAsB,2BAClB,aACA,aACoB;CACpB,MAAM,SAAwB,CAAC;CAG/B,MAAM,WAAW,MAAM,OAAO;CAC9B,MAAM,EAAE,SAAS,SAAS,WAAW;CACrC,MAAM,OAAO,IAAI,KAAK,EAAE,kBAAkB,YAAY,CAAC;CAGvD,MAAM,UAAU,MAAM,qBAAK,IAAI,IAAI;EAC/B;EACA;EACA,GAAG,sBAAsB,WAAW,CAAC,CAChC,OAAO,0BAA0B,CAAC,CAClC,KAAI,MAAK,EAAE,MAAM,CAAC,CAClB,QAAQ,MAAmB,CAAC,CAAC,CAAC;CACvC,CAAC,CAAC;CAEF,IAAI;EAEA,MAAM,eAAe,MAAM,KAAK,MAC5B;;0EAGA,CAAC,OAAO,CACZ;EACA,MAAM,iBAAiB,IAAI,IAAI,aAAa,KAAK,KAAK,MAClD,EAAE,iBAAiB,WAAW,EAAE,aAAa,GAAG,EAAE,aAAa,GAAG,EAAE,YACxE,CAAC;EAUD,MAAM,mBAAmB,MAAM,KAAK,MAA2B,kCAAkC;EACjG,MAAM,sBAAsB,IAAI,IAAI,iBAAiB,KAAK,KAAI,MAAK,EAAE,OAAO,CAAC;EAC7E,OAAO,KAAK,GAAG,uBAAuB,aAAa,mBAAmB,CAAC;EAQvE,OAAO,KAAK,GAAG,MAAM,kBAAkB,MAAM,WAAW,CAAC;EAGzD,MAAM,gBAAgB,MAAM,KAAK,MAC7B;;;mEAIA,CAAC,OAAO,CACZ;EACA,MAAM,iCAAiB,IAAI,IAAwB;EACnD,KAAK,MAAM,OAAO,cAAc,MAAM;GAClC,MAAM,cAAc,IAAI;GACxB,MAAM,YAAY,IAAI;GACtB,MAAM,MAAM,gBAAgB,WAAW,YAAY,GAAG,YAAY,GAAG;GACrE,IAAI,CAAC,eAAe,IAAI,GAAG,GACvB,eAAe,IAAI,KAAK,CAAC,CAAC;GAE9B,eAAe,IAAI,GAAG,CAAC,CAAE,KAAK,GAAG;EACrC;EAKA,MAAM,cAAc,MAAM,KAAK,MAC3B;;;;4DAKJ;EACA,MAAM,8BAAc,IAAI,IAAsB;EAC9C,KAAK,MAAM,OAAO,YAAY,MAAM;GAChC,MAAM,MAAM,GAAG,IAAI,YAAY,GAAG,IAAI;GACtC,IAAI,CAAC,YAAY,IAAI,GAAG,GACpB,YAAY,IAAI,KAAK,CAAC,CAAC;GAE3B,YAAY,IAAI,GAAG,CAAC,CAAE,KAAK,IAAI,UAAU;EAC7C;EAGA,MAAM,YAAY,MAAM,KAAK,MACzB;;;;;;;6CAQA,CAAC,OAAO,CACZ;EACA,MAAM,6BAAa,IAAI,IAAsB;EAC7C,KAAK,MAAM,OAAO,UAAU,MACxB,WAAW,IAAI,IAAI,iBAAiB,WAAW,IAAI,aAAa,GAAG,IAAI,aAAa,GAAG,IAAI,cAAc,IAAI,OAAO;EAIxH,MAAM,YAAY,MAAM,KAAK,MASzB;;;;;;;;;;;;;sFAcA,CAAC,OAAO,CACZ;EACA,MAAM,6BAAa,IAAI,IAAmC;EAC1D,KAAK,MAAM,OAAO,UAAU,MAAM;GAC9B,MAAM,cAAc,IAAI;GACxB,MAAM,YAAY,IAAI;GACtB,MAAM,MAAM,gBAAgB,WAAW,YAAY,GAAG,YAAY,GAAG;GACrE,IAAI,CAAC,WAAW,IAAI,GAAG,GACnB,WAAW,IAAI,KAAK,CAAC,CAAC;GAE1B,WAAW,IAAI,GAAG,CAAC,CAAE,KAAK,GAAG;EACjC;EAIA,MAAM,sBAAsB,sBAAsB,WAAW;EAC7D,MAAM,gBAAgB,qBAAqB,mBAAmB;EAM9D,IAAI;EACJ,IAAI;GACA,OAAO,WAAW,mBAAmB;EACzC,QAAQ;GACJ,OAAO,KAAA;EACX;EACA,MAAM,aAAa,IAAI,KAAwB,MAAM,UAAU,CAAC,EAAA,CAAG,KAAI,MAAK,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;EAC7F,MAAM,mCAAmB,IAAI,IAAY;EAEzC,KAAK,MAAM,cAAc,qBAAqB;GAC1C,MAAM,YAAY,aAAa,UAAU;GACzC,MAAM,aAAc,WAAmC,UAAU;GACjE,MAAM,gBAAgB,eAAe,WAAW,YAAY,GAAG,WAAW,GAAG;GAG7E,IAAI,CAAC,eAAe,IAAI,aAAa,GAAG;IACpC,OAAO,KAAK;KACR,UAAU;KACV,UAAU;KACV,OAAO;KACP,SAAS,UAAU,cAAc;KACjC,KAAK;IACT,CAAC;IACD;GACJ;GAEA,MAAM,YAAY,eAAe,IAAI,aAAa,KAAK,CAAC;GACxD,MAAM,cAAc,IAAI,IAAI,UAAU,KAAK,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;GAKpE,MAAM,YAAY,WAAW,IAAI,GAAG,WAAW,GAAG,WAAW;GAC7D,IAAI,WACA,OAAO,KAAK,GAAG,iBAAiB,WAAW,WAAW,IAAI,aAAa,KAAK,CAAC,GAAG,aAAa,CAAC;GAIlG,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;IACxE,IAAI,KAAK,SAAS,YAAY;KAG1B,MAAM,WAAW,aADS,2BAA2B,UACvB,GAAoB,KAA0B,UAAU,gBAAgB,QAAQ;KAC9G,IAAI,UAAU,SAAS,aAAa;MAChC,MAAM,YAAY,SAAS;MAC3B,IAAI,CAAC,YAAY,IAAI,SAAS,GAC1B,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,SAAS,uBAAuB,UAAU,kBAAkB,SAAS,2BAA2B,cAAc;OAC9G,KAAK;MACT,CAAC;MAIL,MAAM,WAAW,WAAW,IAAI,aAAa,KAAK,CAAC;MACnD,IAAI,kBAAkB;MACtB,IAAI,mBAAmB;MACvB,IAAI;OACA,MAAM,aAAa,SAAS,OAAO;OACnC,kBAAkB,aAAa,UAAU;OACzC,mBAAoB,WAAmC,UAAU;MACrE,QAAQ,CAAe;MAEvB,MAAM,QAAQ,SAAS,MAAM,OACzB,GAAG,gBAAgB,aACnB,GAAG,uBAAuB,mBAC1B,GAAG,yBAAyB,gBAChC;MAEA,IAAI,YAAY,IAAI,SAAS,KAAK,CAAC,OAC/B,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,SAAS,WAAW,UAAU,0DAA0D,qBAAqB,WAAW,kBAAkB,GAAG,iBAAiB,GAAG,kBAAkB;OACnL,KAAK;MACT,CAAC;KAET;KACA;IACJ;IAEA,MAAM,UAAU,kBAAkB,UAAU,IAAI;IAEhD,MAAM,QAAQ,YAAY,IAAI,OAAO;IACrC,IAAI,CAAC,OAAO;KACR,OAAO,KAAK;MACR,UAAU;MACV,UAAU;MACV,OAAO;MACP,QAAQ;MACR,SAAS,WAAW,QAAQ,8BAA8B,WAAW,KAAK,4BAA4B,cAAc;MACpH,KAAK;KACT,CAAC;KACD;IACJ;IAGA,MAAM,eAAe,sBAAsB,IAAI;IAC/C,IAAI,cAAc;KACd,MAAM,aAAa,MAAM;KACzB,IAAI,aAAa,eAAe;KAChC,IAAI,KAAK,SAAS,YAAY,MAAM,aAAa,UAC7C,aAAa;KAEjB,IAAI,KAAK,SAAS,SAAS;MACvB,MAAM,KAAK;MACX,IAAI,kBAAkB,GAAG;MACzB,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,GAAG;OACpD,MAAM,SAAS,GAAG;OAClB,IAAI,OAAO,SAAS,UAAU,kBAAkB;YAC3C,IAAI,OAAO,SAAS,UAAU,kBAAkB,OAAO,YAAY,UAAU,cAAc;YAC3F,IAAI,OAAO,SAAS,WAAW,kBAAkB;MAC1D;MACA,IAAI,mBAAmB,gBAAgB,SAAS,IAAI,GAChD,IAAI,eAAe,SACf,aAAa;WACV;OACH,MAAM,cAAc,oBAAoB,WAAW,UAC/B,oBAAoB,cAAc,UAClC,oBAAoB,cAAc,UAClC,oBAAoB,cAAc,aAAa;OACnE,IAAI,eAAe,MAAM,aAAa,aAClC,aAAa;MAErB;KAER;KACA,IAAI,YAAY;MACZ,MAAM,UAAU,WAAW,QAAQ,MAAK,MAAK,EAAE,WAAW,OAAO,CAAC,EAAE;MACpE,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,UAAU,KAAK,SAAS,WAAW,WAAW;OAC9C,QAAQ,MAAM,aAAa,WAAW,WAAW;OACjD,SAAS,WAAW,QAAQ,cAAc,cAAc,oBAAoB,KAAK,SAAS,WAAW,WAAW,aAAa,eAAe,MAAM,aAAa,WAAW,WAAW,WAAW;OAChM,KAAK,gBAAgB,YAAY,WAAW,SAAS,YAAY,OAAO;MAC5E,CAAC;KACL;IACJ;IAGA,IAAI,KAAK,SAAS,YAAa,KAAwB,MAAM;KACzD,MAAM,aAAc,KAAwB;KAC5C,IAAI,YAAY;MAGZ,MAAM,WAAW,qBAAqB,GAAG,UAAU,GAAG,SAAS;MAC/D,MAAM,eAAe,YAAY,IAAI,GAAG,WAAW,GAAG,UAAU;MAChE,IAAI,CAAC,cACD,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,UAAU;OACV,SAAS,cAAc,WAAW,KAAK,SAAS;OAChD,KAAK;MACT,CAAC;WACE;OAEH,MAAM,iBAAiB,MAAM,QAAQ,UAAU,IACzC,WAAW,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,OAAO,EAAE,EAAE,CAAE,IAChE,OAAO,KAAK,UAAU;OAE5B,MAAM,UAAU,eAAe,QAAQ,MAAM,CAAC,aAAa,SAAS,CAAC,CAAC;OACtE,MAAM,QAAQ,aAAa,QAAQ,MAAM,CAAC,eAAe,SAAS,CAAC,CAAC;OAEpE,IAAI,QAAQ,SAAS,KAAK,MAAM,SAAS,GAAG;QACxC,MAAM,QAAkB,CAAC;QACzB,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,YAAY,QAAQ,KAAK,IAAI,GAAG;QACnE,IAAI,MAAM,SAAS,GAAG,MAAM,KAAK,gBAAgB,MAAM,KAAK,IAAI,GAAG;QACnE,OAAO,KAAK;SACR,UAAU;SACV,UAAU;SACV,OAAO;SACP,QAAQ;SACR,UAAU,eAAe,KAAK,IAAI;SAClC,QAAQ,aAAa,KAAK,IAAI;SAC9B,SAAS,oBAAoB,QAAQ,cAAc,cAAc,qBAAqB,MAAM,KAAK,IAAI,EAAE;SACvG,KAAK;QACT,CAAC;OACL;MACJ;KACJ;IACJ;GACJ;GAMA,MAAM,oBAAoB,2BAA2B,UAAU;GAC/D,KAAK,MAAM,YAAY,OAAO,OAAO,iBAAiB,GAClD,IAAI,aAAa,QAAQ,GAAG;IACxB,MAAM,gBAAgB,SAAS,QAAQ,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,QAAQ;IAClF,MAAM,iBAAiB,cAAc,IAAI,aAAa,CAAC,EAAE,UAAU;IACnE,MAAM,oBAAoB,mBAAmB,WAAW,gBAAgB,GAAG,eAAe,GAAG;IAC7F,IAAI,CAAC,eAAe,IAAI,iBAAiB,GACrC,OAAO,KAAK;KACR,UAAU;KACV,UAAU;KACV,OAAO;KACP,SAAS,mBAAmB,kBAAkB,+BAA+B,SAAS,aAAa;KACnG,KAAK;IACT,CAAC;SACE,IAAI,CAAC,iBAAiB,IAAI,iBAAiB,GAAG;KAEjD,iBAAiB,IAAI,iBAAiB;KACtC,MAAM,eAAe,WAAW,IAAI,GAAG,eAAe,GAAG,eAAe;KACxE,IAAI,cACA,OAAO,KAAK,GAAG,kBACX,cACA,eAAe,IAAI,iBAAiB,KAAK,CAAC,GAC1C,WAAW,IAAI,iBAAiB,KAAK,CAAC,GACtC,iBACJ,CAAC;IAET;GACJ;EAER;CACJ,UAAU;EACN,MAAM,KAAK,IAAI;CACnB;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;AAIA,SAAgB,aAAa,QAA4B;CACrD,IAAI;CACJ,IAAI,MAAM,KAAK,2BAA2B,CAAC;CAC3C,IAAI,MAAM,KAAK,oDAAoD,CAAC;CACpE,IAAI;CAGJ,YAAY,kCAAkC,OAAO,mBAAmB;CAGxE,YAAY,0BAA0B,OAAO,gBAAgB;CAG7D,YAAY,2BAA2B,OAAO,gBAAgB;CAG9D,IAAI,MAAM,KAAK,oDAAoD,CAAC;CACpE,MAAM,EAAE,QAAQ,SAAS,eAAe,UAAU,WAAW,OAAO;CAEpE,MAAM,QAAkB,CAAC;CACzB,MAAM,KAAK,MAAM,MAAM,GAAG,OAAO,QAAQ,CAAC;CAC1C,IAAI,UAAU,GAAG,MAAM,KAAK,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC;CAC9D,IAAI,gBAAgB,GAAG,MAAM,KAAK,MAAM,KAAK,GAAG,cAAc,gBAAgB,CAAC;CAC/E,IAAI,WAAW,GAAG,MAAM,KAAK,MAAM,OAAO,GAAG,SAAS,UAAU,CAAC;CACjE,IAAI,SAAS,GAAG,MAAM,KAAK,MAAM,IAAI,GAAG,OAAO,QAAQ,CAAC;CAExD,IAAI,cAAc,MAAM,KAAK,IAAI,GAAG;CACpC,IAAI;CAEJ,IAAI,SAAS,GACT,IAAI,MAAM,IAAI,KAAK,2DAA2D,CAAC;MAC5E,IAAI,WAAW,GAClB,IAAI,MAAM,OAAO,KAAK,kEAAkE,CAAC;MACtF,IAAI,UAAU,GAGjB,IAAI,MAAM,OAAO,KAAK,iDAAiD,QAAQ,mDAAmD,CAAC;MAEnI,IAAI,MAAM,MAAM,KAAK,8BAA8B,CAAC;CAExD,IAAI;AACR;AAEA,SAAS,YAAY,OAAe,OAA0B;CAI1D,IAAI,MAAM,SAAS;EACf,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,IAAI,MAAM,IAAI,MAAM,OAAO,YAAY,MAAM,QAAQ,EAAE,GAAG;EACrF,IAAI;EACJ;CACJ;CAKA,IAAI,MAAM,eAAe;EACrB,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,IAAI,MAAM,KAAK,MAAM,aAAa,GAAG;EACvE,KAAK,MAAM,SAAS,MAAM,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM,GAChE,IAAI,QAAQ,MAAM,KAAK,MAAM,GAAG,GAAG;EAEvC,IAAI;EACJ;CACJ;CAEA,MAAM,SAAS,MAAM;CACrB,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,aAAa,OAAO,CAAC,CAAC;CAChE,MAAM,YAAY,OAAO,QAAQ,MAAM,EAAE,aAAa,SAAS,CAAC,CAAC;CACjE,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM;CAI7D,IAAI,eAAe,KAAK,cAAc,GAClC,IAAI,KAAK,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,IAAI,MAAM,MAAM,SAAS,GAAG;MAC5D;EACH,MAAM,QAAkB,CAAC;EACzB,IAAI,aAAa,GAAG,MAAM,KAAK,GAAG,WAAW,QAAQ,aAAa,IAAI,MAAM,IAAI;EAChF,IAAI,YAAY,GAAG,MAAM,KAAK,GAAG,UAAU,UAAU,YAAY,IAAI,MAAM,IAAI;EAC/E,IAAI,KAAK,MAAM,OAAO,IAAI,EAAE,IAAI,MAAM,IAAI,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,GAAG;CAC9E;CACA,IAAI;CAGJ,KAAK,MAAM,SAAS,YAAY;EAC5B,MAAM,UAAU,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,KAAK,IAAI;EAC5D,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,KAAK,MAAM,OAAO,IAAI,SAAS;EACjE,IAAI;CACR;CAEA,KAAK,MAAM,SAAS,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM,GAAG;EAC7D,MAAM,eAAe,MAAM,aAAa,UAAU,MAAM,IAAI,GAAG,IAAI,MAAM,OAAO,GAAG;EACnF,MAAM,gBAAgB,eAAe,MAAM,QAAQ;EACnD,IAAI,KAAK,MAAM,KAAK,IAAI,EAAE,GAAG,aAAa,GAAG,MAAM,KAAK,aAAa,EAAE,GAAG,MAAM,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,cAAc,MAAM,CAAC,CAAC,GAAG;EAE1I,IAAI,MAAM,OAAO;GACb,MAAM,UAAU,MAAM,SAAS,cAAc,MAAM,KAAK,MAAM,MAAM,MAAM;GAC1E,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,UAAU,MAAM,KAAK,MAAM,KAAK,IAAI,SAAS;EAC1E;EAEA,IAAI,MAAM,YAAY,MAAM,QACxB,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,aAAa,MAAM,MAAM,MAAM,QAAQ,EAAE,aAAa,MAAM,IAAI,MAAM,MAAM,GAAG;EAG5G,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,SAAS;EAC3C,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,QAAQ,MAAM,KAAK,MAAM,GAAG,GAAG;EACxD,IAAI,KAAK,MAAM,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,GAAG;EAC3C,IAAI;CACR;AACJ;AAEA,SAAS,eAAe,KAAsC;CAgB1D,OAAO;EAdH,eAAe;EACf,gBAAgB;EAChB,eAAe;EACf,oBAAoB;EACpB,sBAAsB;EACtB,cAAc;EACd,cAAc;EACd,qBAAqB;EACrB,qBAAqB;EACrB,mBAAmB;EACnB,WAAW;EACX,mBAAmB;EACnB,mBAAmB;CAEhB,EAAO;AAClB;AAIA,eAAsB,UAAU,SAKN;CACtB,IAAI;CACJ,IAAI,MAAM,KAAK,6BAA6B,CAAC;CAC7C,MAAM,cAAc,MAAM,gBAAgB,QAAQ,eAAe;CACjE,IAAI,YAAY,WAAW,GAAG;EAC1B,SAAS,MAAM,IAAI,2BAA2B,CAAC;EAC/C,QAAQ,KAAK,CAAC;CAClB;CACA,IAAI,MAAM,KAAK,WAAW,YAAY,OAAO,eAAe,CAAC;CAC7D,IAAI;CAGJ,IAAI,MAAM,KAAK,8CAA8C,CAAC;CAC9D,MAAM,sBAAsB,MAAM,yBAAyB,aAAa,QAAQ,UAAU;CAG1F,IAAI;CACJ,IAAI,QAAQ,aAAa;EACrB,IAAI,MAAM,KAAK,sCAAsC,CAAC;EACtD,IAAI;GACA,mBAAmB,MAAM,2BAA2B,aAAa,QAAQ,WAAW;EACxF,SAAS,KAAc;GAOnB,MAAM,SAAS,gBAAgB,KAAK,QAAQ,WAAW;GACvD,IAAI,QAAQ,SAAS,MAAM;GAC3B,MAAM,SAAS,oBAAoB,GAAG,MAC9B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACvD,mBAAmB;IACf,QAAQ;IACR,QAAQ,CAAC;IACT,SAAS,mBAAmB,cAAc,QAAQ,WAAW,EAAE,KAAK;IACpE,SAAS;GACb;EACJ;CACJ,OAAO;EAEH,mBAAmB;GAAE,QAAQ;GACrC,QAAQ,CAAC;GACT,SAAS;EAAuB;EACxB,IAAI,MAAM,OAAO,0DAA0D,CAAC;EAC5E,IAAI,MAAM,KAAK,mEAAmE,CAAC;CACvF;CAGA,IAAI,MAAM,KAAK,uCAAuC,CAAC;CACvD,MAAM,mBAAmB,MAAM,sBAAsB,aAAa,QAAQ,OAAO;CAEjF,MAAM,SAAS;EAAC;EAAqB;EAAkB;CAAgB;CACvE,MAAM,YAAY,OAAO,SAAS,MAAM,EAAE,MAAM;CAChD,MAAM,UAAU;EAIZ,QAAQ,OAAO,QAAQ,MAAM,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,EAAE,aAAa,CAAC,CAAC;EACzE,SAAS,OAAO,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACzC,eAAe,OAAO,QAAQ,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC,CAAC;EACnE,UAAU,UAAU,QAAQ,MAAM,EAAE,aAAa,SAAS,CAAC,CAAC;EAC5D,QAAQ,UAAU,QAAQ,MAAM,EAAE,aAAa,OAAO,CAAC,CAAC;CAC5D;CAEA,MAAM,SAAuB;EAAE;EACnC;EACA;EACA;CAAQ;CACJ,aAAa,MAAM;CAEnB,OAAO;AACX"}
@@ -1,160 +0,0 @@
1
- import { createRequire as __createRequire } from "module";
2
- __createRequire(import.meta.url);
3
- import { a as revokeUserRoleSql } from "./rls-enforcement-CfXOJJaW.js";
4
- import { s as planCollectionPolicies } from "./generate-postgres-ddl-logic-BJsLaVNX.js";
5
- import { i as readExistingSchema, t as describeDriverError } from "./ensure-collection-tables-kkkHk8oo.js";
6
- import { i as isGeneratedPolicyName } from "./policy-drift-B-J2hhm0.js";
7
- //#region src/schema/ensure-collection-policies.ts
8
- var isCreatePolicy = (statement) => /^\s*CREATE POLICY/i.test(statement);
9
- /**
10
- * Bring the declared collections' RLS policies up to date. Returns what it did.
11
- *
12
- * Only tables that already exist are touched: the boot-time table creator runs
13
- * first, so anything still missing is a table this additive path is not allowed
14
- * to create (a junction, or a relation left to a migration). Enabling RLS on a
15
- * non-existent table would error, so those are recorded as skipped, not failed.
16
- */
17
- /**
18
- * Remove generated policies on this table that the current plan does not
19
- * produce.
20
- *
21
- * Scoped hard, because dropping a policy is destructive: only this one table,
22
- * only names matching the generated `<table>_<op>_<sha1>` shape, and only names
23
- * absent from the statements just applied. A hand-written policy, or one
24
- * belonging to another table, is never touched — `isGeneratedPolicyName` is the
25
- * same predicate `db push` uses to draw that line.
26
- */
27
- async function dropOrphanedPoliciesOn(client, plan) {
28
- const expected = /* @__PURE__ */ new Set();
29
- for (const statement of plan.policyStatements) {
30
- const match = /^\s*CREATE POLICY\s+"([^"]+)"/i.exec(statement);
31
- if (match) expected.add(match[1]);
32
- }
33
- const existing = await client.query(`SELECT policyname FROM pg_policies WHERE schemaname = '${plan.schema.replace(/'/g, "''")}' AND tablename = '${plan.table.replace(/'/g, "''")}'`);
34
- let dropped = 0;
35
- for (const row of existing.rows) {
36
- const name = row.policyname;
37
- if (expected.has(name)) continue;
38
- if (!isGeneratedPolicyName(name, plan.table)) continue;
39
- await client.query(`DROP POLICY IF EXISTS "${name}" ON "${plan.schema}"."${plan.table}"`);
40
- dropped++;
41
- }
42
- return dropped;
43
- }
44
- /**
45
- * Switch RLS on for one table, or — when Postgres refuses — take the user
46
- * role's privileges on it back, so the table is unreachable rather than
47
- * unprotected.
48
- */
49
- async function lockTable(client, plan) {
50
- try {
51
- await client.query(plan.enableRls);
52
- return { locked: true };
53
- } catch (err) {
54
- const error = describeDriverError(err);
55
- try {
56
- await client.query(revokeUserRoleSql(plan.schema, plan.table));
57
- return {
58
- locked: false,
59
- error,
60
- grantWithdrawn: true
61
- };
62
- } catch {
63
- return {
64
- locked: false,
65
- error,
66
- grantWithdrawn: false
67
- };
68
- }
69
- }
70
- }
71
- /**
72
- * Switch RLS on for every table the collections declare — each collection's
73
- * and each junction's — and do nothing else.
74
- *
75
- * The first thing `rebase db push` does once Atlas has committed. Atlas cannot
76
- * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),
77
- * so a table it creates arrives with RLS off — while the user role already
78
- * holds DML on it through the schema's default privileges. Everything push runs
79
- * after Atlas can refuse: the vector, search and trigger DDL, the policies
80
- * themselves. Locking first means none of those failures can leave a table
81
- * open: RLS with no policy denies, which is a state a failed push may leave.
82
- *
83
- * A declared table that is not in the database is skipped — nothing can reach
84
- * a table that does not exist, and the step that creates it locks it.
85
- */
86
- async function lockCollectionTables(client, collections) {
87
- const result = {
88
- locked: 0,
89
- unsecured: []
90
- };
91
- const plans = planCollectionPolicies(collections);
92
- if (plans.length === 0) return result;
93
- const existing = await readExistingSchema(client, Array.from(new Set(plans.map((p) => p.schema))));
94
- for (const plan of plans) {
95
- if (!existing.tables.has(plan.qualified)) continue;
96
- const lock = await lockTable(client, plan);
97
- if (lock.locked) result.locked++;
98
- else result.unsecured.push({
99
- table: plan.qualified,
100
- error: lock.error,
101
- grantWithdrawn: lock.grantWithdrawn
102
- });
103
- }
104
- return result;
105
- }
106
- async function ensureCollectionPolicies(client, collections, log) {
107
- const result = {
108
- policiesApplied: 0,
109
- tablesSecured: 0,
110
- skipped: [],
111
- failures: [],
112
- unsecured: [],
113
- orphansDropped: 0
114
- };
115
- const plans = planCollectionPolicies(collections);
116
- if (plans.length === 0) return result;
117
- const existing = await readExistingSchema(client, Array.from(new Set(plans.map((p) => p.schema))));
118
- for (const plan of plans) {
119
- if (!existing.tables.has(plan.qualified)) {
120
- result.skipped.push({
121
- table: plan.qualified,
122
- reason: "table is not present in the database; create it with `rebase db push` / `rebase db migrate`"
123
- });
124
- continue;
125
- }
126
- const lock = await lockTable(client, plan);
127
- if (!lock.locked) {
128
- result.unsecured.push({
129
- table: plan.qualified,
130
- error: lock.error,
131
- grantWithdrawn: lock.grantWithdrawn
132
- });
133
- continue;
134
- }
135
- result.tablesSecured++;
136
- try {
137
- let created = 0;
138
- for (const statement of plan.policyStatements) {
139
- await client.query(statement);
140
- if (isCreatePolicy(statement)) {
141
- result.policiesApplied++;
142
- created++;
143
- }
144
- }
145
- const orphans = await dropOrphanedPoliciesOn(client, plan);
146
- result.orphansDropped += orphans;
147
- log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` + (orphans > 0 ? `, ${orphans} orphan(s) dropped` : ""));
148
- } catch (err) {
149
- result.failures.push({
150
- table: plan.qualified,
151
- error: describeDriverError(err)
152
- });
153
- }
154
- }
155
- return result;
156
- }
157
- //#endregion
158
- export { ensureCollectionPolicies, lockCollectionTables };
159
-
160
- //# sourceMappingURL=ensure-collection-policies-B1ureSIV.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ensure-collection-policies-B1ureSIV.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once\n * enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by\n * a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies, type CollectionPolicyPlan } from \"./generate-postgres-ddl-logic\";\nimport { isGeneratedPolicyName } from \"../security/policy-drift\";\nimport { describeDriverError, readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\nimport { revokeUserRoleSql } from \"../security/rls-enforcement\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled. */\n tablesSecured: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /**\n * Tables that have RLS on but did not get every policy. They deny — RLS\n * with no matching policy is deny-all — so they are safe but not servable.\n */\n failures: { table: string; error: string }[];\n /**\n * Tables RLS could not be enabled on, whose DML grant was withdrawn instead.\n *\n * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`\n * failing was recorded as a `failure` and reported with the same \"it stays\n * locked (denies)\" wording as a failed policy — but the two are opposites.\n * A policy statement failing leaves RLS on and the table denying. `enableRls`\n * failing leaves RLS *off*, and the schema-wide grant to the user role has\n * already been made by `ensureRlsEnforcement`, so the table is readable and\n * writable by every authenticated request with no row filtering at all.\n */\n unsecured: { table: string; error: string; grantWithdrawn: boolean }[];\n /**\n * Generated policies removed because no current rule produces them.\n *\n * A policy's name embeds a hash of the rule's semantics, so editing a rule\n * does not update a policy — it creates a new one and abandons the old.\n * Postgres ORs permissive policies, so the abandoned one keeps granting:\n * a `USING (true)` tightened to an owner check went on admitting everyone,\n * forever, while the deploy logged success.\n *\n * `db push` reconciles this, and cannot reach a managed tenant's in-cluster\n * database — which is the reason this module exists. So boot has to do it.\n */\n orphansDropped: number;\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path is not allowed\n * to create (a junction, or a relation left to a migration). Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n */\n\n/**\n * Remove generated policies on this table that the current plan does not\n * produce.\n *\n * Scoped hard, because dropping a policy is destructive: only this one table,\n * only names matching the generated `<table>_<op>_<sha1>` shape, and only names\n * absent from the statements just applied. A hand-written policy, or one\n * belonging to another table, is never touched — `isGeneratedPolicyName` is the\n * same predicate `db push` uses to draw that line.\n */\nasync function dropOrphanedPoliciesOn(client: Queryable, plan: CollectionPolicyPlan): Promise<number> {\n const expected = new Set<string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) expected.add(match[1]);\n }\n\n const existing = await client.query<{ policyname: string }>(\n `SELECT policyname FROM pg_policies WHERE schemaname = '${plan.schema.replace(/'/g, \"''\")}' ` +\n `AND tablename = '${plan.table.replace(/'/g, \"''\")}'`\n );\n\n let dropped = 0;\n for (const row of existing.rows) {\n const name = row.policyname;\n if (expected.has(name)) continue;\n if (!isGeneratedPolicyName(name, plan.table)) continue;\n await client.query(`DROP POLICY IF EXISTS \"${name}\" ON \"${plan.schema}\".\"${plan.table}\"`);\n dropped++;\n }\n return dropped;\n}\n\n/** What one table's `ENABLE ROW LEVEL SECURITY` came to. */\ntype TableLock =\n | { locked: true }\n | { locked: false; error: string; grantWithdrawn: boolean };\n\n/**\n * Switch RLS on for one table, or — when Postgres refuses — take the user\n * role's privileges on it back, so the table is unreachable rather than\n * unprotected.\n */\nasync function lockTable(client: Queryable, plan: CollectionPolicyPlan): Promise<TableLock> {\n try {\n await client.query(plan.enableRls);\n return { locked: true };\n } catch (err) {\n // The database's own answer, not the ORM's wrapper — see\n // `describeDriverError`. This is the most consequential message in\n // the file: it is what the operator reads about a table that is now\n // denying every request, and `Failed query: <the statement>` told\n // them nothing they did not already have.\n const error = describeDriverError(err);\n // Take the privilege back rather than serve an unprotected table.\n // This is the fail-closed step the old code assumed it already had:\n // per-table, so one collection cannot take the rest of the\n // deployment down, but leaving nothing readable without RLS.\n //\n // Quoted, like every other statement here: unquoted, Postgres folds\n // the name to lower case, so a mixed-case adopted table (`\"User\"`)\n // was revoked as `public.user` — which fails, and a table that could\n // have been closed refused the boot instead.\n try {\n await client.query(revokeUserRoleSql(plan.schema, plan.table));\n return { locked: false, error, grantWithdrawn: true };\n } catch {\n // Reported with `grantWithdrawn: false`, which the caller\n // escalates. There is nothing else this can do to make the table\n // safe.\n return { locked: false, error, grantWithdrawn: false };\n }\n }\n}\n\nexport interface TableLockResult {\n /** Tables RLS is now on for. */\n locked: number;\n /** Tables it could not be switched on for. See {@link PolicyEnsureResult.unsecured}. */\n unsecured: PolicyEnsureResult[\"unsecured\"];\n}\n\n/**\n * Switch RLS on for every table the collections declare — each collection's\n * and each junction's — and do nothing else.\n *\n * The first thing `rebase db push` does once Atlas has committed. Atlas cannot\n * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),\n * so a table it creates arrives with RLS off — while the user role already\n * holds DML on it through the schema's default privileges. Everything push runs\n * after Atlas can refuse: the vector, search and trigger DDL, the policies\n * themselves. Locking first means none of those failures can leave a table\n * open: RLS with no policy denies, which is a state a failed push may leave.\n *\n * A declared table that is not in the database is skipped — nothing can reach\n * a table that does not exist, and the step that creates it locks it.\n */\nexport async function lockCollectionTables(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<TableLockResult> {\n const result: TableLockResult = { locked: 0, unsecured: [] };\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n\n const existing = await readExistingSchema(client, Array.from(new Set(plans.map(p => p.schema))));\n for (const plan of plans) {\n if (!existing.tables.has(plan.qualified)) continue;\n const lock = await lockTable(client, plan);\n if (lock.locked) {\n result.locked++;\n } else {\n result.unsecured.push({ table: plan.qualified, error: lock.error, grantWithdrawn: lock.grantWithdrawn });\n }\n }\n return result;\n}\n\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = { policiesApplied: 0, tablesSecured: 0, skipped: [], failures: [], unsecured: [], orphansDropped: 0 };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const existing = await readExistingSchema(client, schemas);\n\n for (const plan of plans) {\n if (!existing.tables.has(plan.qualified)) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n\n // Enabling RLS is its own step, because its failure is the opposite of\n // every other failure here. Once RLS is on, anything that goes wrong\n // afterwards leaves the table denying; while it is off, the grant made\n // earlier in boot leaves the table wide open. Sharing one `try` meant\n // the dangerous case was reported in the safe case's words.\n const lock = await lockTable(client, plan);\n if (!lock.locked) {\n result.unsecured.push({ table: plan.qualified, error: lock.error, grantWithdrawn: lock.grantWithdrawn });\n continue;\n }\n result.tablesSecured++;\n\n try {\n let created = 0;\n for (const statement of plan.policyStatements) {\n await client.query(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n\n const orphans = await dropOrphanedPoliciesOn(client, plan);\n result.orphansDropped += orphans;\n\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (orphans > 0 ? `, ${orphans} orphan(s) dropped` : \"\"));\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: describeDriverError(err)\n });\n }\n }\n\n return result;\n}\n"],"mappings":"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"}