@rebasepro/server-postgres 0.13.1-canary.gef9608c → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +48 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +56 -0
- package/dist/cli-output.d.ts +34 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1720 -946
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +48 -0
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +24 -0
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +3 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +139 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +8 -7
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +124 -11
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +299 -168
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +129 -25
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +366 -30
- package/src/schema/generate-drizzle-schema-logic.ts +146 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +277 -10
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +14 -7
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +243 -22
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +3 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +347 -86
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
- package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
- package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-CU6WZGYV.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
|
@@ -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, fieldKeyForColumn, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections, sortCollectionsBySlug } from "@rebasepro/common";
|
|
4
5
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
5
6
|
import { logger } from "@rebasepro/server";
|
|
6
7
|
// --- Helper Functions ---
|
|
@@ -10,6 +11,31 @@ import { logger } from "@rebasepro/server";
|
|
|
10
11
|
* Uses the explicit `columnName` when set (e.g. from introspection),
|
|
11
12
|
* falling back to `toSnakeCase(propName)` for manually-authored collections.
|
|
12
13
|
*/
|
|
14
|
+
const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A string literal for the generated schema file.
|
|
18
|
+
*
|
|
19
|
+
* Column names, table names and enum values are all written into this file as
|
|
20
|
+
* literals, and none of them is constrained to be quote-free: a Postgres
|
|
21
|
+
* identifier only has to be quoted, and `O'Brien` is an ordinary enum value.
|
|
22
|
+
* Interpolating them raw ended the literal early — for enum values, inside
|
|
23
|
+
* single quotes, where an apostrophe is not an edge case.
|
|
24
|
+
*/
|
|
25
|
+
const quote = (value: string): string => JSON.stringify(value);
|
|
26
|
+
|
|
27
|
+
/** An object key: verbatim when it is an identifier, quoted otherwise. */
|
|
28
|
+
const propKey = (name: string): string => (JS_IDENTIFIER.test(name) ? name : quote(name));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A property access on a generated table variable.
|
|
32
|
+
*
|
|
33
|
+
* `users.full name` is not an expression; `users["full name"]` is, and Drizzle
|
|
34
|
+
* treats the two identically.
|
|
35
|
+
*/
|
|
36
|
+
const member = (object: string, key: string): string =>
|
|
37
|
+
(JS_IDENTIFIER.test(key) ? `${object}.${key}` : `${object}[${quote(key)}]`);
|
|
38
|
+
|
|
13
39
|
const resolveColumnName = (propName: string, prop?: Property | null): string => {
|
|
14
40
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
15
41
|
return prop.columnName;
|
|
@@ -42,29 +68,17 @@ isUuid: isUuid ?? false };
|
|
|
42
68
|
};
|
|
43
69
|
|
|
44
70
|
/**
|
|
45
|
-
* Given a raw DB column name (e.g. "client_id"),
|
|
46
|
-
*
|
|
47
|
-
* (a) it has an explicit `columnName` equal to the given column, OR
|
|
48
|
-
* (b) its snake_case form equals the given column.
|
|
71
|
+
* Given a raw DB column name (e.g. "client_id"), the Drizzle property key that
|
|
72
|
+
* maps to it.
|
|
49
73
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
74
|
+
* One line, because the rule is shared: the Drizzle object key is the wire
|
|
75
|
+
* name, and {@link fieldKeyForColumn} is the one definition of what a column is
|
|
76
|
+
* named on the wire. This used to be a private copy that fell back to the
|
|
77
|
+
* column verbatim, which is how a derived foreign key ended up served as
|
|
78
|
+
* `author_id` beside a hand-authored `displayName`.
|
|
52
79
|
*/
|
|
53
|
-
const resolvePropertyKeyForColumn = (collection: CollectionConfig, column: string): string =>
|
|
54
|
-
|
|
55
|
-
for (const [propKey, prop] of Object.entries(collection.properties)) {
|
|
56
|
-
const p = prop as Property;
|
|
57
|
-
// Explicit columnName match
|
|
58
|
-
if ("columnName" in p && typeof (p as unknown as Record<string, unknown>).columnName === "string") {
|
|
59
|
-
if ((p as unknown as Record<string, unknown>).columnName === column) return propKey;
|
|
60
|
-
}
|
|
61
|
-
// Convention match: snake_case(propKey) === column
|
|
62
|
-
if (toSnakeCase(propKey) === column) return propKey;
|
|
63
|
-
// Exact match (propKey is already the column name)
|
|
64
|
-
if (propKey === column) return propKey;
|
|
65
|
-
}
|
|
66
|
-
return column;
|
|
67
|
-
};
|
|
80
|
+
const resolvePropertyKeyForColumn = (collection: CollectionConfig, column: string): string =>
|
|
81
|
+
fieldKeyForColumn(collection, column);
|
|
68
82
|
|
|
69
83
|
const isNumericId = (collection: CollectionConfig): boolean => {
|
|
70
84
|
return getPrimaryKeyProp(collection).type === "number";
|
|
@@ -82,7 +96,14 @@ const isIdProperty = (propName: string, prop: Property, collection: CollectionCo
|
|
|
82
96
|
return !hasExplicitId && propName === "id";
|
|
83
97
|
};
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The Drizzle column declaration a property compiles to, or `null` when the
|
|
101
|
+
* property puts no column on *this* table (an inverse relation, whose column
|
|
102
|
+
* lives on the target). Exported so it can be checked against its DDL twin
|
|
103
|
+
* `getSqlColumnType` directly — the two disagreeing is what left `geopoint`
|
|
104
|
+
* with a database column and no Drizzle key.
|
|
105
|
+
*/
|
|
106
|
+
export const getDrizzleColumn = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string | null => {
|
|
86
107
|
|
|
87
108
|
const colName = resolveColumnName(propName, prop);
|
|
88
109
|
let columnDefinition: string;
|
|
@@ -92,23 +113,23 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
92
113
|
const stringProp = prop as unknown as StringProperty;
|
|
93
114
|
if (stringProp.enum) {
|
|
94
115
|
const enumName = getEnumVarName(getTableName(collection), propName);
|
|
95
|
-
columnDefinition = `${enumName}(
|
|
116
|
+
columnDefinition = `${enumName}(${quote(colName)})`;
|
|
96
117
|
} else if ("isId" in stringProp && stringProp.isId === "uuid") {
|
|
97
|
-
columnDefinition = `uuid(
|
|
118
|
+
columnDefinition = `uuid(${quote(colName)})`;
|
|
98
119
|
} else if (stringProp.columnType === "uuid") {
|
|
99
|
-
columnDefinition = `uuid(
|
|
120
|
+
columnDefinition = `uuid(${quote(colName)})`;
|
|
100
121
|
} else if (stringProp.columnType === "char") {
|
|
101
|
-
columnDefinition = `char(
|
|
122
|
+
columnDefinition = `char(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
102
123
|
} else if (stringProp.columnType === "varchar") {
|
|
103
124
|
// The length is not optional decoration: `varchar("col")` with
|
|
104
125
|
// no length is an UNBOUNDED varchar in Postgres, which is what
|
|
105
126
|
// this emitted while the DDL generator emitted VARCHAR(255) for
|
|
106
127
|
// the very same property.
|
|
107
|
-
columnDefinition = `varchar(
|
|
128
|
+
columnDefinition = `varchar(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
108
129
|
} else {
|
|
109
130
|
// `text` is the default, and the only length-unbounded choice.
|
|
110
131
|
// Ask for `varchar` explicitly if you want the length constraint.
|
|
111
|
-
columnDefinition = `text(
|
|
132
|
+
columnDefinition = `text(${quote(colName)})`;
|
|
112
133
|
}
|
|
113
134
|
if (isIdProperty(propName, prop, collection)) {
|
|
114
135
|
columnDefinition += ".primaryKey()";
|
|
@@ -134,9 +155,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
134
155
|
const numProp = prop as unknown as NumberProperty;
|
|
135
156
|
const isId = isIdProperty(propName, prop, collection);
|
|
136
157
|
|
|
137
|
-
let baseType = (numProp.validation?.integer || isId) ? `integer(
|
|
158
|
+
let baseType = (numProp.validation?.integer || isId) ? `integer(${quote(colName)})` : `numeric(${quote(colName)})`;
|
|
138
159
|
if (numProp.columnType) {
|
|
139
|
-
if (numProp.columnType === "double precision") baseType = `doublePrecision(
|
|
160
|
+
if (numProp.columnType === "double precision") baseType = `doublePrecision(${quote(colName)})`;
|
|
140
161
|
// `bigint` and `bigserial` are the only pg-core builders that
|
|
141
162
|
// *require* a config argument: without `mode`, drizzle cannot
|
|
142
163
|
// know whether to hand back a `number` or a `bigint`, and the
|
|
@@ -154,9 +175,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
154
175
|
// switching the runtime type would be a breaking change to
|
|
155
176
|
// every consumer of the generated schema.
|
|
156
177
|
else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") {
|
|
157
|
-
baseType = `${numProp.columnType}(
|
|
178
|
+
baseType = `${numProp.columnType}(${quote(colName)}, { mode: "number" })`;
|
|
158
179
|
}
|
|
159
|
-
else baseType = `${numProp.columnType}(
|
|
180
|
+
else baseType = `${numProp.columnType}(${quote(colName)})`;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
if ("isId" in numProp && numProp.isId === "increment") {
|
|
@@ -180,16 +201,16 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
180
201
|
break;
|
|
181
202
|
}
|
|
182
203
|
case "boolean":
|
|
183
|
-
columnDefinition = `boolean(
|
|
204
|
+
columnDefinition = `boolean(${quote(colName)})`;
|
|
184
205
|
break;
|
|
185
206
|
case "date": {
|
|
186
207
|
const dateProp = prop as DateProperty;
|
|
187
208
|
if (dateProp.columnType === "date") {
|
|
188
|
-
columnDefinition = `date(
|
|
209
|
+
columnDefinition = `date(${quote(colName)}, { mode: 'string' })`;
|
|
189
210
|
} else if (dateProp.columnType === "time") {
|
|
190
|
-
columnDefinition = `time(
|
|
211
|
+
columnDefinition = `time(${quote(colName)})`;
|
|
191
212
|
} else {
|
|
192
|
-
columnDefinition = `timestamp(
|
|
213
|
+
columnDefinition = `timestamp(${quote(colName)}, { withTimezone: true, mode: 'string' })`;
|
|
193
214
|
}
|
|
194
215
|
// autoValue: database-level default for initial value on INSERT
|
|
195
216
|
if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
|
|
@@ -200,12 +221,22 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
200
221
|
case "map": {
|
|
201
222
|
const mapProp = prop as MapProperty;
|
|
202
223
|
if (mapProp.columnType === "json") {
|
|
203
|
-
columnDefinition = `json(
|
|
224
|
+
columnDefinition = `json(${quote(colName)})`;
|
|
204
225
|
} else {
|
|
205
|
-
columnDefinition = `jsonb(
|
|
226
|
+
columnDefinition = `jsonb(${quote(colName)})`;
|
|
206
227
|
}
|
|
207
228
|
break;
|
|
208
229
|
}
|
|
230
|
+
case "geopoint": {
|
|
231
|
+
// `{ latitude, longitude }`, which is what the OpenAPI schema, the
|
|
232
|
+
// generated TS type and the admin's field binding all describe.
|
|
233
|
+
// This arm did not exist: `geopoint` fell to `default: return null`
|
|
234
|
+
// and the caller dropped the column, so the DDL generator created a
|
|
235
|
+
// column the Drizzle table had no key for — and every write to it
|
|
236
|
+
// was silently discarded with a 201.
|
|
237
|
+
columnDefinition = `jsonb(${quote(colName)})`;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
209
240
|
case "array": {
|
|
210
241
|
const arrayProp = prop as ArrayProperty;
|
|
211
242
|
let colType = arrayProp.columnType;
|
|
@@ -221,27 +252,27 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
if (colType === "json") {
|
|
224
|
-
columnDefinition = `json(
|
|
255
|
+
columnDefinition = `json(${quote(colName)})`;
|
|
225
256
|
} else if (colType === "text[]") {
|
|
226
|
-
columnDefinition = `text(
|
|
257
|
+
columnDefinition = `text(${quote(colName)}).array()`;
|
|
227
258
|
} else if (colType === "integer[]") {
|
|
228
|
-
columnDefinition = `integer(
|
|
259
|
+
columnDefinition = `integer(${quote(colName)}).array()`;
|
|
229
260
|
} else if (colType === "boolean[]") {
|
|
230
|
-
columnDefinition = `boolean(
|
|
261
|
+
columnDefinition = `boolean(${quote(colName)}).array()`;
|
|
231
262
|
} else if (colType === "numeric[]") {
|
|
232
|
-
columnDefinition = `numeric(
|
|
263
|
+
columnDefinition = `numeric(${quote(colName)}).array()`;
|
|
233
264
|
} else {
|
|
234
|
-
columnDefinition = `jsonb(
|
|
265
|
+
columnDefinition = `jsonb(${quote(colName)})`;
|
|
235
266
|
}
|
|
236
267
|
break;
|
|
237
268
|
}
|
|
238
269
|
case "vector": {
|
|
239
270
|
const vp = prop as VectorProperty;
|
|
240
|
-
columnDefinition = `vector(
|
|
271
|
+
columnDefinition = `vector(${quote(colName)}, { dimensions: ${vp.dimensions} })`;
|
|
241
272
|
break;
|
|
242
273
|
}
|
|
243
274
|
case "binary": {
|
|
244
|
-
columnDefinition = `customType({ dataType() { return 'bytea'; } })(
|
|
275
|
+
columnDefinition = `customType({ dataType() { return 'bytea'; } })(${quote(colName)})`;
|
|
245
276
|
break;
|
|
246
277
|
}
|
|
247
278
|
case "relation": {
|
|
@@ -255,9 +286,14 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
255
286
|
return null;
|
|
256
287
|
}
|
|
257
288
|
|
|
258
|
-
// If
|
|
259
|
-
//
|
|
260
|
-
|
|
289
|
+
// If a property of its own already declares this foreign key, that
|
|
290
|
+
// property emits the column and this relation must not emit a second
|
|
291
|
+
// key for it. Asked of the *field key*, not the column: a property
|
|
292
|
+
// declared `authorId` with `columnName: "author_id"` did not answer
|
|
293
|
+
// to `properties["author_id"]`, so both sides emitted and the table
|
|
294
|
+
// carried two Drizzle keys pointing at one column.
|
|
295
|
+
const fkFieldKey = fieldKeyForColumn(collection, relation.localKey);
|
|
296
|
+
if (collection.properties[fkFieldKey] && propName !== fkFieldKey) {
|
|
261
297
|
return null;
|
|
262
298
|
}
|
|
263
299
|
|
|
@@ -282,47 +318,63 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
282
318
|
const refOptionsParts = [onUpdate, onDelete].filter(Boolean);
|
|
283
319
|
const refOptions = refOptionsParts.length > 0 ? `{ ${refOptionsParts.join(", ")} }` : "";
|
|
284
320
|
|
|
285
|
-
let columnDef = `${baseColumn}.references(() => ${targetTableVar
|
|
321
|
+
let columnDef = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}${refOptions ? `, ${refOptions}` : ""})`;
|
|
286
322
|
|
|
287
323
|
if (required) {
|
|
288
324
|
columnDef += ".notNull()";
|
|
289
325
|
}
|
|
290
326
|
|
|
291
|
-
|
|
327
|
+
// Key by the wire name, column by `localKey`. They are two different
|
|
328
|
+
// names for one thing and the generated line is where they meet:
|
|
329
|
+
// `authorId: integer("author_id")`.
|
|
330
|
+
return ` ${propKey(fkFieldKey)}: ${columnDef}`;
|
|
292
331
|
}
|
|
293
332
|
case "reference": {
|
|
294
333
|
const refProp = prop as ReferenceProperty;
|
|
295
334
|
const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
296
335
|
if (!targetCollection) {
|
|
297
|
-
columnDefinition = `text(
|
|
336
|
+
columnDefinition = `text(${quote(colName)})`;
|
|
298
337
|
break;
|
|
299
338
|
}
|
|
300
339
|
|
|
301
340
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
302
341
|
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
303
342
|
const targetIdField = pkProp.name;
|
|
304
|
-
const baseColumn = pkProp.type === "number" ? `integer(
|
|
343
|
+
const baseColumn = pkProp.type === "number" ? `integer(${quote(colName)})` : (pkProp.isUuid ? `uuid(${quote(colName)})` : `text(${quote(colName)})`);
|
|
305
344
|
|
|
306
345
|
const required = prop.validation?.required;
|
|
307
346
|
const onDelete = required ? "cascade" : "set null";
|
|
308
347
|
const refOptions = `{ onDelete: "${onDelete}" }`;
|
|
309
348
|
|
|
310
|
-
columnDefinition = `${baseColumn}.references(() => ${targetTableVar
|
|
349
|
+
columnDefinition = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}, ${refOptions})`;
|
|
311
350
|
if (required) {
|
|
312
351
|
columnDefinition += ".notNull()";
|
|
313
352
|
}
|
|
314
353
|
// Skip the standard notNull() handling below because we did it here with references
|
|
315
|
-
return ` ${propName}: ${columnDefinition}`;
|
|
354
|
+
return ` ${propKey(propName)}: ${columnDefinition}`;
|
|
316
355
|
}
|
|
317
356
|
default:
|
|
318
|
-
return null
|
|
357
|
+
// Not `return null`. A `null` here means "this property puts no
|
|
358
|
+
// column on this table", which is true of an inverse relation and
|
|
359
|
+
// false of everything else — and the caller cannot tell the two
|
|
360
|
+
// apart, so a type this switch simply forgot produced a table
|
|
361
|
+
// missing a column while the DDL generator happily created one.
|
|
362
|
+
// That is how `geopoint` stayed unpersistable: writes to it were
|
|
363
|
+
// dropped before the SQL was built, forever, with a 201. A property
|
|
364
|
+
// type nobody mapped is a generator bug, and it says so here rather
|
|
365
|
+
// than at some caller's next INSERT.
|
|
366
|
+
throw new Error(
|
|
367
|
+
`No Postgres column mapping for property '${propName}' of type ` +
|
|
368
|
+
`'${(prop as Property).type}' in collection '${collection.slug}'. ` +
|
|
369
|
+
"Add a case to `getDrizzleColumn` (and to `getSqlColumnType`, which must agree)."
|
|
370
|
+
);
|
|
319
371
|
}
|
|
320
372
|
|
|
321
373
|
if (prop.validation?.required) {
|
|
322
374
|
columnDefinition += ".notNull()";
|
|
323
375
|
}
|
|
324
376
|
|
|
325
|
-
return ` ${propName}: ${columnDefinition}`;
|
|
377
|
+
return ` ${propKey(propName)}: ${columnDefinition}`;
|
|
326
378
|
};
|
|
327
379
|
|
|
328
380
|
/**
|
|
@@ -399,7 +451,7 @@ const generateSinglePolicyCode = (collection: CollectionConfig, rule: SecurityRu
|
|
|
399
451
|
if (usingClause) parts.push(`using: ${usingClause}`);
|
|
400
452
|
if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
|
|
401
453
|
|
|
402
|
-
return ` pgPolicy(
|
|
454
|
+
return ` pgPolicy(${quote(policyName)}, { ${parts.join(", ")} }),\n`;
|
|
403
455
|
};
|
|
404
456
|
|
|
405
457
|
/**
|
|
@@ -457,7 +509,12 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
457
509
|
// `rebase doctor` would then report the store the collection actually reads
|
|
458
510
|
// from as drift. Non-SQL collections leave the toolchain here, once, rather
|
|
459
511
|
// than being filtered again in each stage below.
|
|
460
|
-
|
|
512
|
+
//
|
|
513
|
+
// Sorted here rather than in the writer: the output is order-dependent and
|
|
514
|
+
// `rebase doctor` regenerates it in memory to diff against the file on
|
|
515
|
+
// disk. With the sort in the writer only, a project whose file order
|
|
516
|
+
// differed from its slug order was reported stale forever.
|
|
517
|
+
const collections = sortCollectionsBySlug(relationalCollections(allCollections));
|
|
461
518
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
462
519
|
|
|
463
520
|
|
|
@@ -479,11 +536,15 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
479
536
|
)
|
|
480
537
|
);
|
|
481
538
|
|
|
539
|
+
// drizzle ships no `tsvector` builder, so an opted-in search column reaches
|
|
540
|
+
// the schema the same way `bytea` does — through `customType`.
|
|
541
|
+
const hasSearch = collections.some(c => buildSearchColumnSpec(c) !== undefined);
|
|
542
|
+
|
|
482
543
|
// Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
|
|
483
544
|
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
484
545
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
485
546
|
if (hasVector) pgCoreImports.push("vector");
|
|
486
|
-
if (hasBinary) pgCoreImports.push("customType");
|
|
547
|
+
if (hasBinary || hasSearch) pgCoreImports.push("customType");
|
|
487
548
|
|
|
488
549
|
const uniqueSchemas = Array.from(new Set(
|
|
489
550
|
collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)
|
|
@@ -527,7 +588,7 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
527
588
|
)
|
|
528
589
|
: Object.keys(prop.enum);
|
|
529
590
|
if (values.length > 0) {
|
|
530
|
-
schemaContent += `export const ${enumVarName} = pgEnum(
|
|
591
|
+
schemaContent += `export const ${enumVarName} = pgEnum(${quote(enumDbName)}, [${values.map(v => quote(v)).join(", ")}]);\n`;
|
|
531
592
|
if (!exportedEnumVars.includes(enumVarName)) exportedEnumVars.push(enumVarName);
|
|
532
593
|
}
|
|
533
594
|
}
|
|
@@ -632,6 +693,25 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
632
693
|
|
|
633
694
|
});
|
|
634
695
|
|
|
696
|
+
// The opt-in search column. Declared with its real generation
|
|
697
|
+
// expression rather than as a bare custom type: `schema.generated.ts`
|
|
698
|
+
// is what a developer running drizzle-kit themselves diffs against,
|
|
699
|
+
// and a column declared without its expression reads to drizzle-kit
|
|
700
|
+
// as one it should alter.
|
|
701
|
+
const searchSpec = buildSearchColumnSpec(collection);
|
|
702
|
+
if (searchSpec) {
|
|
703
|
+
columns.add(
|
|
704
|
+
` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}")` +
|
|
705
|
+
`.generatedAlwaysAs(sql\`${searchSpec.expression}\`)`
|
|
706
|
+
);
|
|
707
|
+
if (searchSpec.fuzzy) {
|
|
708
|
+
columns.add(
|
|
709
|
+
` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}")` +
|
|
710
|
+
`.generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
635
715
|
// Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
|
|
636
716
|
// We should generate a basic id column if one was completely omitted.
|
|
637
717
|
const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
|
|
@@ -704,7 +784,7 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
704
784
|
}
|
|
705
785
|
|
|
706
786
|
// Source side one(): pairs with owning table's many(junctionTable, { relationName })
|
|
707
|
-
tableRelations.push(`
|
|
787
|
+
tableRelations.push(` ${quote(relation.through.sourceColumn)}: one(${sourceTableVar}, {\n fields: [${member(tableVarName, relation.through.sourceColumn)}],\n references: [${member(sourceTableVar, sourceId)}],\n relationName: ${quote(owningRelationName)}\n })`);
|
|
708
788
|
|
|
709
789
|
// Target side one(): pairs with inverse table's many(junctionTable, { relationName })
|
|
710
790
|
// Always emit a relationName to avoid collisions with the source-side's owningRelationName.
|
|
@@ -712,7 +792,7 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
712
792
|
const targetRelationName = inverseRelationName
|
|
713
793
|
? inverseRelationName
|
|
714
794
|
: `${tableName}_${relation.through.targetColumn}`;
|
|
715
|
-
tableRelations.push(`
|
|
795
|
+
tableRelations.push(` ${quote(relation.through.targetColumn)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, relation.through.targetColumn)}],\n references: [${member(targetTableVar, targetId)}],\n relationName: ${quote(targetRelationName)}\n })`);
|
|
716
796
|
}
|
|
717
797
|
} else {
|
|
718
798
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -752,7 +832,7 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
752
832
|
// schema that does not compile. The three other
|
|
753
833
|
// emission sites normalise; this one did not.
|
|
754
834
|
const localFieldKey = resolvePropertyKeyForColumn(collection, rel.localKey);
|
|
755
|
-
tableRelations.push(`
|
|
835
|
+
tableRelations.push(` ${quote(relationKey)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, localFieldKey)}],\n references: [${member(targetTableVar, getPrimaryKeyName(target))}],\n relationName: ${quote(drizzleRelationName)}\n })`);
|
|
756
836
|
break;
|
|
757
837
|
}
|
|
758
838
|
|
|
@@ -817,7 +897,7 @@ export const generateSchema = async (allCollections: CollectionConfig[], stripPo
|
|
|
817
897
|
? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey)
|
|
818
898
|
: getPrimaryKeyName(otherCollection);
|
|
819
899
|
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
820
|
-
tableRelations.push(`
|
|
900
|
+
tableRelations.push(` ${quote(synthKey)}: one(${otherTableVar}, {\n fields: [${member(tableVarName, drizzleFieldKey)}],\n references: [${member(otherTableVar, referencedKey)}],\n relationName: ${quote(drizzleRelationName)}\n })`);
|
|
821
901
|
emittedRelationNames.add(deduplicationKey);
|
|
822
902
|
}
|
|
823
903
|
}
|
|
@@ -5,7 +5,8 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { CollectionConfig } from "@rebasepro/types";
|
|
8
|
-
import {
|
|
8
|
+
import { loadCollectionsFromDirectory } from "@rebasepro/server";
|
|
9
|
+
import { out, outError } from "../cli-output";
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
// --- Helper Functions ---
|
|
@@ -49,7 +50,7 @@ const formatTerminalText = (text: string, options: {
|
|
|
49
50
|
const runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {
|
|
50
51
|
try {
|
|
51
52
|
if (!collectionsFilePath) {
|
|
52
|
-
|
|
53
|
+
outError("Error: No collections file path provided. Skipping schema generation.");
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -75,20 +76,20 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
75
76
|
const outputDir = path.dirname(outputPath);
|
|
76
77
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
77
78
|
await fsPromises.writeFile(outputPath, schemaContent);
|
|
78
|
-
|
|
79
|
+
out(`✅ Drizzle schema generated successfully at ${outputPath}`);
|
|
79
80
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
out("✅ Drizzle schema generated successfully.");
|
|
82
|
+
out(String(schemaContent));
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
out(`You can now run ${formatTerminalText("rebase db generate", {
|
|
85
86
|
bold: true,
|
|
86
87
|
backgroundColor: "blue",
|
|
87
88
|
textColor: "black"
|
|
88
89
|
})} to generate the SQL migration files.`);
|
|
89
90
|
|
|
90
91
|
} catch (error) {
|
|
91
|
-
|
|
92
|
+
outError(`Error generating schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
|
|
@@ -102,7 +103,7 @@ const main = () => {
|
|
|
102
103
|
const watch = process.argv.includes("--watch");
|
|
103
104
|
|
|
104
105
|
if (!collectionsFilePath) {
|
|
105
|
-
|
|
106
|
+
out("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -110,14 +111,14 @@ const main = () => {
|
|
|
110
111
|
const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;
|
|
111
112
|
|
|
112
113
|
if (watch) {
|
|
113
|
-
|
|
114
|
+
out(`Watching for changes in ${resolvedPath}...`);
|
|
114
115
|
const watcher = chokidar.watch(resolvedPath, {
|
|
115
116
|
persistent: true,
|
|
116
117
|
ignoreInitial: false
|
|
117
118
|
});
|
|
118
119
|
|
|
119
120
|
watcher.on("all", (event, filePath) => {
|
|
120
|
-
|
|
121
|
+
out(`[${event}] ${filePath}. Regenerating schema...`);
|
|
121
122
|
runGeneration(resolvedPath, resolvedOutputPath);
|
|
122
123
|
});
|
|
123
124
|
} else {
|