@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g249daa1
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/PostgresBackendDriver.d.ts +48 -1
- package/dist/{src-DlPBctw_.js → auth-users-columns-Dt9g712t.js} +721 -47
- package/dist/auth-users-columns-Dt9g712t.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
- package/dist/cli-helpers.d.ts +57 -1
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-CwYUliAa.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-CwYUliAa.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DRxaUG96.js} +86 -15
- package/dist/ensure-collection-tables-DRxaUG96.js.map +1 -0
- package/dist/index.es.js +765 -223
- package/dist/index.es.js.map +1 -1
- package/dist/{policy-CeA1JcxP.js → policy-CPkCqVTz.js} +4 -4
- package/dist/policy-CPkCqVTz.js.map +1 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-tables.d.ts +2 -2
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +199 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +25 -7
- package/dist/services/dataService.d.ts +3 -0
- package/dist/services/realtimeService.d.ts +27 -21
- package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +71 -2
- package/dist/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
- package/dist/websocket-D0TBU3ia.js.map +1 -0
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +165 -3
- package/src/PostgresBootstrapper.ts +41 -2
- package/src/auth/ensure-tables.ts +185 -86
- package/src/cli-helpers.ts +129 -10
- package/src/cli.ts +232 -30
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +31 -3
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-tables.ts +165 -20
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-postgres-ddl-logic.ts +266 -15
- package/src/schema/generate-postgres-ddl.ts +25 -2
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +558 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +148 -108
- package/src/services/PersistService.ts +14 -1
- package/src/services/RelationService.ts +2 -1
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/dataService.ts +3 -0
- package/src/services/realtimeService.ts +46 -37
- package/src/utils/drizzle-conditions.ts +223 -2
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-DlPBctw_.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -26,8 +26,18 @@
|
|
|
26
26
|
* no-op.
|
|
27
27
|
*/
|
|
28
28
|
import { type CollectionConfig, type Property, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
29
|
-
import { getTableName } from "@rebasepro/common";
|
|
29
|
+
import { getTableName, relationalCollections } from "@rebasepro/common";
|
|
30
30
|
import { logger } from "@rebasepro/server";
|
|
31
|
+
import {
|
|
32
|
+
assertSearchIsPostgresOnly,
|
|
33
|
+
buildSearchColumnSpec,
|
|
34
|
+
searchExtensionStatements,
|
|
35
|
+
searchHelperFunctions,
|
|
36
|
+
searchIndexStatements,
|
|
37
|
+
SEARCH_TEXT_FN,
|
|
38
|
+
SEARCH_UNACCENT_FN,
|
|
39
|
+
type SearchColumnSpec
|
|
40
|
+
} from "./search-column";
|
|
31
41
|
import {
|
|
32
42
|
getSqlColumnType,
|
|
33
43
|
resolveColumnName,
|
|
@@ -36,6 +46,12 @@ import {
|
|
|
36
46
|
planJunctionTables,
|
|
37
47
|
quoteSqlLiteral
|
|
38
48
|
} from "./generate-postgres-ddl-logic";
|
|
49
|
+
import {
|
|
50
|
+
AUTH_USERS_COLUMNS,
|
|
51
|
+
authUsersColumnDefinition,
|
|
52
|
+
authUsersColumnSql,
|
|
53
|
+
isAuthCollection
|
|
54
|
+
} from "./auth-users-columns";
|
|
39
55
|
|
|
40
56
|
/**
|
|
41
57
|
* The subset of a database handle this needs: run a statement, get rows back.
|
|
@@ -77,7 +93,8 @@ export interface ExistingSchema {
|
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
export interface EnsureAction {
|
|
80
|
-
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column"
|
|
96
|
+
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column"
|
|
97
|
+
| "create-extension" | "create-function" | "create-index";
|
|
81
98
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
82
99
|
target: string;
|
|
83
100
|
sql: string;
|
|
@@ -167,9 +184,19 @@ function requiredEnums(collection: CollectionConfig): { name: string; values: st
|
|
|
167
184
|
* table may be the target of a relation), and nothing is emitted twice.
|
|
168
185
|
*/
|
|
169
186
|
export function planCollectionSchemaEnsure(
|
|
170
|
-
|
|
187
|
+
allCollections: CollectionConfig[],
|
|
171
188
|
existing: ExistingSchema
|
|
172
189
|
): EnsurePlan {
|
|
190
|
+
// Boot receives every collection the bundle declares, including the ones
|
|
191
|
+
// served by another engine entirely. Creating a Postgres table for a
|
|
192
|
+
// Firestore collection is not a harmless extra: the app keeps reading
|
|
193
|
+
// documents from Firestore while an empty table with the same name accretes
|
|
194
|
+
// policies and shows up in every drift report.
|
|
195
|
+
// Before the filter, deliberately: a `search` block on a collection this
|
|
196
|
+
// engine does not store would otherwise be dropped here without a word.
|
|
197
|
+
assertSearchIsPostgresOnly(allCollections);
|
|
198
|
+
|
|
199
|
+
const collections = relationalCollections(allCollections);
|
|
173
200
|
const actions: EnsureAction[] = [];
|
|
174
201
|
const plannedEnums = new Set<string>();
|
|
175
202
|
|
|
@@ -188,6 +215,34 @@ export function planCollectionSchemaEnsure(
|
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
// 1b. Search support, for collections that declared a `search` block.
|
|
219
|
+
//
|
|
220
|
+
// Before the tables, because a generated column's expression is
|
|
221
|
+
// resolved when the column is created: a table whose search column
|
|
222
|
+
// calls `rebase_search_text` cannot be added before that function
|
|
223
|
+
// exists. Both forms are idempotent, so a boot against a database that
|
|
224
|
+
// already has them plans nothing.
|
|
225
|
+
const searchSpecs = collections
|
|
226
|
+
.map(c => buildSearchColumnSpec(c))
|
|
227
|
+
.filter((spec): spec is SearchColumnSpec => spec !== undefined);
|
|
228
|
+
|
|
229
|
+
const plannedExtensions = new Set<string>();
|
|
230
|
+
for (const spec of searchSpecs) {
|
|
231
|
+
for (const statement of searchExtensionStatements(spec)) {
|
|
232
|
+
if (plannedExtensions.has(statement)) continue;
|
|
233
|
+
plannedExtensions.add(statement);
|
|
234
|
+
actions.push({ kind: "create-extension", target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""), sql: statement });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const plannedFunctions = new Set<string>();
|
|
238
|
+
for (const spec of searchSpecs) {
|
|
239
|
+
for (const statement of searchHelperFunctions(spec)) {
|
|
240
|
+
if (plannedFunctions.has(statement)) continue;
|
|
241
|
+
plannedFunctions.add(statement);
|
|
242
|
+
actions.push({ kind: "create-function", target: statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN, sql: statement });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
191
246
|
// 2. Missing tables. Only the identity column is created here; every other
|
|
192
247
|
// column is added by step 3, so a new table and an existing table that
|
|
193
248
|
// gained a field travel the exact same code path. One way to build a
|
|
@@ -204,17 +259,22 @@ export function planCollectionSchemaEnsure(
|
|
|
204
259
|
);
|
|
205
260
|
const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1] as Property) : "id";
|
|
206
261
|
const idProp = idEntry?.[1] as Property | undefined;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
262
|
+
// Derived through the generator's own type mapping, not re-decided here.
|
|
263
|
+
// This branch used to emit BIGSERIAL for a numeric id while `db push`
|
|
264
|
+
// emitted INTEGER GENERATED BY DEFAULT AS IDENTITY, so the same project
|
|
265
|
+
// got an int8 key when the runtime brought the schema up and an int4 key
|
|
266
|
+
// when a human pushed it. Two consequences, both real: node-postgres
|
|
267
|
+
// hands back int8 as a *string*, so a collection declaring
|
|
268
|
+
// `type: "number"` served `"1"` instead of `1` on the managed path only;
|
|
269
|
+
// and every foreign key and junction column pointing at it stayed
|
|
270
|
+
// INTEGER on both paths, which is a truncation waiting for the sequence
|
|
271
|
+
// to pass 2^31. The agreement test now pins this.
|
|
272
|
+
const idType = idProp
|
|
273
|
+
? getSqlColumnType(idEntry![0], idProp, collection, collections)
|
|
274
|
+
: "TEXT";
|
|
275
|
+
let idDef = `"${idName}" ${idType} PRIMARY KEY`;
|
|
276
|
+
if (idProp?.type === "string" && (idProp as { isId?: unknown }).isId === "uuid") {
|
|
277
|
+
idDef += " DEFAULT gen_random_uuid()";
|
|
218
278
|
}
|
|
219
279
|
actions.push({
|
|
220
280
|
kind: "create-table",
|
|
@@ -276,17 +336,14 @@ export function planCollectionSchemaEnsure(
|
|
|
276
336
|
schema: string,
|
|
277
337
|
table: string,
|
|
278
338
|
column: string,
|
|
279
|
-
|
|
339
|
+
definition: string
|
|
280
340
|
): void => {
|
|
281
341
|
const present = existing.tables.get(key);
|
|
282
342
|
if (present?.has(column)) return;
|
|
283
343
|
actions.push({
|
|
284
344
|
kind: "add-column",
|
|
285
345
|
target: `${key}.${column}`,
|
|
286
|
-
|
|
287
|
-
// non-null column without a default, and inventing one would be
|
|
288
|
-
// guessing at the customer's data.
|
|
289
|
-
sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${type};`
|
|
346
|
+
sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
|
|
290
347
|
});
|
|
291
348
|
};
|
|
292
349
|
|
|
@@ -294,6 +351,16 @@ export function planCollectionSchemaEnsure(
|
|
|
294
351
|
const key = qualified(collection);
|
|
295
352
|
const schema = schemaOf(collection);
|
|
296
353
|
const table = getTableName(collection);
|
|
354
|
+
// A table this run is creating has no rows yet, so the constraints
|
|
355
|
+
// `db push` writes are free to apply. On a table that already exists
|
|
356
|
+
// they are not: `SET NOT NULL` is checked against live rows and a UNIQUE
|
|
357
|
+
// would fail on existing duplicates, and this module runs unattended
|
|
358
|
+
// against customer data with nobody reading a diff. So the constraints
|
|
359
|
+
// are emitted for the fresh case — which is the whole managed-runtime
|
|
360
|
+
// path, and the one that diverged from `db push` — and withheld for the
|
|
361
|
+
// adopted one. `rebase db push` remains how an existing table gets them.
|
|
362
|
+
const fresh = created.has(key);
|
|
363
|
+
const auth = isAuthCollection(collection);
|
|
297
364
|
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
298
365
|
const p = prop as Property;
|
|
299
366
|
if (isIdProperty(propName, p, collection)) continue;
|
|
@@ -302,7 +369,68 @@ export function planCollectionSchemaEnsure(
|
|
|
302
369
|
// foreign key the same way `db push` does. Deriving them here as
|
|
303
370
|
// plain columns is what once produced a column with no constraint.
|
|
304
371
|
if (p.type === "reference" || p.type === "relation") continue;
|
|
305
|
-
|
|
372
|
+
|
|
373
|
+
const column = resolveColumnName(propName, p);
|
|
374
|
+
// On an auth collection, the columns auth itself reads and writes
|
|
375
|
+
// have exactly one definition, wherever the table is created from —
|
|
376
|
+
// see `auth-users-columns`. Anything else on that collection is an
|
|
377
|
+
// ordinary user-declared field and is generated like any other.
|
|
378
|
+
const authDefinition = auth ? authUsersColumnDefinition(column) : undefined;
|
|
379
|
+
if (authDefinition) {
|
|
380
|
+
addColumn(key, schema, table, column, authDefinition);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Assembled in the generator's order — type, UNIQUE, DEFAULT,
|
|
385
|
+
// NOT NULL — so the two produce byte-identical column definitions
|
|
386
|
+
// and the agreement test can compare them directly instead of
|
|
387
|
+
// checking that a column merely exists, which is how BIGSERIAL-vs-
|
|
388
|
+
// INTEGER and every missing constraint went unnoticed.
|
|
389
|
+
let definition = getSqlColumnType(propName, p, collection, collections);
|
|
390
|
+
if (fresh && p.validation?.unique) definition += " UNIQUE";
|
|
391
|
+
// Not gated on `fresh`: a default binds future writes only, so it is
|
|
392
|
+
// safe on a live table, and a column added without it would take the
|
|
393
|
+
// value the application forgot to send rather than `now()`.
|
|
394
|
+
const autoValue = (p as { autoValue?: string }).autoValue;
|
|
395
|
+
if (p.type === "date" && (autoValue === "on_create" || autoValue === "on_update")) {
|
|
396
|
+
definition += " DEFAULT now()";
|
|
397
|
+
}
|
|
398
|
+
if (fresh && p.validation?.required) definition += " NOT NULL";
|
|
399
|
+
addColumn(key, schema, table, column, definition);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// The auth columns the collection never mentions. The scaffold's users
|
|
403
|
+
// collection describes 12 of the 14 auth reads and writes, so planning
|
|
404
|
+
// only from properties left `is_anonymous` and `tokens_valid_after` to
|
|
405
|
+
// `ensureAuthTablesExist` — which does create them, but only because
|
|
406
|
+
// that function happens to run later in the same boot. Planning them
|
|
407
|
+
// here makes this path self-contained and identical to `db push`, so
|
|
408
|
+
// neither depends on the other having run.
|
|
409
|
+
if (auth) {
|
|
410
|
+
const declared = new Set(
|
|
411
|
+
Object.entries(collection.properties ?? {})
|
|
412
|
+
.map(([name, prop]) => resolveColumnName(name, prop as Property))
|
|
413
|
+
);
|
|
414
|
+
for (const spec of AUTH_USERS_COLUMNS) {
|
|
415
|
+
if (declared.has(spec.column)) continue;
|
|
416
|
+
addColumn(key, schema, table, spec.column, authUsersColumnSql(spec));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 3aa. The generated search columns.
|
|
422
|
+
//
|
|
423
|
+
// Adding a STORED generated column rewrites the table, which on a large
|
|
424
|
+
// one is not free — but it is the same additive shape as every other
|
|
425
|
+
// column here, and the alternative (leaving it out until someone runs a
|
|
426
|
+
// migration) is a declared `search` block that silently does nothing.
|
|
427
|
+
for (const spec of searchSpecs) {
|
|
428
|
+
const key = `${spec.schema}.${spec.table}`;
|
|
429
|
+
addColumn(key, spec.schema, spec.table, spec.column,
|
|
430
|
+
`tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`);
|
|
431
|
+
if (spec.fuzzy) {
|
|
432
|
+
addColumn(key, spec.schema, spec.table, spec.fuzzy.column,
|
|
433
|
+
`text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`);
|
|
306
434
|
}
|
|
307
435
|
}
|
|
308
436
|
|
|
@@ -353,6 +481,23 @@ export function planCollectionSchemaEnsure(
|
|
|
353
481
|
});
|
|
354
482
|
}
|
|
355
483
|
|
|
484
|
+
// 5. Search indexes, after everything — the column has to exist, and this is
|
|
485
|
+
// the one step that runs against a populated table for real work.
|
|
486
|
+
//
|
|
487
|
+
// CONCURRENTLY: a plain CREATE INDEX takes a lock that blocks writes for
|
|
488
|
+
// the duration of the build, which on a live table is an outage. Each
|
|
489
|
+
// statement here is issued on its own, outside any transaction, which is
|
|
490
|
+
// the condition CONCURRENTLY requires.
|
|
491
|
+
for (const spec of searchSpecs) {
|
|
492
|
+
for (const statement of searchIndexStatements(spec)) {
|
|
493
|
+
actions.push({
|
|
494
|
+
kind: "create-index",
|
|
495
|
+
target: `${spec.schema}.${spec.table}`,
|
|
496
|
+
sql: statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS")
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
356
501
|
return { actions, statements: actions.map(a => a.sql), legacyForeignKeys };
|
|
357
502
|
}
|
|
358
503
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/collection-helpers";
|
|
3
|
-
import {
|
|
3
|
+
import { buildSearchColumnSpec } from "./search-column";
|
|
4
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections } from "@rebasepro/common";
|
|
4
5
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
5
6
|
import { logger } from "@rebasepro/server";
|
|
6
7
|
// --- Helper Functions ---
|
|
@@ -451,7 +452,13 @@ const computeSharedRelationName = (
|
|
|
451
452
|
};
|
|
452
453
|
|
|
453
454
|
// --- Main Schema Generation Logic ---
|
|
454
|
-
export const generateSchema = async (
|
|
455
|
+
export const generateSchema = async (allCollections: CollectionConfig[], stripPolicies = false): Promise<string> => {
|
|
456
|
+
// A Firestore or MongoDB collection has no table to generate, and generating
|
|
457
|
+
// one for it is not merely wasted output: `db push` would create it, and
|
|
458
|
+
// `rebase doctor` would then report the store the collection actually reads
|
|
459
|
+
// from as drift. Non-SQL collections leave the toolchain here, once, rather
|
|
460
|
+
// than being filtered again in each stage below.
|
|
461
|
+
const collections = relationalCollections(allCollections);
|
|
455
462
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
456
463
|
|
|
457
464
|
|
|
@@ -473,11 +480,15 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
473
480
|
)
|
|
474
481
|
);
|
|
475
482
|
|
|
483
|
+
// drizzle ships no `tsvector` builder, so an opted-in search column reaches
|
|
484
|
+
// the schema the same way `bytea` does — through `customType`.
|
|
485
|
+
const hasSearch = collections.some(c => buildSearchColumnSpec(c) !== undefined);
|
|
486
|
+
|
|
476
487
|
// Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
|
|
477
488
|
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
478
489
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
479
490
|
if (hasVector) pgCoreImports.push("vector");
|
|
480
|
-
if (hasBinary) pgCoreImports.push("customType");
|
|
491
|
+
if (hasBinary || hasSearch) pgCoreImports.push("customType");
|
|
481
492
|
|
|
482
493
|
const uniqueSchemas = Array.from(new Set(
|
|
483
494
|
collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)
|
|
@@ -626,6 +637,25 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
626
637
|
|
|
627
638
|
});
|
|
628
639
|
|
|
640
|
+
// The opt-in search column. Declared with its real generation
|
|
641
|
+
// expression rather than as a bare custom type: `schema.generated.ts`
|
|
642
|
+
// is what a developer running drizzle-kit themselves diffs against,
|
|
643
|
+
// and a column declared without its expression reads to drizzle-kit
|
|
644
|
+
// as one it should alter.
|
|
645
|
+
const searchSpec = buildSearchColumnSpec(collection);
|
|
646
|
+
if (searchSpec) {
|
|
647
|
+
columns.add(
|
|
648
|
+
` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}")` +
|
|
649
|
+
`.generatedAlwaysAs(sql\`${searchSpec.expression}\`)`
|
|
650
|
+
);
|
|
651
|
+
if (searchSpec.fuzzy) {
|
|
652
|
+
columns.add(
|
|
653
|
+
` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}")` +
|
|
654
|
+
`.generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
629
659
|
// Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
|
|
630
660
|
// We should generate a basic id column if one was completely omitted.
|
|
631
661
|
const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
|