@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.
- package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
- package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +83 -2
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +6 -0
- package/dist/backup/backup-cli.d.ts +14 -0
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
- package/dist/backup-cli-aUfRv16h.js.map +1 -0
- package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
- package/dist/backup-service-BTBB7RXn.js.map +1 -0
- package/dist/boot-ddl-DVpsFN6r.js +105 -0
- package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
- package/dist/cli-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +51 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
- package/dist/cli-flags-B-Dawocl.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -0
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +563 -182
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
- package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
- package/dist/connection-BPoNILnD.js.map +1 -0
- package/dist/connection.d.ts +9 -1
- package/dist/databasePoolManager-BkEy2w18.js +119 -0
- package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
- package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
- package/dist/doctor-C4WiftHm.js.map +1 -0
- package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
- package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
- package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
- package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
- package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
- package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
- package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
- package/dist/generate-files-CdEckX6c.js +199 -0
- package/dist/generate-files-CdEckX6c.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
- package/dist/generated-sql.d.ts +8 -2
- package/dist/history/HistoryService.d.ts +38 -6
- package/dist/history/ensure-history-table.d.ts +42 -4
- package/dist/index.es.js +2922 -7475
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
- package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
- package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
- package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
- package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
- package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
- package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +2 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +5 -0
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +7 -0
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +20 -1
- package/dist/schema/ensure-collection-policies.d.ts +73 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/introspect-db-logic.d.ts +12 -0
- package/dist/schema/introspect-db-queries.d.ts +11 -1
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +4 -5
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +126 -0
- package/dist/schema/plan/types.d.ts +8 -0
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/security/rls-enforcement.d.ts +32 -4
- package/dist/services/FetchService.d.ts +58 -8
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +88 -3
- package/dist/services/cdc/CdcListener.d.ts +17 -6
- package/dist/services/cdc/trigger-cdc.d.ts +151 -27
- package/dist/services/channel-history.d.ts +15 -4
- package/dist/services/collection-helpers.d.ts +15 -0
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/realtimeService.d.ts +314 -14
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +42 -2
- package/dist/services/sql-script.d.ts +207 -3
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +6 -0
- package/dist/services/write-transaction-scope.d.ts +9 -0
- package/dist/socket-liveness-D3UD5_BW.js +7570 -0
- package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DBDbBTL4.js +811 -0
- package/dist/source-DBDbBTL4.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/drizzle-conditions.d.ts +21 -1
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/websocket.d.ts +13 -0
- package/package.json +7 -7
- package/dist/backup-cli-DW5p9_zv.js.map +0 -1
- package/dist/backup-service-EwcDVG-8.js.map +0 -1
- package/dist/cli-errors-DsA-K9uP.js +0 -893
- package/dist/cli-errors-DsA-K9uP.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-C-sYWbmt.js.map +0 -1
- package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
- package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
- package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
- package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
- package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
- package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
- package/dist/plan-schema-DU9exq6C.js.map +0 -1
- package/dist/policy-drift-B-J2hhm0.js.map +0 -1
- package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
- package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
- package/dist/utils/sql-redaction.d.ts +0 -21
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
+
import { i as outError, n as describeFailure, r as out, t as chalk } from "./source-DBDbBTL4.js";
|
|
5
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-B74WN37S.js";
|
|
6
|
+
import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "./generate-postgres-ddl-logic-B64O2oZF.js";
|
|
7
|
+
import { loadCollectionsFromDirectory } from "@rebasepro/server";
|
|
8
|
+
import { declaredDatabaseExtensions } from "@rebasepro/types";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import * as fs$1 from "fs";
|
|
11
|
+
import { promises } from "fs";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
13
|
+
//#region src/schema/generate-next-step.ts
|
|
14
|
+
/**
|
|
15
|
+
* The line `rebase schema generate` ends on.
|
|
16
|
+
*
|
|
17
|
+
* Its own module because `generate-drizzle-schema.ts` uses `import.meta` to
|
|
18
|
+
* decide whether it was executed directly, which the driver's jest suite cannot
|
|
19
|
+
* load at all — so the one sentence in that file with a decision in it had no
|
|
20
|
+
* test and could not have one where it stood.
|
|
21
|
+
*
|
|
22
|
+
* Printed only by `rebase schema generate` itself. `db push` and `db generate`
|
|
23
|
+
* run the same generator as their first step, and inside them the line was
|
|
24
|
+
* wrong advice: `db push` was told to run `db generate`, and `db generate` to
|
|
25
|
+
* run itself.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A command name, highlighted the way the rest of the driver's output is: with
|
|
29
|
+
* chalk, which drops the colour under `NO_COLOR`, when stdout is not a terminal,
|
|
30
|
+
* and in CI logs. The escapes used to be written by hand here, so
|
|
31
|
+
* `NO_COLOR=1 rebase schema generate > log` put `\x1b[1m\x1b[44m…` in the file.
|
|
32
|
+
*/
|
|
33
|
+
var command = (text) => chalk.bold.bgBlue.black(text);
|
|
34
|
+
/**
|
|
35
|
+
* What to do with the file that was just generated.
|
|
36
|
+
*
|
|
37
|
+
* Not the same answer on the managed development database: `rebase db generate`
|
|
38
|
+
* plans with Atlas, Atlas needs a second empty database to diff against, and
|
|
39
|
+
* PGlite serves exactly one — so the CLI refuses that command outright there.
|
|
40
|
+
* This line recommended it unconditionally, which meant every stock scaffold
|
|
41
|
+
* was told, by the tool itself, to run something the same tool would refuse.
|
|
42
|
+
*
|
|
43
|
+
* The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the
|
|
44
|
+
* ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is
|
|
45
|
+
* started to make it — and the driver cannot make it for itself: on the managed
|
|
46
|
+
* path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres
|
|
47
|
+
* on loopback. Absent (the driver run directly, outside the CLI) keeps the
|
|
48
|
+
* original wording.
|
|
49
|
+
*/
|
|
50
|
+
var nextStepAfterGenerate = () => {
|
|
51
|
+
if (process.env.REBASE_DEV_DATABASE_KIND === "managed") return `Start or restart ${command("rebase dev")} — boot applies your collections to the managed development database.`;
|
|
52
|
+
return `You can now run ${command("rebase db generate")} to generate the SQL migration files.`;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/schema/generate-files.ts
|
|
56
|
+
var generate_files_exports = /* @__PURE__ */ __exportAll({
|
|
57
|
+
GenerationFailed: () => GenerationFailed,
|
|
58
|
+
writeDrizzleSchema: () => writeDrizzleSchema,
|
|
59
|
+
writePostgresDdl: () => writePostgresDdl
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* The two generators `rebase db push` runs as its step 1, as functions.
|
|
63
|
+
*
|
|
64
|
+
* They used to exist only as scripts, each its own `tsx` process: the driver
|
|
65
|
+
* (itself a `tsx` process) spawned one for the Drizzle schema and then one for
|
|
66
|
+
* the DDL, and each paid a Node start, tsx's loader and an import of the whole
|
|
67
|
+
* server package before reading a single collection — about 0.5 s apiece, a
|
|
68
|
+
* second per push. The driver process is one-shot, so a fresh module graph per
|
|
69
|
+
* generation bought nothing: the collections cannot change under it.
|
|
70
|
+
*
|
|
71
|
+
* The scripts remain (`generate-drizzle-schema.ts`, `generate-postgres-ddl.ts`)
|
|
72
|
+
* for `--watch`, which re-imports the collections on every save and does need a
|
|
73
|
+
* process of its own — Node's module cache would serve it the first version
|
|
74
|
+
* forever.
|
|
75
|
+
*
|
|
76
|
+
* Both functions print their own success lines and their own error, exactly as
|
|
77
|
+
* the scripts did, and throw {@link GenerationFailed} after printing — so a
|
|
78
|
+
* caller can tell "the generator said why" from "the generator never ran".
|
|
79
|
+
*/
|
|
80
|
+
/** Thrown once the generator has printed why it failed. */
|
|
81
|
+
var GenerationFailed = class extends Error {
|
|
82
|
+
constructor(message, options) {
|
|
83
|
+
super(message, options);
|
|
84
|
+
this.name = "GenerationFailed";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** The collections, in slug order so the output does not depend on the directory's. */
|
|
88
|
+
async function loadSortedCollections(collectionsPath) {
|
|
89
|
+
const loaded = await loadCollectionsFromDirectory(path.resolve(collectionsPath));
|
|
90
|
+
const collections = Array.isArray(loaded) ? [...loaded] : [];
|
|
91
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
92
|
+
return collections;
|
|
93
|
+
}
|
|
94
|
+
/** `src/schema.generated.ts`, from the collections. */
|
|
95
|
+
async function writeDrizzleSchema(options) {
|
|
96
|
+
try {
|
|
97
|
+
const schemaContent = generateSchema(await loadSortedCollections(options.collectionsPath));
|
|
98
|
+
if (options.outputPath) {
|
|
99
|
+
await promises.mkdir(path.dirname(options.outputPath), { recursive: true });
|
|
100
|
+
await promises.writeFile(options.outputPath, schemaContent);
|
|
101
|
+
out(`✅ Drizzle schema generated successfully at ${options.outputPath}`);
|
|
102
|
+
} else {
|
|
103
|
+
out("✅ Drizzle schema generated successfully.");
|
|
104
|
+
out(String(schemaContent));
|
|
105
|
+
}
|
|
106
|
+
if (options.nextStep !== false) out(nextStepAfterGenerate());
|
|
107
|
+
} catch (error) {
|
|
108
|
+
outError(`Error generating schema: ${describeFailure(error)}`);
|
|
109
|
+
throw new GenerationFailed("Drizzle schema generation failed", { cause: error });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Where a project declares its resources, relative to the config directory. */
|
|
113
|
+
var RESOURCE_ENTRIES = [
|
|
114
|
+
"resources.ts",
|
|
115
|
+
"resources.js",
|
|
116
|
+
"resources/index.ts",
|
|
117
|
+
"resources/index.js"
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can
|
|
121
|
+
* answer.
|
|
122
|
+
*
|
|
123
|
+
* The DDL generator loads only the collections directory, so nothing else in it
|
|
124
|
+
* would ever import the resources module — and a permission that reads as
|
|
125
|
+
* absent because nobody loaded it is the worst shape this could take: the push
|
|
126
|
+
* succeeds, `vector.sql` quietly omits the `CREATE EXTENSION`, and the failure
|
|
127
|
+
* lands one statement later looking like a database problem.
|
|
128
|
+
*
|
|
129
|
+
* Imported directly rather than through the config index, for the reason
|
|
130
|
+
* `boot/resource-loading.ts` gives: re-exporting from the index would work only
|
|
131
|
+
* for projects that remember to.
|
|
132
|
+
*
|
|
133
|
+
* A project with no resources module is the normal case and declares nothing.
|
|
134
|
+
* An import that *throws* is not swallowed — a resources module that does not
|
|
135
|
+
* evaluate is a configuration error, and treating it as "declared nothing"
|
|
136
|
+
* would turn it into a silently missing extension.
|
|
137
|
+
*/
|
|
138
|
+
async function loadDeclaredResources(configDir) {
|
|
139
|
+
const entry = RESOURCE_ENTRIES.map((name) => path.join(configDir, name)).find((candidate) => fs$1.existsSync(candidate));
|
|
140
|
+
if (!entry) return;
|
|
141
|
+
await import(pathToFileURL(entry).href);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `schema.sql` (Atlas's desired state) and, beside it, `policies.sql`,
|
|
145
|
+
* `search.sql`, `vector.sql` and `triggers.sql` — the parts Atlas does not own.
|
|
146
|
+
*/
|
|
147
|
+
async function writePostgresDdl(options) {
|
|
148
|
+
try {
|
|
149
|
+
const resolvedPath = path.resolve(options.collectionsPath);
|
|
150
|
+
await loadDeclaredResources(path.dirname(resolvedPath));
|
|
151
|
+
const collections = await loadSortedCollections(resolvedPath);
|
|
152
|
+
const ddlContent = generatePostgresDdl(collections, {
|
|
153
|
+
includePolicies: false,
|
|
154
|
+
includeSearch: false,
|
|
155
|
+
includeVector: false
|
|
156
|
+
});
|
|
157
|
+
const policiesContent = generatePostgresPoliciesDdl(collections);
|
|
158
|
+
const searchContent = generatePostgresSearchDdl(collections);
|
|
159
|
+
const vectorContent = generatePostgresVectorDdl(collections, { extensions: declaredDatabaseExtensions() });
|
|
160
|
+
const triggersContent = generatePostgresTriggersDdl(collections);
|
|
161
|
+
const outputPath = options.outputPath;
|
|
162
|
+
if (outputPath) {
|
|
163
|
+
const outputDir = path.dirname(outputPath);
|
|
164
|
+
await promises.mkdir(outputDir, { recursive: true });
|
|
165
|
+
await promises.writeFile(outputPath, ddlContent);
|
|
166
|
+
out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);
|
|
167
|
+
const policiesPath = path.join(outputDir, "policies.sql");
|
|
168
|
+
await promises.writeFile(policiesPath, policiesContent);
|
|
169
|
+
out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);
|
|
170
|
+
const searchPath = path.join(outputDir, "search.sql");
|
|
171
|
+
if (searchContent) {
|
|
172
|
+
await promises.writeFile(searchPath, searchContent);
|
|
173
|
+
out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);
|
|
174
|
+
} else if (fs$1.existsSync(searchPath)) await promises.rm(searchPath);
|
|
175
|
+
const vectorPath = path.join(outputDir, "vector.sql");
|
|
176
|
+
if (vectorContent) {
|
|
177
|
+
await promises.writeFile(vectorPath, vectorContent);
|
|
178
|
+
out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);
|
|
179
|
+
} else if (fs$1.existsSync(vectorPath)) await promises.rm(vectorPath);
|
|
180
|
+
const triggersPath = path.join(outputDir, "triggers.sql");
|
|
181
|
+
if (triggersContent) {
|
|
182
|
+
await promises.writeFile(triggersPath, triggersContent);
|
|
183
|
+
out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);
|
|
184
|
+
} else if (fs$1.existsSync(triggersPath)) await promises.rm(triggersPath);
|
|
185
|
+
} else {
|
|
186
|
+
out("✅ PostgreSQL DDL generated successfully.");
|
|
187
|
+
out(String(ddlContent));
|
|
188
|
+
out("\n✅ PostgreSQL Policies DDL generated successfully.");
|
|
189
|
+
out(String(policiesContent));
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
outError(`Error generating DDL schema: ${describeFailure(error)}`);
|
|
193
|
+
throw new GenerationFailed("Postgres DDL generation failed", { cause: error });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { writeDrizzleSchema as n, writePostgresDdl as r, generate_files_exports as t };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=generate-files-CdEckX6c.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-files-CdEckX6c.js","names":[],"sources":["../src/schema/generate-next-step.ts","../src/schema/generate-files.ts"],"sourcesContent":["/**\n * The line `rebase schema generate` ends on.\n *\n * Its own module because `generate-drizzle-schema.ts` uses `import.meta` to\n * decide whether it was executed directly, which the driver's jest suite cannot\n * load at all — so the one sentence in that file with a decision in it had no\n * test and could not have one where it stood.\n *\n * Printed only by `rebase schema generate` itself. `db push` and `db generate`\n * run the same generator as their first step, and inside them the line was\n * wrong advice: `db push` was told to run `db generate`, and `db generate` to\n * run itself.\n */\nimport chalk from \"chalk\";\n\n/**\n * A command name, highlighted the way the rest of the driver's output is: with\n * chalk, which drops the colour under `NO_COLOR`, when stdout is not a terminal,\n * and in CI logs. The escapes used to be written by hand here, so\n * `NO_COLOR=1 rebase schema generate > log` put `\\x1b[1m\\x1b[44m…` in the file.\n */\nconst command = (text: string): string => chalk.bold.bgBlue.black(text);\n\n/**\n * What to do with the file that was just generated.\n *\n * Not the same answer on the managed development database: `rebase db generate`\n * plans with Atlas, Atlas needs a second empty database to diff against, and\n * PGlite serves exactly one — so the CLI refuses that command outright there.\n * This line recommended it unconditionally, which meant every stock scaffold\n * was told, by the tool itself, to run something the same tool would refuse.\n *\n * The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the\n * ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is\n * started to make it — and the driver cannot make it for itself: on the managed\n * path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres\n * on loopback. Absent (the driver run directly, outside the CLI) keeps the\n * original wording.\n */\nexport const nextStepAfterGenerate = (): string => {\n if (process.env.REBASE_DEV_DATABASE_KIND === \"managed\") {\n return `Start or restart ${command(\"rebase dev\")} — boot applies your collections to the managed development database.`;\n }\n\n return `You can now run ${command(\"rebase db generate\")} to generate the SQL migration files.`;\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { CollectionConfig, declaredDatabaseExtensions } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport {\n generatePostgresDdl,\n generatePostgresPoliciesDdl,\n generatePostgresSearchDdl,\n generatePostgresTriggersDdl,\n generatePostgresVectorDdl\n} from \"./generate-postgres-ddl-logic\";\nimport { describeFailure, out, outError } from \"../cli-output\";\nimport { nextStepAfterGenerate } from \"./generate-next-step\";\n\n/**\n * The two generators `rebase db push` runs as its step 1, as functions.\n *\n * They used to exist only as scripts, each its own `tsx` process: the driver\n * (itself a `tsx` process) spawned one for the Drizzle schema and then one for\n * the DDL, and each paid a Node start, tsx's loader and an import of the whole\n * server package before reading a single collection — about 0.5 s apiece, a\n * second per push. The driver process is one-shot, so a fresh module graph per\n * generation bought nothing: the collections cannot change under it.\n *\n * The scripts remain (`generate-drizzle-schema.ts`, `generate-postgres-ddl.ts`)\n * for `--watch`, which re-imports the collections on every save and does need a\n * process of its own — Node's module cache would serve it the first version\n * forever.\n *\n * Both functions print their own success lines and their own error, exactly as\n * the scripts did, and throw {@link GenerationFailed} after printing — so a\n * caller can tell \"the generator said why\" from \"the generator never ran\".\n */\n\n/** Thrown once the generator has printed why it failed. */\nexport class GenerationFailed extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"GenerationFailed\";\n }\n}\n\nexport interface GenerateFileOptions {\n /** The collections directory (or single module), resolved against the cwd. */\n collectionsPath: string;\n /** Where to write. Absent: print to stdout instead. */\n outputPath?: string;\n /**\n * End on the line that says what to run next. Only `rebase schema\n * generate` itself wants it: inside `db push` it told the push to run\n * `db generate`, and inside `db generate` to run itself.\n */\n nextStep?: boolean;\n}\n\n/** The collections, in slug order so the output does not depend on the directory's. */\nasync function loadSortedCollections(collectionsPath: string): Promise<CollectionConfig[]> {\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n const loaded = await loadCollectionsFromDirectory(path.resolve(collectionsPath));\n // A copy: the loader's array is its caller's, and in-process the same\n // collections are read again by the steps after this one.\n const collections: CollectionConfig[] = Array.isArray(loaded) ? [...loaded] : [];\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n return collections;\n}\n\n/** `src/schema.generated.ts`, from the collections. */\nexport async function writeDrizzleSchema(options: GenerateFileOptions): Promise<void> {\n try {\n const collections = await loadSortedCollections(options.collectionsPath);\n const schemaContent = generateSchema(collections);\n\n if (options.outputPath) {\n await fsPromises.mkdir(path.dirname(options.outputPath), { recursive: true });\n await fsPromises.writeFile(options.outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${options.outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n if (options.nextStep !== false) out(nextStepAfterGenerate());\n } catch (error) {\n outError(`Error generating schema: ${describeFailure(error)}`);\n // `rebase db push` runs this as step 1 and must stop on it: going on\n // applied whatever `src/schema.generated.ts` was already there.\n throw new GenerationFailed(\"Drizzle schema generation failed\", { cause: error });\n }\n}\n\n/** Where a project declares its resources, relative to the config directory. */\nconst RESOURCE_ENTRIES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/**\n * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can\n * answer.\n *\n * The DDL generator loads only the collections directory, so nothing else in it\n * would ever import the resources module — and a permission that reads as\n * absent because nobody loaded it is the worst shape this could take: the push\n * succeeds, `vector.sql` quietly omits the `CREATE EXTENSION`, and the failure\n * lands one statement later looking like a database problem.\n *\n * Imported directly rather than through the config index, for the reason\n * `boot/resource-loading.ts` gives: re-exporting from the index would work only\n * for projects that remember to.\n *\n * A project with no resources module is the normal case and declares nothing.\n * An import that *throws* is not swallowed — a resources module that does not\n * evaluate is a configuration error, and treating it as \"declared nothing\"\n * would turn it into a silently missing extension.\n */\nasync function loadDeclaredResources(configDir: string): Promise<void> {\n const entry = RESOURCE_ENTRIES\n .map(name => path.join(configDir, name))\n .find(candidate => fs.existsSync(candidate));\n if (!entry) return;\n await import(pathToFileURL(entry).href);\n}\n\n/**\n * `schema.sql` (Atlas's desired state) and, beside it, `policies.sql`,\n * `search.sql`, `vector.sql` and `triggers.sql` — the parts Atlas does not own.\n */\nexport async function writePostgresDdl(options: GenerateFileOptions): Promise<void> {\n try {\n const resolvedPath = path.resolve(options.collectionsPath);\n\n // `<config>/collections` is the path this is given, so the config\n // directory is its parent — the same relationship `rebase resources`\n // and the boot loader assume.\n await loadDeclaredResources(path.dirname(resolvedPath));\n\n const collections = await loadSortedCollections(resolvedPath);\n\n // `schema.sql` is Atlas's desired state, and it carries neither the RLS\n // policies, nor the search apparatus, nor the vector columns: Atlas\n // manages none of them, and in the search and vector cases cannot. All\n // three are written beside it and applied by the CLI in their own right.\n const ddlContent = generatePostgresDdl(collections, {\n includePolicies: false,\n includeSearch: false,\n includeVector: false\n });\n const policiesContent = generatePostgresPoliciesDdl(collections);\n const searchContent = generatePostgresSearchDdl(collections);\n const vectorContent = generatePostgresVectorDdl(collections, {\n extensions: declaredDatabaseExtensions()\n });\n const triggersContent = generatePostgresTriggersDdl(collections);\n\n const outputPath = options.outputPath;\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, ddlContent);\n out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);\n\n const policiesPath = path.join(outputDir, \"policies.sql\");\n await fsPromises.writeFile(policiesPath, policiesContent);\n out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);\n\n // Removed when the last `search` block goes: the CLI applies this\n // file whenever it exists, and a stale one would keep re-creating\n // functions for collections that no longer want them.\n const searchPath = path.join(outputDir, \"search.sql\");\n if (searchContent) {\n await fsPromises.writeFile(searchPath, searchContent);\n out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);\n } else if (fs.existsSync(searchPath)) {\n await fsPromises.rm(searchPath);\n }\n\n // Removed when the last `vector` property goes, for the same reason\n // `search.sql` is: the CLI applies whichever of these files exists,\n // and a stale one would keep re-adding a column the collections no\n // longer declare — which Atlas, no longer told to exclude it, would\n // then plan a DROP for on the very next push.\n const vectorPath = path.join(outputDir, \"vector.sql\");\n if (vectorContent) {\n await fsPromises.writeFile(vectorPath, vectorContent);\n out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);\n } else if (fs.existsSync(vectorPath)) {\n await fsPromises.rm(vectorPath);\n }\n\n // `autoValue: \"on_update\"`, on the same terms as the two above: a\n // trigger is a function plus a binding, which Atlas's free tier\n // will not parse, and a stale file would keep re-installing a\n // trigger for a column nothing declares any more.\n const triggersPath = path.join(outputDir, \"triggers.sql\");\n if (triggersContent) {\n await fsPromises.writeFile(triggersPath, triggersContent);\n out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);\n } else if (fs.existsSync(triggersPath)) {\n await fsPromises.rm(triggersPath);\n }\n } else {\n out(\"✅ PostgreSQL DDL generated successfully.\");\n out(String(ddlContent));\n out(\"\\n✅ PostgreSQL Policies DDL generated successfully.\");\n out(String(policiesContent));\n }\n } catch (error) {\n outError(`Error generating DDL schema: ${describeFailure(error)}`);\n // Step 1 of `rebase db push` is this, and a failure has to stop it.\n // Going on made a resources module that does not evaluate — the single\n // most common cause here — report `✓ rebase db push completed\n // successfully` while Atlas applied whatever `drizzle/schema.sql`\n // happened to be on disk, sometimes minutes old. With no prior file the\n // same run died inside Atlas on `stat drizzle/schema.sql: no such file\n // or directory`, which hides the real cause two screens up.\n throw new GenerationFailed(\"Postgres DDL generation failed\", { cause: error });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,WAAW,SAAyB,MAAM,KAAK,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;AAkBtE,IAAa,8BAAsC;CAC/C,IAAI,QAAQ,IAAI,6BAA6B,WACzC,OAAO,oBAAoB,QAAQ,YAAY,EAAE;CAGrD,OAAO,mBAAmB,QAAQ,oBAAoB,EAAE;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPA,IAAa,mBAAb,cAAsC,MAAM;CACxC,YAAY,SAAiB,SAA+B;EACxD,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CAChB;AACJ;;AAgBA,eAAe,sBAAsB,iBAAsD;CAGvF,MAAM,SAAS,MAAM,6BAA6B,KAAK,QAAQ,eAAe,CAAC;CAG/E,MAAM,cAAkC,MAAM,QAAQ,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC;CAC/E,YAAY,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;CACvD,OAAO;AACX;;AAGA,eAAsB,mBAAmB,SAA6C;CAClF,IAAI;EAEA,MAAM,gBAAgB,eAAe,MADX,sBAAsB,QAAQ,eAAe,CACvB;EAEhD,IAAI,QAAQ,YAAY;GACpB,MAAM,SAAW,MAAM,KAAK,QAAQ,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;GAC5E,MAAM,SAAW,UAAU,QAAQ,YAAY,aAAa;GAC5D,IAAI,8CAA8C,QAAQ,YAAY;EAC1E,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,aAAa,CAAC;EAC7B;EAEA,IAAI,QAAQ,aAAa,OAAO,IAAI,sBAAsB,CAAC;CAC/D,SAAS,OAAO;EACZ,SAAS,4BAA4B,gBAAgB,KAAK,GAAG;EAG7D,MAAM,IAAI,iBAAiB,oCAAoC,EAAE,OAAO,MAAM,CAAC;CACnF;AACJ;;AAGA,IAAM,mBAAmB;CAAC;CAAgB;CAAgB;CAAsB;AAAoB;;;;;;;;;;;;;;;;;;;;AAqBpG,eAAe,sBAAsB,WAAkC;CACnE,MAAM,QAAQ,iBACT,KAAI,SAAQ,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,CACvC,MAAK,cAAa,KAAG,WAAW,SAAS,CAAC;CAC/C,IAAI,CAAC,OAAO;CACZ,MAAM,OAAO,cAAc,KAAK,CAAC,CAAC;AACtC;;;;;AAMA,eAAsB,iBAAiB,SAA6C;CAChF,IAAI;EACA,MAAM,eAAe,KAAK,QAAQ,QAAQ,eAAe;EAKzD,MAAM,sBAAsB,KAAK,QAAQ,YAAY,CAAC;EAEtD,MAAM,cAAc,MAAM,sBAAsB,YAAY;EAM5D,MAAM,aAAa,oBAAoB,aAAa;GAChD,iBAAiB;GACjB,eAAe;GACf,eAAe;EACnB,CAAC;EACD,MAAM,kBAAkB,4BAA4B,WAAW;EAC/D,MAAM,gBAAgB,0BAA0B,WAAW;EAC3D,MAAM,gBAAgB,0BAA0B,aAAa,EACzD,YAAY,2BAA2B,EAC3C,CAAC;EACD,MAAM,kBAAkB,4BAA4B,WAAW;EAE/D,MAAM,aAAa,QAAQ;EAC3B,IAAI,YAAY;GACZ,MAAM,YAAY,KAAK,QAAQ,UAAU;GACzC,MAAM,SAAW,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;GACrD,MAAM,SAAW,UAAU,YAAY,UAAU;GACjD,IAAI,8CAA8C,YAAY;GAE9D,MAAM,eAAe,KAAK,KAAK,WAAW,cAAc;GACxD,MAAM,SAAW,UAAU,cAAc,eAAe;GACxD,IAAI,uDAAuD,cAAc;GAKzE,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;GAQlC,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;GAOlC,MAAM,eAAe,KAAK,KAAK,WAAW,cAAc;GACxD,IAAI,iBAAiB;IACjB,MAAM,SAAW,UAAU,cAAc,eAAe;IACxD,IAAI,sDAAsD,cAAc;GAC5E,OAAO,IAAI,KAAG,WAAW,YAAY,GACjC,MAAM,SAAW,GAAG,YAAY;EAExC,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,UAAU,CAAC;GACtB,IAAI,qDAAqD;GACzD,IAAI,OAAO,eAAe,CAAC;EAC/B;CACJ,SAAS,OAAO;EACZ,SAAS,gCAAgC,gBAAgB,KAAK,GAAG;EAQjE,MAAM,IAAI,iBAAiB,kCAAkC,EAAE,OAAO,MAAM,CAAC;CACjF;AACJ"}
|
package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import { n as planSchema } from "./plan-schema-
|
|
5
|
-
import { a as renderPolicyStatements, c as renderSearchDdl, d as searchExcludePatternsOf, f as triggerExcludePatternsOf, i as renderPoliciesDdl, l as renderTriggersDdl, o as renderPostgresDdl, p as vectorExcludePatternsOf, r as renderPgType, u as renderVectorDdl } from "./render-ddl-
|
|
4
|
+
import { n as planSchema } from "./plan-schema-B1YF_tYq.js";
|
|
5
|
+
import { a as renderPolicyStatements, c as renderSearchDdl, d as searchExcludePatternsOf, f as triggerExcludePatternsOf, i as renderPoliciesDdl, l as renderTriggersDdl, o as renderPostgresDdl, p as vectorExcludePatternsOf, r as renderPgType, u as renderVectorDdl } from "./render-ddl-CtC3y9Ol.js";
|
|
6
6
|
import "@rebasepro/types";
|
|
7
7
|
import "@rebasepro/common";
|
|
8
8
|
//#region src/schema/generate-postgres-ddl-logic.ts
|
|
@@ -149,4 +149,4 @@ var searchColumnDependencies = (allCollections) => {
|
|
|
149
149
|
//#endregion
|
|
150
150
|
export { generatePostgresVectorDdl as a, planJunctionTables as c, generatePostgresTriggersDdl as i, generatePostgresPoliciesDdl as n, generate_postgres_ddl_logic_exports as o, generatePostgresSearchDdl as r, planCollectionPolicies as s, generatePostgresDdl as t };
|
|
151
151
|
|
|
152
|
-
//# sourceMappingURL=generate-postgres-ddl-logic-
|
|
152
|
+
//# sourceMappingURL=generate-postgres-ddl-logic-B64O2oZF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-postgres-ddl-logic-BJsLaVNX.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts"],"sourcesContent":["/**\n * The SQL half of `rebase db generate`, as a plan and a renderer.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`\n * writes the files; what remains here is the public entry points those two are\n * reached through, plus the smaller plans (`planRelationalColumns`,\n * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as\n * slices of the same plan.\n *\n * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own\n * reading of relations, junctions, enums and search — one of three such\n * readings. See `schema/plan/types.ts` for what that cost.\n */\nimport { CollectionConfig, SecurityRule, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { planSchema, compileSecurityRule, quoteSqlLiteral, defaultBelongsToOnDelete } from \"./plan/plan-schema\";\nimport {\n renderPgType,\n renderPoliciesDdl,\n renderPolicyStatements,\n renderPostgresDdl,\n renderSearchDdl,\n renderTriggersDdl,\n renderVectorDdl,\n searchExcludePatternsOf,\n triggerExcludePatternsOf,\n vectorExcludePatternsOf,\n type DdlRenderOptions\n} from \"./plan/render-ddl\";\nimport type { ForeignKeyPlan, SchemaPlan } from \"./plan/types\";\nimport type { GeneratedColumnDependency } from \"./generated-column-conflicts\";\n\nexport { quoteSqlLiteral, defaultBelongsToOnDelete };\nexport type { ForeignKeyPlan };\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (\n collection: CollectionConfig,\n rule: SecurityRule,\n resolveCollection: ResolveCollection\n): string[] => {\n const table = {\n schema: isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\",\n table: getTableName(collection)\n };\n return compileSecurityRule(collection, rule, resolveCollection, false)\n .flatMap(policy => renderPolicyStatements(table, policy));\n};\n\n/**\n * `.rebase/sql/schema.sql` — the desired state Atlas diffs the database against.\n *\n * Synchronous. It was `async` with nothing to await, which made every caller\n * (the writer script, the doctor, the live editor, two gates) carry a promise\n * for no reason and made the one place that forgot to await it silently compare\n * `[object Promise]`.\n */\nexport const generatePostgresDdl = (\n allCollections: CollectionConfig[],\n options: DdlRenderOptions = {}\n): string => renderPostgresDdl(planSchema(allCollections), options);\n\n/** `.rebase/sql/policies.sql` — the RLS policies. Atlas has no model for a policy. */\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string =>\n renderPoliciesDdl(planSchema(allCollections));\n\n/**\n * `.rebase/sql/search.sql` — everything a `search` block needs, as a file Rebase\n * applies itself.\n *\n * Search is one of the parts of the schema Atlas does not own, for two\n * independent reasons and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * Empty when nothing opted in — the caller writes no file then.\n */\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string =>\n renderSearchDdl(planSchema(allCollections));\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n searchExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/vector.sql` — the `vector` columns, their ANN indexes and, when a\n * database declared it, the extension.\n *\n * Vector is out of Atlas's hands for a narrower reason than search: not that\n * Atlas refuses to parse the file, but that it cannot *execute* it. To compute\n * a desired state Atlas materialises `schema.sql` in a dev database, that\n * database is created empty, and Atlas empties it again at the start of every\n * run — so `VECTOR(384)` resolves against a database that structurally cannot\n * have pgvector, and every push dies with `type \"vector\" does not exist`.\n */\nexport const generatePostgresVectorDdl = (\n allCollections: CollectionConfig[],\n options: { extensions?: readonly string[] } = {}\n): string => renderVectorDdl(planSchema(allCollections, { databaseExtensions: options.extensions }));\n\n/** @see searchExcludePatterns */\nexport const vectorExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n vectorExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/triggers.sql` — the `BEFORE UPDATE` triggers behind\n * `autoValue: \"on_update\"`, and the one function they share.\n *\n * A trigger is a function plus a binding, so it is carved out of Atlas's view\n * for the first of the two reasons search is. Empty when no property declares\n * `on_update` — the caller writes no file then, and removes a stale one.\n */\nexport const generatePostgresTriggersDdl = (allCollections: CollectionConfig[]): string =>\n renderTriggersDdl(planSchema(allCollections));\n\n/** @see searchExcludePatterns */\nexport const triggerExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n triggerExcludePatternsOf(planSchema(allCollections));\n\n// ── Slices of the plan other modules ask for ─────────────────────────────────\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL renderer declares it. */\n type: string;\n /** `validation.required` on the property that owns the link. */\n required: boolean;\n /**\n * True when a declared property emits this column and the relation\n * contributes only the constraint — `postId` with `columnName: \"post_id\"`\n * beside a `belongsTo` on `post_id`.\n */\n columnOwnedByProperty?: boolean;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize. Carried so the boot-time ensure can notice a\n * database provisioned under the old rule; never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst planOf = (collections: CollectionConfig[]): SchemaPlan => planSchema(collections);\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * A view over the schema plan. It was a second walk of the collections beside\n * the `CREATE TABLE` one, which is how boot-ensure came to add every relation\n * column bare while `db push` made the same column `NOT NULL`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const plan = planOf(allCollections);\n const out: RelationalColumnPlan[] = [];\n for (const table of plan.tables) {\n if (table.kind !== \"collection\") continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n out.push({\n schema: table.schema,\n table: table.table,\n column: column.column,\n type: renderPgType(column.type),\n required: !column.nullable,\n columnOwnedByProperty: column.columnOwnedByProperty,\n legacyColumn: column.legacyColumn,\n foreignKey: column.foreignKey\n });\n }\n }\n return out;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from the same plan the derived RLS comes from, so a table created\n * here always has policies planned for it — a junction with row-level security\n * left off is readable and writable by every signed-in user.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] =>\n planOf(allCollections).tables\n .filter(table => table.kind === \"junction\")\n .map(table => ({\n schema: table.schema,\n table: table.table,\n columns: table.columns.map(c => ({\n name: c.column,\n type: renderPgType(c.type),\n legacyName: c.legacyColumn\n })),\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (` +\n table.columns.map(c => `\"${c.column}\" ${renderPgType(c.type)} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`,\n foreignKeys: table.columns.map(c => c.foreignKey).filter((fk): fk is ForeignKeyPlan => fk !== undefined)\n }));\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the declared collections and their junctions, as\n * executable statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * The same policies `policies.sql` carries, from the same plan, so boot and\n * `db push` produce identical RLS from identical collections.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n const plan = planOf(allCollections);\n // Collections first, then the junctions no collection declares.\n const ordered = [\n ...plan.tables.filter(t => t.kind === \"collection\"),\n ...plan.tables.filter(t => t.kind === \"junction\")\n ];\n return ordered.map(table => ({\n schema: table.schema,\n table: table.table,\n qualified: table.qualified,\n enableRls: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements: table.policies.flatMap(policy => renderPolicyStatements(table, policy))\n }));\n};\n\n/**\n * The `generated column → column it reads` edges a project's `search` blocks\n * imply, derived from the collections alone.\n *\n * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,\n * but available where no database is: `rebase db generate` writes a migration\n * that will run somewhere else, later, against a database it cannot inspect.\n * A spec's `fields[].column` is the physical column each search field starts\n * at, which is exactly what the generated expression depends on.\n */\nexport const searchColumnDependencies = (\n allCollections: CollectionConfig[]\n): GeneratedColumnDependency[] => {\n const dependencies: GeneratedColumnDependency[] = [];\n for (const table of planOf(allCollections).tables) {\n const spec = table.search;\n if (!spec) continue;\n const generated = [spec.column, ...(spec.fuzzy ? [spec.fuzzy.column] : [])];\n const sources = new Set(spec.fields.map(field => field.column));\n for (const column of generated) {\n for (const dependsOn of sources) {\n dependencies.push({ schema: table.schema, table: table.table, column, dependsOn });\n }\n }\n }\n return dependencies;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,uBACT,gBACA,UAA4B,CAAC,MACpB,kBAAkB,WAAW,cAAc,GAAG,OAAO;;AAGlE,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;AAqBhD,IAAa,6BAA6B,mBACtC,gBAAgB,WAAW,cAAc,CAAC;;;;;;;;AAS9C,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;;;;AAatD,IAAa,6BACT,gBACA,UAA8C,CAAC,MACtC,gBAAgB,WAAW,gBAAgB,EAAE,oBAAoB,QAAQ,WAAW,CAAC,CAAC;;AAGnG,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;AAUtD,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;AAGhD,IAAa,0BAA0B,mBACnC,yBAAyB,WAAW,cAAc,CAAC;AAyCvD,IAAM,UAAU,gBAAgD,WAAW,WAAW;;;;;;;;AAuCtF,IAAa,sBAAsB,mBAC/B,OAAO,cAAc,CAAC,CAAC,OAClB,QAAO,UAAS,MAAM,SAAS,UAAU,CAAC,CAC1C,KAAI,WAAU;CACX,QAAQ,MAAM;CACd,OAAO,MAAM;CACb,SAAS,MAAM,QAAQ,KAAI,OAAM;EAC7B,MAAM,EAAE;EACR,MAAM,aAAa,EAAE,IAAI;EACzB,YAAY,EAAE;CAClB,EAAE;CACF,aACI,+BAA+B,MAAM,OAAO,KAAK,MAAM,MAAM,OAC7D,MAAM,QAAQ,KAAI,MAAK,IAAI,EAAE,OAAO,IAAI,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,IAAI,IAClF,kBAAkB,MAAM,WAAW,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;CACrE,aAAa,MAAM,QAAQ,KAAI,MAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,OAA6B,OAAO,KAAA,CAAS;AAC3G,EAAE;;;;;;;;;AAuBV,IAAa,0BAA0B,mBAA+D;CAClG,MAAM,OAAO,OAAO,cAAc;CAMlC,OAAO,CAHH,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,YAAY,GAClD,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,UAAU,CAE7C,CAAA,CAAQ,KAAI,WAAU;EACzB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,gBAAgB,MAAM,OAAO,KAAK,MAAM,MAAM;EACzD,kBAAkB,MAAM,SAAS,SAAQ,WAAU,uBAAuB,OAAO,MAAM,CAAC;CAC5F,EAAE;AACN;;;;;;;;;;;AAYA,IAAa,4BACT,mBAC8B;CAC9B,MAAM,eAA4C,CAAC;CACnD,KAAK,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC,QAAQ;EAC/C,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MAAM;EACX,MAAM,YAAY,CAAC,KAAK,QAAQ,GAAI,KAAK,QAAQ,CAAC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAE;EAC1E,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC;EAC9D,KAAK,MAAM,UAAU,WACjB,KAAK,MAAM,aAAa,SACpB,aAAa,KAAK;GAAE,QAAQ,MAAM;GAAQ,OAAO,MAAM;GAAO;GAAQ;EAAU,CAAC;CAG7F;CACA,OAAO;AACX"}
|
|
1
|
+
{"version":3,"file":"generate-postgres-ddl-logic-B64O2oZF.js","names":[],"sources":["../src/schema/generate-postgres-ddl-logic.ts"],"sourcesContent":["/**\n * The SQL half of `rebase db generate`, as a plan and a renderer.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`\n * writes the files; what remains here is the public entry points those two are\n * reached through, plus the smaller plans (`planRelationalColumns`,\n * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as\n * slices of the same plan.\n *\n * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own\n * reading of relations, junctions, enums and search — one of three such\n * readings. See `schema/plan/types.ts` for what that cost.\n */\nimport { CollectionConfig, SecurityRule, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { planSchema, compileSecurityRule, quoteSqlLiteral, defaultBelongsToOnDelete } from \"./plan/plan-schema\";\nimport {\n renderPgType,\n renderPoliciesDdl,\n renderPolicyStatements,\n renderPostgresDdl,\n renderSearchDdl,\n renderTriggersDdl,\n renderVectorDdl,\n searchExcludePatternsOf,\n triggerExcludePatternsOf,\n vectorExcludePatternsOf,\n type DdlRenderOptions\n} from \"./plan/render-ddl\";\nimport type { ForeignKeyPlan, SchemaPlan } from \"./plan/types\";\nimport type { GeneratedColumnDependency } from \"./generated-column-conflicts\";\n\nexport { quoteSqlLiteral, defaultBelongsToOnDelete };\nexport type { ForeignKeyPlan };\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (\n collection: CollectionConfig,\n rule: SecurityRule,\n resolveCollection: ResolveCollection\n): string[] => {\n const table = {\n schema: isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\",\n table: getTableName(collection)\n };\n return compileSecurityRule(collection, rule, resolveCollection, false)\n .flatMap(policy => renderPolicyStatements(table, policy));\n};\n\n/**\n * `.rebase/sql/schema.sql` — the desired state Atlas diffs the database against.\n *\n * Synchronous. It was `async` with nothing to await, which made every caller\n * (the writer script, the doctor, the live editor, two gates) carry a promise\n * for no reason and made the one place that forgot to await it silently compare\n * `[object Promise]`.\n */\nexport const generatePostgresDdl = (\n allCollections: CollectionConfig[],\n options: DdlRenderOptions = {}\n): string => renderPostgresDdl(planSchema(allCollections), options);\n\n/** `.rebase/sql/policies.sql` — the RLS policies. Atlas has no model for a policy. */\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string =>\n renderPoliciesDdl(planSchema(allCollections));\n\n/**\n * `.rebase/sql/search.sql` — everything a `search` block needs, as a file Rebase\n * applies itself.\n *\n * Search is one of the parts of the schema Atlas does not own, for two\n * independent reasons and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * Empty when nothing opted in — the caller writes no file then.\n */\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string =>\n renderSearchDdl(planSchema(allCollections));\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n searchExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/vector.sql` — the `vector` columns, their ANN indexes and, when a\n * database declared it, the extension.\n *\n * Vector is out of Atlas's hands for a narrower reason than search: not that\n * Atlas refuses to parse the file, but that it cannot *execute* it. To compute\n * a desired state Atlas materialises `schema.sql` in a dev database, that\n * database is created empty, and Atlas empties it again at the start of every\n * run — so `VECTOR(384)` resolves against a database that structurally cannot\n * have pgvector, and every push dies with `type \"vector\" does not exist`.\n */\nexport const generatePostgresVectorDdl = (\n allCollections: CollectionConfig[],\n options: { extensions?: readonly string[] } = {}\n): string => renderVectorDdl(planSchema(allCollections, { databaseExtensions: options.extensions }));\n\n/** @see searchExcludePatterns */\nexport const vectorExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n vectorExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `.rebase/sql/triggers.sql` — the `BEFORE UPDATE` triggers behind\n * `autoValue: \"on_update\"`, and the one function they share.\n *\n * A trigger is a function plus a binding, so it is carved out of Atlas's view\n * for the first of the two reasons search is. Empty when no property declares\n * `on_update` — the caller writes no file then, and removes a stale one.\n */\nexport const generatePostgresTriggersDdl = (allCollections: CollectionConfig[]): string =>\n renderTriggersDdl(planSchema(allCollections));\n\n/** @see searchExcludePatterns */\nexport const triggerExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n triggerExcludePatternsOf(planSchema(allCollections));\n\n// ── Slices of the plan other modules ask for ─────────────────────────────────\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL renderer declares it. */\n type: string;\n /** `validation.required` on the property that owns the link. */\n required: boolean;\n /**\n * True when a declared property emits this column and the relation\n * contributes only the constraint — `postId` with `columnName: \"post_id\"`\n * beside a `belongsTo` on `post_id`.\n */\n columnOwnedByProperty?: boolean;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize. Carried so the boot-time ensure can notice a\n * database provisioned under the old rule; never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst planOf = (collections: CollectionConfig[]): SchemaPlan => planSchema(collections);\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * A view over the schema plan. It was a second walk of the collections beside\n * the `CREATE TABLE` one, which is how boot-ensure came to add every relation\n * column bare while `db push` made the same column `NOT NULL`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const plan = planOf(allCollections);\n const out: RelationalColumnPlan[] = [];\n for (const table of plan.tables) {\n if (table.kind !== \"collection\") continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n out.push({\n schema: table.schema,\n table: table.table,\n column: column.column,\n type: renderPgType(column.type),\n required: !column.nullable,\n columnOwnedByProperty: column.columnOwnedByProperty,\n legacyColumn: column.legacyColumn,\n foreignKey: column.foreignKey\n });\n }\n }\n return out;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from the same plan the derived RLS comes from, so a table created\n * here always has policies planned for it — a junction with row-level security\n * left off is readable and writable by every signed-in user.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] =>\n planOf(allCollections).tables\n .filter(table => table.kind === \"junction\")\n .map(table => ({\n schema: table.schema,\n table: table.table,\n columns: table.columns.map(c => ({\n name: c.column,\n type: renderPgType(c.type),\n legacyName: c.legacyColumn\n })),\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (` +\n table.columns.map(c => `\"${c.column}\" ${renderPgType(c.type)} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`,\n foreignKeys: table.columns.map(c => c.foreignKey).filter((fk): fk is ForeignKeyPlan => fk !== undefined)\n }));\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the declared collections and their junctions, as\n * executable statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * The same policies `policies.sql` carries, from the same plan, so boot and\n * `db push` produce identical RLS from identical collections.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n const plan = planOf(allCollections);\n // Collections first, then the junctions no collection declares.\n const ordered = [\n ...plan.tables.filter(t => t.kind === \"collection\"),\n ...plan.tables.filter(t => t.kind === \"junction\")\n ];\n return ordered.map(table => ({\n schema: table.schema,\n table: table.table,\n qualified: table.qualified,\n enableRls: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements: table.policies.flatMap(policy => renderPolicyStatements(table, policy))\n }));\n};\n\n/**\n * The `generated column → column it reads` edges a project's `search` blocks\n * imply, derived from the collections alone.\n *\n * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,\n * but available where no database is: `rebase db generate` writes a migration\n * that will run somewhere else, later, against a database it cannot inspect.\n * A spec's `fields[].column` is the physical column each search field starts\n * at, which is exactly what the generated expression depends on.\n */\nexport const searchColumnDependencies = (\n allCollections: CollectionConfig[]\n): GeneratedColumnDependency[] => {\n const dependencies: GeneratedColumnDependency[] = [];\n for (const table of planOf(allCollections).tables) {\n const spec = table.search;\n if (!spec) continue;\n const generated = [spec.column, ...(spec.fuzzy ? [spec.fuzzy.column] : [])];\n const sources = new Set(spec.fields.map(field => field.column));\n for (const column of generated) {\n for (const dependsOn of sources) {\n dependencies.push({ schema: table.schema, table: table.table, column, dependsOn });\n }\n }\n }\n return dependencies;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,IAAa,uBACT,gBACA,UAA4B,CAAC,MACpB,kBAAkB,WAAW,cAAc,GAAG,OAAO;;AAGlE,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;AAqBhD,IAAa,6BAA6B,mBACtC,gBAAgB,WAAW,cAAc,CAAC;;;;;;;;AAS9C,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;;;;AAatD,IAAa,6BACT,gBACA,UAA8C,CAAC,MACtC,gBAAgB,WAAW,gBAAgB,EAAE,oBAAoB,QAAQ,WAAW,CAAC,CAAC;;AAGnG,IAAa,yBAAyB,mBAClC,wBAAwB,WAAW,cAAc,CAAC;;;;;;;;;AAUtD,IAAa,+BAA+B,mBACxC,kBAAkB,WAAW,cAAc,CAAC;;AAGhD,IAAa,0BAA0B,mBACnC,yBAAyB,WAAW,cAAc,CAAC;AAyCvD,IAAM,UAAU,gBAAgD,WAAW,WAAW;;;;;;;;AAuCtF,IAAa,sBAAsB,mBAC/B,OAAO,cAAc,CAAC,CAAC,OAClB,QAAO,UAAS,MAAM,SAAS,UAAU,CAAC,CAC1C,KAAI,WAAU;CACX,QAAQ,MAAM;CACd,OAAO,MAAM;CACb,SAAS,MAAM,QAAQ,KAAI,OAAM;EAC7B,MAAM,EAAE;EACR,MAAM,aAAa,EAAE,IAAI;EACzB,YAAY,EAAE;CAClB,EAAE;CACF,aACI,+BAA+B,MAAM,OAAO,KAAK,MAAM,MAAM,OAC7D,MAAM,QAAQ,KAAI,MAAK,IAAI,EAAE,OAAO,IAAI,aAAa,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,KAAK,IAAI,IAClF,kBAAkB,MAAM,WAAW,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;CACrE,aAAa,MAAM,QAAQ,KAAI,MAAK,EAAE,UAAU,CAAC,CAAC,QAAQ,OAA6B,OAAO,KAAA,CAAS;AAC3G,EAAE;;;;;;;;;AAuBV,IAAa,0BAA0B,mBAA+D;CAClG,MAAM,OAAO,OAAO,cAAc;CAMlC,OAAO,CAHH,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,YAAY,GAClD,GAAG,KAAK,OAAO,QAAO,MAAK,EAAE,SAAS,UAAU,CAE7C,CAAA,CAAQ,KAAI,WAAU;EACzB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,gBAAgB,MAAM,OAAO,KAAK,MAAM,MAAM;EACzD,kBAAkB,MAAM,SAAS,SAAQ,WAAU,uBAAuB,OAAO,MAAM,CAAC;CAC5F,EAAE;AACN;;;;;;;;;;;AAYA,IAAa,4BACT,mBAC8B;CAC9B,MAAM,eAA4C,CAAC;CACnD,KAAK,MAAM,SAAS,OAAO,cAAc,CAAC,CAAC,QAAQ;EAC/C,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MAAM;EACX,MAAM,YAAY,CAAC,KAAK,QAAQ,GAAI,KAAK,QAAQ,CAAC,KAAK,MAAM,MAAM,IAAI,CAAC,CAAE;EAC1E,MAAM,UAAU,IAAI,IAAI,KAAK,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC;EAC9D,KAAK,MAAM,UAAU,WACjB,KAAK,MAAM,aAAa,SACpB,aAAa,KAAK;GAAE,QAAQ,MAAM;GAAQ,OAAO,MAAM;GAAO;GAAQ;EAAU,CAAC;CAG7F;CACA,OAAO;AACX"}
|
package/dist/generated-sql.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare function generatedSqlDir(cwd?: string): string;
|
|
|
23
23
|
* behind — and a stale `policies.sql` in the repository is exactly what someone
|
|
24
24
|
* reviewing the project's RLS would read.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* With `retire: false` (`db push --dry-run`, which writes nothing of the
|
|
27
|
+
* project's) the old copies are only found, not removed.
|
|
28
|
+
*
|
|
29
|
+
* @returns the old files it removed — or, with `retire: false`, would remove —
|
|
30
|
+
* relative to the working directory.
|
|
27
31
|
*/
|
|
28
|
-
export declare function prepareGeneratedSqlDir(cwd?: string
|
|
32
|
+
export declare function prepareGeneratedSqlDir(cwd?: string, options?: {
|
|
33
|
+
retire?: boolean;
|
|
34
|
+
}): string[];
|
|
@@ -2,12 +2,15 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
2
2
|
import type { EntityHistoryEntry } from "@rebasepro/types";
|
|
3
3
|
export type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from "@rebasepro/types";
|
|
4
4
|
import type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from "@rebasepro/types";
|
|
5
|
+
import { type HistoryTableState } from "./ensure-history-table.js";
|
|
5
6
|
/**
|
|
6
7
|
* A Postgres history row is already the wire shape — `updated_at` comes back
|
|
7
8
|
* from the driver as a string. Kept as an alias because the name is used
|
|
8
9
|
* throughout this package and in `PostgresBackendDriver`.
|
|
9
10
|
*/
|
|
10
11
|
export type HistoryEntry = EntityHistoryEntry;
|
|
12
|
+
/** Entries one statement of {@link HistoryService.pruneExpired} deletes at most. */
|
|
13
|
+
export declare const PRUNE_BATCH_SIZE = 5000;
|
|
11
14
|
/**
|
|
12
15
|
* Service for recording and querying row change history.
|
|
13
16
|
* Stores history entries in the `rebase.entity_history` table.
|
|
@@ -21,9 +24,14 @@ export declare class HistoryService {
|
|
|
21
24
|
* through the pool and commit on their own.
|
|
22
25
|
*/
|
|
23
26
|
private readonly inTransaction;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* What new entries are stored as — see `EntityHistoryEntry.format`. `2`,
|
|
29
|
+
* the previous values of the changed fields only, needs the table's
|
|
30
|
+
* `format` column, so it is what `ensureHistoryTableExists` found; a
|
|
31
|
+
* service built without saying stores whole rows, as it always did.
|
|
32
|
+
*/
|
|
33
|
+
private readonly entryFormat;
|
|
34
|
+
constructor(db: NodePgDatabase, retention?: Partial<HistoryRetentionConfig>, options?: Partial<HistoryTableState>);
|
|
27
35
|
/**
|
|
28
36
|
* Record a history entry for a row change.
|
|
29
37
|
*
|
|
@@ -68,15 +76,39 @@ export declare class HistoryService {
|
|
|
68
76
|
* Fetch a single history entry by ID.
|
|
69
77
|
*/
|
|
70
78
|
fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Each entry's `format`. A table without the column holds only entries
|
|
81
|
+
* that store the whole previous row, which is what `1` says.
|
|
82
|
+
*/
|
|
83
|
+
private formatColumn;
|
|
71
84
|
/**
|
|
72
85
|
* Prune history for a single row: enforce maxEntries and TTL.
|
|
73
86
|
*/
|
|
74
87
|
pruneEntity(tableName: string, id: string): Promise<number>;
|
|
75
88
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
89
|
+
* Delete every entry older than `ttlDays`, whichever row it belongs to.
|
|
90
|
+
*
|
|
91
|
+
* The inline prune above only ever visits a row that was just written,
|
|
92
|
+
* so a row nobody writes again — every deleted one — kept its history for
|
|
93
|
+
* good, whatever `ttlDays` said. This is the other half: the backend runs
|
|
94
|
+
* it once an hour, on one instance of the fleet (see
|
|
95
|
+
* `startHistoryRetentionSweep` in `@rebasepro/server`).
|
|
96
|
+
*
|
|
97
|
+
* In batches of {@link PRUNE_BATCH_SIZE}, each its own statement, repeated
|
|
98
|
+
* until one comes back short: a first sweep over a year of backlog is many
|
|
99
|
+
* short deletes, not one that holds millions of row locks and a pool
|
|
100
|
+
* connection for minutes. `SKIP LOCKED`, so two instances sweeping at once
|
|
101
|
+
* (or a sweep meeting an inline prune) split the work instead of waiting
|
|
102
|
+
* on each other. Each batch reads `idx_history_updated_at`, built in the
|
|
103
|
+
* background at boot (`ensureHistoryIndexes`).
|
|
104
|
+
*
|
|
105
|
+
* `signal` ends it after the batch in flight: a shutdown waits for the
|
|
106
|
+
* sweep, and a first sweep over a large backlog is hundreds of batches.
|
|
107
|
+
* What is left goes in the next sweep.
|
|
78
108
|
*/
|
|
79
|
-
pruneExpired(
|
|
109
|
+
pruneExpired(options?: {
|
|
110
|
+
signal?: AbortSignal;
|
|
111
|
+
}): Promise<number>;
|
|
80
112
|
}
|
|
81
113
|
/**
|
|
82
114
|
* Shallow comparison to find top-level keys that changed between two objects.
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import type { DeferredIndexBuild } from "../schema/plan/diff-plan.js";
|
|
3
|
+
import { type BootLockOptions } from "../schema/boot-ddl.js";
|
|
2
4
|
/** How a history entry gets written from inside the write it records. */
|
|
3
5
|
export declare const RECORD_HISTORY_FUNCTION = "rebase.record_history";
|
|
6
|
+
/** What {@link ensureHistoryTableExists} left in place, for `HistoryService`. */
|
|
7
|
+
export interface HistoryTableState {
|
|
8
|
+
/**
|
|
9
|
+
* {@link RECORD_HISTORY_FUNCTION} exists, so an entry inside a write goes
|
|
10
|
+
* on that write's transaction. Without it, entries go through the pool and
|
|
11
|
+
* commit on their own.
|
|
12
|
+
*/
|
|
13
|
+
inTransaction: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* What new entries are stored as. `2` — an update keeps the previous values
|
|
16
|
+
* of its changed fields only (`EntityHistoryEntry.format`) — once the table
|
|
17
|
+
* has its `format` column; `1`, the whole previous row, where the column
|
|
18
|
+
* could not be added this boot.
|
|
19
|
+
*/
|
|
20
|
+
entryFormat: 1 | 2;
|
|
21
|
+
}
|
|
22
|
+
/** Entries by age alone, for the retention sweep (`HistoryService.pruneExpired`). */
|
|
23
|
+
export declare const HISTORY_SWEEP_INDEX = "idx_history_updated_at";
|
|
4
24
|
/**
|
|
5
25
|
* Auto-create the row history table if it doesn't exist.
|
|
6
26
|
* This runs on startup when history is enabled, following the same
|
|
7
27
|
* pattern as `ensureAuthTablesExist`.
|
|
8
28
|
*
|
|
9
|
-
* Resolves
|
|
10
|
-
* be written on the write's own transaction; `false` sends entries through the
|
|
11
|
-
* pool as before, committing on their own.
|
|
29
|
+
* Resolves to what the history service may rely on: see {@link HistoryTableState}.
|
|
12
30
|
*/
|
|
13
|
-
export declare function ensureHistoryTableExists(db: NodePgDatabase): Promise<
|
|
31
|
+
export declare function ensureHistoryTableExists(db: NodePgDatabase, options?: BootLockOptions): Promise<HistoryTableState>;
|
|
32
|
+
/**
|
|
33
|
+
* Bring the history table's indexes to what its reads need, and say which one
|
|
34
|
+
* is left to build in the background.
|
|
35
|
+
*
|
|
36
|
+
* - `idx_history_entity` is dropped: `idx_history_time` leads with the same two
|
|
37
|
+
* columns, so it served nothing the other does not, and cost a btree insert
|
|
38
|
+
* per recorded change. A `DROP INDEX` takes the table's `ACCESS EXCLUSIVE`
|
|
39
|
+
* lock for an instant, so it runs under the boot's lock budget — one second
|
|
40
|
+
* per attempt, a few attempts — and a table something holds the whole time
|
|
41
|
+
* keeps the index until the next boot. Only once `idx_history_time` is there
|
|
42
|
+
* and valid, so a per-row lookup never loses its index.
|
|
43
|
+
* - `idx_history_updated_at`, which the retention sweep reads, is returned for
|
|
44
|
+
* `buildDeferredIndexes`: on a table that already holds years of history the
|
|
45
|
+
* build takes seconds to minutes, so it is built `CONCURRENTLY` after the boot,
|
|
46
|
+
* blocking no read or write. Nothing is returned when it is already valid.
|
|
47
|
+
*
|
|
48
|
+
* Never throws: an index that cannot be dropped or read about is logged, and
|
|
49
|
+
* the next boot asks again.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureHistoryIndexes(db: NodePgDatabase, options?: BootLockOptions): Promise<DeferredIndexBuild[]>;
|