@rebasepro/server-postgres 0.10.1-canary.b1e3dbf → 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/chunk-DSJWtz9O.js +40 -0
- package/dist/collections/validate-relations.d.ts +53 -0
- package/dist/data-transformer.d.ts +3 -3
- package/dist/ensure-collection-tables-DGMYK0fr.js +304 -0
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
- package/dist/index.es.js +1093 -4941
- package/dist/index.es.js.map +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +79 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -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-3VmUJ8Xn.js +3994 -0
- package/dist/src-3VmUJ8Xn.js.map +1 -0
- package/dist/src-D5xBTl32.js +346 -0
- 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 +55 -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/ensure-collection-tables.test.ts +156 -0
- package/src/schema/ensure-collection-tables.ts +297 -0
- package/src/schema/generate-drizzle-schema-logic.ts +62 -110
- package/src/schema/generate-postgres-ddl-logic.ts +31 -24
- 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
|
@@ -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, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/collection-helpers";
|
|
3
3
|
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
@@ -97,12 +97,14 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
97
97
|
columnDefinition = `uuid("${colName}")`;
|
|
98
98
|
} else if (stringProp.columnType === "uuid") {
|
|
99
99
|
columnDefinition = `uuid("${colName}")`;
|
|
100
|
-
} else if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
|
|
101
|
-
columnDefinition = `text("${colName}")`;
|
|
102
100
|
} else if (stringProp.columnType === "char") {
|
|
103
101
|
columnDefinition = `char("${colName}")`;
|
|
104
|
-
} else {
|
|
102
|
+
} else if (stringProp.columnType === "varchar") {
|
|
105
103
|
columnDefinition = `varchar("${colName}")`;
|
|
104
|
+
} else {
|
|
105
|
+
// `text` is the default, and the only length-unbounded choice.
|
|
106
|
+
// Ask for `varchar` explicitly if you want the length constraint.
|
|
107
|
+
columnDefinition = `text("${colName}")`;
|
|
106
108
|
}
|
|
107
109
|
if (isIdProperty(propName, prop, collection)) {
|
|
108
110
|
columnDefinition += ".primaryKey()";
|
|
@@ -222,16 +224,11 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
222
224
|
case "relation": {
|
|
223
225
|
const refProp = prop as RelationProperty;
|
|
224
226
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
225
|
-
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
227
|
+
const relation = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
226
228
|
|
|
227
|
-
// Only
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// The localKey property is the source of truth for the FK column name.
|
|
233
|
-
if (!relation.localKey) {
|
|
234
|
-
logger.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
|
|
229
|
+
// Only `belongsTo` puts a column on this table; every other kind
|
|
230
|
+
// is a column on the target, a junction row, or a join chain.
|
|
231
|
+
if (!relation || relation.kind !== "belongsTo") {
|
|
235
232
|
return null;
|
|
236
233
|
}
|
|
237
234
|
|
|
@@ -252,7 +249,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
252
249
|
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
253
250
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
254
251
|
const targetIdField = pkProp.name;
|
|
255
|
-
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : (pkProp.isUuid ? `uuid("${fkColumnName}")` : `
|
|
252
|
+
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : (pkProp.isUuid ? `uuid("${fkColumnName}")` : `text("${fkColumnName}")`);
|
|
256
253
|
|
|
257
254
|
const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
|
|
258
255
|
const required = prop.validation?.required;
|
|
@@ -274,14 +271,14 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
274
271
|
const refProp = prop as ReferenceProperty;
|
|
275
272
|
const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
276
273
|
if (!targetCollection) {
|
|
277
|
-
columnDefinition = `
|
|
274
|
+
columnDefinition = `text("${colName}")`;
|
|
278
275
|
break;
|
|
279
276
|
}
|
|
280
277
|
|
|
281
278
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
282
279
|
const targetTableVar = getTableVarName(getTableName(targetCollection));
|
|
283
280
|
const targetIdField = pkProp.name;
|
|
284
|
-
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : (pkProp.isUuid ? `uuid("${colName}")` : `
|
|
281
|
+
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : (pkProp.isUuid ? `uuid("${colName}")` : `text("${colName}")`);
|
|
285
282
|
|
|
286
283
|
const required = prop.validation?.required;
|
|
287
284
|
const onDelete = required ? "cascade" : "set null";
|
|
@@ -400,26 +397,22 @@ const generateSinglePolicyCode = (collection: CollectionConfig, rule: SecurityRu
|
|
|
400
397
|
* Falls back to the local relation name when the counterpart can't be resolved.
|
|
401
398
|
*/
|
|
402
399
|
const computeSharedRelationName = (
|
|
403
|
-
rel:
|
|
400
|
+
rel: ResolvedRelation,
|
|
404
401
|
sourceCollection: CollectionConfig,
|
|
405
402
|
_collections: CollectionConfig[]
|
|
406
403
|
): string => {
|
|
407
404
|
const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
|
|
408
405
|
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// (which resolves foreignKeyOnTarget via the same helper).
|
|
406
|
+
// Both sides of a link must derive the same name, so each resolves the
|
|
407
|
+
// column to its Drizzle property key and builds the name from the table
|
|
408
|
+
// that actually owns it.
|
|
409
|
+
if (rel.kind === "belongsTo") {
|
|
414
410
|
const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
|
|
415
411
|
return `${getTableName(sourceCollection)}_${normalisedKey}`;
|
|
416
412
|
}
|
|
417
413
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
// The owning table is the *target*, the FK column is foreignKeyOnTarget.
|
|
421
|
-
// Resolve to the Drizzle property key on the target so it matches the
|
|
422
|
-
// owning side's normalised localKey.
|
|
414
|
+
if (rel.kind === "hasMany" || rel.kind === "hasOne") {
|
|
415
|
+
// The owning table is the *target*; the column is foreignKeyOnTarget.
|
|
423
416
|
try {
|
|
424
417
|
const targetCollection = rel.target();
|
|
425
418
|
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
@@ -429,40 +422,8 @@ const computeSharedRelationName = (
|
|
|
429
422
|
}
|
|
430
423
|
}
|
|
431
424
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
if (rel.foreignKeyOnTarget) {
|
|
435
|
-
// FK lives on the target table — resolve to Drizzle property key
|
|
436
|
-
try {
|
|
437
|
-
const targetCollection = rel.target();
|
|
438
|
-
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
439
|
-
return `${getTableName(targetCollection)}_${normalisedFK}`;
|
|
440
|
-
} catch {
|
|
441
|
-
return fallback;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// No explicit foreignKeyOnTarget — try to find the corresponding owning relation
|
|
445
|
-
try {
|
|
446
|
-
const targetCollection = rel.target();
|
|
447
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
448
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
449
|
-
targetRel.direction === "owning" &&
|
|
450
|
-
targetRel.cardinality === "one" &&
|
|
451
|
-
targetRel.localKey &&
|
|
452
|
-
targetRel.target().slug === sourceCollection.slug
|
|
453
|
-
);
|
|
454
|
-
if (correspondingRelation && correspondingRelation.localKey) {
|
|
455
|
-
return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
|
|
456
|
-
}
|
|
457
|
-
} catch {
|
|
458
|
-
// ignore
|
|
459
|
-
}
|
|
460
|
-
return fallback;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// --- M2M owning (through) — keep local name (already shared via junction wiring) ---
|
|
464
|
-
// --- M2M inverse — keep local name ---
|
|
465
|
-
// --- joinPath — not emitted as Drizzle relations ---
|
|
425
|
+
// manyToMany is named through its junction wiring; `via` is not emitted as
|
|
426
|
+
// a Drizzle relation at all. Both keep the local name.
|
|
466
427
|
return fallback;
|
|
467
428
|
};
|
|
468
429
|
|
|
@@ -519,7 +480,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
519
480
|
const allTablesToGenerate = new Map<string, {
|
|
520
481
|
collection: CollectionConfig,
|
|
521
482
|
isJunction?: boolean,
|
|
522
|
-
relation?:
|
|
483
|
+
relation?: ResolvedRelation,
|
|
523
484
|
sourceCollection?: CollectionConfig
|
|
524
485
|
}>();
|
|
525
486
|
|
|
@@ -558,7 +519,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
558
519
|
|
|
559
520
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
560
521
|
for (const relation of Object.values(resolvedRelations)) {
|
|
561
|
-
if (relation
|
|
522
|
+
if (isManyToMany(relation)) { // Standard M2M junction table
|
|
562
523
|
const junctionTableName = relation.through.table;
|
|
563
524
|
if (!allTablesToGenerate.has(junctionTableName)) {
|
|
564
525
|
allTablesToGenerate.set(junctionTableName, {
|
|
@@ -584,7 +545,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
584
545
|
sourceCollection
|
|
585
546
|
}] of allTablesToGenerate.entries()) {
|
|
586
547
|
const tableVarName = getTableVarName(tableName);
|
|
587
|
-
if (isJunction && relation && sourceCollection && relation
|
|
548
|
+
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
588
549
|
const targetCollection = relation.target();
|
|
589
550
|
const schema = (isPostgresCollectionConfig(targetCollection) ? targetCollection.schema : undefined) || (isPostgresCollectionConfig(sourceCollection) ? sourceCollection.schema : undefined);
|
|
590
551
|
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
@@ -597,8 +558,10 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
597
558
|
const onDelete = relation.onDelete ?? "cascade";
|
|
598
559
|
const refOptions = `{ onDelete: \"${onDelete}\" }`;
|
|
599
560
|
|
|
600
|
-
|
|
601
|
-
|
|
561
|
+
// `text`, matching the string default: a junction column must have the
|
|
562
|
+
// same type as the primary key it references.
|
|
563
|
+
const sourceColType = isNumericId(sourceCollection) ? "integer" : (getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "text");
|
|
564
|
+
const targetColType = isNumericId(targetCollection) ? "integer" : (getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "text");
|
|
602
565
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
603
566
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
604
567
|
|
|
@@ -637,7 +600,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
637
600
|
// We should generate a basic id column if one was completely omitted.
|
|
638
601
|
const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
|
|
639
602
|
if (!hasIdColumn) {
|
|
640
|
-
columns.add(" id:
|
|
603
|
+
columns.add(" id: text(\"id\").primaryKey()");
|
|
641
604
|
}
|
|
642
605
|
|
|
643
606
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
@@ -669,7 +632,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
669
632
|
|
|
670
633
|
if (isJunction) {
|
|
671
634
|
const relationInfo = Array.from(allTablesToGenerate.values()).find(v => v.isJunction && getTableName(v.collection) === tableName);
|
|
672
|
-
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && relationInfo.relation
|
|
635
|
+
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && isManyToMany(relationInfo.relation)) {
|
|
673
636
|
const {
|
|
674
637
|
relation,
|
|
675
638
|
sourceCollection
|
|
@@ -693,9 +656,9 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
693
656
|
try {
|
|
694
657
|
const targetRelations = resolveCollectionRelations(targetCollection);
|
|
695
658
|
for (const [, targetRel] of Object.entries(targetRelations)) {
|
|
696
|
-
if (targetRel.
|
|
659
|
+
if (targetRel.kind !== "belongsTo" &&
|
|
697
660
|
targetRel.cardinality === "many" &&
|
|
698
|
-
targetRel.
|
|
661
|
+
targetRel.relationName === owningRelationName) {
|
|
699
662
|
inverseRelationName = targetRel.relationName ?? null;
|
|
700
663
|
break;
|
|
701
664
|
}
|
|
@@ -740,54 +703,43 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
740
703
|
// Skip if we've already emitted a relation with this drizzleRelationName
|
|
741
704
|
// for this table — prevents duplicate definitions when
|
|
742
705
|
// resolveCollectionRelations returns alias entries for the same FK.
|
|
743
|
-
const deduplicationKey = `${drizzleRelationName}::${rel.
|
|
706
|
+
const deduplicationKey = `${drizzleRelationName}::${rel.kind}`;
|
|
744
707
|
if (emittedRelationNames.has(deduplicationKey)) continue;
|
|
745
708
|
emittedRelationNames.add(deduplicationKey);
|
|
746
709
|
|
|
747
|
-
|
|
748
|
-
|
|
710
|
+
switch (rel.kind) {
|
|
711
|
+
case "belongsTo":
|
|
749
712
|
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
//
|
|
713
|
+
break;
|
|
714
|
+
|
|
715
|
+
case "hasOne":
|
|
716
|
+
// The foreign key lives on the TARGET table. Drizzle pairs
|
|
717
|
+
// the two sides by `relationName` alone — giving
|
|
718
|
+
// `fields`/`references` here is invalid and crashes
|
|
719
|
+
// `normalizeRelation` with "Cannot read properties of
|
|
755
720
|
// undefined (reading 'referencedTable')".
|
|
756
721
|
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// One-to-many inverse relation
|
|
722
|
+
break;
|
|
723
|
+
|
|
724
|
+
case "hasMany":
|
|
761
725
|
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
762
|
-
|
|
763
|
-
|
|
726
|
+
break;
|
|
727
|
+
|
|
728
|
+
case "manyToMany": {
|
|
729
|
+
// Both sides point at the junction. This used to have a
|
|
730
|
+
// second arm that searched the *target's* relations for an
|
|
731
|
+
// owning many-to-many whose name matched, to borrow its
|
|
732
|
+
// junction table — unnecessary now that each side names
|
|
733
|
+
// its own.
|
|
764
734
|
const junctionTableVar = getTableVarName(rel.through.table);
|
|
765
735
|
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
766
|
-
|
|
767
|
-
// Many-to-many inverse relation - find the corresponding owning relation's junction table
|
|
768
|
-
try {
|
|
769
|
-
const targetCollection = rel.target();
|
|
770
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
771
|
-
|
|
772
|
-
// Find the corresponding owning many-to-many relation on the target
|
|
773
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
774
|
-
targetRel.direction === "owning" &&
|
|
775
|
-
targetRel.cardinality === "many" &&
|
|
776
|
-
targetRel.through &&
|
|
777
|
-
targetRel.relationName === rel.inverseRelationName
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
if (correspondingRelation && correspondingRelation.through) {
|
|
781
|
-
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
782
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
783
|
-
} else {
|
|
784
|
-
logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
785
|
-
}
|
|
786
|
-
} catch (e) {
|
|
787
|
-
logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
|
|
788
|
-
}
|
|
736
|
+
break;
|
|
789
737
|
}
|
|
790
|
-
|
|
738
|
+
|
|
739
|
+
case "via":
|
|
740
|
+
// A join chain is resolved at query time, not modelled
|
|
741
|
+
// as a Drizzle relation.
|
|
742
|
+
break;
|
|
791
743
|
}
|
|
792
744
|
} catch (e) {
|
|
793
745
|
logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
|
|
@@ -800,12 +752,12 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
800
752
|
|
|
801
753
|
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
802
754
|
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
803
|
-
if (otherRel
|
|
755
|
+
if (hasForeignKeyOnTarget(otherRel)) {
|
|
804
756
|
try {
|
|
805
757
|
const otherTarget = otherRel.target();
|
|
806
758
|
if (otherTarget.slug === collection.slug) {
|
|
807
759
|
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
808
|
-
const deduplicationKey = `${drizzleRelationName}::
|
|
760
|
+
const deduplicationKey = `${drizzleRelationName}::belongsTo`;
|
|
809
761
|
|
|
810
762
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
811
763
|
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
@@ -1,10 +1,10 @@
|
|
|
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
|
|
|
5
5
|
// --- Helper Functions ---
|
|
6
6
|
|
|
7
|
-
const resolveColumnName = (propName: string, prop?: Property | null): string => {
|
|
7
|
+
export const resolveColumnName = (propName: string, prop?: Property | null): string => {
|
|
8
8
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") {
|
|
9
9
|
return prop.columnName;
|
|
10
10
|
}
|
|
@@ -36,7 +36,7 @@ const getPrimaryKeyName = (collection: CollectionConfig): string => {
|
|
|
36
36
|
return getPrimaryKeyProp(collection).name;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {
|
|
39
|
+
export const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {
|
|
40
40
|
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
41
41
|
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
42
42
|
return !hasExplicitId && propName === "id";
|
|
@@ -90,7 +90,7 @@ const generateSinglePolicyDdl = (collection: CollectionConfig, rule: SecurityRul
|
|
|
90
90
|
return `${ddl};\n`;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {
|
|
93
|
+
export const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {
|
|
94
94
|
switch (prop.type) {
|
|
95
95
|
case "string": {
|
|
96
96
|
const stringProp = prop as StringProperty;
|
|
@@ -103,13 +103,18 @@ const getSqlColumnType = (propName: string, prop: Property, collection: Collecti
|
|
|
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 @@ const getSqlColumnType = (propName: string, prop: Property, collection: Collecti
|
|
|
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
|
}
|