@rebasepro/server-postgres 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed8caed

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.
@@ -18,6 +18,47 @@ export interface DrizzleDynamicQuery {
18
18
  * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
19
19
  */
20
20
  export declare class DrizzleConditionBuilder {
21
+ /**
22
+ * Express "reachable from this parent through this relation" as a plain
23
+ * `WHERE` condition on the target table.
24
+ *
25
+ * This is the primitive that lets a relation be a *filter* rather than an
26
+ * addressing scheme. A nested listing used to be served by its own query
27
+ * builder — `fetchEntitiesUsingJoins`, which grew joins the root pipeline
28
+ * did not have and lost the options the root pipeline did have (offset,
29
+ * filter, orderBy, include). Reduced to a condition, the same listing runs
30
+ * through the ordinary collection query, so it inherits all of them and
31
+ * there is one read path instead of two.
32
+ *
33
+ * The shapes:
34
+ * - inverse FK → `target.<fk> = :parentId`, a column comparison.
35
+ * - `through` → `EXISTS (SELECT 1 FROM junction …)`, correlated on the
36
+ * target's key, so the junction never multiplies rows the
37
+ * way an `INNER JOIN` would.
38
+ * - `joinPath` → the same `EXISTS`, with the path's steps joined inside
39
+ * it and the final step correlating to the outer row.
40
+ */
41
+ static buildRelationScopeCondition(relation: Relation,
42
+ /**
43
+ * Lazy: only `joinPath` and `localKey` relations need the parent's own
44
+ * table. An inverse foreign key and a junction are both expressible
45
+ * from the parent's *id* alone, and requiring the table for them would
46
+ * make a child listing fail on a parent whose table isn't registered.
47
+ */
48
+ parent: () => {
49
+ table: PgTable<any>;
50
+ idColumn: AnyPgColumn;
51
+ }, parentId: string | number, targetTable: PgTable<any>, targetIdColumn: AnyPgColumn, registry: PostgresCollectionRegistry): SQL;
52
+ /**
53
+ * `EXISTS` for an explicit `joinPath`.
54
+ *
55
+ * The path is declared source → target. The subquery replays every step but
56
+ * the last from inside, and turns the last one into the correlation with the
57
+ * outer target row — so the target table is never named twice and needs no
58
+ * alias. Each intermediate table is aliased positionally, which keeps a path
59
+ * that revisits a table (a self-referencing many-to-many) unambiguous.
60
+ */
61
+ private static buildJoinPathScopeCondition;
21
62
  /**
22
63
  * Build filter conditions from FilterValues
23
64
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgres",
3
3
  "type": "module",
4
- "version": "0.10.1-canary.d8d45b2",
4
+ "version": "0.10.1-canary.ed8caed",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -47,21 +47,20 @@
47
47
  "execa": "^9.6.1",
48
48
  "pg": "^8.21.0",
49
49
  "ws": "^8.21.0",
50
- "@rebasepro/codegen": "0.10.1-canary.d8d45b2",
51
- "@rebasepro/server": "0.10.1-canary.d8d45b2",
52
- "@rebasepro/utils": "0.10.1-canary.d8d45b2",
53
- "@rebasepro/common": "0.10.1-canary.d8d45b2",
54
- "@rebasepro/types": "0.10.1-canary.d8d45b2"
50
+ "@rebasepro/codegen": "0.10.1-canary.ed8caed",
51
+ "@rebasepro/common": "0.10.1-canary.ed8caed",
52
+ "@rebasepro/server": "0.10.1-canary.ed8caed",
53
+ "@rebasepro/types": "0.10.1-canary.ed8caed",
54
+ "@rebasepro/utils": "0.10.1-canary.ed8caed"
55
55
  },
56
56
  "devDependencies": {
57
- "@hono/node-server": "^2.0.9",
57
+ "@hono/node-server": "^2.0.11",
58
58
  "@jest/globals": "^30.4.1",
59
59
  "@types/jest": "^30.0.0",
60
60
  "@types/node": "^25.9.3",
61
61
  "@types/pg": "^8.20.0",
62
62
  "@types/ws": "^8.18.1",
63
- "@vitejs/plugin-react": "^6.0.2",
64
- "hono": "^4.12.25",
63
+ "hono": "^4.12.27",
65
64
  "jest": "^30.4.2",
66
65
  "ts-jest": "^29.4.11",
67
66
  "typescript": "^6.0.3",
@@ -62,12 +62,10 @@ export function getExpectedColumnType(prop: Property): string | null {
62
62
  if (sp.enum) return "USER-DEFINED"; // pgEnum → USER-DEFINED in information_schema
63
63
  if ("isId" in sp && sp.isId === "uuid") return "uuid";
64
64
  if (sp.columnType === "uuid") return "uuid";
65
- // A markdown/multiline string compiles to `text`, not varchar — the
66
- // generator treats those UI hints as column-type signals, so the
67
- // expectation here must too or every such column reads as drift.
68
- if (sp.columnType === "text" || sp.ui?.markdown || sp.ui?.multiline) return "text";
69
65
  if (sp.columnType === "char") return "character";
70
- return "character varying";
66
+ if (sp.columnType === "varchar") return "character varying";
67
+ // `text` is the default — see generate-postgres-ddl-logic.
68
+ return "text";
71
69
  }
72
70
  case "number": {
73
71
  const np = prop as NumberProperty;
@@ -122,7 +120,9 @@ export function getExpectedColumnType(prop: Property): string | null {
122
120
  case "relation":
123
121
  return null; // FK columns are derived from the relation, not from the property
124
122
  case "reference":
125
- return "character varying"; // References default to varchar FK
123
+ // A reference FK follows the key it points at, and a string key is
124
+ // `text` — see generate-postgres-ddl-logic.
125
+ return "text";
126
126
  case "vector":
127
127
  return "USER-DEFINED";
128
128
  case "binary":
@@ -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()";
@@ -252,7 +254,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
252
254
  const targetTableVar = getTableVarName(getTableName(targetCollection));
253
255
  const pkProp = getPrimaryKeyProp(targetCollection);
254
256
  const targetIdField = pkProp.name;
255
- const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : (pkProp.isUuid ? `uuid("${fkColumnName}")` : `varchar("${fkColumnName}")`);
257
+ const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : (pkProp.isUuid ? `uuid("${fkColumnName}")` : `text("${fkColumnName}")`);
256
258
 
257
259
  const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
258
260
  const required = prop.validation?.required;
@@ -274,14 +276,14 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
274
276
  const refProp = prop as ReferenceProperty;
275
277
  const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
276
278
  if (!targetCollection) {
277
- columnDefinition = `varchar("${colName}")`;
279
+ columnDefinition = `text("${colName}")`;
278
280
  break;
279
281
  }
280
282
 
281
283
  const pkProp = getPrimaryKeyProp(targetCollection);
282
284
  const targetTableVar = getTableVarName(getTableName(targetCollection));
283
285
  const targetIdField = pkProp.name;
284
- const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : (pkProp.isUuid ? `uuid("${colName}")` : `varchar("${colName}")`);
286
+ const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : (pkProp.isUuid ? `uuid("${colName}")` : `text("${colName}")`);
285
287
 
286
288
  const required = prop.validation?.required;
287
289
  const onDelete = required ? "cascade" : "set null";
@@ -597,8 +599,10 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
597
599
  const onDelete = relation.onDelete ?? "cascade";
598
600
  const refOptions = `{ onDelete: \"${onDelete}\" }`;
599
601
 
600
- const sourceColType = isNumericId(sourceCollection) ? "integer" : (getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "varchar");
601
- const targetColType = isNumericId(targetCollection) ? "integer" : (getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "varchar");
602
+ // `text`, matching the string default: a junction column must have the
603
+ // same type as the primary key it references.
604
+ const sourceColType = isNumericId(sourceCollection) ? "integer" : (getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "text");
605
+ const targetColType = isNumericId(targetCollection) ? "integer" : (getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "text");
602
606
  const sourceId = getPrimaryKeyName(sourceCollection);
603
607
  const targetId = getPrimaryKeyName(targetCollection);
604
608
 
@@ -637,7 +641,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
637
641
  // We should generate a basic id column if one was completely omitted.
638
642
  const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
639
643
  if (!hasIdColumn) {
640
- columns.add(" id: varchar(\"id\").primaryKey()");
644
+ columns.add(" id: text(\"id\").primaryKey()");
641
645
  }
642
646
 
643
647
  schemaContent += `${Array.from(columns).join(",\n")}`;
@@ -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;
@@ -173,20 +178,20 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
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
 
@@ -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
 
@@ -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