@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
package/src/data-transformer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { CollectionConfig, Properties, Property,
|
|
4
|
+
import { CollectionConfig, Properties, Property, ResolvedRelation, RelationProperty, Vector, BinaryProperty, hasForeignKeyOnTarget, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, type ResolvedVia } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations, findRelation, createRelationRef, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "@rebasepro/common";
|
|
6
6
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
7
7
|
import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
|
|
@@ -31,13 +31,13 @@ export interface SerializedEntityData {
|
|
|
31
31
|
*/
|
|
32
32
|
inverseRelationUpdates: Array<{
|
|
33
33
|
relationKey: string;
|
|
34
|
-
relation:
|
|
34
|
+
relation: ResolvedRelation;
|
|
35
35
|
newValue: unknown;
|
|
36
36
|
}>;
|
|
37
37
|
/** JoinPath relation updates that require multi-hop writes. */
|
|
38
38
|
joinPathRelationUpdates: Array<{
|
|
39
39
|
relationKey: string;
|
|
40
|
-
relation:
|
|
40
|
+
relation: ResolvedVia;
|
|
41
41
|
newTargetId: string | number | null;
|
|
42
42
|
}>;
|
|
43
43
|
}
|
|
@@ -110,19 +110,19 @@ joinPathRelationUpdates: [] };
|
|
|
110
110
|
// Track inverse relations that need to be handled separately
|
|
111
111
|
const inverseRelationUpdates: Array<{
|
|
112
112
|
relationKey: string;
|
|
113
|
-
relation:
|
|
113
|
+
relation: ResolvedRelation;
|
|
114
114
|
newValue: unknown;
|
|
115
115
|
}> = [];
|
|
116
116
|
const joinPathRelationUpdates: Array<{
|
|
117
117
|
relationKey: string;
|
|
118
|
-
relation:
|
|
118
|
+
relation: ResolvedVia;
|
|
119
119
|
newTargetId: string | number | null;
|
|
120
120
|
}> = [];
|
|
121
121
|
|
|
122
122
|
// Pre-calculate all local keys used as foreign keys
|
|
123
123
|
const foreignKeys = new Set<string>();
|
|
124
124
|
Object.values(resolvedRelations).forEach(relation => {
|
|
125
|
-
if (relation.
|
|
125
|
+
if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
for (const [key, value] of Object.entries(row)) {
|
|
@@ -141,7 +141,7 @@ joinPathRelationUpdates: [] };
|
|
|
141
141
|
if (property.type === "relation" && collection) {
|
|
142
142
|
const relation = findRelation(resolvedRelations, key);
|
|
143
143
|
if (relation) {
|
|
144
|
-
if (relation.
|
|
144
|
+
if (relation.kind === "belongsTo") {
|
|
145
145
|
// Owning relation: Map relation object to FK column on current table
|
|
146
146
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
147
147
|
if (serializedValue !== undefined) {
|
|
@@ -149,7 +149,7 @@ joinPathRelationUpdates: [] };
|
|
|
149
149
|
}
|
|
150
150
|
// Don't add the original relation property to the result
|
|
151
151
|
continue;
|
|
152
|
-
} else if (relation
|
|
152
|
+
} else if (hasForeignKeyOnTarget(relation)) {
|
|
153
153
|
// Inverse relation: Need to update the target table's FK
|
|
154
154
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
155
155
|
inverseRelationUpdates.push({
|
|
@@ -159,38 +159,29 @@ joinPathRelationUpdates: [] };
|
|
|
159
159
|
});
|
|
160
160
|
// Don't add the original relation property to the result
|
|
161
161
|
continue;
|
|
162
|
-
} else if (relation.
|
|
162
|
+
} else if (relation.kind === "via") {
|
|
163
|
+
// A join chain is written through its own machinery, one
|
|
164
|
+
// row at a time for a to-one and as a set for a to-many.
|
|
165
|
+
// There used to be two arms here — "owning" and "inverse"
|
|
166
|
+
// joinPath — but a join chain has no owning side, so they
|
|
167
|
+
// only ever differed by which list they pushed to.
|
|
163
168
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
164
169
|
if (relation.cardinality === "one") {
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// This prevents stale values from corrupting related rows when an
|
|
169
|
-
// intermediate FK (e.g. author_id) changes in the same save.
|
|
170
|
+
// Ordering matters: PersistService applies these BEFORE
|
|
171
|
+
// the main UPDATE, so the mapping reads the pre-update
|
|
172
|
+
// foreign key rather than a value the same save changed.
|
|
170
173
|
joinPathRelationUpdates.push({
|
|
171
174
|
relationKey: key,
|
|
172
175
|
relation,
|
|
173
176
|
newTargetId: serializedValue as string | number | null
|
|
174
177
|
});
|
|
175
178
|
} else {
|
|
176
|
-
// Many inverse joinPath: capture as inverse relation update
|
|
177
179
|
inverseRelationUpdates.push({
|
|
178
180
|
relationKey: key,
|
|
179
181
|
relation,
|
|
180
182
|
newValue: serializedValue
|
|
181
183
|
});
|
|
182
184
|
}
|
|
183
|
-
// Don't add the original relation property to the result
|
|
184
|
-
continue;
|
|
185
|
-
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
186
|
-
// Owning one-to-one via joinPath: capture as a write intent
|
|
187
|
-
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
188
|
-
joinPathRelationUpdates.push({
|
|
189
|
-
relationKey: key,
|
|
190
|
-
relation,
|
|
191
|
-
newTargetId: serializedValue as string | number | null
|
|
192
|
-
});
|
|
193
|
-
// Don't include this property directly in payload
|
|
194
185
|
continue;
|
|
195
186
|
}
|
|
196
187
|
}
|
|
@@ -320,7 +311,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
320
311
|
// Find the normalized relation for this property
|
|
321
312
|
const relation = findRelation(resolvedRelations, propKey);
|
|
322
313
|
if (relation) {
|
|
323
|
-
if (relation.
|
|
314
|
+
if (relation.kind === "belongsTo" && relation.localKey in data) {
|
|
324
315
|
// Owning relation: FK is in current table
|
|
325
316
|
const fkValue = data[relation.localKey as keyof M];
|
|
326
317
|
if (fkValue !== null && fkValue !== undefined) {
|
|
@@ -331,7 +322,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
331
322
|
logger.warn(`Could not resolve target collection for relation property: ${propKey}`, { error: e });
|
|
332
323
|
}
|
|
333
324
|
}
|
|
334
|
-
} else if (relation
|
|
325
|
+
} else if (hasForeignKeyOnTarget(relation) && db && registry) {
|
|
335
326
|
// Inverse relation: FK is in target table, need to query for it
|
|
336
327
|
try {
|
|
337
328
|
const targetCollection = relation.target();
|
|
@@ -368,7 +359,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
368
359
|
} catch (e) {
|
|
369
360
|
logger.warn(`Could not resolve inverse relation property: ${propKey}`, { error: e });
|
|
370
361
|
}
|
|
371
|
-
} else if (relation.
|
|
362
|
+
} else if (relation.kind === "via" && db && registry) {
|
|
372
363
|
// Join path relation: Multi-hop relation using joins
|
|
373
364
|
try {
|
|
374
365
|
const targetCollection = relation.target();
|
|
@@ -547,17 +538,16 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
547
538
|
case "relation":
|
|
548
539
|
// Transform ID back to relation object with type information
|
|
549
540
|
if (typeof value === "string" || typeof value === "number") {
|
|
550
|
-
|
|
541
|
+
// Normalization stamps the resolved relation onto the property;
|
|
542
|
+
// fall back to the collection's map for a property that never
|
|
543
|
+
// went through the registry.
|
|
544
|
+
let relationDef: ResolvedRelation | undefined = (property as RelationProperty).resolvedRelation;
|
|
551
545
|
if (!relationDef && propertyKey) {
|
|
552
|
-
|
|
553
|
-
relationDef = findRelation(resolvedRelations, propertyKey);
|
|
554
|
-
}
|
|
555
|
-
if (!relationDef) {
|
|
556
|
-
relationDef = collection.relations?.find((rel: Relation) => rel.relationName === (property as RelationProperty).relationName);
|
|
546
|
+
relationDef = findRelation(resolveCollectionRelations(collection), propertyKey);
|
|
557
547
|
}
|
|
558
548
|
|
|
559
549
|
if (!relationDef) {
|
|
560
|
-
logger.warn(`
|
|
550
|
+
logger.warn(`ResolvedRelation not defined in property for key: ${propertyKey || "unknown"}`);
|
|
561
551
|
return value;
|
|
562
552
|
}
|
|
563
553
|
|
|
@@ -683,7 +673,7 @@ function normalizeScalarValues<M extends Record<string, unknown>>(
|
|
|
683
673
|
data: M,
|
|
684
674
|
properties: Properties,
|
|
685
675
|
collection: CollectionConfig,
|
|
686
|
-
resolvedRelations: Record<string,
|
|
676
|
+
resolvedRelations: Record<string, ResolvedRelation>,
|
|
687
677
|
options: { skipRelations: boolean }
|
|
688
678
|
): Record<string, unknown> {
|
|
689
679
|
const result: Record<string, unknown> = {};
|
|
@@ -691,7 +681,7 @@ function normalizeScalarValues<M extends Record<string, unknown>>(
|
|
|
691
681
|
// Identify FK columns used only for relations and not exposed as properties
|
|
692
682
|
const internalFKColumns = new Set<string>();
|
|
693
683
|
Object.values(resolvedRelations).forEach(relation => {
|
|
694
|
-
if (relation.
|
|
684
|
+
if (relation.kind === "belongsTo" && !properties[relation.localKey]) {
|
|
695
685
|
internalFKColumns.add(relation.localKey);
|
|
696
686
|
}
|
|
697
687
|
});
|
package/src/schema/doctor.ts
CHANGED
|
@@ -12,7 +12,7 @@ import * as fs from "fs";
|
|
|
12
12
|
import path from "path";
|
|
13
13
|
import { pathToFileURL } from "url";
|
|
14
14
|
import chalk from "chalk";
|
|
15
|
-
import { CollectionConfig, isPostgresCollectionConfig, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty } from "@rebasepro/types";
|
|
15
|
+
import { CollectionConfig, isPostgresCollectionConfig, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty, type ResolvedManyToMany, type ResolvedBelongsTo, isManyToMany } from "@rebasepro/types";
|
|
16
16
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
17
17
|
import { generateTypedefs } from "@rebasepro/codegen";
|
|
18
18
|
import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
@@ -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":
|
|
@@ -387,7 +387,7 @@ export async function checkCollectionsVsDatabase(
|
|
|
387
387
|
|
|
388
388
|
for (const collection of postgresCollections) {
|
|
389
389
|
const tableName = getTableName(collection);
|
|
390
|
-
const schemaName = collection.schema || "public";
|
|
390
|
+
const schemaName = (collection as { schema?: string }).schema || "public";
|
|
391
391
|
const fullTableName = schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
|
|
392
392
|
|
|
393
393
|
// Check table existence
|
|
@@ -411,10 +411,10 @@ export async function checkCollectionsVsDatabase(
|
|
|
411
411
|
// Check properties → columns
|
|
412
412
|
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
413
413
|
if (prop.type === "relation") {
|
|
414
|
-
//
|
|
414
|
+
// ResolvedRelation columns are derived from localKey
|
|
415
415
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
416
|
-
const relation = findRelation(resolvedRelations, (prop as RelationProperty).relationName ?? propName);
|
|
417
|
-
if (relation?.
|
|
416
|
+
const relation = findRelation(resolvedRelations, (prop as RelationProperty).relation?.relationName ?? propName);
|
|
417
|
+
if (relation?.kind === "belongsTo") {
|
|
418
418
|
const fkColName = relation.localKey;
|
|
419
419
|
if (!dbColumnMap.has(fkColName)) {
|
|
420
420
|
issues.push({
|
|
@@ -434,7 +434,7 @@ export async function checkCollectionsVsDatabase(
|
|
|
434
434
|
try {
|
|
435
435
|
const targetColl = relation.target();
|
|
436
436
|
targetTableName = getTableName(targetColl);
|
|
437
|
-
targetSchemaName = targetColl.schema || "public";
|
|
437
|
+
targetSchemaName = (targetColl as { schema?: string }).schema || "public";
|
|
438
438
|
} catch { /* ignore */ }
|
|
439
439
|
|
|
440
440
|
const hasFk = tableFks.some((fk) =>
|
|
@@ -568,9 +568,9 @@ export async function checkCollectionsVsDatabase(
|
|
|
568
568
|
// Also check junction tables for many-to-many relations
|
|
569
569
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
570
570
|
for (const relation of Object.values(resolvedRelations)) {
|
|
571
|
-
if (relation
|
|
571
|
+
if (isManyToMany(relation)) {
|
|
572
572
|
const junctionTable = relation.through.table;
|
|
573
|
-
const junctionSchema = collection.schema || "public";
|
|
573
|
+
const junctionSchema = (collection as { schema?: string }).schema || "public";
|
|
574
574
|
const fullJunctionTable = junctionSchema === "public" ? junctionTable : `${junctionSchema}.${junctionTable}`;
|
|
575
575
|
if (!existingTables.has(fullJunctionTable)) {
|
|
576
576
|
issues.push({
|
|
@@ -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));
|