@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.
Files changed (45) hide show
  1. package/dist/auth/schema-version.d.ts +106 -0
  2. package/dist/collections/validate-relations.d.ts +53 -0
  3. package/dist/data-transformer.d.ts +3 -3
  4. package/dist/{ensure-collection-tables-CNlIONzj.js → ensure-collection-tables-DGMYK0fr.js} +12 -12
  5. package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
  6. package/dist/index.es.js +913 -391
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/services/FetchService.d.ts +21 -8
  9. package/dist/services/PersistService.d.ts +12 -0
  10. package/dist/services/RelationService.d.ts +39 -8
  11. package/dist/services/cdc/junction-tables.d.ts +38 -0
  12. package/dist/services/nested-path.d.ts +59 -0
  13. package/dist/services/realtimeService.d.ts +19 -0
  14. package/dist/services/row-pipeline.d.ts +2 -2
  15. package/dist/{src-DmsRg8MR.js → src-3VmUJ8Xn.js} +214 -276
  16. package/dist/src-3VmUJ8Xn.js.map +1 -0
  17. package/dist/{src-B0v4IKaI.js → src-D5xBTl32.js} +19 -2
  18. package/dist/src-D5xBTl32.js.map +1 -0
  19. package/dist/utils/drizzle-conditions.d.ts +71 -18
  20. package/package.json +8 -9
  21. package/src/PostgresBootstrapper.ts +15 -2
  22. package/src/auth/ensure-tables.ts +23 -0
  23. package/src/auth/schema-version.ts +260 -0
  24. package/src/cli-errors.ts +1 -1
  25. package/src/cli-helpers.ts +4 -3
  26. package/src/collections/PostgresCollectionRegistry.ts +9 -4
  27. package/src/collections/buildRegistry.ts +7 -0
  28. package/src/collections/validate-relations.ts +280 -0
  29. package/src/data-transformer.ts +28 -38
  30. package/src/schema/doctor.ts +14 -14
  31. package/src/schema/generate-drizzle-schema-logic.ts +62 -110
  32. package/src/schema/generate-postgres-ddl-logic.ts +28 -21
  33. package/src/schema/introspect-db-inference.ts +13 -13
  34. package/src/schema/introspect-db-logic.ts +25 -29
  35. package/src/services/FetchService.ts +116 -126
  36. package/src/services/PersistService.ts +126 -88
  37. package/src/services/RelationService.ts +157 -86
  38. package/src/services/cdc/junction-tables.ts +91 -0
  39. package/src/services/nested-path.ts +145 -0
  40. package/src/services/realtimeService.ts +60 -0
  41. package/src/services/row-pipeline.ts +5 -6
  42. package/src/utils/drizzle-conditions.ts +268 -330
  43. package/dist/ensure-collection-tables-CNlIONzj.js.map +0 -1
  44. package/dist/src-B0v4IKaI.js.map +0 -1
  45. package/dist/src-DmsRg8MR.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { CollectionConfig, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
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
- return "VARCHAR(255)";
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 (!relation || relation.direction !== "owning" || relation.cardinality !== "one") {
170
- throw new Error(`Relation ${propName} is not an owning one-to-one/many-to-one relation`);
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 "VARCHAR(255)";
181
+ return "TEXT";
177
182
  }
178
183
  const pkProp = getPrimaryKeyProp(targetCollection);
179
- return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "VARCHAR(255)");
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 "VARCHAR(255)";
189
+ if (!targetCollection) return "TEXT";
185
190
  const pkProp = getPrimaryKeyProp(targetCollection);
186
- return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "VARCHAR(255)");
191
+ return pkProp.type === "number" ? "INTEGER" : (pkProp.isUuid ? "UUID" : "TEXT");
187
192
  }
188
193
  default:
189
- return "VARCHAR(255)";
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?: 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.through) {
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.through) {
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
- const sourceColType = isNumericId(sourceCollection) ? "INTEGER" : (getPrimaryKeyProp(sourceCollection).isUuid ? "UUID" : "VARCHAR(255)");
291
- const targetColType = isNumericId(targetCollection) ? "INTEGER" : (getPrimaryKeyProp(targetCollection).isUuid ? "UUID" : "VARCHAR(255)");
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 (!relInfo || relInfo.direction !== "owning" || relInfo.cardinality !== "one" || !relInfo.localKey) {
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" VARCHAR(255) PRIMARY KEY');
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
- // Currency
55
- if (colNameLower.includes("price") || colNameLower.includes("cost") || colNameLower.includes("amount") || colNameLower.includes("fee") || pgDataType === "money") {
56
- extraLines.push(" ui: {\n currency: true\n }");
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
- // Color Codes
173
- const allColors = validValues.every(v => typeof v === "string" && COLOR_HEX_REGEX.test(v));
174
- if (allColors) {
175
- extraLines.push(" ui: {\n color: true\n }");
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(" ui: {\n url: \"image\"\n }");
215
+ extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
216
216
  } else {
217
- extraLines.push(" ui: {\n url: true\n }");
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(" ui: {\n url: \"image\"\n }");
228
+ extraLines.push(" url: true,\n admin: {\n urlPreview: \"image\"\n }");
229
229
  } else {
230
- extraLines.push(" ui: {\n url: true\n }");
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 ui: {\n readOnly: true,\n hideFromCollection: true\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 ui: {\n readOnly: true,\n hideFromCollection: true\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 ui: {\n readOnly: true\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 ui: {\n url: true\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
- cardinality: "many",
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
- // Determine direction (alphabetically first table is owning)
780
- const direction = tableName < targetTableName ? "owning" : "inverse";
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
- let throughCode = "";
785
- if (direction === "owning" && thisFk) {
786
- throughCode = `\n through: {\n table: "${jt}",\n sourceColumn: "${thisFk.column_name}",\n targetColumn: "${otherFk.column_name}"\n },`;
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
  }