@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g09316f6
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-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
- package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +81 -7
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
- package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
- package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
- package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
- package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
- package/dist/cli-errors-C3g_kHBw.js.map +1 -0
- package/dist/cli-errors.d.ts +19 -0
- package/dist/cli-helpers.d.ts +47 -0
- package/dist/cli.js +233 -19
- package/dist/cli.js.map +1 -1
- package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
- package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
- package/dist/collections/validate-relations.d.ts +7 -0
- package/dist/column-plan-helpers-CpILzHJS.js +145 -0
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
- package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
- package/dist/doctor-D5SrGJ5P.js.map +1 -0
- package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
- package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
- package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
- package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
- package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
- package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
- package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
- package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
- package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
- package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +23 -4
- package/dist/index.es.js +1991 -580
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js +1531 -0
- package/dist/plan-schema-Hgl62S-w.js.map +1 -0
- package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
- package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
- package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
- package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
- package/dist/schema/catalogue-schema.d.ts +97 -0
- package/dist/schema/collection-index.d.ts +9 -0
- package/dist/schema/column-plan-helpers.d.ts +125 -0
- package/dist/schema/config-relations.d.ts +32 -0
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +9 -0
- package/dist/schema/dynamic-tables.d.ts +27 -6
- package/dist/schema/ensure-collection-tables.d.ts +31 -268
- package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
- package/dist/schema/generate-postgres-ddl.js +8 -2
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-column-conflicts.d.ts +84 -0
- package/dist/schema/generated-schema-diff.d.ts +56 -0
- package/dist/schema/generated-schema-staleness.d.ts +33 -0
- package/dist/schema/introspect-db-logic.d.ts +2 -0
- package/dist/schema/plan/diff-plan.d.ts +220 -0
- package/dist/schema/plan/plan-schema.d.ts +84 -0
- package/dist/schema/plan/render-ddl.d.ts +62 -0
- package/dist/schema/plan/render-drizzle.d.ts +31 -0
- package/dist/schema/plan/types.d.ts +410 -0
- package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
- package/dist/schema/relation-names.d.ts +28 -0
- package/dist/schema/search-column.d.ts +9 -0
- package/dist/security/rls-enforcement.d.ts +17 -0
- package/dist/services/FetchService.d.ts +176 -36
- package/dist/services/PersistService.d.ts +17 -4
- package/dist/services/RelationService.d.ts +11 -3
- package/dist/services/collection-helpers.d.ts +16 -2
- package/dist/services/dataService.d.ts +29 -2
- package/dist/services/field-op-sql.d.ts +16 -0
- package/dist/services/field-viewer.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +35 -1
- package/dist/services/row-pipeline.d.ts +29 -5
- package/dist/services/soft-delete.d.ts +73 -0
- package/dist/utils/pg-error-utils.d.ts +41 -1
- package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
- package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
- package/package.json +18 -10
- package/dist/cli-errors-DxN4yzdH.js.map +0 -1
- package/dist/doctor-D7HWmGzs.js.map +0 -1
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
- package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
|
@@ -1,1312 +0,0 @@
|
|
|
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 isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
|
|
5
|
-
import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorObjectNames, i as vectorColumnDefinition, n as buildVectorColumnSpecs, o as vectorExtensionDeclared, r as buildVectorIndexPlan, t as VECTOR_EXTENSION_OPT_IN, u as vectorIndexStatements } from "./vector-index-DkWxm2de.js";
|
|
6
|
-
import { i as collectionIndexStatements, n as buildCollectionIndexSpecs } from "./collection-index-BHgT_TKd.js";
|
|
7
|
-
import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, REBASE_SCHEMA, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
8
|
-
import { findRelation, getEffectiveSecurityRules, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getTableName, policyToPostgres, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, resolveStringColumnLength, securityRuleToConditions } from "@rebasepro/common";
|
|
9
|
-
import { generateForeignKeyName, getPolicyNamesForRule, legacyForeignKeyName, toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
//#region src/schema/search-column.ts
|
|
12
|
-
/**
|
|
13
|
-
* The one place a collection's `search` block becomes SQL.
|
|
14
|
-
*
|
|
15
|
-
* Four things describe a Postgres table in this codebase — the DDL generator,
|
|
16
|
-
* the Drizzle schema generator, the runtime table builder for BaaS mode, and
|
|
17
|
-
* the boot-time schema ensure — and each of them has, at some point, described
|
|
18
|
-
* a column differently from the others. The `varchar(255)` note in
|
|
19
|
-
* `generate-postgres-ddl-logic` is one such scar: the same property produced a
|
|
20
|
-
* capped column down one path and an uncapped one down the other, and nothing
|
|
21
|
-
* failed until a user hit the cap.
|
|
22
|
-
*
|
|
23
|
-
* So the search column is not implemented four times. It is computed once,
|
|
24
|
-
* here, and every generator renders the same {@link SearchColumnSpec}. There is
|
|
25
|
-
* a test asserting exactly that (`search-column-contract.test.ts`); the point of
|
|
26
|
-
* this module is that the test has something to assert *about*.
|
|
27
|
-
*
|
|
28
|
-
* ## Why the expressions look the way they do
|
|
29
|
-
*
|
|
30
|
-
* A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
|
|
31
|
-
* Postgres is stricter here than intuition. Verified against PostgreSQL 18:
|
|
32
|
-
*
|
|
33
|
-
* | expression | immutable |
|
|
34
|
-
* |-----------------------------------------|-----------|
|
|
35
|
-
* | `to_tsvector('spanish', col)` | yes |
|
|
36
|
-
* | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
|
|
37
|
-
* | `array_to_string(col, ' ')` | **no** |
|
|
38
|
-
* | `col::text` on `text[]` | **no** |
|
|
39
|
-
* | `to_jsonb(col)` | **no** |
|
|
40
|
-
* | `unaccent(col)` | **no** — dictionary lookup is STABLE |
|
|
41
|
-
* | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
|
|
42
|
-
* | `setweight(...) || setweight(...)` | yes |
|
|
43
|
-
*
|
|
44
|
-
* Three of the four things a real search column needs are therefore unavailable
|
|
45
|
-
* directly, which is why {@link searchHelperFunctions} exists: each wraps a
|
|
46
|
-
* stable built-in in an SQL function declared IMMUTABLE. That declaration is a
|
|
47
|
-
* promise, and it is a true one for these three — array joining, JSON string
|
|
48
|
-
* extraction and accent folding are all deterministic for a given input; the
|
|
49
|
-
* built-ins are marked stable only because they must account for element types
|
|
50
|
-
* and dictionaries in general.
|
|
51
|
-
*
|
|
52
|
-
* The alternative was to skip `unaccent` and text arrays entirely. That is not
|
|
53
|
-
* a real option in an accented language: Postgres stems `auditoría` to
|
|
54
|
-
* `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
|
|
55
|
-
* typed without accents misses every row that carries them.
|
|
56
|
-
*/
|
|
57
|
-
/** Schema-qualified so a collection outside `public` still resolves them. */
|
|
58
|
-
var HELPER_SCHEMA = "public";
|
|
59
|
-
/**
|
|
60
|
-
* Names of the helper functions. Frozen: they are recorded in the stored
|
|
61
|
-
* generation expression of every search column ever created, so renaming one
|
|
62
|
-
* orphans every table that already has a search column.
|
|
63
|
-
*/
|
|
64
|
-
var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
|
|
65
|
-
var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
|
|
66
|
-
/** Raised when a `search` block names something that cannot be searched. */
|
|
67
|
-
var SearchConfigError = class extends Error {
|
|
68
|
-
constructor(message) {
|
|
69
|
-
super(message);
|
|
70
|
-
this.name = "SearchConfigError";
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
/** The `search` block of a collection, or undefined when it has none. */
|
|
74
|
-
var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
|
|
75
|
-
/**
|
|
76
|
-
* Refuse a `search` block on a collection this engine does not store.
|
|
77
|
-
*
|
|
78
|
-
* The type only permits one on a `PostgresCollectionConfig`, so TypeScript
|
|
79
|
-
* already stops the ordinary case. This catches the rest — a JS config, a cast,
|
|
80
|
-
* a collection whose `engine` was changed after the block was written — because
|
|
81
|
-
* the alternative is the exact failure the block exists to prevent: a developer
|
|
82
|
-
* who declared what to index, saw no error, and got the substring fallback.
|
|
83
|
-
*
|
|
84
|
-
* Called with *every* collection, before the Postgres ones are filtered out.
|
|
85
|
-
*/
|
|
86
|
-
var assertSearchIsPostgresOnly = (collections) => {
|
|
87
|
-
for (const collection of collections) {
|
|
88
|
-
if (isPostgresCollectionConfig(collection)) continue;
|
|
89
|
-
if (!collection.search) continue;
|
|
90
|
-
const engine = collection.engine ?? "non-postgres";
|
|
91
|
-
throw new SearchConfigError(`${collection.slug}.search: full-text search is a Postgres feature, and this collection is served by \`${engine}\`. Remove the block — it would otherwise look configured while \`.search()\` kept using the default substring match.`);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
|
|
95
|
-
/**
|
|
96
|
-
* Classify a property for search purposes.
|
|
97
|
-
*
|
|
98
|
-
* Deliberately narrower than `getSqlColumnType`: search only cares whether a
|
|
99
|
-
* value reaches text, and the mapping from property to *physical* type is
|
|
100
|
-
* asserted against `getSqlColumnType` in the contract test rather than
|
|
101
|
-
* duplicated here.
|
|
102
|
-
*
|
|
103
|
-
* Returns null for anything that is not text-bearing, which the caller turns
|
|
104
|
-
* into a boot error naming the property.
|
|
105
|
-
*/
|
|
106
|
-
var classify = (prop) => {
|
|
107
|
-
switch (prop.type) {
|
|
108
|
-
case "string": {
|
|
109
|
-
const sp = prop;
|
|
110
|
-
if (sp.enum) return {
|
|
111
|
-
kind: "text",
|
|
112
|
-
reason: "enum"
|
|
113
|
-
};
|
|
114
|
-
if (sp.isId === "uuid" || sp.columnType === "uuid") return {
|
|
115
|
-
kind: "text",
|
|
116
|
-
reason: "uuid"
|
|
117
|
-
};
|
|
118
|
-
return { kind: "text" };
|
|
119
|
-
}
|
|
120
|
-
case "map":
|
|
121
|
-
if (prop.columnType === "json") return {
|
|
122
|
-
kind: "jsonb",
|
|
123
|
-
reason: "json"
|
|
124
|
-
};
|
|
125
|
-
return { kind: "jsonb" };
|
|
126
|
-
case "array": {
|
|
127
|
-
const ap = prop;
|
|
128
|
-
let colType = ap.columnType;
|
|
129
|
-
if (!colType && ap.of && !Array.isArray(ap.of)) {
|
|
130
|
-
const of = ap.of;
|
|
131
|
-
if (of.type === "string") colType = "text[]";
|
|
132
|
-
else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
|
|
133
|
-
else if (of.type === "boolean") colType = "boolean[]";
|
|
134
|
-
}
|
|
135
|
-
if (colType === "text[]") return { kind: "text_array" };
|
|
136
|
-
if (colType === "json") return {
|
|
137
|
-
kind: "jsonb",
|
|
138
|
-
reason: "json"
|
|
139
|
-
};
|
|
140
|
-
if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
|
|
141
|
-
kind: "text_array",
|
|
142
|
-
reason: "non_text_array"
|
|
143
|
-
};
|
|
144
|
-
return { kind: "jsonb" };
|
|
145
|
-
}
|
|
146
|
-
default: return null;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
|
|
150
|
-
/** SQL reading one field as plain text, before normalization. */
|
|
151
|
-
var rawTextSql = (field) => {
|
|
152
|
-
const col = `"${field.column}"`;
|
|
153
|
-
if (field.kind === "text") return `coalesce(${col}, '')`;
|
|
154
|
-
if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
|
|
155
|
-
return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote(field.jsonPath[0])}` : `${col} #> ${quote(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
|
|
156
|
-
};
|
|
157
|
-
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
158
|
-
/**
|
|
159
|
-
* Resolve and validate one declared field path.
|
|
160
|
-
*
|
|
161
|
-
* A path that does not resolve throws. The whole point of an explicit block is
|
|
162
|
-
* that the author knows what is indexed; a silently dropped field would make it
|
|
163
|
-
* a guess again, and the failure — a search that returns nothing for content
|
|
164
|
-
* that is plainly in the row — is invisible from the outside.
|
|
165
|
-
*/
|
|
166
|
-
var resolveField = (entry, collection, cfg) => {
|
|
167
|
-
const path = typeof entry === "string" ? entry : entry.path;
|
|
168
|
-
const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;
|
|
169
|
-
const where = `${collection.slug}.search`;
|
|
170
|
-
if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
|
|
171
|
-
const [head, ...rest] = path.split(".");
|
|
172
|
-
const prop = collection.properties?.[head];
|
|
173
|
-
if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
|
|
174
|
-
const classified = classify(prop);
|
|
175
|
-
if (!classified) throw new SearchConfigError(`${where}: "${path}" is a \`${prop.type}\` property, which holds no text to search. Searchable kinds are \`string\`, \`string[]\` and \`map\` (or a path inside one).`);
|
|
176
|
-
if (classified.reason === "enum") throw new SearchConfigError(`${where}: "${path}" is an enum. Enums are a fixed vocabulary — filter on them with \`where\` instead, which is exact and uses an index.`);
|
|
177
|
-
if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
|
|
178
|
-
if (classified.reason === "json") throw new SearchConfigError(`${where}: "${path}" is a \`json\` column, and the cast from \`json\` to \`jsonb\` is not immutable, so it cannot feed a generated column. Declare the property as \`jsonb\` (the default) to search it.`);
|
|
179
|
-
if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
|
|
180
|
-
if (rest.length > 0 && classified.kind !== "jsonb") throw new SearchConfigError(`${where}: "${path}" addresses a path inside "${head}", but "${head}" is a \`${prop.type}\` property, not a \`map\`. Only map properties have paths inside them.`);
|
|
181
|
-
const column = columnNameOf(head, prop);
|
|
182
|
-
const textSql = normalize(rawTextSql({
|
|
183
|
-
column,
|
|
184
|
-
jsonPath: rest,
|
|
185
|
-
kind: classified.kind
|
|
186
|
-
}), cfg.unaccent === true);
|
|
187
|
-
const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
|
|
188
|
-
return {
|
|
189
|
-
path,
|
|
190
|
-
column,
|
|
191
|
-
jsonPath: rest,
|
|
192
|
-
kind: classified.kind,
|
|
193
|
-
weight,
|
|
194
|
-
sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
|
|
195
|
-
textSql
|
|
196
|
-
};
|
|
197
|
-
};
|
|
198
|
-
/**
|
|
199
|
-
* Build the full spec for a collection, or undefined when it has not opted in.
|
|
200
|
-
*
|
|
201
|
-
* Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
|
|
202
|
-
* at boot surface that as a startup failure — a search block that half-works is
|
|
203
|
-
* worse than one that refuses.
|
|
204
|
-
*/
|
|
205
|
-
var buildSearchColumnSpec = (collection) => {
|
|
206
|
-
const cfg = getSearchConfig(collection);
|
|
207
|
-
if (!cfg) return void 0;
|
|
208
|
-
if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) throw new SearchConfigError(`${collection.slug}.search: \`fields\` is empty. Name the properties to index, or remove the \`search\` block to keep the default ILIKE behaviour.`);
|
|
209
|
-
const table = getTableName(collection);
|
|
210
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
211
|
-
const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
|
|
212
|
-
if (collection.properties?.[column]) throw new SearchConfigError(`${collection.slug}.search: the generated column "${column}" collides with a declared property of the same name. Set \`search.column\` to something else.`);
|
|
213
|
-
const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
|
|
214
|
-
const seen = /* @__PURE__ */ new Set();
|
|
215
|
-
for (const f of fields) {
|
|
216
|
-
if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
|
|
217
|
-
seen.add(f.path);
|
|
218
|
-
}
|
|
219
|
-
const extensions = [];
|
|
220
|
-
if (cfg.unaccent) extensions.push("unaccent");
|
|
221
|
-
if (cfg.fuzzy) extensions.push("pg_trgm");
|
|
222
|
-
const spec = {
|
|
223
|
-
schema,
|
|
224
|
-
table,
|
|
225
|
-
column,
|
|
226
|
-
language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
|
|
227
|
-
unaccent: cfg.unaccent === true,
|
|
228
|
-
fields,
|
|
229
|
-
expression: fields.map((f) => f.sql).join(" || "),
|
|
230
|
-
indexName: toPostgresIdentifier(`${table}_${column}_gin`),
|
|
231
|
-
extensions
|
|
232
|
-
};
|
|
233
|
-
if (cfg.fuzzy) {
|
|
234
|
-
const fuzzyColumn = `${column}_text`;
|
|
235
|
-
if (collection.properties?.[fuzzyColumn]) throw new SearchConfigError(`${collection.slug}.search: \`fuzzy\` needs the column "${fuzzyColumn}", which collides with a declared property. Set \`search.column\` to something else.`);
|
|
236
|
-
spec.fuzzy = {
|
|
237
|
-
column: fuzzyColumn,
|
|
238
|
-
expression: fields.map((f) => f.textSql).join(" || ' ' || "),
|
|
239
|
-
indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
|
|
240
|
-
threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return spec;
|
|
244
|
-
};
|
|
245
|
-
/**
|
|
246
|
-
* The IMMUTABLE wrappers the generated expressions call.
|
|
247
|
-
*
|
|
248
|
-
* `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
|
|
249
|
-
* than an error, and idempotent for the same reason every other boot-time DDL
|
|
250
|
-
* statement here is.
|
|
251
|
-
*
|
|
252
|
-
* The bodies are stable built-ins wrapped in an immutable promise — see the
|
|
253
|
-
* module comment for why that promise is sound. `STRICT` matters: it makes NULL
|
|
254
|
-
* in mean NULL out without executing the body, which is what the `coalesce` at
|
|
255
|
-
* each call site then absorbs.
|
|
256
|
-
*/
|
|
257
|
-
var searchHelperFunctions = (spec) => {
|
|
258
|
-
const statements = [`CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(text[]) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT array_to_string($1, ' ') $$;`, `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(jsonb) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT coalesce(string_agg(v, ' '), '')\n FROM jsonb_array_elements_text(jsonb_path_query_array($1, 'strict $.**?(@.type() == "string")')) AS v $$;`];
|
|
259
|
-
if (spec.unaccent) statements.push(`CREATE OR REPLACE FUNCTION ${SEARCH_UNACCENT_FN}(text) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT ${HELPER_SCHEMA}.unaccent('${HELPER_SCHEMA}.unaccent'::regdictionary, $1) $$;`);
|
|
260
|
-
return statements;
|
|
261
|
-
};
|
|
262
|
-
/**
|
|
263
|
-
* `CREATE EXTENSION` statements the spec's expressions depend on.
|
|
264
|
-
*
|
|
265
|
-
* `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
|
|
266
|
-
* `CREATE EXTENSION` installs into the first schema on `search_path`, which
|
|
267
|
-
* defaults to `"$user", public` — and the scaffold's database role is named
|
|
268
|
-
* `rebase`, the same as the schema the generator creates one statement earlier.
|
|
269
|
-
* So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
|
|
270
|
-
* dictionary in `rebase`, and every reference to `public.unaccent` below fails
|
|
271
|
-
* with "text search dictionary does not exist". Observed, not theorised.
|
|
272
|
-
*/
|
|
273
|
-
var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
|
|
274
|
-
/** The column definition as it appears inside `CREATE TABLE`. */
|
|
275
|
-
var searchColumnDefinition = (spec) => `"${spec.column}" tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`;
|
|
276
|
-
/** The fuzzy column definition, when the spec asks for one. */
|
|
277
|
-
var fuzzyColumnDefinition = (spec) => spec.fuzzy ? `"${spec.fuzzy.column}" text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED` : void 0;
|
|
278
|
-
/**
|
|
279
|
-
* Index statements for the spec.
|
|
280
|
-
*
|
|
281
|
-
* `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
|
|
282
|
-
* SQL file replayed as one unit — a migration, or `search.sql` — where a
|
|
283
|
-
* concurrent build is not allowed. The boot-time ensure path runs statement by
|
|
284
|
-
* statement against tables that are live and populated, and uses the
|
|
285
|
-
* concurrent form instead; see `ensureSearchColumns`.
|
|
286
|
-
*/
|
|
287
|
-
var searchIndexStatements = (spec) => {
|
|
288
|
-
const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
|
|
289
|
-
if (spec.fuzzy) statements.push(`CREATE INDEX IF NOT EXISTS "${spec.fuzzy.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.fuzzy.column}" ${HELPER_SCHEMA}.gin_trgm_ops);`);
|
|
290
|
-
return statements;
|
|
291
|
-
};
|
|
292
|
-
/**
|
|
293
|
-
* Marker on the comment of every generated search column this module creates.
|
|
294
|
-
*
|
|
295
|
-
* Versioned because the fingerprint below is only comparable against itself: a
|
|
296
|
-
* future change to how it is computed has to read as "not stamped by this
|
|
297
|
-
* version" rather than as drift on every existing column.
|
|
298
|
-
*/
|
|
299
|
-
var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
|
|
300
|
-
/**
|
|
301
|
-
* A stable fingerprint of one generated column's expression.
|
|
302
|
-
*
|
|
303
|
-
* Why a stamp rather than reading the expression back: Postgres stores a
|
|
304
|
-
* generated column's expression *parsed*, and hands it back deparsed — casts
|
|
305
|
-
* made explicit, identifiers requoted, schema qualifications added or dropped
|
|
306
|
-
* according to `search_path`. Comparing that text to the text we generated
|
|
307
|
-
* would report drift on wording, and this comparison decides whether a boot
|
|
308
|
-
* refuses, so a false positive is an outage. The stamp is written by the same
|
|
309
|
-
* code that writes the column, so equality means what it says.
|
|
310
|
-
*/
|
|
311
|
-
var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
|
|
312
|
-
/**
|
|
313
|
-
* The stamps for a spec's generated columns — one per column, never shared.
|
|
314
|
-
*
|
|
315
|
-
* Per column on purpose: turning `fuzzy` on adds a second column and changes
|
|
316
|
-
* nothing about the first, and a spec-wide fingerprint would report the
|
|
317
|
-
* untouched `tsvector` column as drifted and refuse a boot over a change that
|
|
318
|
-
* is purely additive.
|
|
319
|
-
*/
|
|
320
|
-
var searchColumnStamps = (spec) => {
|
|
321
|
-
const stamp = (column, expression) => {
|
|
322
|
-
const fingerprint = searchExpressionFingerprint(expression);
|
|
323
|
-
return {
|
|
324
|
-
column,
|
|
325
|
-
expression,
|
|
326
|
-
fingerprint,
|
|
327
|
-
sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
|
|
328
|
-
};
|
|
329
|
-
};
|
|
330
|
-
const stamps = [stamp(spec.column, spec.expression)];
|
|
331
|
-
if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
|
|
332
|
-
return stamps;
|
|
333
|
-
};
|
|
334
|
-
/**
|
|
335
|
-
* The same drift check as the boot ensure, for the SQL file.
|
|
336
|
-
*
|
|
337
|
-
* Needed because {@link searchColumnStamps} would otherwise *launder* drift on
|
|
338
|
-
* the migration path: `ADD COLUMN IF NOT EXISTS` does nothing to a column that
|
|
339
|
-
* exists, so a re-generated `search.sql` would stamp a stale column with the
|
|
340
|
-
* new block's fingerprint and the next boot would find them in agreement.
|
|
341
|
-
* Guarding first means the file refuses instead — `rebase db push` is attended,
|
|
342
|
-
* and the operator reading the failure is the person who changed the block.
|
|
343
|
-
*/
|
|
344
|
-
var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
|
|
345
|
-
const relation = quote(`"${spec.schema}"."${spec.table}"`);
|
|
346
|
-
return `DO $rebase_search$
|
|
347
|
-
DECLARE recorded text;
|
|
348
|
-
BEGIN
|
|
349
|
-
SELECT col_description(a.attrelid, a.attnum) INTO recorded
|
|
350
|
-
FROM pg_attribute a
|
|
351
|
-
WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;
|
|
352
|
-
IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN
|
|
353
|
-
RAISE EXCEPTION 'Rebase: the search block for ${spec.schema}.${spec.table} changed after the generated column "${stamp.column}" was built (recorded %, expected ${stamp.fingerprint}). Postgres cannot alter a generated expression in place. Drop the column and re-apply this file — it rewrites the table and rebuilds the index: ALTER TABLE ${relation.slice(1, -1)} DROP COLUMN "${stamp.column}";', recorded;
|
|
354
|
-
END IF;
|
|
355
|
-
END
|
|
356
|
-
$rebase_search$;`;
|
|
357
|
-
});
|
|
358
|
-
/**
|
|
359
|
-
* The index names the spec creates.
|
|
360
|
-
*
|
|
361
|
-
* Needed by name, not just by statement, so Atlas can be told to exclude them
|
|
362
|
-
* from its diff — see `searchExcludePatterns`.
|
|
363
|
-
*/
|
|
364
|
-
var searchIndexNames = (spec) => spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];
|
|
365
|
-
/**
|
|
366
|
-
* The generated column names a collection's search block adds, if any.
|
|
367
|
-
*
|
|
368
|
-
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
369
|
-
* an index in column form — a list of lexeme positions, or a concatenation of
|
|
370
|
-
* every searchable field on the row — and nothing outside the query planner has
|
|
371
|
-
* any use for them. Left in, every list response carries a second, larger copy
|
|
372
|
-
* of the row's text.
|
|
373
|
-
*/
|
|
374
|
-
var searchColumnNames = (collection) => {
|
|
375
|
-
let spec;
|
|
376
|
-
try {
|
|
377
|
-
spec = buildSearchColumnSpec(collection);
|
|
378
|
-
} catch {
|
|
379
|
-
return [];
|
|
380
|
-
}
|
|
381
|
-
if (!spec) return [];
|
|
382
|
-
return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
|
|
383
|
-
};
|
|
384
|
-
/**
|
|
385
|
-
* True for a column whose type only ever holds a search index.
|
|
386
|
-
*
|
|
387
|
-
* Independent of any collection config on purpose: an introspected database
|
|
388
|
-
* (BaaS mode) can carry a `tsvector` column this framework never created —
|
|
389
|
-
* Pagila's `film.fulltext` is the canonical one — and it should not be returned
|
|
390
|
-
* to callers either. `isDerivedIndexColumn` already keeps such a column out of
|
|
391
|
-
* the *properties*; this keeps it out of the *rows*.
|
|
392
|
-
*/
|
|
393
|
-
var isSearchIndexColumn = (column) => {
|
|
394
|
-
const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
|
|
395
|
-
return sqlType === "tsvector" || sqlType === "tsquery";
|
|
396
|
-
};
|
|
397
|
-
/**
|
|
398
|
-
* A drizzle select projection over `table` with the search columns dropped.
|
|
399
|
-
*
|
|
400
|
-
* Returns undefined when nothing needs dropping, so the common case keeps using
|
|
401
|
-
* a plain `select()` and this stays invisible in the generated SQL.
|
|
402
|
-
*/
|
|
403
|
-
var visibleColumnProjection = (tableColumns, collection) => {
|
|
404
|
-
const excluded = excludedColumnNames(tableColumns, collection);
|
|
405
|
-
if (!tableColumns || excluded.length === 0) return void 0;
|
|
406
|
-
const projection = {};
|
|
407
|
-
for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
|
|
408
|
-
return projection;
|
|
409
|
-
};
|
|
410
|
-
/** The same exclusion as a drizzle `db.query` `columns` denylist. */
|
|
411
|
-
var hiddenColumnsOption = (tableColumns, collection) => {
|
|
412
|
-
const excluded = excludedColumnNames(tableColumns, collection);
|
|
413
|
-
if (excluded.length === 0) return void 0;
|
|
414
|
-
return Object.fromEntries(excluded.map((name) => [name, false]));
|
|
415
|
-
};
|
|
416
|
-
/**
|
|
417
|
-
* The columns to keep out of a response, by name.
|
|
418
|
-
*
|
|
419
|
-
* `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
|
|
420
|
-
* for anything that is not a real drizzle table — a stub in a test, a derived
|
|
421
|
-
* or nested path with no table behind it. Nothing to exclude is the right
|
|
422
|
-
* answer there, and it has to be an answer rather than a throw: this runs on
|
|
423
|
-
* the read path of every collection, opted in or not.
|
|
424
|
-
*/
|
|
425
|
-
var excludedColumnNames = (tableColumns, collection) => {
|
|
426
|
-
if (!tableColumns || typeof tableColumns !== "object") return [];
|
|
427
|
-
const byName = new Set(collection ? searchColumnNames(collection) : []);
|
|
428
|
-
return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
|
|
429
|
-
};
|
|
430
|
-
//#endregion
|
|
431
|
-
//#region src/schema/generate-postgres-ddl-logic.ts
|
|
432
|
-
var generate_postgres_ddl_logic_exports = /* @__PURE__ */ __exportAll({
|
|
433
|
-
defaultBelongsToOnDelete: () => defaultBelongsToOnDelete,
|
|
434
|
-
generatePolicyStatements: () => generatePolicyStatements,
|
|
435
|
-
generatePostgresDdl: () => generatePostgresDdl,
|
|
436
|
-
generatePostgresPoliciesDdl: () => generatePostgresPoliciesDdl,
|
|
437
|
-
generatePostgresSearchDdl: () => generatePostgresSearchDdl,
|
|
438
|
-
generatePostgresVectorDdl: () => generatePostgresVectorDdl,
|
|
439
|
-
getPrimaryKeyName: () => getPrimaryKeyName,
|
|
440
|
-
getPrimaryKeyProp: () => getPrimaryKeyProp,
|
|
441
|
-
getSqlColumnType: () => getSqlColumnType,
|
|
442
|
-
isIdProperty: () => isIdProperty,
|
|
443
|
-
isNumericId: () => isNumericId,
|
|
444
|
-
planCollectionPolicies: () => planCollectionPolicies,
|
|
445
|
-
planJunctionTables: () => planJunctionTables,
|
|
446
|
-
planRelationalColumns: () => planRelationalColumns,
|
|
447
|
-
quoteSqlLiteral: () => quoteSqlLiteral,
|
|
448
|
-
resolveColumnName: () => resolveColumnName,
|
|
449
|
-
searchExcludePatterns: () => searchExcludePatterns,
|
|
450
|
-
vectorExcludePatterns: () => vectorExcludePatterns
|
|
451
|
-
});
|
|
452
|
-
var resolveColumnName = (propName, prop) => {
|
|
453
|
-
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
454
|
-
return toSnakeCase(propName);
|
|
455
|
-
};
|
|
456
|
-
var getPrimaryKeyProp = (collection) => {
|
|
457
|
-
if (collection.properties) {
|
|
458
|
-
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
|
|
459
|
-
if (idPropEntry) {
|
|
460
|
-
const prop = idPropEntry[1];
|
|
461
|
-
const isUuid = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
|
|
462
|
-
return {
|
|
463
|
-
name: idPropEntry[0],
|
|
464
|
-
type: prop.type === "number" ? "number" : "string",
|
|
465
|
-
isUuid
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
const idProp = collection.properties?.["id"];
|
|
470
|
-
if (idProp?.type === "number") return {
|
|
471
|
-
name: "id",
|
|
472
|
-
type: "number",
|
|
473
|
-
isUuid: false
|
|
474
|
-
};
|
|
475
|
-
return {
|
|
476
|
-
name: "id",
|
|
477
|
-
type: "string",
|
|
478
|
-
isUuid: idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid"
|
|
479
|
-
};
|
|
480
|
-
};
|
|
481
|
-
var isNumericId = (collection) => {
|
|
482
|
-
return getPrimaryKeyProp(collection).type === "number";
|
|
483
|
-
};
|
|
484
|
-
var getPrimaryKeyName = (collection) => {
|
|
485
|
-
return getPrimaryKeyProp(collection).name;
|
|
486
|
-
};
|
|
487
|
-
/** The column type a junction holds for one endpoint's primary key. */
|
|
488
|
-
var junctionKeyType = (collection) => isNumericId(collection) ? "INTEGER" : getPrimaryKeyProp(collection).isUuid ? "UUID" : "TEXT";
|
|
489
|
-
var isIdProperty = (propName, prop, collection) => {
|
|
490
|
-
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
491
|
-
return !Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId)) && propName === "id";
|
|
492
|
-
};
|
|
493
|
-
/**
|
|
494
|
-
* Render statements produced by {@link generatePolicyStatements} back into the
|
|
495
|
-
* exact string the DDL/policies files have always carried: each statement on
|
|
496
|
-
* its own line, terminated by a newline. Keeping the string form derived from
|
|
497
|
-
* the statement array means the two can never drift — the boot-time applier and
|
|
498
|
-
* the generated `policies.sql` emit the same SQL, from the same source.
|
|
499
|
-
*/
|
|
500
|
-
var statementsToDdl = (statements) => statements.map((s) => `${s}\n`).join("");
|
|
501
|
-
/**
|
|
502
|
-
* The `ON DELETE` a foreign key gets when the author did not say.
|
|
503
|
-
*
|
|
504
|
-
* One rule for every property that emits one — `belongsTo` relations and
|
|
505
|
-
* `reference` properties alike. `reference` kept its own `CASCADE` default for
|
|
506
|
-
* a release after this rule was written, which is exactly the split this
|
|
507
|
-
* function exists to prevent: two spellings of the same link, two data-retention
|
|
508
|
-
* behaviours, and only one of them documented.
|
|
509
|
-
*
|
|
510
|
-
* An optional link is `SET NULL`: the column can hold NULL, so dropping the
|
|
511
|
-
* parent leaves the child row with an empty pointer, which is what "optional"
|
|
512
|
-
* already means.
|
|
513
|
-
*
|
|
514
|
-
* A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
|
|
515
|
-
* cannot exist without a parent; it does not say deleting the parent should
|
|
516
|
-
* take the child with it. That second claim is a data-retention decision, and
|
|
517
|
-
* defaulting to it meant `onDelete` — a field nobody has to write — silently
|
|
518
|
-
* turned every `DELETE FROM authors` into a cascade through posts, comments and
|
|
519
|
-
* anything else that hung off them. `RESTRICT` fails the delete instead and
|
|
520
|
-
* names the constraint, so the author chooses `cascade` on purpose or clears
|
|
521
|
-
* the children first.
|
|
522
|
-
*
|
|
523
|
-
* Both DDL paths — the `CREATE TABLE` generator and {@link planRelationalColumns},
|
|
524
|
-
* which boot-ensure compares the live database against — call this, so a
|
|
525
|
-
* default that appears in one and not the other cannot make `db push` plan a
|
|
526
|
-
* constraint rewrite on every run.
|
|
527
|
-
*/
|
|
528
|
-
var defaultBelongsToOnDelete = (required) => required ? "RESTRICT" : "SET NULL";
|
|
529
|
-
var generatePolicyDdl = (collection, rule, resolveCollection) => statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
|
|
530
|
-
/**
|
|
531
|
-
* The individual SQL statements a single security rule compiles to: a
|
|
532
|
-
* `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
|
|
533
|
-
* statement (terminated by `;`, no trailing newline).
|
|
534
|
-
*
|
|
535
|
-
* This is the primitive the boot-time RLS applier runs one statement at a time
|
|
536
|
-
* (the runtime's DB handle speaks the extended query protocol, which forbids
|
|
537
|
-
* multiple commands in one execute), while `db push` writes the joined string.
|
|
538
|
-
*/
|
|
539
|
-
var generatePolicyStatements = (collection, rule, resolveCollection) => {
|
|
540
|
-
const tableName = getTableName(collection);
|
|
541
|
-
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
542
|
-
const policyNames = getPolicyNamesForRule(rule, tableName);
|
|
543
|
-
return ops.flatMap((op, opIdx) => {
|
|
544
|
-
return generateSinglePolicyStatements(collection, rule, op, policyNames[opIdx], resolveCollection);
|
|
545
|
-
});
|
|
546
|
-
};
|
|
547
|
-
var generateSinglePolicyStatements = (collection, rule, operation, policyName, resolveCollection) => {
|
|
548
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
549
|
-
const tableName = getTableName(collection);
|
|
550
|
-
const mode = (rule.mode ?? "permissive").toUpperCase();
|
|
551
|
-
const operationUpper = operation.toUpperCase();
|
|
552
|
-
const pgRoles = rule.pgRoles ? [...rule.pgRoles].sort() : ["public"];
|
|
553
|
-
const needsUsing = operation !== "insert";
|
|
554
|
-
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
555
|
-
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
556
|
-
let usingClause = needsUsing && usingExpr ? policyToPostgres(usingExpr, collection, { resolveCollection }) : null;
|
|
557
|
-
let withCheckClause = needsWithCheck && withCheckExpr ? policyToPostgres(withCheckExpr, collection, { resolveCollection }) : null;
|
|
558
|
-
if (!usingClause && needsUsing) usingClause = "false";
|
|
559
|
-
if (!withCheckClause && needsWithCheck) withCheckClause = "false";
|
|
560
|
-
const drop = `DROP POLICY IF EXISTS "${policyName}" ON "${schema}"."${tableName}";`;
|
|
561
|
-
let create = `CREATE POLICY "${policyName}" ON "${schema}"."${tableName}" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map((r) => `"${r}"`).join(", ")}`;
|
|
562
|
-
if (usingClause) create += ` USING (${usingClause})`;
|
|
563
|
-
if (withCheckClause) create += ` WITH CHECK (${withCheckClause})`;
|
|
564
|
-
create += ";";
|
|
565
|
-
return [drop, create];
|
|
566
|
-
};
|
|
567
|
-
/**
|
|
568
|
-
* Single-quote escaping for a SQL string literal (PostgreSQL doubles the
|
|
569
|
-
* quote). Enum labels come straight from user-authored collection config, so a
|
|
570
|
-
* label like `it's` closes the literal early and the whole generated file stops
|
|
571
|
-
* parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
|
|
572
|
-
* because ensure-collection-tables already imports from this module — the
|
|
573
|
-
* reverse would be a cycle.
|
|
574
|
-
*/
|
|
575
|
-
var quoteSqlLiteral = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
576
|
-
var getSqlColumnType = (propName, prop, collection, collections) => {
|
|
577
|
-
switch (prop.type) {
|
|
578
|
-
case "string": {
|
|
579
|
-
const stringProp = prop;
|
|
580
|
-
if (stringProp.enum) {
|
|
581
|
-
const tableName = getTableName(collection);
|
|
582
|
-
const colName = resolveColumnName(propName, prop);
|
|
583
|
-
return `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${tableName}_${colName}"`;
|
|
584
|
-
}
|
|
585
|
-
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") return "UUID";
|
|
586
|
-
if (stringProp.columnType === "char") return `CHAR(${resolveStringColumnLength(stringProp)})`;
|
|
587
|
-
if (stringProp.columnType === "varchar") return `VARCHAR(${resolveStringColumnLength(stringProp)})`;
|
|
588
|
-
return "TEXT";
|
|
589
|
-
}
|
|
590
|
-
case "number": {
|
|
591
|
-
const numProp = prop;
|
|
592
|
-
const isId = isIdProperty(propName, prop, collection);
|
|
593
|
-
if ("isId" in numProp && numProp.isId === "increment") return "INTEGER GENERATED BY DEFAULT AS IDENTITY";
|
|
594
|
-
if (numProp.columnType) {
|
|
595
|
-
if (numProp.columnType === "double precision") return "DOUBLE PRECISION";
|
|
596
|
-
return numProp.columnType.toUpperCase();
|
|
597
|
-
}
|
|
598
|
-
return numProp.validation?.integer || isId ? "INTEGER" : "NUMERIC";
|
|
599
|
-
}
|
|
600
|
-
case "boolean": return "BOOLEAN";
|
|
601
|
-
case "date": {
|
|
602
|
-
const dateProp = prop;
|
|
603
|
-
if (dateProp.columnType === "date") return "DATE";
|
|
604
|
-
if (dateProp.columnType === "time") return "TIME";
|
|
605
|
-
return "TIMESTAMP WITH TIME ZONE";
|
|
606
|
-
}
|
|
607
|
-
case "map": return prop.columnType === "json" ? "JSON" : "JSONB";
|
|
608
|
-
case "geopoint": return "JSONB";
|
|
609
|
-
case "array": {
|
|
610
|
-
const arrayProp = prop;
|
|
611
|
-
let colType = arrayProp.columnType;
|
|
612
|
-
if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
|
|
613
|
-
const ofProp = arrayProp.of;
|
|
614
|
-
if (ofProp.type === "string") colType = "text[]";
|
|
615
|
-
else if (ofProp.type === "number") colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
616
|
-
else if (ofProp.type === "boolean") colType = "boolean[]";
|
|
617
|
-
}
|
|
618
|
-
if (colType === "json") return "JSON";
|
|
619
|
-
if (colType === "text[]") return "TEXT[]";
|
|
620
|
-
if (colType === "integer[]") return "INTEGER[]";
|
|
621
|
-
if (colType === "boolean[]") return "BOOLEAN[]";
|
|
622
|
-
if (colType === "numeric[]") return "NUMERIC[]";
|
|
623
|
-
return "JSONB";
|
|
624
|
-
}
|
|
625
|
-
case "vector": return `VECTOR(${prop.dimensions})`;
|
|
626
|
-
case "binary": return "BYTEA";
|
|
627
|
-
case "relation": {
|
|
628
|
-
const refProp = prop;
|
|
629
|
-
const relation = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
|
|
630
|
-
if (relation?.kind !== "belongsTo") throw new Error(`Relation ${propName} does not put a column on this table (only \`belongsTo\` does)`);
|
|
631
|
-
let targetCollection;
|
|
632
|
-
try {
|
|
633
|
-
targetCollection = relation.target();
|
|
634
|
-
} catch {
|
|
635
|
-
return "TEXT";
|
|
636
|
-
}
|
|
637
|
-
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
638
|
-
return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
|
|
639
|
-
}
|
|
640
|
-
case "reference": {
|
|
641
|
-
const refProp = prop;
|
|
642
|
-
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
643
|
-
if (!targetCollection) return "TEXT";
|
|
644
|
-
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
645
|
-
return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
|
|
646
|
-
}
|
|
647
|
-
default: throw new Error(`No Postgres column type for property '${propName}' of type '${prop.type}' in collection '${collection.slug}'. Add a case to \`getSqlColumnType\` (and to \`getDrizzleColumn\`, which must agree).`);
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
/**
|
|
651
|
-
* Everything a `search` block needs, as a file Rebase applies itself.
|
|
652
|
-
*
|
|
653
|
-
* Search is the one part of the schema Atlas does not own. Two independent
|
|
654
|
-
* reasons, and either alone would be enough:
|
|
655
|
-
*
|
|
656
|
-
* 1. Its free tier refuses to *parse* a desired-state file that so much as
|
|
657
|
-
* contains a function — "functions and procedures are available to
|
|
658
|
-
* logged-in users only". A generated `tsvector` column cannot avoid one:
|
|
659
|
-
* `unaccent` is STABLE and jsonb flattening needs a set-returning function,
|
|
660
|
-
* so both have to be wrapped in an IMMUTABLE helper to be legal in a
|
|
661
|
-
* generated column at all.
|
|
662
|
-
* 2. Even with the file accepted, Atlas *wipes* the dev database it diffs
|
|
663
|
-
* against, so a helper seeded there beforehand is gone by the time the plan
|
|
664
|
-
* is analysed. There is no hook to reinstate it.
|
|
665
|
-
*
|
|
666
|
-
* So the column, its index and its helpers are excluded from Atlas's view
|
|
667
|
-
* (`searchExcludePatterns`) and applied from here — the same arrangement the
|
|
668
|
-
* RLS policies already use, and for the same underlying reason.
|
|
669
|
-
*
|
|
670
|
-
* Ordered as it must run: extensions, then helpers, then the column whose
|
|
671
|
-
* expression calls them, then the index over that column. Every statement is
|
|
672
|
-
* `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and
|
|
673
|
-
* appended to migrations that run against databases at any stage of their
|
|
674
|
-
* life. Empty when nothing opted in — the caller writes no file then.
|
|
675
|
-
*
|
|
676
|
-
* The dev database Atlas plans against needs none of this. `searchExcludePatterns`
|
|
677
|
-
* removes the search column from the *inspected* state, so the state Atlas
|
|
678
|
-
* replays there never mentions the column and never calls a helper. Seeding the
|
|
679
|
-
* helpers into that database was tried and removed: measured on Atlas 1.2.3, a
|
|
680
|
-
* `schema apply --dry-run` produces byte-identical output seeded or not, and
|
|
681
|
-
* Atlas wipes the dev database before it plans anyway — so a seed could not have
|
|
682
|
-
* helped even where the excludes are missing, which is the case that really does
|
|
683
|
-
* fail with `function public.rebase_search_unaccent(text) does not exist`.
|
|
684
|
-
*/
|
|
685
|
-
var generatePostgresSearchDdl = (allCollections) => {
|
|
686
|
-
const collections = relationalCollections(allCollections);
|
|
687
|
-
const specs = collections.map((c) => buildSearchColumnSpec(c)).filter((s) => s !== void 0);
|
|
688
|
-
if (specs.length === 0) return "";
|
|
689
|
-
const extensions = Array.from(new Set(specs.flatMap(searchExtensionStatements)));
|
|
690
|
-
const helpers = Array.from(new Set(specs.flatMap(searchHelperFunctions)));
|
|
691
|
-
let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n";
|
|
692
|
-
ddl += "--\n";
|
|
693
|
-
ddl += "-- Full-text search for the collections declaring a `search` block.\n";
|
|
694
|
-
ddl += "-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\n\n";
|
|
695
|
-
extensions.forEach((s) => {
|
|
696
|
-
ddl += `${s}\n`;
|
|
697
|
-
});
|
|
698
|
-
if (extensions.length > 0) ddl += "\n";
|
|
699
|
-
helpers.forEach((s) => {
|
|
700
|
-
ddl += `${s}\n\n`;
|
|
701
|
-
});
|
|
702
|
-
for (const collection of collections) {
|
|
703
|
-
const spec = buildSearchColumnSpec(collection);
|
|
704
|
-
if (!spec) continue;
|
|
705
|
-
const table = `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${getTableName(collection)}"`;
|
|
706
|
-
searchStampGuards(spec).forEach((s) => {
|
|
707
|
-
ddl += `${s}\n`;
|
|
708
|
-
});
|
|
709
|
-
ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\n`;
|
|
710
|
-
const fuzzyDef = fuzzyColumnDefinition(spec);
|
|
711
|
-
if (fuzzyDef) ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${fuzzyDef};\n`;
|
|
712
|
-
searchColumnStamps(spec).forEach((s) => {
|
|
713
|
-
ddl += `${s.sql}\n`;
|
|
714
|
-
});
|
|
715
|
-
searchIndexStatements(spec).forEach((s) => {
|
|
716
|
-
ddl += `${s}\n`;
|
|
717
|
-
});
|
|
718
|
-
ddl += "\n";
|
|
719
|
-
}
|
|
720
|
-
return ddl;
|
|
721
|
-
};
|
|
722
|
-
/**
|
|
723
|
-
* Glob patterns telling Atlas to leave the search column and its index alone.
|
|
724
|
-
*
|
|
725
|
-
* Without these, a desired state that omits search reads to Atlas as an
|
|
726
|
-
* instruction to drop the column — taking the index and the whole search
|
|
727
|
-
* feature with it on the next push.
|
|
728
|
-
*
|
|
729
|
-
* Fully qualified, `schema.table.object`, matching the include list. The
|
|
730
|
-
* two-part form is what Atlas wants when the connection URL scopes it to one
|
|
731
|
-
* schema and is *silently ignored* otherwise: it reads `posts.search_vector`
|
|
732
|
-
* as a table named `search_vector` in a schema named `posts`, matches nothing,
|
|
733
|
-
* and reports no error for the pattern that never fired.
|
|
734
|
-
*/
|
|
735
|
-
var searchExcludePatterns = (allCollections) => {
|
|
736
|
-
const patterns = [];
|
|
737
|
-
for (const collection of relationalCollections(allCollections)) {
|
|
738
|
-
const spec = buildSearchColumnSpec(collection);
|
|
739
|
-
if (!spec) continue;
|
|
740
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
741
|
-
const table = getTableName(collection);
|
|
742
|
-
for (const name of searchColumnNames(collection)) patterns.push(`${schema}.${table}.${name}`);
|
|
743
|
-
for (const index of searchIndexNames(spec)) patterns.push(`${schema}.${table}.${index}`);
|
|
744
|
-
}
|
|
745
|
-
return patterns;
|
|
746
|
-
};
|
|
747
|
-
/**
|
|
748
|
-
* Everything a `{ type: "vector" }` property needs, as a file Rebase applies
|
|
749
|
-
* itself.
|
|
750
|
-
*
|
|
751
|
-
* Vector is the second part of the schema Atlas does not own, and it is out for
|
|
752
|
-
* a narrower reason than search: not that Atlas refuses to parse the file, but
|
|
753
|
-
* that it cannot *execute* it. To compute a desired state Atlas materialises
|
|
754
|
-
* `schema.sql` in a dev database, that database is created empty, and Atlas
|
|
755
|
-
* empties it again at the start of every run — so `VECTOR(384)` resolves
|
|
756
|
-
* against a database that structurally cannot have pgvector, and every push
|
|
757
|
-
* dies with `type "vector" does not exist`. Seeding the extension beforehand
|
|
758
|
-
* does not survive the clean (measured: present before the run, gone after),
|
|
759
|
-
* putting `CREATE EXTENSION` in `schema.sql` is refused as an Atlas Pro
|
|
760
|
-
* feature, and `--exclude` alone cannot help because Atlas still parses and
|
|
761
|
-
* applies the whole file.
|
|
762
|
-
*
|
|
763
|
-
* So the column, its ANN indexes and the extension are excluded from Atlas's
|
|
764
|
-
* view ({@link vectorExcludePatterns}) and applied from here — the same
|
|
765
|
-
* arrangement search and the RLS policies already use.
|
|
766
|
-
*
|
|
767
|
-
* `CREATE EXTENSION` appears only when the project's database named `vector` in
|
|
768
|
-
* its `extensions` — see `DatabaseOptions.extensions`. Everything else is
|
|
769
|
-
* emitted either way: a database that already has pgvector installed by hand
|
|
770
|
-
* needs no permission from anyone, and one that does not gets the column
|
|
771
|
-
* refused by Postgres with a message this repo makes name the opt-in.
|
|
772
|
-
*
|
|
773
|
-
* Ordered as it must run: the extension, then the drift guard, then the column
|
|
774
|
-
* whose type needs the extension, then the indexes over that column. Every
|
|
775
|
-
* statement is `IF NOT EXISTS`, because this is replayed on every push and
|
|
776
|
-
* appended to migrations that run against databases at any stage of their life.
|
|
777
|
-
* Empty when no collection declares a vector — the caller writes no file then.
|
|
778
|
-
*/
|
|
779
|
-
var generatePostgresVectorDdl = (allCollections, options = {}) => {
|
|
780
|
-
const withVectors = relationalCollections(allCollections).filter((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0);
|
|
781
|
-
if (withVectors.length === 0) return "";
|
|
782
|
-
let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n";
|
|
783
|
-
ddl += "--\n";
|
|
784
|
-
ddl += "-- Vector columns and their ANN indexes, for the collections declaring a\n";
|
|
785
|
-
ddl += "-- `vector` property. Applied by Rebase, not by Atlas — see\n";
|
|
786
|
-
ddl += "-- generatePostgresVectorDdl.\n\n";
|
|
787
|
-
ddl += vectorExtensionDeclared(options.extensions) ? `${vectorExtensionStatement()}\n\n` : `-- pgvector is not installed from here: no database declared it. To let Rebase\n-- install it, add ${VECTOR_EXTENSION_OPT_IN} in config/resources.ts.\n-- Otherwise install it once by hand; the column below needs the type either way.\n\n`;
|
|
788
|
-
for (const collection of withVectors) {
|
|
789
|
-
const table = `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${getTableName(collection)}"`;
|
|
790
|
-
for (const spec of buildVectorColumnSpecs(collection, resolveColumnName)) {
|
|
791
|
-
ddl += `${vectorDimensionGuard(spec)}\n`;
|
|
792
|
-
ddl += `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${vectorColumnDefinition(spec)};\n`;
|
|
793
|
-
}
|
|
794
|
-
const plan = buildVectorIndexPlan(collection, resolveColumnName);
|
|
795
|
-
vectorIndexStatements(plan).forEach((s) => {
|
|
796
|
-
ddl += `${s}\n`;
|
|
797
|
-
});
|
|
798
|
-
for (const skip of plan.skipped) ddl += `-- No ANN index on "${skip.schema}"."${skip.table}"."${skip.column}": ${skip.reason}\n`;
|
|
799
|
-
ddl += "\n";
|
|
800
|
-
}
|
|
801
|
-
return ddl;
|
|
802
|
-
};
|
|
803
|
-
/**
|
|
804
|
-
* Glob patterns telling Atlas to leave the vector column and its ANN indexes
|
|
805
|
-
* alone.
|
|
806
|
-
*
|
|
807
|
-
* Without these, a desired state that omits the column reads to Atlas as an
|
|
808
|
-
* instruction to drop it, taking the embeddings and the indexes with it on the
|
|
809
|
-
* next push.
|
|
810
|
-
*
|
|
811
|
-
* Fully qualified, `schema.table.object`, for the reason spelled out on
|
|
812
|
-
* {@link searchExcludePatterns}: the two-part form matches nothing and reports
|
|
813
|
-
* no error, which turns a protection into a `DROP COLUMN`.
|
|
814
|
-
*/
|
|
815
|
-
var vectorExcludePatterns = (allCollections) => {
|
|
816
|
-
const patterns = [];
|
|
817
|
-
for (const collection of relationalCollections(allCollections)) {
|
|
818
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
819
|
-
const table = getTableName(collection);
|
|
820
|
-
for (const name of vectorObjectNames(collection, resolveColumnName)) patterns.push(`${schema}.${table}.${name}`);
|
|
821
|
-
}
|
|
822
|
-
return patterns;
|
|
823
|
-
};
|
|
824
|
-
var generatePostgresDdl = async (allCollections, options = {
|
|
825
|
-
includePolicies: true,
|
|
826
|
-
includeSearch: true,
|
|
827
|
-
includeVector: true
|
|
828
|
-
}) => {
|
|
829
|
-
const collections = relationalCollections(allCollections);
|
|
830
|
-
let ddl = "-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\n\n";
|
|
831
|
-
const uniqueSchemas = Array.from(/* @__PURE__ */ new Set([REBASE_SCHEMA, ...collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)]));
|
|
832
|
-
uniqueSchemas.forEach((schema) => {
|
|
833
|
-
if (schema) ddl += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
|
834
|
-
});
|
|
835
|
-
if (uniqueSchemas.length > 0) ddl += "\n";
|
|
836
|
-
const searchSpecs = collections.map((c) => buildSearchColumnSpec(c)).filter((s) => s !== void 0);
|
|
837
|
-
if (searchSpecs.length > 0) if (options.includeSearch === false) ddl += "-- Full-text search support lives in `search.sql`, applied separately.\n\n";
|
|
838
|
-
else {
|
|
839
|
-
const extensions = Array.from(new Set(searchSpecs.flatMap(searchExtensionStatements)));
|
|
840
|
-
const helpers = Array.from(new Set(searchSpecs.flatMap(searchHelperFunctions)));
|
|
841
|
-
ddl += "-- Full-text search support (collections declaring a `search` block)\n";
|
|
842
|
-
extensions.forEach((s) => {
|
|
843
|
-
ddl += `${s}\n`;
|
|
844
|
-
});
|
|
845
|
-
if (extensions.length > 0) ddl += "\n";
|
|
846
|
-
helpers.forEach((s) => {
|
|
847
|
-
ddl += `${s}\n\n`;
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
if (options.includeVector === false && collections.some((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0)) ddl += "-- Vector columns and their ANN indexes live in `vector.sql`, applied separately.\n\n";
|
|
851
|
-
const emittedEnums = /* @__PURE__ */ new Set();
|
|
852
|
-
collections.forEach((collection) => {
|
|
853
|
-
const collectionTable = getTableName(collection);
|
|
854
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
855
|
-
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
856
|
-
if ("enum" in prop && (prop.type === "string" || prop.type === "number") && prop.enum) {
|
|
857
|
-
const enumDbName = `${collectionTable}_${resolveColumnName(propName, prop)}`;
|
|
858
|
-
const values = Array.isArray(prop.enum) ? prop.enum.map((v) => String(typeof v === "object" && v !== null && "id" in v ? v.id : v)) : Object.keys(prop.enum);
|
|
859
|
-
if (values.length > 0 && !emittedEnums.has(`${schema}.${enumDbName}`)) {
|
|
860
|
-
emittedEnums.add(`${schema}.${enumDbName}`);
|
|
861
|
-
ddl += `CREATE TYPE "${schema}"."${enumDbName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});\n`;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
});
|
|
866
|
-
if (ddl.endsWith(";\n")) ddl += "\n";
|
|
867
|
-
const junctionSpecs = resolveJunctionSpecs(collections);
|
|
868
|
-
const allTablesToGenerate = /* @__PURE__ */ new Map();
|
|
869
|
-
for (const collection of collections) {
|
|
870
|
-
const tableName = getTableName(collection);
|
|
871
|
-
if (tableName) allTablesToGenerate.set(tableName, { collection });
|
|
872
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
873
|
-
for (const relation of Object.values(resolvedRelations)) if (isManyToMany(relation)) {
|
|
874
|
-
const junctionTableName = relation.through.table;
|
|
875
|
-
if (!allTablesToGenerate.has(junctionTableName)) allTablesToGenerate.set(junctionTableName, {
|
|
876
|
-
collection: {
|
|
877
|
-
table: junctionTableName,
|
|
878
|
-
properties: {}
|
|
879
|
-
},
|
|
880
|
-
isJunction: true,
|
|
881
|
-
relation,
|
|
882
|
-
sourceCollection: collection
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
const fkStatements = [];
|
|
887
|
-
const indexStatements = [];
|
|
888
|
-
const policyStatements = [];
|
|
889
|
-
for (const [tableName, { collection, isJunction, relation, sourceCollection }] of allTablesToGenerate.entries()) {
|
|
890
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
891
|
-
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
892
|
-
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
893
|
-
const targetCollection = relation.target();
|
|
894
|
-
const sourceTable = getTableName(sourceCollection);
|
|
895
|
-
const targetTable = getTableName(targetCollection);
|
|
896
|
-
const sourceSchema = isPostgresCollectionConfig(sourceCollection) && sourceCollection.schema ? sourceCollection.schema : "public";
|
|
897
|
-
const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
|
|
898
|
-
const { sourceColumn, targetColumn } = relation.through;
|
|
899
|
-
const sourceColType = isNumericId(sourceCollection) ? "INTEGER" : getPrimaryKeyProp(sourceCollection).isUuid ? "UUID" : "TEXT";
|
|
900
|
-
const targetColType = isNumericId(targetCollection) ? "INTEGER" : getPrimaryKeyProp(targetCollection).isUuid ? "UUID" : "TEXT";
|
|
901
|
-
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
902
|
-
const targetId = getPrimaryKeyName(targetCollection);
|
|
903
|
-
const onDelete = relation.onDelete ?? "CASCADE";
|
|
904
|
-
ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
|
|
905
|
-
ddl += ` "${sourceColumn}" ${sourceColType} NOT NULL,\n`;
|
|
906
|
-
ddl += ` "${targetColumn}" ${targetColType} NOT NULL,\n`;
|
|
907
|
-
ddl += ` PRIMARY KEY ("${sourceColumn}", "${targetColumn}")\n`;
|
|
908
|
-
ddl += `);\n\n`;
|
|
909
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${sourceColumn}_fkey`)}" FOREIGN KEY ("${sourceColumn}") REFERENCES "${sourceSchema}"."${sourceTable}" ("${sourceId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
910
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${targetColumn}_fkey`)}" FOREIGN KEY ("${targetColumn}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
911
|
-
if (options.includePolicies) {
|
|
912
|
-
ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
|
|
913
|
-
ddl += `\n`;
|
|
914
|
-
const spec = junctionSpecs.get(baseTableName);
|
|
915
|
-
if (spec) {
|
|
916
|
-
const junctionCollection = getJunctionCollectionConfig(spec);
|
|
917
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
918
|
-
getJunctionSecurityRules(spec).forEach((rule) => {
|
|
919
|
-
policyStatements.push(generatePolicyDdl(junctionCollection, rule, resolveCollection));
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
} else if (!isJunction) {
|
|
924
|
-
ddl += `CREATE TABLE "${schema}"."${baseTableName}" (\n`;
|
|
925
|
-
const columns = [];
|
|
926
|
-
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
927
|
-
if (prop.type === "vector" && options.includeVector === false) return;
|
|
928
|
-
if (prop.type === "relation") {
|
|
929
|
-
const refProp = prop;
|
|
930
|
-
const relInfo = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
|
|
931
|
-
if (relInfo?.kind !== "belongsTo") return;
|
|
932
|
-
if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) return;
|
|
933
|
-
let targetCollection;
|
|
934
|
-
try {
|
|
935
|
-
targetCollection = relInfo.target();
|
|
936
|
-
} catch {
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
const targetTable = getTableName(targetCollection);
|
|
940
|
-
const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
|
|
941
|
-
const targetId = getPrimaryKeyName(targetCollection);
|
|
942
|
-
const fkColType = getSqlColumnType(propName, prop, collection, collections);
|
|
943
|
-
const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : "";
|
|
944
|
-
const required = prop.validation?.required;
|
|
945
|
-
const onDeleteVal = relInfo.onDelete ?? defaultBelongsToOnDelete(required);
|
|
946
|
-
let colDef = ` "${relInfo.localKey}" ${fkColType}`;
|
|
947
|
-
if (required) colDef += " NOT NULL";
|
|
948
|
-
columns.push(colDef);
|
|
949
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${relInfo.localKey}_fkey`)}" FOREIGN KEY ("${relInfo.localKey}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDeleteVal.toUpperCase()}${onUpdate};`);
|
|
950
|
-
} else if (prop.type === "reference") {
|
|
951
|
-
const refProp = prop;
|
|
952
|
-
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
953
|
-
const colName = resolveColumnName(propName, prop);
|
|
954
|
-
const colType = getSqlColumnType(propName, prop, collection, collections);
|
|
955
|
-
const required = prop.validation?.required;
|
|
956
|
-
if (!targetCollection) {
|
|
957
|
-
let colDef = ` "${colName}" ${colType}`;
|
|
958
|
-
if (required) colDef += " NOT NULL";
|
|
959
|
-
columns.push(colDef);
|
|
960
|
-
} else {
|
|
961
|
-
const targetTable = getTableName(targetCollection);
|
|
962
|
-
const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
|
|
963
|
-
const targetId = getPrimaryKeyName(targetCollection);
|
|
964
|
-
const onDelete = defaultBelongsToOnDelete(required);
|
|
965
|
-
let colDef = ` "${colName}" ${colType}`;
|
|
966
|
-
if (required) colDef += " NOT NULL";
|
|
967
|
-
columns.push(colDef);
|
|
968
|
-
fkStatements.push(`ALTER TABLE "${schema}"."${baseTableName}" ADD CONSTRAINT "${toPostgresIdentifier(`${baseTableName}_${colName}_fkey`)}" FOREIGN KEY ("${colName}") REFERENCES "${targetSchema}"."${targetTable}" ("${targetId}") ON DELETE ${onDelete.toUpperCase()};`);
|
|
969
|
-
}
|
|
970
|
-
} else {
|
|
971
|
-
const colName = resolveColumnName(propName, prop);
|
|
972
|
-
const authDefinition = isAuthCollection(collection) ? authUsersColumnDefinition(colName) : void 0;
|
|
973
|
-
if (authDefinition && !isIdProperty(propName, prop, collection)) {
|
|
974
|
-
columns.push(` "${colName}" ${authDefinition}`);
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
let colDef = ` "${colName}" ${getSqlColumnType(propName, prop, collection, collections)}`;
|
|
978
|
-
if (isIdProperty(propName, prop, collection)) colDef += " PRIMARY KEY";
|
|
979
|
-
if ("isId" in prop && prop.isId !== "manual" && prop.isId !== true && prop.isId !== "increment") {
|
|
980
|
-
if (prop.isId === "uuid") colDef += " DEFAULT gen_random_uuid()";
|
|
981
|
-
else if (prop.isId === "cuid") colDef += " DEFAULT cuid()";
|
|
982
|
-
else if (typeof prop.isId === "string") colDef += ` DEFAULT ${prop.isId}`;
|
|
983
|
-
}
|
|
984
|
-
if (!isIdProperty(propName, prop, collection) && prop.validation?.unique) colDef += " UNIQUE";
|
|
985
|
-
if (prop.type === "date") {
|
|
986
|
-
const dateProp = prop;
|
|
987
|
-
if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") colDef += " DEFAULT now()";
|
|
988
|
-
}
|
|
989
|
-
if (prop.validation?.required && !colDef.includes("PRIMARY KEY")) colDef += " NOT NULL";
|
|
990
|
-
columns.push(colDef);
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
if (isAuthCollection(collection)) {
|
|
994
|
-
const declared = new Set(Object.entries(collection.properties ?? {}).map(([name, prop]) => resolveColumnName(name, prop)));
|
|
995
|
-
for (const spec of AUTH_USERS_COLUMNS) {
|
|
996
|
-
if (declared.has(spec.column)) continue;
|
|
997
|
-
columns.push(` "${spec.column}" ${authUsersColumnSql(spec)}`);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
const searchSpec = options.includeSearch === false ? void 0 : buildSearchColumnSpec(collection);
|
|
1001
|
-
if (searchSpec) {
|
|
1002
|
-
columns.push(` ${searchColumnDefinition(searchSpec)}`);
|
|
1003
|
-
const fuzzyDef = fuzzyColumnDefinition(searchSpec);
|
|
1004
|
-
if (fuzzyDef) columns.push(` ${fuzzyDef}`);
|
|
1005
|
-
indexStatements.push(...searchIndexStatements(searchSpec));
|
|
1006
|
-
}
|
|
1007
|
-
if (options.includeVector !== false) {
|
|
1008
|
-
const vectorPlan = buildVectorIndexPlan(collection, resolveColumnName);
|
|
1009
|
-
indexStatements.push(...vectorIndexStatements(vectorPlan));
|
|
1010
|
-
for (const skip of vectorPlan.skipped) indexStatements.push(`-- No ANN index on "${skip.schema}"."${skip.table}"."${skip.column}": ${skip.reason}`);
|
|
1011
|
-
}
|
|
1012
|
-
indexStatements.push(...collectionIndexStatements(buildCollectionIndexSpecs(collection, resolveColumnName)));
|
|
1013
|
-
if (!columns.some((c) => c.includes("PRIMARY KEY"))) columns.unshift(" \"id\" TEXT PRIMARY KEY");
|
|
1014
|
-
ddl += columns.join(",\n");
|
|
1015
|
-
ddl += `\n);\n\n`;
|
|
1016
|
-
if (options.includePolicies) {
|
|
1017
|
-
ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
|
|
1018
|
-
ddl += `\n`;
|
|
1019
|
-
const securityRules = getEffectiveSecurityRules(collection);
|
|
1020
|
-
if (securityRules.length > 0) {
|
|
1021
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
1022
|
-
securityRules.forEach((rule) => {
|
|
1023
|
-
policyStatements.push(generatePolicyDdl(collection, rule, resolveCollection));
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if (indexStatements.length > 0) {
|
|
1030
|
-
ddl += "-- Indexes\n";
|
|
1031
|
-
ddl += indexStatements.join("\n") + "\n\n";
|
|
1032
|
-
}
|
|
1033
|
-
if (fkStatements.length > 0) {
|
|
1034
|
-
ddl += "-- Foreign Key Constraints\n";
|
|
1035
|
-
ddl += fkStatements.join("\n") + "\n\n";
|
|
1036
|
-
}
|
|
1037
|
-
if (policyStatements.length > 0) {
|
|
1038
|
-
ddl += "-- Row Level Security Policies\n";
|
|
1039
|
-
ddl += policyStatements.join("");
|
|
1040
|
-
ddl += "\n";
|
|
1041
|
-
}
|
|
1042
|
-
return ddl;
|
|
1043
|
-
};
|
|
1044
|
-
var schemaOfCollection = (collection) => isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
1045
|
-
var bareTableName = (name) => name.includes(".") ? name.split(".").pop() : name;
|
|
1046
|
-
/**
|
|
1047
|
-
* Truncate a derived identifier the way Postgres does: to 63 bytes, silently.
|
|
1048
|
-
*
|
|
1049
|
-
* This is not cosmetic, and not really a naming choice at all — it is agreeing
|
|
1050
|
-
* with the name the database ALREADY stored. `ADD CONSTRAINT` on a longer name
|
|
1051
|
-
* succeeds and records the truncated form, so the untruncated name this used to
|
|
1052
|
-
* derive matched nothing in the catalogue. Boot-ensure compares its planned
|
|
1053
|
-
* constraints against `readExistingSchema`, which reads catalogue names, so the
|
|
1054
|
-
* comparison could never hit: every boot re-issued `ADD CONSTRAINT` for the same
|
|
1055
|
-
* constraint, forever, and got "already exists" every time. Non-fatal (foreign
|
|
1056
|
-
* keys are the one action allowed to fail) and therefore permanent — an error in
|
|
1057
|
-
* the log on every restart of a project whose table and column names happened to
|
|
1058
|
-
* be long.
|
|
1059
|
-
*
|
|
1060
|
-
* Byte length, not string length: NAMEDATALEN is 64 bytes, and a multi-byte
|
|
1061
|
-
* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
|
|
1062
|
-
*/
|
|
1063
|
-
var foreignKeyPlan = (args) => {
|
|
1064
|
-
const constraintName = toPostgresIdentifier(`${args.table}_${args.column}_fkey`);
|
|
1065
|
-
const onUpdate = args.onUpdate ? ` ON UPDATE ${args.onUpdate.toUpperCase()}` : "";
|
|
1066
|
-
return {
|
|
1067
|
-
constraintName,
|
|
1068
|
-
schema: args.schema,
|
|
1069
|
-
table: args.table,
|
|
1070
|
-
column: args.column,
|
|
1071
|
-
targetSchema: args.targetSchema,
|
|
1072
|
-
targetTable: args.targetTable,
|
|
1073
|
-
targetColumn: args.targetColumn,
|
|
1074
|
-
sql: `ALTER TABLE "${args.schema}"."${args.table}" ADD CONSTRAINT "${constraintName}" FOREIGN KEY ("${args.column}") REFERENCES "${args.targetSchema}"."${args.targetTable}" ("${args.targetColumn}") ON DELETE ${args.onDelete.toUpperCase()}${onUpdate};`
|
|
1075
|
-
};
|
|
1076
|
-
};
|
|
1077
|
-
/**
|
|
1078
|
-
* The FK columns the declared collections own — one entry per `relation`
|
|
1079
|
-
* (`belongsTo` side) or `reference` property.
|
|
1080
|
-
*
|
|
1081
|
-
* Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
|
|
1082
|
-
* create the same columns with the same names, types and constraints. Before
|
|
1083
|
-
* this it skipped them outright, which was survivable only because `db push`
|
|
1084
|
-
* always followed; on a managed tenant nothing follows, so a table arrived
|
|
1085
|
-
* without the column its own collection reads and wrote 400 on every insert.
|
|
1086
|
-
*
|
|
1087
|
-
* A relation whose target is not in the bundle yields no column at all (the
|
|
1088
|
-
* generator returns early on an unresolvable target); a `reference` whose target
|
|
1089
|
-
* is unknown yields the column without a constraint. Both mirror the generator
|
|
1090
|
-
* exactly — a divergence here is a schema fork between boot and `db push`.
|
|
1091
|
-
*/
|
|
1092
|
-
var planRelationalColumns = (allCollections) => {
|
|
1093
|
-
const collections = relationalCollections(allCollections);
|
|
1094
|
-
const plans = [];
|
|
1095
|
-
for (const collection of collections) {
|
|
1096
|
-
const tableName = getTableName(collection);
|
|
1097
|
-
if (!tableName) continue;
|
|
1098
|
-
const schema = schemaOfCollection(collection);
|
|
1099
|
-
const table = bareTableName(tableName);
|
|
1100
|
-
for (const [propName, rawProp] of Object.entries(collection.properties ?? {})) {
|
|
1101
|
-
const prop = rawProp;
|
|
1102
|
-
if (prop.type === "relation") {
|
|
1103
|
-
const refProp = prop;
|
|
1104
|
-
const relInfo = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
|
|
1105
|
-
if (relInfo?.kind !== "belongsTo") continue;
|
|
1106
|
-
if (collection.properties[relInfo.localKey] && propName !== relInfo.localKey) continue;
|
|
1107
|
-
let targetCollection;
|
|
1108
|
-
try {
|
|
1109
|
-
targetCollection = relInfo.target();
|
|
1110
|
-
} catch {
|
|
1111
|
-
continue;
|
|
1112
|
-
}
|
|
1113
|
-
if (!targetCollection) continue;
|
|
1114
|
-
const required = prop.validation?.required;
|
|
1115
|
-
const relationName = refProp.relation?.relationName ?? propName;
|
|
1116
|
-
const legacyKey = legacyForeignKeyName(relationName);
|
|
1117
|
-
const derived = relInfo.localKey === generateForeignKeyName(relationName);
|
|
1118
|
-
plans.push({
|
|
1119
|
-
schema,
|
|
1120
|
-
table,
|
|
1121
|
-
column: relInfo.localKey,
|
|
1122
|
-
legacyColumn: derived && legacyKey !== relInfo.localKey ? legacyKey : void 0,
|
|
1123
|
-
type: getSqlColumnType(propName, prop, collection, collections),
|
|
1124
|
-
foreignKey: foreignKeyPlan({
|
|
1125
|
-
schema,
|
|
1126
|
-
table,
|
|
1127
|
-
column: relInfo.localKey,
|
|
1128
|
-
targetSchema: schemaOfCollection(targetCollection),
|
|
1129
|
-
targetTable: bareTableName(getTableName(targetCollection)),
|
|
1130
|
-
targetColumn: getPrimaryKeyName(targetCollection),
|
|
1131
|
-
onDelete: relInfo.onDelete ?? defaultBelongsToOnDelete(required),
|
|
1132
|
-
onUpdate: relInfo.onUpdate
|
|
1133
|
-
})
|
|
1134
|
-
});
|
|
1135
|
-
} else if (prop.type === "reference") {
|
|
1136
|
-
const refProp = prop;
|
|
1137
|
-
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
1138
|
-
const column = resolveColumnName(propName, prop);
|
|
1139
|
-
const type = getSqlColumnType(propName, prop, collection, collections);
|
|
1140
|
-
const required = prop.validation?.required;
|
|
1141
|
-
plans.push({
|
|
1142
|
-
schema,
|
|
1143
|
-
table,
|
|
1144
|
-
column,
|
|
1145
|
-
type,
|
|
1146
|
-
foreignKey: targetCollection ? foreignKeyPlan({
|
|
1147
|
-
schema,
|
|
1148
|
-
table,
|
|
1149
|
-
column,
|
|
1150
|
-
targetSchema: schemaOfCollection(targetCollection),
|
|
1151
|
-
targetTable: bareTableName(getTableName(targetCollection)),
|
|
1152
|
-
targetColumn: getPrimaryKeyName(targetCollection),
|
|
1153
|
-
onDelete: defaultBelongsToOnDelete(required)
|
|
1154
|
-
}) : void 0
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return plans;
|
|
1160
|
-
};
|
|
1161
|
-
/**
|
|
1162
|
-
* The junction tables a bundle's many-to-many relations imply.
|
|
1163
|
-
*
|
|
1164
|
-
* Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
|
|
1165
|
-
* comes from, so a table created here always has policies planned for it — a
|
|
1166
|
-
* junction with row-level security left off is readable and writable by every
|
|
1167
|
-
* signed-in user, which is why the two must ship together.
|
|
1168
|
-
*/
|
|
1169
|
-
var planJunctionTables = (allCollections) => {
|
|
1170
|
-
const collections = relationalCollections(allCollections);
|
|
1171
|
-
const plans = [];
|
|
1172
|
-
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
1173
|
-
const [source, target] = spec.endpoints;
|
|
1174
|
-
const legacyFor = (endpoint) => {
|
|
1175
|
-
const slug = toSnakeCase(endpoint.collection.slug ?? endpoint.collection.name ?? "");
|
|
1176
|
-
const legacy = legacyForeignKeyName(slug);
|
|
1177
|
-
return endpoint.junctionColumn === generateForeignKeyName(slug) && legacy !== endpoint.junctionColumn ? legacy : void 0;
|
|
1178
|
-
};
|
|
1179
|
-
const columns = [{
|
|
1180
|
-
name: source.junctionColumn,
|
|
1181
|
-
type: junctionKeyType(source.collection),
|
|
1182
|
-
legacyName: legacyFor(source)
|
|
1183
|
-
}, {
|
|
1184
|
-
name: target.junctionColumn,
|
|
1185
|
-
type: junctionKeyType(target.collection),
|
|
1186
|
-
legacyName: legacyFor(target)
|
|
1187
|
-
}];
|
|
1188
|
-
const onDelete = spec.declaringSides[0]?.relation.onDelete ?? "CASCADE";
|
|
1189
|
-
plans.push({
|
|
1190
|
-
schema: spec.schema,
|
|
1191
|
-
table: spec.table,
|
|
1192
|
-
columns,
|
|
1193
|
-
createTable: `CREATE TABLE IF NOT EXISTS "${spec.schema}"."${spec.table}" (` + columns.map((c) => `"${c.name}" ${c.type} NOT NULL`).join(", ") + `, PRIMARY KEY (${columns.map((c) => `"${c.name}"`).join(", ")}));`,
|
|
1194
|
-
foreignKeys: [source, target].map((endpoint, i) => foreignKeyPlan({
|
|
1195
|
-
schema: spec.schema,
|
|
1196
|
-
table: spec.table,
|
|
1197
|
-
column: columns[i].name,
|
|
1198
|
-
targetSchema: schemaOfCollection(endpoint.collection),
|
|
1199
|
-
targetTable: bareTableName(getTableName(endpoint.collection)),
|
|
1200
|
-
targetColumn: getPrimaryKeyName(endpoint.collection),
|
|
1201
|
-
onDelete
|
|
1202
|
-
}))
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
return plans;
|
|
1206
|
-
};
|
|
1207
|
-
/**
|
|
1208
|
-
* The per-table RLS plan for the *declared* collections, as executable
|
|
1209
|
-
* statements — what the managed runtime applies at boot so a freshly
|
|
1210
|
-
* provisioned tenant database serves data instead of 401ing every read.
|
|
1211
|
-
*
|
|
1212
|
-
* Mirrors {@link generatePostgresPoliciesDdl} exactly (same
|
|
1213
|
-
* `generatePolicyStatements`, same enable-RLS, same effective rules, same
|
|
1214
|
-
* derived junction rules), so boot and `db push` produce identical policies from
|
|
1215
|
-
* identical collections.
|
|
1216
|
-
*
|
|
1217
|
-
* Junction tables are included, and have to be: boot creates them now
|
|
1218
|
-
* ({@link planJunctionTables}), and a junction with RLS left off is readable and
|
|
1219
|
-
* writable by every signed-in user. A junction whose table is still absent is
|
|
1220
|
-
* skipped by the applier, not planned away here.
|
|
1221
|
-
*/
|
|
1222
|
-
var planCollectionPolicies = (allCollections) => {
|
|
1223
|
-
const collections = relationalCollections(allCollections);
|
|
1224
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
1225
|
-
const plans = [];
|
|
1226
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1227
|
-
for (const collection of collections) {
|
|
1228
|
-
const tableName = getTableName(collection);
|
|
1229
|
-
if (!tableName) continue;
|
|
1230
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
1231
|
-
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
1232
|
-
const qualified = `${schema}.${baseTableName}`;
|
|
1233
|
-
if (seen.has(qualified)) continue;
|
|
1234
|
-
seen.add(qualified);
|
|
1235
|
-
const policyStatements = [];
|
|
1236
|
-
for (const rule of getEffectiveSecurityRules(collection)) policyStatements.push(...generatePolicyStatements(collection, rule, resolveCollection));
|
|
1237
|
-
plans.push({
|
|
1238
|
-
schema,
|
|
1239
|
-
table: baseTableName,
|
|
1240
|
-
qualified,
|
|
1241
|
-
enableRls: `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;`,
|
|
1242
|
-
policyStatements
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
for (const spec of resolveJunctionSpecs(collections).values()) {
|
|
1246
|
-
const qualified = `${spec.schema}.${spec.table}`;
|
|
1247
|
-
if (seen.has(qualified)) continue;
|
|
1248
|
-
seen.add(qualified);
|
|
1249
|
-
const junctionCollection = getJunctionCollectionConfig(spec);
|
|
1250
|
-
const policyStatements = [];
|
|
1251
|
-
for (const rule of getJunctionSecurityRules(spec)) policyStatements.push(...generatePolicyStatements(junctionCollection, rule, resolveCollection));
|
|
1252
|
-
plans.push({
|
|
1253
|
-
schema: spec.schema,
|
|
1254
|
-
table: spec.table,
|
|
1255
|
-
qualified,
|
|
1256
|
-
enableRls: `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;`,
|
|
1257
|
-
policyStatements
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
return plans;
|
|
1261
|
-
};
|
|
1262
|
-
var generatePostgresPoliciesDdl = (allCollections) => {
|
|
1263
|
-
const collections = relationalCollections(allCollections);
|
|
1264
|
-
let ddl = "-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\n\n";
|
|
1265
|
-
const allTablesToGenerate = /* @__PURE__ */ new Map();
|
|
1266
|
-
for (const collection of collections) {
|
|
1267
|
-
const tableName = getTableName(collection);
|
|
1268
|
-
if (tableName) allTablesToGenerate.set(tableName, { collection });
|
|
1269
|
-
}
|
|
1270
|
-
for (const [tableName, { collection }] of allTablesToGenerate.entries()) {
|
|
1271
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
1272
|
-
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
1273
|
-
ddl += `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;\n`;
|
|
1274
|
-
ddl += `\n`;
|
|
1275
|
-
const securityRules = getEffectiveSecurityRules(collection);
|
|
1276
|
-
if (securityRules.length > 0) {
|
|
1277
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
1278
|
-
const injectedNames = new Set(getInjectedSecurityRules(collection).map((rule) => rule.name));
|
|
1279
|
-
securityRules.forEach((rule) => {
|
|
1280
|
-
if (rule.name && injectedNames.has(rule.name)) {
|
|
1281
|
-
ddl += `-- Injected by Rebase (not from this collection's securityRules).\n`;
|
|
1282
|
-
ddl += `-- Set \`disableDefaultPolicies: true\` on "${collection.slug}" to drop these and own its RLS outright.\n`;
|
|
1283
|
-
}
|
|
1284
|
-
ddl += generatePolicyDdl(collection, rule, resolveCollection);
|
|
1285
|
-
});
|
|
1286
|
-
ddl += "\n";
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
const junctionSpecs = resolveJunctionSpecs(collections);
|
|
1290
|
-
for (const spec of junctionSpecs.values()) {
|
|
1291
|
-
ddl += `ALTER TABLE "${spec.schema}"."${spec.table}" ENABLE ROW LEVEL SECURITY;\n`;
|
|
1292
|
-
ddl += `\n`;
|
|
1293
|
-
const junctionRules = getJunctionSecurityRules(spec);
|
|
1294
|
-
if (junctionRules.length === 0) continue;
|
|
1295
|
-
const junctionCollection = getJunctionCollectionConfig(spec);
|
|
1296
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
|
|
1297
|
-
const declaringSlugs = spec.declaringSides.map((s) => s.collection.slug).join("\", \"");
|
|
1298
|
-
ddl += `-- Derived by Rebase for the junction "${spec.table}" (no collection declares it).\n`;
|
|
1299
|
-
ddl += `-- Reads require both endpoint rows to be visible; writes follow the update\n`;
|
|
1300
|
-
ddl += `-- rules of "${declaringSlugs}". Set \`disableDefaultPolicies: true\` on the\n`;
|
|
1301
|
-
ddl += `-- declaring collection(s) to drop these and police the junction yourself.\n`;
|
|
1302
|
-
junctionRules.forEach((rule) => {
|
|
1303
|
-
ddl += generatePolicyDdl(junctionCollection, rule, resolveCollection);
|
|
1304
|
-
});
|
|
1305
|
-
ddl += "\n";
|
|
1306
|
-
}
|
|
1307
|
-
return ddl;
|
|
1308
|
-
};
|
|
1309
|
-
//#endregion
|
|
1310
|
-
export { searchIndexStatements as C, searchHelperFunctions as S, assertSearchIsPostgresOnly as _, generatePostgresVectorDdl as a, searchColumnStamps as b, isIdProperty as c, planRelationalColumns as d, quoteSqlLiteral as f, SEARCH_UNACCENT_FN as g, SEARCH_TEXT_FN as h, generatePostgresSearchDdl as i, planCollectionPolicies as l, SEARCH_STAMP_PREFIX as m, generatePostgresDdl as n, generate_postgres_ddl_logic_exports as o, resolveColumnName as p, generatePostgresPoliciesDdl as r, getSqlColumnType as s, defaultBelongsToOnDelete as t, planJunctionTables as u, buildSearchColumnSpec as v, visibleColumnProjection as w, searchExtensionStatements as x, hiddenColumnsOption as y };
|
|
1311
|
-
|
|
1312
|
-
//# sourceMappingURL=generate-postgres-ddl-logic-CtnpIBVv.js.map
|