@rebasepro/server-postgres 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed78a2c
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/auth/schema-version.d.ts +106 -0
- package/dist/collections/validate-relations.d.ts +53 -0
- package/dist/data-transformer.d.ts +3 -3
- package/dist/{ensure-collection-tables-CNlIONzj.js → ensure-collection-tables-DGMYK0fr.js} +12 -12
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
- package/dist/index.es.js +913 -391
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +39 -8
- package/dist/services/cdc/junction-tables.d.ts +38 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/services/realtimeService.d.ts +19 -0
- package/dist/services/row-pipeline.d.ts +2 -2
- package/dist/{src-DmsRg8MR.js → src-3VmUJ8Xn.js} +214 -276
- package/dist/src-3VmUJ8Xn.js.map +1 -0
- package/dist/{src-B0v4IKaI.js → src-D5xBTl32.js} +19 -2
- package/dist/src-D5xBTl32.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +71 -18
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +15 -2
- package/src/auth/ensure-tables.ts +23 -0
- package/src/auth/schema-version.ts +260 -0
- package/src/cli-errors.ts +1 -1
- package/src/cli-helpers.ts +4 -3
- package/src/collections/PostgresCollectionRegistry.ts +9 -4
- package/src/collections/buildRegistry.ts +7 -0
- package/src/collections/validate-relations.ts +280 -0
- package/src/data-transformer.ts +28 -38
- package/src/schema/doctor.ts +14 -14
- package/src/schema/generate-drizzle-schema-logic.ts +62 -110
- package/src/schema/generate-postgres-ddl-logic.ts +28 -21
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +25 -29
- package/src/services/FetchService.ts +116 -126
- package/src/services/PersistService.ts +126 -88
- package/src/services/RelationService.ts +157 -86
- package/src/services/cdc/junction-tables.ts +91 -0
- package/src/services/nested-path.ts +145 -0
- package/src/services/realtimeService.ts +60 -0
- package/src/services/row-pipeline.ts +5 -6
- package/src/utils/drizzle-conditions.ts +268 -330
- package/dist/ensure-collection-tables-CNlIONzj.js.map +0 -1
- package/dist/src-B0v4IKaI.js.map +0 -1
- package/dist/src-DmsRg8MR.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CollectionConfig, NumberProperty, Property,
|
|
1
|
+
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from "@rebasepro/types";
|
|
2
2
|
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
4
4
|
|
|
@@ -103,13 +103,18 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
103
103
|
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
|
|
104
104
|
return "UUID";
|
|
105
105
|
}
|
|
106
|
-
if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
|
|
107
|
-
return "TEXT";
|
|
108
|
-
}
|
|
109
106
|
if (stringProp.columnType === "char") {
|
|
110
107
|
return "CHAR(255)";
|
|
111
108
|
}
|
|
112
|
-
|
|
109
|
+
if (stringProp.columnType === "varchar") {
|
|
110
|
+
return "VARCHAR(255)";
|
|
111
|
+
}
|
|
112
|
+
// `text` is the default. The two generators disagreed here before:
|
|
113
|
+
// this one emitted VARCHAR(255) while the drizzle path emitted a bare
|
|
114
|
+
// `varchar()`, which Postgres treats as unbounded — so the same
|
|
115
|
+
// property produced a capped column down one path and an uncapped one
|
|
116
|
+
// down the other.
|
|
117
|
+
return "TEXT";
|
|
113
118
|
}
|
|
114
119
|
case "number": {
|
|
115
120
|
const numProp = prop as NumberProperty;
|
|
@@ -165,28 +170,28 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
165
170
|
case "relation": {
|
|
166
171
|
const refProp = prop as RelationProperty;
|
|
167
172
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
168
|
-
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
169
|
-
if (
|
|
170
|
-
throw new Error(`Relation ${propName}
|
|
173
|
+
const relation = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
174
|
+
if (relation?.kind !== "belongsTo") {
|
|
175
|
+
throw new Error(`Relation ${propName} does not put a column on this table (only \`belongsTo\` does)`);
|
|
171
176
|
}
|
|
172
177
|
let targetCollection: CollectionConfig;
|
|
173
178
|
try {
|
|
174
179
|
targetCollection = relation.target();
|
|
175
180
|
} catch {
|
|
176
|
-
return "
|
|
181
|
+
return "TEXT";
|
|
177
182
|
}
|
|
178
183
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
179
|
-
return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "
|
|
184
|
+
return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "TEXT");
|
|
180
185
|
}
|
|
181
186
|
case "reference": {
|
|
182
187
|
const refProp = prop as ReferenceProperty;
|
|
183
188
|
const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
184
|
-
if (!targetCollection) return "
|
|
189
|
+
if (!targetCollection) return "TEXT";
|
|
185
190
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
186
|
-
return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "
|
|
191
|
+
return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "TEXT");
|
|
187
192
|
}
|
|
188
193
|
default:
|
|
189
|
-
return "
|
|
194
|
+
return "TEXT";
|
|
190
195
|
}
|
|
191
196
|
};
|
|
192
197
|
|
|
@@ -233,7 +238,7 @@ export const generatePostgresDdl = async (
|
|
|
233
238
|
const allTablesToGenerate = new Map<string, {
|
|
234
239
|
collection: CollectionConfig,
|
|
235
240
|
isJunction?: boolean,
|
|
236
|
-
relation?:
|
|
241
|
+
relation?: ResolvedRelation,
|
|
237
242
|
sourceCollection?: CollectionConfig
|
|
238
243
|
}>();
|
|
239
244
|
|
|
@@ -246,7 +251,7 @@ export const generatePostgresDdl = async (
|
|
|
246
251
|
|
|
247
252
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
248
253
|
for (const relation of Object.values(resolvedRelations)) {
|
|
249
|
-
if (relation
|
|
254
|
+
if (isManyToMany(relation)) {
|
|
250
255
|
const junctionTableName = relation.through.table;
|
|
251
256
|
if (!allTablesToGenerate.has(junctionTableName)) {
|
|
252
257
|
allTablesToGenerate.set(junctionTableName, {
|
|
@@ -279,7 +284,7 @@ export const generatePostgresDdl = async (
|
|
|
279
284
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
280
285
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
281
286
|
|
|
282
|
-
if (isJunction && relation && sourceCollection && relation
|
|
287
|
+
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
283
288
|
const targetCollection = relation.target();
|
|
284
289
|
const sourceTable = getTableName(sourceCollection);
|
|
285
290
|
const targetTable = getTableName(targetCollection);
|
|
@@ -287,8 +292,10 @@ export const generatePostgresDdl = async (
|
|
|
287
292
|
const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
|
|
288
293
|
const { sourceColumn, targetColumn } = relation.through;
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
// TEXT, matching the string default: a junction column has to have the
|
|
296
|
+
// same type as the primary key it references.
|
|
297
|
+
const sourceColType = isNumericId(sourceCollection) ? "INTEGER" : (getPrimaryKeyProp(sourceCollection).isUuid ? "UUID" : "TEXT");
|
|
298
|
+
const targetColType = isNumericId(targetCollection) ? "INTEGER" : (getPrimaryKeyProp(targetCollection).isUuid ? "UUID" : "TEXT");
|
|
292
299
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
293
300
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
294
301
|
|
|
@@ -329,9 +336,9 @@ export const generatePostgresDdl = async (
|
|
|
329
336
|
if (prop.type === "relation") {
|
|
330
337
|
const refProp = prop as RelationProperty;
|
|
331
338
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
332
|
-
const relInfo = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
339
|
+
const relInfo = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
333
340
|
|
|
334
|
-
if (
|
|
341
|
+
if (relInfo?.kind !== "belongsTo") {
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
|
|
@@ -424,7 +431,7 @@ export const generatePostgresDdl = async (
|
|
|
424
431
|
// Backwards compatibility: add default id primary key if missing
|
|
425
432
|
const hasPk = columns.some(c => c.includes("PRIMARY KEY"));
|
|
426
433
|
if (!hasPk) {
|
|
427
|
-
columns.unshift(' "id"
|
|
434
|
+
columns.unshift(' "id" TEXT PRIMARY KEY');
|
|
428
435
|
}
|
|
429
436
|
|
|
430
437
|
ddl += columns.join(",\n");
|
|
@@ -51,10 +51,11 @@ export function inferPropertyFromData(
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// No currency heuristic. `admin.currency` was never a declared option and
|
|
55
|
+
// nothing reads it, so a column called `price` got a block of config that did
|
|
56
|
+
// nothing — invisible because this generator emits *source text*, which no
|
|
57
|
+
// typechecker ever saw. Reinstate it by adding `currency` to
|
|
58
|
+
// AdminNumberOptions first, and something that renders it.
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// ── JSON / JSONB Analysis ────────────────────────────────────────────
|
|
@@ -169,11 +170,10 @@ export function inferPropertyFromData(
|
|
|
169
170
|
if (isPk) extraLines.push(" isId: \"cuid\"");
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
173
|
+
// No colour heuristic. `admin.color` was never a declared option and nothing
|
|
174
|
+
// reads it — the same dead branch `currency` was. This generator emits *source
|
|
175
|
+
// text*, so no typechecker ever saw either. Reinstate by adding the option and
|
|
176
|
+
// something that renders it first.
|
|
177
177
|
|
|
178
178
|
// Text Lengths, Multiline & Markdown
|
|
179
179
|
let maxLength = 0;
|
|
@@ -212,9 +212,9 @@ export function inferPropertyFromData(
|
|
|
212
212
|
if (allAbsoluteUrls) {
|
|
213
213
|
const isImage = validValues.some(v => typeof v === "string" && v.match(/\.(jpeg|jpg|gif|png|webp|svg)/i));
|
|
214
214
|
if (isImage || isMedia) {
|
|
215
|
-
extraLines.push("
|
|
215
|
+
extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
|
|
216
216
|
} else {
|
|
217
|
-
extraLines.push("
|
|
217
|
+
extraLines.push(" url: true");
|
|
218
218
|
}
|
|
219
219
|
} else {
|
|
220
220
|
const hasFileExtension = validValues.some(v => typeof v === "string" && v.match(/\.[a-zA-Z0-9]+$/));
|
|
@@ -225,9 +225,9 @@ export function inferPropertyFromData(
|
|
|
225
225
|
extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
|
|
226
226
|
} else if (isUrl) {
|
|
227
227
|
if (isMedia) {
|
|
228
|
-
extraLines.push("
|
|
228
|
+
extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
|
|
229
229
|
} else {
|
|
230
|
-
extraLines.push("
|
|
230
|
+
extraLines.push(" url: true");
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -569,11 +569,11 @@ export function generateCollectionFile(
|
|
|
569
569
|
// Date auto-value heuristics
|
|
570
570
|
if (finalPropType === "date") {
|
|
571
571
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
572
|
-
extra += "\n autoValue: \"on_create\",\n
|
|
572
|
+
extra += "\n autoValue: \"on_create\",\n admin: {\n readOnly: true,\n hideFromCollection: true\n },";
|
|
573
573
|
} else if (colNameLower === "updated_at" || colNameLower === "updatedat") {
|
|
574
|
-
extra += "\n autoValue: \"on_update\",\n
|
|
574
|
+
extra += "\n autoValue: \"on_update\",\n admin: {\n readOnly: true,\n hideFromCollection: true\n },";
|
|
575
575
|
} else if (col.column_default && (col.column_default.includes("now()") || col.column_default.includes("CURRENT_TIMESTAMP"))) {
|
|
576
|
-
extra += "\n autoValue: \"on_create\",\n
|
|
576
|
+
extra += "\n autoValue: \"on_create\",\n admin: {\n readOnly: true\n },";
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
579
|
|
|
@@ -604,13 +604,13 @@ export function generateCollectionFile(
|
|
|
604
604
|
if (isMedia) {
|
|
605
605
|
extra += `\n storage: {\n storagePath: "${tableName}/${col.column_name}"\n },`;
|
|
606
606
|
} else if (isUrl) {
|
|
607
|
-
extra += "\n
|
|
607
|
+
extra += "\n url: true,";
|
|
608
608
|
} else if (colNameLower === "description" || colNameLower === "summary" || colNameLower === "excerpt") {
|
|
609
|
-
extra += "\n multiline: true,";
|
|
609
|
+
extra += "\n admin: {\n multiline: true\n },";
|
|
610
610
|
} else if (colNameLower === "content" || colNameLower === "body") {
|
|
611
611
|
extra += "\n multiline: true,\n markdown: true,";
|
|
612
612
|
} else if (col.data_type === "text") {
|
|
613
|
-
extra += "\n multiline: true,";
|
|
613
|
+
extra += "\n admin: {\n multiline: true\n },";
|
|
614
614
|
}
|
|
615
615
|
}
|
|
616
616
|
|
|
@@ -700,11 +700,12 @@ export function generateCollectionFile(
|
|
|
700
700
|
${relName}: {
|
|
701
701
|
name: "${relHumanName}",
|
|
702
702
|
type: "relation",
|
|
703
|
-
target: () => ${targetCollectionCamel},
|
|
704
|
-
cardinality: "one",
|
|
705
|
-
direction: "owning",
|
|
706
|
-
localKey: "${fk.column_name}",
|
|
707
703
|
// mapped from foreign key: ${fk.column_name} -> ${targetTableName}(${fk.foreign_column_name})
|
|
704
|
+
relation: {
|
|
705
|
+
kind: "belongsTo",
|
|
706
|
+
target: () => ${targetCollectionCamel},
|
|
707
|
+
localKey: "${fk.column_name}"
|
|
708
|
+
}
|
|
708
709
|
},`);
|
|
709
710
|
}
|
|
710
711
|
}
|
|
@@ -718,15 +719,12 @@ export function generateCollectionFile(
|
|
|
718
719
|
const targetCollectionCamel = toCollectionVarName(sourceTableName);
|
|
719
720
|
imports.add(`import ${targetCollectionCamel} from "./${sourceTableName}";`);
|
|
720
721
|
|
|
721
|
-
const inverseRelName = fk.column_name.replace(/_id$/, "");
|
|
722
|
-
|
|
723
722
|
relationsOutput += `
|
|
724
723
|
{
|
|
724
|
+
kind: "hasMany",
|
|
725
725
|
relationName: "${sourceTableName}",
|
|
726
726
|
target: () => ${targetCollectionCamel},
|
|
727
|
-
|
|
728
|
-
direction: "inverse",
|
|
729
|
-
inverseRelationName: "${inverseRelName}",
|
|
727
|
+
// the ${sourceTableName}.${fk.column_name} FK points back here
|
|
730
728
|
foreignKeyOnTarget: "${fk.column_name}"
|
|
731
729
|
},`;
|
|
732
730
|
}
|
|
@@ -755,10 +753,9 @@ export function generateCollectionFile(
|
|
|
755
753
|
|
|
756
754
|
relationsOutput += `
|
|
757
755
|
{
|
|
756
|
+
kind: "manyToMany",
|
|
758
757
|
relationName: "${relPropName}",
|
|
759
758
|
target: () => ${tableName}Collection,
|
|
760
|
-
cardinality: "many",
|
|
761
|
-
direction: "owning",
|
|
762
759
|
through: {
|
|
763
760
|
table: "${jt}",
|
|
764
761
|
sourceColumn: "${thisFk.column_name}",
|
|
@@ -776,24 +773,23 @@ export function generateCollectionFile(
|
|
|
776
773
|
const targetCollectionCamel = toCollectionVarName(targetTableName);
|
|
777
774
|
imports.add(`import ${targetCollectionCamel} from "./${targetTableName}";`);
|
|
778
775
|
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
776
|
+
// Both sides of a many-to-many are `manyToMany`. There is no owning
|
|
777
|
+
// and inverse side to pick between any more, so this no longer
|
|
778
|
+
// guesses one from table-name ordering and no longer emits a
|
|
779
|
+
// half-configured relation on the losing side with a comment asking
|
|
780
|
+
// the reader to finish it by hand. Introspection already knows both
|
|
781
|
+
// junction columns; each side just names them from its own end.
|
|
782
782
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
783
783
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
} else if (direction === "inverse") {
|
|
788
|
-
throughCode = "\n // Make sure the target collection configures the 'through' property.";
|
|
789
|
-
}
|
|
784
|
+
const throughCode = thisFk
|
|
785
|
+
? `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n }`
|
|
786
|
+
: "";
|
|
790
787
|
|
|
791
788
|
relationsOutput += `
|
|
792
789
|
{
|
|
790
|
+
kind: "manyToMany",
|
|
793
791
|
relationName: "${targetTableName}",
|
|
794
|
-
target: () => ${targetCollectionCamel}
|
|
795
|
-
cardinality: "many",
|
|
796
|
-
direction: "${direction}",${throughCode}
|
|
792
|
+
target: () => ${targetCollectionCamel},${throughCode}
|
|
797
793
|
},`;
|
|
798
794
|
}
|
|
799
795
|
}
|