@rebasepro/server-postgres 0.10.1-canary.ed8caed → 0.10.1-canary.g4db1bb1
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-BVvtkkRm.js → ensure-collection-tables-DGMYK0fr.js} +5 -5
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
- package/dist/index.es.js +610 -313
- package/dist/index.es.js.map +1 -1
- package/dist/services/RelationService.d.ts +9 -8
- package/dist/services/cdc/junction-tables.d.ts +38 -0
- package/dist/services/nested-path.d.ts +3 -3
- package/dist/services/realtimeService.d.ts +19 -0
- package/dist/services/row-pipeline.d.ts +2 -2
- package/dist/{src-lcfUP4xg.js → src-3VmUJ8Xn.js} +135 -247
- package/dist/src-3VmUJ8Xn.js.map +1 -0
- package/dist/{src-CBgtrPhJ.js → src-D5xBTl32.js} +12 -2
- package/dist/src-D5xBTl32.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +35 -23
- package/package.json +6 -6
- 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 +8 -8
- package/src/schema/generate-drizzle-schema-logic.ts +49 -101
- package/src/schema/generate-postgres-ddl-logic.ts +9 -9
- package/src/schema/introspect-db-logic.ts +19 -23
- package/src/services/FetchService.ts +12 -12
- package/src/services/PersistService.ts +23 -27
- package/src/services/RelationService.ts +65 -83
- package/src/services/cdc/junction-tables.ts +91 -0
- package/src/services/nested-path.ts +24 -9
- package/src/services/realtimeService.ts +60 -0
- package/src/services/row-pipeline.ts +5 -6
- package/src/utils/drizzle-conditions.ts +177 -382
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +0 -1
- package/dist/src-CBgtrPhJ.js.map +0 -1
- package/dist/src-lcfUP4xg.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";
|
|
@@ -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";
|
|
@@ -224,16 +224,11 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
224
224
|
case "relation": {
|
|
225
225
|
const refProp = prop as RelationProperty;
|
|
226
226
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
227
|
-
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
227
|
+
const relation = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
228
228
|
|
|
229
|
-
// Only
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// The localKey property is the source of truth for the FK column name.
|
|
235
|
-
if (!relation.localKey) {
|
|
236
|
-
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") {
|
|
237
232
|
return null;
|
|
238
233
|
}
|
|
239
234
|
|
|
@@ -402,26 +397,22 @@ const generateSinglePolicyCode = (collection: CollectionConfig, rule: SecurityRu
|
|
|
402
397
|
* Falls back to the local relation name when the counterpart can't be resolved.
|
|
403
398
|
*/
|
|
404
399
|
const computeSharedRelationName = (
|
|
405
|
-
rel:
|
|
400
|
+
rel: ResolvedRelation,
|
|
406
401
|
sourceCollection: CollectionConfig,
|
|
407
402
|
_collections: CollectionConfig[]
|
|
408
403
|
): string => {
|
|
409
404
|
const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
|
|
410
405
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// (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") {
|
|
416
410
|
const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
|
|
417
411
|
return `${getTableName(sourceCollection)}_${normalisedKey}`;
|
|
418
412
|
}
|
|
419
413
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// The owning table is the *target*, the FK column is foreignKeyOnTarget.
|
|
423
|
-
// Resolve to the Drizzle property key on the target so it matches the
|
|
424
|
-
// owning side's normalised localKey.
|
|
414
|
+
if (rel.kind === "hasMany" || rel.kind === "hasOne") {
|
|
415
|
+
// The owning table is the *target*; the column is foreignKeyOnTarget.
|
|
425
416
|
try {
|
|
426
417
|
const targetCollection = rel.target();
|
|
427
418
|
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
@@ -431,40 +422,8 @@ const computeSharedRelationName = (
|
|
|
431
422
|
}
|
|
432
423
|
}
|
|
433
424
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
if (rel.foreignKeyOnTarget) {
|
|
437
|
-
// FK lives on the target table — resolve to Drizzle property key
|
|
438
|
-
try {
|
|
439
|
-
const targetCollection = rel.target();
|
|
440
|
-
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
441
|
-
return `${getTableName(targetCollection)}_${normalisedFK}`;
|
|
442
|
-
} catch {
|
|
443
|
-
return fallback;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
// No explicit foreignKeyOnTarget — try to find the corresponding owning relation
|
|
447
|
-
try {
|
|
448
|
-
const targetCollection = rel.target();
|
|
449
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
450
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
451
|
-
targetRel.direction === "owning" &&
|
|
452
|
-
targetRel.cardinality === "one" &&
|
|
453
|
-
targetRel.localKey &&
|
|
454
|
-
targetRel.target().slug === sourceCollection.slug
|
|
455
|
-
);
|
|
456
|
-
if (correspondingRelation && correspondingRelation.localKey) {
|
|
457
|
-
return `${getTableName(targetCollection)}_${correspondingRelation.localKey}`;
|
|
458
|
-
}
|
|
459
|
-
} catch {
|
|
460
|
-
// ignore
|
|
461
|
-
}
|
|
462
|
-
return fallback;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// --- M2M owning (through) — keep local name (already shared via junction wiring) ---
|
|
466
|
-
// --- M2M inverse — keep local name ---
|
|
467
|
-
// --- 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.
|
|
468
427
|
return fallback;
|
|
469
428
|
};
|
|
470
429
|
|
|
@@ -521,7 +480,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
521
480
|
const allTablesToGenerate = new Map<string, {
|
|
522
481
|
collection: CollectionConfig,
|
|
523
482
|
isJunction?: boolean,
|
|
524
|
-
relation?:
|
|
483
|
+
relation?: ResolvedRelation,
|
|
525
484
|
sourceCollection?: CollectionConfig
|
|
526
485
|
}>();
|
|
527
486
|
|
|
@@ -560,7 +519,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
560
519
|
|
|
561
520
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
562
521
|
for (const relation of Object.values(resolvedRelations)) {
|
|
563
|
-
if (relation
|
|
522
|
+
if (isManyToMany(relation)) { // Standard M2M junction table
|
|
564
523
|
const junctionTableName = relation.through.table;
|
|
565
524
|
if (!allTablesToGenerate.has(junctionTableName)) {
|
|
566
525
|
allTablesToGenerate.set(junctionTableName, {
|
|
@@ -586,7 +545,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
586
545
|
sourceCollection
|
|
587
546
|
}] of allTablesToGenerate.entries()) {
|
|
588
547
|
const tableVarName = getTableVarName(tableName);
|
|
589
|
-
if (isJunction && relation && sourceCollection && relation
|
|
548
|
+
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
590
549
|
const targetCollection = relation.target();
|
|
591
550
|
const schema = (isPostgresCollectionConfig(targetCollection) ? targetCollection.schema : undefined) || (isPostgresCollectionConfig(sourceCollection) ? sourceCollection.schema : undefined);
|
|
592
551
|
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
@@ -673,7 +632,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
673
632
|
|
|
674
633
|
if (isJunction) {
|
|
675
634
|
const relationInfo = Array.from(allTablesToGenerate.values()).find(v => v.isJunction && getTableName(v.collection) === tableName);
|
|
676
|
-
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && relationInfo.relation
|
|
635
|
+
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && isManyToMany(relationInfo.relation)) {
|
|
677
636
|
const {
|
|
678
637
|
relation,
|
|
679
638
|
sourceCollection
|
|
@@ -697,9 +656,9 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
697
656
|
try {
|
|
698
657
|
const targetRelations = resolveCollectionRelations(targetCollection);
|
|
699
658
|
for (const [, targetRel] of Object.entries(targetRelations)) {
|
|
700
|
-
if (targetRel.
|
|
659
|
+
if (targetRel.kind !== "belongsTo" &&
|
|
701
660
|
targetRel.cardinality === "many" &&
|
|
702
|
-
targetRel.
|
|
661
|
+
targetRel.relationName === owningRelationName) {
|
|
703
662
|
inverseRelationName = targetRel.relationName ?? null;
|
|
704
663
|
break;
|
|
705
664
|
}
|
|
@@ -744,54 +703,43 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
744
703
|
// Skip if we've already emitted a relation with this drizzleRelationName
|
|
745
704
|
// for this table — prevents duplicate definitions when
|
|
746
705
|
// resolveCollectionRelations returns alias entries for the same FK.
|
|
747
|
-
const deduplicationKey = `${drizzleRelationName}::${rel.
|
|
706
|
+
const deduplicationKey = `${drizzleRelationName}::${rel.kind}`;
|
|
748
707
|
if (emittedRelationNames.has(deduplicationKey)) continue;
|
|
749
708
|
emittedRelationNames.add(deduplicationKey);
|
|
750
709
|
|
|
751
|
-
|
|
752
|
-
|
|
710
|
+
switch (rel.kind) {
|
|
711
|
+
case "belongsTo":
|
|
753
712
|
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
//
|
|
758
|
-
//
|
|
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
|
|
759
720
|
// undefined (reading 'referencedTable')".
|
|
760
721
|
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
// One-to-many inverse relation
|
|
722
|
+
break;
|
|
723
|
+
|
|
724
|
+
case "hasMany":
|
|
765
725
|
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
766
|
-
|
|
767
|
-
|
|
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.
|
|
768
734
|
const junctionTableVar = getTableVarName(rel.through.table);
|
|
769
735
|
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
770
|
-
|
|
771
|
-
// Many-to-many inverse relation - find the corresponding owning relation's junction table
|
|
772
|
-
try {
|
|
773
|
-
const targetCollection = rel.target();
|
|
774
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
775
|
-
|
|
776
|
-
// Find the corresponding owning many-to-many relation on the target
|
|
777
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find(targetRel =>
|
|
778
|
-
targetRel.direction === "owning" &&
|
|
779
|
-
targetRel.cardinality === "many" &&
|
|
780
|
-
targetRel.through &&
|
|
781
|
-
targetRel.relationName === rel.inverseRelationName
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
if (correspondingRelation && correspondingRelation.through) {
|
|
785
|
-
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
786
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
787
|
-
} else {
|
|
788
|
-
logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
789
|
-
}
|
|
790
|
-
} catch (e) {
|
|
791
|
-
logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
|
|
792
|
-
}
|
|
736
|
+
break;
|
|
793
737
|
}
|
|
794
|
-
|
|
738
|
+
|
|
739
|
+
case "via":
|
|
740
|
+
// A join chain is resolved at query time, not modelled
|
|
741
|
+
// as a Drizzle relation.
|
|
742
|
+
break;
|
|
795
743
|
}
|
|
796
744
|
} catch (e) {
|
|
797
745
|
logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
|
|
@@ -804,12 +752,12 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
|
|
|
804
752
|
|
|
805
753
|
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
806
754
|
for (const [otherKey, otherRel] of Object.entries(otherRelations)) {
|
|
807
|
-
if (otherRel
|
|
755
|
+
if (hasForeignKeyOnTarget(otherRel)) {
|
|
808
756
|
try {
|
|
809
757
|
const otherTarget = otherRel.target();
|
|
810
758
|
if (otherTarget.slug === collection.slug) {
|
|
811
759
|
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
812
|
-
const deduplicationKey = `${drizzleRelationName}::
|
|
760
|
+
const deduplicationKey = `${drizzleRelationName}::belongsTo`;
|
|
813
761
|
|
|
814
762
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
815
763
|
const otherTableVar = getTableVarName(getTableName(otherCollection));
|
|
@@ -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 } 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
|
|
|
@@ -170,9 +170,9 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
170
170
|
case "relation": {
|
|
171
171
|
const refProp = prop as RelationProperty;
|
|
172
172
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
173
|
-
const relation = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
174
|
-
if (
|
|
175
|
-
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)`);
|
|
176
176
|
}
|
|
177
177
|
let targetCollection: CollectionConfig;
|
|
178
178
|
try {
|
|
@@ -238,7 +238,7 @@ export const generatePostgresDdl = async (
|
|
|
238
238
|
const allTablesToGenerate = new Map<string, {
|
|
239
239
|
collection: CollectionConfig,
|
|
240
240
|
isJunction?: boolean,
|
|
241
|
-
relation?:
|
|
241
|
+
relation?: ResolvedRelation,
|
|
242
242
|
sourceCollection?: CollectionConfig
|
|
243
243
|
}>();
|
|
244
244
|
|
|
@@ -251,7 +251,7 @@ export const generatePostgresDdl = async (
|
|
|
251
251
|
|
|
252
252
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
253
253
|
for (const relation of Object.values(resolvedRelations)) {
|
|
254
|
-
if (relation
|
|
254
|
+
if (isManyToMany(relation)) {
|
|
255
255
|
const junctionTableName = relation.through.table;
|
|
256
256
|
if (!allTablesToGenerate.has(junctionTableName)) {
|
|
257
257
|
allTablesToGenerate.set(junctionTableName, {
|
|
@@ -284,7 +284,7 @@ export const generatePostgresDdl = async (
|
|
|
284
284
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
285
285
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
286
286
|
|
|
287
|
-
if (isJunction && relation && sourceCollection && relation
|
|
287
|
+
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
288
288
|
const targetCollection = relation.target();
|
|
289
289
|
const sourceTable = getTableName(sourceCollection);
|
|
290
290
|
const targetTable = getTableName(targetCollection);
|
|
@@ -336,9 +336,9 @@ export const generatePostgresDdl = async (
|
|
|
336
336
|
if (prop.type === "relation") {
|
|
337
337
|
const refProp = prop as RelationProperty;
|
|
338
338
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
339
|
-
const relInfo = findRelation(resolvedRelations, refProp.relationName ?? propName);
|
|
339
|
+
const relInfo = findRelation(resolvedRelations, refProp.relation?.relationName ?? propName);
|
|
340
340
|
|
|
341
|
-
if (
|
|
341
|
+
if (relInfo?.kind !== "belongsTo") {
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
344
|
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
780
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
}
|