@rebasepro/server-postgres 0.17.3-canary.gdd23447 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. package/src/websocket.ts +0 -855
@@ -1,180 +0,0 @@
1
- import { promises as fsPromises } from "fs";
2
- import * as fs from "fs";
3
- import path from "path";
4
- import { pathToFileURL } from "url";
5
- import {
6
- generatePostgresDdl,
7
- generatePostgresPoliciesDdl,
8
- generatePostgresSearchDdl,
9
- generatePostgresVectorDdl
10
- } from "./generate-postgres-ddl-logic";
11
- import { CollectionConfig, declaredDatabaseExtensions } from "@rebasepro/types";
12
- import { loadCollectionsFromDirectory } from "@rebasepro/server";
13
- import { out, outError } from "../cli-output";
14
-
15
- /** Where a project declares its resources, relative to the config directory. */
16
- const RESOURCE_ENTRIES = ["resources.ts", "resources.js", "resources/index.ts", "resources/index.js"];
17
-
18
- /**
19
- * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can
20
- * answer.
21
- *
22
- * This generator runs as its own tsx subprocess and loads only the collections
23
- * directory, so nothing else in it would ever import the resources module — and
24
- * a permission that reads as absent because nobody loaded it is the worst shape
25
- * this could take: the push succeeds, `vector.sql` quietly omits the
26
- * `CREATE EXTENSION`, and the failure lands one statement later looking like a
27
- * database problem.
28
- *
29
- * Imported directly rather than through the config index, for the reason
30
- * `boot/resource-loading.ts` gives: re-exporting from the index would work only
31
- * for projects that remember to.
32
- *
33
- * A project with no resources module is the normal case and declares nothing.
34
- * An import that *throws* is not swallowed — a resources module that does not
35
- * evaluate is a configuration error, and treating it as "declared nothing"
36
- * would turn it into a silently missing extension.
37
- */
38
- async function loadDeclaredResources(configDir: string): Promise<void> {
39
- const entry = RESOURCE_ENTRIES
40
- .map(name => path.join(configDir, name))
41
- .find(candidate => fs.existsSync(candidate));
42
- if (!entry) return;
43
- await import(pathToFileURL(entry).href);
44
- }
45
-
46
- const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
47
- try {
48
- if (!collectionsFilePath) {
49
- outError("Error: No collections file path provided. Skipping schema generation.");
50
- return;
51
- }
52
-
53
- const resolvedPath = path.resolve(collectionsFilePath);
54
-
55
- // `<config>/collections` is the path this is given, so the config
56
- // directory is its parent — the same relationship `rebase resources`
57
- // and the boot loader assume.
58
- await loadDeclaredResources(path.dirname(resolvedPath));
59
-
60
- // Shared with the runtime and the doctor: what gets generated here must
61
- // be exactly what the server serves, including directory-level defaults.
62
- let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);
63
-
64
-
65
- if (!collections || !Array.isArray(collections)) {
66
- collections = [];
67
- }
68
-
69
- // Sort collections by slug alphabetically to ensure deterministic DDL generation
70
- collections.sort((a, b) => a.slug.localeCompare(b.slug));
71
-
72
- // `schema.sql` is Atlas's desired state, and it carries neither the RLS
73
- // policies, nor the search apparatus, nor the vector columns: Atlas
74
- // manages none of them, and in the search and vector cases cannot. All
75
- // three are written beside it and applied by the CLI in their own right.
76
- const ddlContent = await generatePostgresDdl(collections, {
77
- includePolicies: false,
78
- includeSearch: false,
79
- includeVector: false
80
- });
81
- const policiesContent = generatePostgresPoliciesDdl(collections);
82
- const searchContent = generatePostgresSearchDdl(collections);
83
- const vectorContent = generatePostgresVectorDdl(collections, {
84
- extensions: declaredDatabaseExtensions()
85
- });
86
-
87
- if (outputPath) {
88
- const outputDir = path.dirname(outputPath);
89
- await fsPromises.mkdir(outputDir, { recursive: true });
90
- await fsPromises.writeFile(outputPath, ddlContent);
91
- out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);
92
-
93
- const policiesPath = path.join(outputDir, "policies.sql");
94
- await fsPromises.writeFile(policiesPath, policiesContent);
95
- out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);
96
-
97
- // Removed when the last `search` block goes: the CLI applies this
98
- // file whenever it exists, and a stale one would keep re-creating
99
- // functions for collections that no longer want them.
100
- const searchPath = path.join(outputDir, "search.sql");
101
- if (searchContent) {
102
- await fsPromises.writeFile(searchPath, searchContent);
103
- out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);
104
- } else if (fs.existsSync(searchPath)) {
105
- await fsPromises.rm(searchPath);
106
- }
107
-
108
- // Removed when the last `vector` property goes, for the same reason
109
- // `search.sql` is: the CLI applies whichever of these files exists,
110
- // and a stale one would keep re-adding a column the collections no
111
- // longer declare — which Atlas, no longer told to exclude it, would
112
- // then plan a DROP for on the very next push.
113
- const vectorPath = path.join(outputDir, "vector.sql");
114
- if (vectorContent) {
115
- await fsPromises.writeFile(vectorPath, vectorContent);
116
- out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);
117
- } else if (fs.existsSync(vectorPath)) {
118
- await fsPromises.rm(vectorPath);
119
- }
120
- } else {
121
- out("✅ PostgreSQL DDL generated successfully.");
122
- out(String(ddlContent));
123
- out("\n✅ PostgreSQL Policies DDL generated successfully.");
124
- out(String(policiesContent));
125
- }
126
-
127
- } catch (error) {
128
- outError(`Error generating DDL schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
129
- }
130
- };
131
-
132
- const main = async () => {
133
- const collectionsFilePathArg = process.argv.find(arg => arg.startsWith("--collections="));
134
- const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split("=")[1] : process.argv[2];
135
-
136
- const outputPathArg = process.argv.find(arg => arg.startsWith("--output="));
137
- const outputPath = outputPathArg ? outputPathArg.split("=")[1] : undefined;
138
-
139
- const watch = process.argv.includes("--watch");
140
-
141
- if (!collectionsFilePath) {
142
- out("Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
143
- return;
144
- }
145
-
146
- const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);
147
- const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
148
-
149
- if (watch) {
150
- out(`Watching for changes in ${resolvedPath}...`);
151
- // Imported here rather than at module scope, and this is not a style
152
- // choice: chokidar is needed only by `--watch`, which is a
153
- // schema-authoring path that never runs inside the runtime image. A
154
- // top-level import puts it on the boot path of the published driver
155
- // bundle, and the image installs a hand-listed set of runtime
156
- // dependencies that does not include it — so the whole driver failed to
157
- // load with "Cannot find package 'chokidar'", and every self-hosted
158
- // container answered 500 with a stack trace about a file watcher.
159
- //
160
- // Same reasoning the image already applies to @ariga/atlas: an
161
- // authoring-only dependency does not belong on a boot path.
162
- const { default: chokidar } = await import("chokidar");
163
- const watcher = chokidar.watch(resolvedPath, {
164
- persistent: true,
165
- ignoreInitial: false
166
- });
167
-
168
- watcher.on("all", (event, filePath) => {
169
- out(`[${event}] ${filePath}. Regenerating DDL schema...`);
170
- runGeneration(resolvedPath, resolvedOutputPath);
171
- });
172
- } else {
173
- runGeneration(resolvedPath, resolvedOutputPath);
174
- }
175
- };
176
-
177
- // This check ensures the script only runs when executed directly
178
- if (import.meta.url.endsWith(process.argv[1])) {
179
- main();
180
- }
@@ -1,267 +0,0 @@
1
- /**
2
- * Everything a schema change has to write, as file contents.
3
- *
4
- * A live schema editor that only edits the collection source produces a repo
5
- * that does not build: `backend/src/schema.generated.ts` is a committed
6
- * artifact, and a stale one has broken every deploy at least once. So the unit
7
- * of a schema change is not a file, it is a **commit** — and this module
8
- * produces one, without touching a disk, a database or a network.
9
- *
10
- * Pure on purpose. The risky half of "commit, then apply" is generating a
11
- * correct commit; keeping it a function from collections to file contents is
12
- * what lets that half be tested by building a database from the result and
13
- * comparing it to the one the change describes.
14
- *
15
- * ## Where the migration comes from, and why not from Atlas
16
- *
17
- * `rebase db generate` mints migrations by running Atlas over the generated
18
- * `schema.sql`. Atlas is an external binary, it wants a dev database, and it
19
- * maintains an `atlas.sum` integrity file whose hash this module would have to
20
- * reproduce byte-for-byte to stay valid.
21
- *
22
- * None of that is necessary here, because of what the editor is allowed to do.
23
- * `classify-change.ts` refuses anything the boot-time ensure path cannot
24
- * express, which leaves only additive statements — and those are computable as
25
- * a plain difference between two ensure plans:
26
- *
27
- * plan(after, nothing) − plan(before, nothing)
28
- *
29
- * Both plans are pure functions of the collections, every statement is
30
- * idempotent, and the difference is exactly what the change adds. No diff
31
- * engine, no database, no binary.
32
- *
33
- * The statements are *returned* rather than written into a migration file. A
34
- * project provisioned by boot-ensure needs no migration at all — its
35
- * collections are the schema — while a project provisioned by migrations needs
36
- * the file to carry an Atlas hash, which only Atlas can mint. Writing a
37
- * migration this module cannot make valid would be worse than handing the
38
- * statements to a caller who knows which kind of project it is.
39
- */
40
- import {
41
- DEFAULT_COMMIT_PATHS,
42
- declaredDatabaseExtensions,
43
- type CollectionConfig,
44
- type SchemaCommitPaths
45
- } from "@rebasepro/types";
46
- import {
47
- generatePostgresDdl,
48
- generatePostgresPoliciesDdl,
49
- generatePostgresSearchDdl,
50
- generatePostgresVectorDdl
51
- } from "./generate-postgres-ddl-logic";
52
- import { generateSchema } from "./generate-drizzle-schema-logic";
53
- import {
54
- planCollectionSchemaEnsure,
55
- type EnsureOptions,
56
- type ExistingSchema,
57
- type WithheldConstraint
58
- } from "./ensure-collection-tables";
59
- import { classifyCollectionChanges, type ClassifiedChanges } from "./classify-change";
60
-
61
- /**
62
- * Re-exported from the shared kernel. `@rebasepro/server` derives these for a
63
- * project in a subdirectory and cannot import a driver to do it, so the shape
64
- * and the defaults live in `@rebasepro/types`.
65
- */
66
- export { DEFAULT_COMMIT_PATHS, type SchemaCommitPaths } from "@rebasepro/types";
67
-
68
- export interface SchemaCommitFile {
69
- path: string;
70
- contents: string;
71
- }
72
-
73
- export interface SchemaCommitInput {
74
- /** What the running database was built from. */
75
- before: CollectionConfig[];
76
- /** What the editor is proposing. */
77
- after: CollectionConfig[];
78
- /**
79
- * Files the caller has already produced — in practice the rewritten
80
- * collection source from the AST editor. Carried through unchanged so the
81
- * commit is complete in one object.
82
- */
83
- sourceFiles?: SchemaCommitFile[];
84
- paths?: Partial<SchemaCommitPaths>;
85
- /**
86
- * What the database this change is destined for actually has.
87
- *
88
- * Supplied by the live editor, which read it a moment ago; omitted by the
89
- * pure callers, which have no database. It decides two things a plan cannot
90
- * know from the collections alone — whether a table holds rows, and which
91
- * values an enum type already carries — and both are the difference between
92
- * a statement that applies and one that is rejected.
93
- */
94
- existing?: ExistingSchema;
95
- }
96
-
97
- export interface SchemaCommit {
98
- /** Every file the commit writes, source and generated alike. */
99
- files: SchemaCommitFile[];
100
- /**
101
- * The additive statements this change adds, in dependency order.
102
- *
103
- * Empty when the change needs no DDL. Not written to a migration file —
104
- * see the module comment.
105
- */
106
- statements: string[];
107
- classified: ClassifiedChanges;
108
- /** A commit message describing the change in the terms a reader wants. */
109
- message: string;
110
- /**
111
- * Constraints this change asks for that the statements do not carry, and
112
- * why. Empty for almost every change; when it is not, it is the thing the
113
- * person confirming needs to read before they confirm.
114
- */
115
- withheldConstraints: WithheldConstraint[];
116
- }
117
-
118
- export class SchemaCommitError extends Error {
119
- constructor(message: string, readonly classified: ClassifiedChanges) {
120
- super(message);
121
- this.name = "SchemaCommitError";
122
- }
123
- }
124
-
125
- /** An `ExistingSchema` describing a database that has nothing in it. */
126
- const nothing = () => ({ tables: new Map<string, Set<string>>(), enums: new Set<string>() });
127
-
128
- /**
129
- * The statements that take `before` to `after`.
130
- *
131
- * Both sides are planned against the *same* database and the difference is
132
- * taken by exact statement text. That works because the planner is
133
- * deterministic: the same collections against the same schema produce the same
134
- * strings, so anything in the second plan and absent from the first is what
135
- * this change adds — and nothing else. Planning both sides is what keeps
136
- * pre-existing drift, which belongs to neither side of the edit, out of the
137
- * statements this change gets credited with.
138
- *
139
- * ## Why `existing` matters more than it looks
140
- *
141
- * Planned against `nothing()`, every table reads as one this plan is creating,
142
- * and the planner is then free to attach constraints that only hold on a table
143
- * with no rows: a new required property comes out as
144
- * `ADD COLUMN "x" TEXT NOT NULL`, which is right for a fresh table and fails
145
- * against a live one holding rows. Those statements would be generated,
146
- * committed, and then rejected by the very database they were written for.
147
- *
148
- * So a caller holding a real database passes it, and gets statements that
149
- * describe that database. `nothing()` stays the default for the pure uses —
150
- * generating a commit for inspection, and the tests that compare two plans —
151
- * where there is no database to describe.
152
- */
153
- export function additiveStatements(
154
- before: CollectionConfig[],
155
- after: CollectionConfig[],
156
- existing: ExistingSchema = nothing(),
157
- options: EnsureOptions = {}
158
- ): string[] {
159
- const previous = new Set(planCollectionSchemaEnsure(before, existing, options).statements);
160
- return planCollectionSchemaEnsure(after, existing, options).statements
161
- .filter(statement => !previous.has(statement));
162
- }
163
-
164
- /** A commit message that says what changed rather than that something did. */
165
- export function commitMessage(classified: ClassifiedChanges): string {
166
- const { changes } = classified;
167
- if (changes.length === 0) return "chore(schema): no change";
168
-
169
- const collections = [...new Set(changes.map(change => change.collection))].sort();
170
- const added = changes.filter(c => c.kind === "add-collection").map(c => c.collection);
171
- const properties = changes.filter(c => c.kind === "add-property");
172
-
173
- let subject: string;
174
- if (added.length === 1 && changes.length === 1) {
175
- subject = `add the ${added[0]} collection`;
176
- } else if (properties.length === 1 && changes.length === 1) {
177
- subject = `add ${properties[0].property} to ${properties[0].collection}`;
178
- } else if (collections.length === 1) {
179
- subject = `${changes.length} change(s) to ${collections[0]}`;
180
- } else {
181
- subject = `${changes.length} change(s) across ${collections.length} collections`;
182
- }
183
-
184
- const body = changes.map(change => `- ${change.detail}`).join("\n");
185
- return `feat(schema): ${subject}\n\n${body}\n`;
186
- }
187
-
188
- /**
189
- * Build the commit.
190
- *
191
- * Refuses when the change is not applicable — a commit describing a schema the
192
- * ensure path will not produce is a commit that makes the repository lie about
193
- * the database. The classification travels on the error so a caller can show
194
- * exactly which change was the problem.
195
- */
196
- export async function generateSchemaCommit(input: SchemaCommitInput): Promise<SchemaCommit> {
197
- const paths = { ...DEFAULT_COMMIT_PATHS, ...input.paths };
198
- const existing = input.existing ?? nothing();
199
- // `converge` because every statement this produces is shown to somebody
200
- // before it runs. See `ConstraintPolicy` for why the unattended boot does
201
- // not get the same latitude.
202
- const options: EnsureOptions = { constraints: "converge" };
203
- const classified = classifyCollectionChanges(input.before, input.after, input.existing);
204
-
205
- if (!classified.applicable) {
206
- const blocking = classified.changes.filter(change => change.verdict !== "safe");
207
- throw new SchemaCommitError(
208
- `This change cannot be applied to a running database:\n` +
209
- blocking.map(change =>
210
- ` • ${change.detail}${change.remedy ? `\n ${change.remedy}` : ""}`
211
- ).join("\n"),
212
- classified
213
- );
214
- }
215
-
216
- // The same split `rebase db generate` writes, and it has to be the same:
217
- // these files land in the repository, and the next `db push` runs Atlas
218
- // against `schema.sql`. Generated whole, it carries the RLS policies (which
219
- // Atlas would then own and drop), the search helpers (which its free tier
220
- // refuses to parse at all) and `VECTOR(n)` (which its dev database cannot
221
- // resolve) — so a live schema edit used to leave behind a desired-state
222
- // file that `db push` chokes on.
223
- const [schema, ddl, policies, search, vector] = await Promise.all([
224
- generateSchema(input.after),
225
- generatePostgresDdl(input.after, {
226
- includePolicies: false,
227
- includeSearch: false,
228
- includeVector: false
229
- }),
230
- Promise.resolve(generatePostgresPoliciesDdl(input.after)),
231
- Promise.resolve(generatePostgresSearchDdl(input.after)),
232
- // The registry, because this runs inside the booted server, which has
233
- // already evaluated the project's `resources.ts`. A commit that wrote a
234
- // `vector.sql` disagreeing with the one `rebase db generate` produces
235
- // would show up as drift in the repository the moment anyone regenerated.
236
- Promise.resolve(generatePostgresVectorDdl(input.after, {
237
- extensions: declaredDatabaseExtensions()
238
- }))
239
- ]);
240
-
241
- const generated: SchemaCommitFile[] = [
242
- { path: paths.schemaFile, contents: schema },
243
- { path: paths.ddlFile, contents: ddl },
244
- { path: paths.policiesFile, contents: policies },
245
- { path: paths.searchFile, contents: search },
246
- { path: paths.vectorFile, contents: vector }
247
- ];
248
-
249
- // Both sides planned once, here, rather than through `additiveStatements` —
250
- // which would plan `after` a second time for the withheld constraints. The
251
- // planner is pure, so a second call is only wasted work rather than a
252
- // correctness problem, but this runs on every `/plan` keystroke in the panel.
253
- const previous = planCollectionSchemaEnsure(input.before, existing, options);
254
- const next = planCollectionSchemaEnsure(input.after, existing, options);
255
- const already = new Set(previous.statements);
256
-
257
- return {
258
- files: [...(input.sourceFiles ?? []), ...generated],
259
- statements: next.statements.filter(statement => !already.has(statement)),
260
- classified,
261
- message: commitMessage(classified),
262
- // From the `after` plan alone rather than differenced against `before`:
263
- // a constraint that was already unenforceable is still something the
264
- // person confirming this change should see named.
265
- withheldConstraints: next.withheldConstraints
266
- };
267
- }