@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.
- package/dist/{ensure-collection-tables-CNlIONzj.js → ensure-collection-tables-BVvtkkRm.js} +10 -10
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.es.js +344 -119
- 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 +30 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/{src-B0v4IKaI.js → src-CBgtrPhJ.js} +8 -1
- package/dist/{src-B0v4IKaI.js.map → src-CBgtrPhJ.js.map} +1 -1
- package/dist/{src-DmsRg8MR.js → src-lcfUP4xg.js} +126 -76
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/schema/doctor.ts +6 -6
- package/src/schema/generate-drizzle-schema-logic.ts +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +19 -12
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/nested-path.ts +130 -0
- package/src/utils/drizzle-conditions.ts +143 -0
- package/dist/ensure-collection-tables-CNlIONzj.js.map +0 -1
- package/dist/src-DmsRg8MR.js.map +0 -1
|
@@ -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.
|
|
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.
|
|
51
|
-
"@rebasepro/
|
|
52
|
-
"@rebasepro/
|
|
53
|
-
"@rebasepro/
|
|
54
|
-
"@rebasepro/
|
|
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.
|
|
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
|
-
"
|
|
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",
|
package/src/schema/doctor.ts
CHANGED
|
@@ -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
|
-
|
|
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}")` : `
|
|
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 = `
|
|
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}")` : `
|
|
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
|
-
|
|
601
|
-
|
|
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:
|
|
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
|
-
|
|
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 "
|
|
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
|
|
|
@@ -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
|
|
|
@@ -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
|
|