@rebasepro/server-postgres 0.10.1-canary.3d09338 → 0.10.1-canary.60301c3
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 +100 -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 +507 -309
- package/dist/index.es.js.map +1 -1
- package/dist/services/RelationService.d.ts +9 -8
- package/dist/services/nested-path.d.ts +3 -3
- 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 +5 -1
- package/src/auth/ensure-tables.ts +23 -0
- package/src/auth/schema-version.ts +254 -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 +4 -4
- package/src/services/nested-path.ts +24 -9
- package/src/services/row-pipeline.ts +5 -6
- package/src/utils/drizzle-conditions.ts +156 -372
- 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/dist/index.es.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./chunk-DSJWtz9O.js";
|
|
5
|
-
import { a as isSchemaAdmin,
|
|
6
|
-
import { C as createRelationRefWithData, D as mergeDeep, E as getPolicyNamesForRule, O as camelCase, S as createRelationRef, T as updateDateAutoValues, _ as getTableVarName, a as getJunctionCollectionConfig, b as getDeclaredPrimaryKeys, c as getEffectiveSecurityRules, d as securityRuleToConditions, f as findAnonymousGrants, g as getTableName$1, h as getEnumVarName, i as CollectionRegistry, k as toSnakeCase, l as buildPropertyCallbacks, m as getColumnName, n as detectJunctionTables, o as getJunctionSecurityRules, p as findRelation, r as buildSdkData, s as resolveJunctionSpecs, t as classifyTable, u as policyToPostgres, v as resolveCollectionRelations, w as normalizeToEntityRelation, x as parseIdValues, y as buildCompositeId } from "./src-
|
|
5
|
+
import { a as isSchemaAdmin, c as hasForeignKeyOnTarget, d as isPostgresCollectionConfig, g as Vector, i as isSQLAdmin, l as isManyToMany, o as ANONYMOUS_USER_ID, r as isChannelBusInstance } from "./src-D5xBTl32.js";
|
|
6
|
+
import { C as createRelationRefWithData, D as mergeDeep, E as getPolicyNamesForRule, O as camelCase, S as createRelationRef, T as updateDateAutoValues, _ as getTableVarName, a as getJunctionCollectionConfig, b as getDeclaredPrimaryKeys, c as getEffectiveSecurityRules, d as securityRuleToConditions, f as findAnonymousGrants, g as getTableName$1, h as getEnumVarName, i as CollectionRegistry, k as toSnakeCase, l as buildPropertyCallbacks, m as getColumnName, n as detectJunctionTables, o as getJunctionSecurityRules, p as findRelation, r as buildSdkData, s as resolveJunctionSpecs, t as classifyTable, u as policyToPostgres, v as resolveCollectionRelations, w as normalizeToEntityRelation, x as parseIdValues, y as buildCompositeId } from "./src-3VmUJ8Xn.js";
|
|
7
7
|
import { Client, Pool } from "pg";
|
|
8
8
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
9
9
|
import { ApiError, createEmailService, extractUserFromToken, loadCollectionsFromDirectory, logger, safeCompare } from "@rebasepro/server";
|
|
@@ -413,28 +413,32 @@ var DrizzleConditionBuilder = class {
|
|
|
413
413
|
* it and the final step correlating to the outer row.
|
|
414
414
|
*/
|
|
415
415
|
static buildRelationScopeCondition(relation, parent, parentId, targetTable, targetIdColumn, registry) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
416
|
+
switch (relation.kind) {
|
|
417
|
+
case "via": {
|
|
418
|
+
const { table, idColumn } = parent();
|
|
419
|
+
return this.buildJoinPathScopeCondition(relation.joinPath, table, idColumn, parentId, targetTable, registry);
|
|
420
|
+
}
|
|
421
|
+
case "manyToMany": {
|
|
422
|
+
const { table: junctionName, sourceColumn, targetColumn } = relation.through;
|
|
423
|
+
const junctionTable = registry.getTable(junctionName);
|
|
424
|
+
if (!junctionTable) throw new Error(`Junction table not found: ${junctionName}`);
|
|
425
|
+
const sourceCol = junctionTable[sourceColumn];
|
|
426
|
+
const targetCol = junctionTable[targetColumn];
|
|
427
|
+
if (!sourceCol || !targetCol) throw new Error(`Junction columns '${sourceColumn}'/'${targetColumn}' not found in '${junctionName}'`);
|
|
428
|
+
return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
|
|
429
|
+
}
|
|
430
|
+
case "hasOne":
|
|
431
|
+
case "hasMany": {
|
|
432
|
+
const fkColumn = targetTable[relation.foreignKeyOnTarget];
|
|
433
|
+
if (!fkColumn) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'.`);
|
|
434
|
+
return eq(fkColumn, parentId);
|
|
435
|
+
}
|
|
436
|
+
case "belongsTo": {
|
|
437
|
+
const { table, idColumn } = parent();
|
|
438
|
+
return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
439
|
+
}
|
|
440
|
+
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
436
441
|
}
|
|
437
|
-
throw new Error(`Relation '${relation.relationName}' declares no \`foreignKeyOnTarget\`, \`through\`, \`joinPath\` or \`localKey\`, so there is no way to tell which target rows belong to a parent.`);
|
|
438
442
|
}
|
|
439
443
|
/**
|
|
440
444
|
* `EXISTS` for an explicit `joinPath`.
|
|
@@ -572,58 +576,43 @@ var DrizzleConditionBuilder = class {
|
|
|
572
576
|
/**
|
|
573
577
|
* Build relation-based conditions for different relation types
|
|
574
578
|
*/
|
|
579
|
+
/**
|
|
580
|
+
* Joins and where-conditions that reach a relation's target rows.
|
|
581
|
+
*
|
|
582
|
+
* One case per kind. This used to be a chain of six `else if`s over
|
|
583
|
+
* `cardinality`/`direction`/`through`, ending in
|
|
584
|
+
* `findCorrespondingJunctionTable` — a search through the *target's* own
|
|
585
|
+
* relations to work out whether an "inverse many" was a one-to-many or the
|
|
586
|
+
* far side of a junction. That search is gone: the kind says which it is.
|
|
587
|
+
*
|
|
588
|
+
* The owning/inverse split for junctions is gone too. Both variants built
|
|
589
|
+
* the identical condition — `through` is always written from the declaring
|
|
590
|
+
* side's point of view — so the second was a distinction without a
|
|
591
|
+
* difference and one of the places the two could drift apart.
|
|
592
|
+
*/
|
|
575
593
|
static buildRelationConditions(relation, parentId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry) {
|
|
576
|
-
console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
|
|
577
|
-
relationName: relation.relationName,
|
|
578
|
-
cardinality: relation.cardinality,
|
|
579
|
-
direction: relation.direction,
|
|
580
|
-
hasThrough: !!relation.through,
|
|
581
|
-
hasForeignKeyOnTarget: !!relation.foreignKeyOnTarget,
|
|
582
|
-
inverseRelationName: relation.inverseRelationName,
|
|
583
|
-
parentId
|
|
584
|
-
});
|
|
585
594
|
const joinConditions = [];
|
|
586
595
|
const whereConditions = [];
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
whereConditions.push(junctionResult.condition);
|
|
597
|
-
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
|
|
598
|
-
console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
|
|
599
|
-
const junctionResult = this.buildInverseJunctionTableConditions(relation.through, targetIdColumn, parentId, registry);
|
|
600
|
-
joinConditions.push(junctionResult.join);
|
|
601
|
-
whereConditions.push(junctionResult.condition);
|
|
602
|
-
} else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
|
|
603
|
-
console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
|
|
604
|
-
const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
|
|
605
|
-
if (junctionInfo) {
|
|
606
|
-
console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
|
|
607
|
-
const junctionResult = this.buildInverseJunctionTableConditions(junctionInfo, targetIdColumn, parentId, registry);
|
|
596
|
+
switch (relation.kind) {
|
|
597
|
+
case "via": {
|
|
598
|
+
const { joins, finalCondition } = this.buildJoinPathConditions(relation.joinPath, targetTable, parentTable, parentIdColumn, parentId, registry);
|
|
599
|
+
joinConditions.push(...joins);
|
|
600
|
+
whereConditions.push(finalCondition);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "manyToMany": {
|
|
604
|
+
const junctionResult = this.buildJunctionTableConditions(relation.through, targetIdColumn, parentId, registry);
|
|
608
605
|
joinConditions.push(junctionResult.join);
|
|
609
606
|
whereConditions.push(junctionResult.condition);
|
|
610
|
-
|
|
611
|
-
console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
|
|
612
|
-
const simpleCondition = this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId);
|
|
613
|
-
whereConditions.push(simpleCondition);
|
|
614
|
-
} else {
|
|
615
|
-
logger.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
|
|
616
|
-
throw new Error(`Cannot resolve inverse many relation '${relation.relationName}'. Either specify 'through' property, ensure corresponding owning relation exists with junction table configuration, or specify 'foreignKeyOnTarget' for one-to-many relationships.`);
|
|
607
|
+
break;
|
|
617
608
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
609
|
+
case "hasOne":
|
|
610
|
+
case "hasMany":
|
|
611
|
+
case "belongsTo":
|
|
612
|
+
whereConditions.push(this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId));
|
|
613
|
+
break;
|
|
614
|
+
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
622
615
|
}
|
|
623
|
-
console.debug("🔍 [buildRelationConditions] Final result:", {
|
|
624
|
-
joinConditionsCount: joinConditions.length,
|
|
625
|
-
whereConditionsCount: whereConditions.length
|
|
626
|
-
});
|
|
627
616
|
return {
|
|
628
617
|
joinConditions,
|
|
629
618
|
whereConditions
|
|
@@ -763,48 +752,24 @@ var DrizzleConditionBuilder = class {
|
|
|
763
752
|
};
|
|
764
753
|
}
|
|
765
754
|
/**
|
|
766
|
-
*
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (!junctionSourceCol) throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
|
|
774
|
-
if (!junctionTargetCol) throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
|
|
775
|
-
const condition = Array.isArray(parentId) ? inArray(junctionSourceCol, parentId) : eq(junctionSourceCol, parentId);
|
|
776
|
-
return {
|
|
777
|
-
join: {
|
|
778
|
-
table: junctionTable,
|
|
779
|
-
condition: eq(targetIdColumn, junctionTargetCol)
|
|
780
|
-
},
|
|
781
|
-
condition
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Build conditions for simple relations (owning/inverse without join paths)
|
|
755
|
+
* The condition for a relation whose link is a single column.
|
|
756
|
+
*
|
|
757
|
+
* Two cases. It had five: two of them existed only to throw ("should not be
|
|
758
|
+
* called directly", "lacks proper configuration"), and one guessed a column
|
|
759
|
+
* name by appending `_id` to `inverseRelationName` when no foreign key had
|
|
760
|
+
* been resolved. All three were reachable only because the old type let a
|
|
761
|
+
* relation arrive here under-specified. It cannot now.
|
|
786
762
|
*/
|
|
787
763
|
static buildSimpleRelationCondition(relation, targetTable, parentTable, parentId) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const foreignKeyCol = targetTable[relation.foreignKeyOnTarget];
|
|
798
|
-
if (!foreignKeyCol) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table. This might be a many-to-many relationship that requires a junction table. Consider using 'through' property or ensure the corresponding owning relation exists with junction table configuration.`);
|
|
799
|
-
return Array.isArray(parentId) ? inArray(foreignKeyCol, parentId) : eq(foreignKeyCol, parentId);
|
|
800
|
-
} else if (relation.direction === "inverse" && relation.cardinality === "many" && relation.inverseRelationName) throw new Error(`Inverse many-to-many relation '${relation.relationName}' requires a junction table. Either specify 'through' property or ensure the corresponding owning relation exists with junction table configuration.`);
|
|
801
|
-
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
|
|
802
|
-
const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
|
|
803
|
-
const foreignKeyCol = targetTable[inferredForeignKeyName];
|
|
804
|
-
if (!foreignKeyCol) throw new Error(`Auto-inferred foreign key column '${inferredForeignKeyName}' not found in target table for inverse relation '${relation.relationName}'. Please specify 'foreignKeyOnTarget' explicitly.`);
|
|
805
|
-
console.debug(`🔍 [DrizzleConditionBuilder] Auto-inferred foreign key '${inferredForeignKeyName}' for inverse relation '${relation.relationName}'`);
|
|
806
|
-
return Array.isArray(parentId) ? inArray(foreignKeyCol, parentId) : eq(foreignKeyCol, parentId);
|
|
807
|
-
} else throw new Error(`Relation '${relation.relationName}' lacks proper configuration. For many-to-many relations, use 'through' property. For simple relations, use 'localKey' or 'foreignKeyOnTarget'.`);
|
|
764
|
+
const match = (column) => Array.isArray(parentId) ? inArray(column, parentId) : eq(column, parentId);
|
|
765
|
+
if (relation.kind === "belongsTo") {
|
|
766
|
+
const targetIdCol = Object.values(targetTable).find((col) => col.primary) ?? Object.values(targetTable).find((col) => col.name === "id");
|
|
767
|
+
if (!targetIdCol) throw new Error(`No primary key or "id" column in the target table of relation '${relation.relationName}'.`);
|
|
768
|
+
return match(targetIdCol);
|
|
769
|
+
}
|
|
770
|
+
const foreignKeyCol = targetTable[relation.foreignKeyOnTarget];
|
|
771
|
+
if (!foreignKeyCol) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'. A link through a junction is \`kind: "manyToMany"\`.`);
|
|
772
|
+
return match(foreignKeyCol);
|
|
808
773
|
}
|
|
809
774
|
/**
|
|
810
775
|
* Combine multiple conditions with AND operator
|
|
@@ -859,16 +824,23 @@ var DrizzleConditionBuilder = class {
|
|
|
859
824
|
return query;
|
|
860
825
|
}
|
|
861
826
|
/**
|
|
862
|
-
*
|
|
827
|
+
* A count over a relation's target rows.
|
|
828
|
+
*
|
|
829
|
+
* The junction case counts `distinct` because the caller's query joins the
|
|
830
|
+
* junction; the owning/inverse pair that used to sit here built the same
|
|
831
|
+
* query twice.
|
|
863
832
|
*/
|
|
864
833
|
static buildRelationCountQuery(baseCountQuery, relation, parentId, targetTable, parentTable, parentIdColumn, targetIdColumn, registry, additionalFilters) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
834
|
+
switch (relation.kind) {
|
|
835
|
+
case "via": return this.buildJoinPathCountQuery(baseCountQuery, relation.joinPath, targetTable, parentTable, parentIdColumn, parentId, registry, additionalFilters);
|
|
836
|
+
case "manyToMany": return this.buildJunctionCountQuery(baseCountQuery, relation.through, targetIdColumn, parentId, registry, additionalFilters);
|
|
837
|
+
case "belongsTo":
|
|
838
|
+
case "hasOne":
|
|
839
|
+
case "hasMany": {
|
|
840
|
+
const allConditions = [this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId), ...additionalFilters ?? []];
|
|
841
|
+
return baseCountQuery.where(and(...allConditions));
|
|
842
|
+
}
|
|
843
|
+
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
872
844
|
}
|
|
873
845
|
}
|
|
874
846
|
/**
|
|
@@ -909,20 +881,6 @@ var DrizzleConditionBuilder = class {
|
|
|
909
881
|
return baseCountQuery.innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol)).where(and(...baseConditions));
|
|
910
882
|
}
|
|
911
883
|
/**
|
|
912
|
-
* Build inverse junction table conditions for count queries
|
|
913
|
-
*/
|
|
914
|
-
static buildInverseJunctionCountQuery(baseCountQuery, through, targetIdColumn, parentId, registry, additionalFilters) {
|
|
915
|
-
const junctionTable = registry.getTable(through.table);
|
|
916
|
-
if (!junctionTable) throw new Error(`Junction table not found: ${through.table}`);
|
|
917
|
-
const junctionSourceCol = junctionTable[through.sourceColumn];
|
|
918
|
-
const junctionTargetCol = junctionTable[through.targetColumn];
|
|
919
|
-
if (!junctionSourceCol) throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
|
|
920
|
-
if (!junctionTargetCol) throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
|
|
921
|
-
const baseConditions = [eq(junctionSourceCol, parentId)];
|
|
922
|
-
if (additionalFilters && additionalFilters.length > 0) baseConditions.push(...additionalFilters);
|
|
923
|
-
return baseCountQuery.innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol)).where(and(...baseConditions));
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
884
|
* Helper method to extract table names from columns
|
|
927
885
|
*/
|
|
928
886
|
static getTableNamesFromColumns(columns) {
|
|
@@ -937,49 +895,6 @@ var DrizzleConditionBuilder = class {
|
|
|
937
895
|
return [getColumnName(columns)];
|
|
938
896
|
}
|
|
939
897
|
/**
|
|
940
|
-
* Find the corresponding junction table for an inverse many-to-many relation
|
|
941
|
-
*/
|
|
942
|
-
static findCorrespondingJunctionTable(relation, registry) {
|
|
943
|
-
try {
|
|
944
|
-
console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
|
|
945
|
-
if (!relation.inverseRelationName) {
|
|
946
|
-
console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
|
|
947
|
-
return null;
|
|
948
|
-
}
|
|
949
|
-
const targetCollection = relation.target();
|
|
950
|
-
console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
|
|
951
|
-
const targetCollectionRelations = resolveCollectionRelations(targetCollection);
|
|
952
|
-
console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
|
|
953
|
-
const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
|
|
954
|
-
if (!correspondingRelation) {
|
|
955
|
-
console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
|
|
956
|
-
return null;
|
|
957
|
-
}
|
|
958
|
-
console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
|
|
959
|
-
relationName: correspondingRelation.relationName,
|
|
960
|
-
cardinality: correspondingRelation.cardinality,
|
|
961
|
-
direction: correspondingRelation.direction,
|
|
962
|
-
hasThrough: !!correspondingRelation.through
|
|
963
|
-
});
|
|
964
|
-
if (correspondingRelation.cardinality !== "many" || correspondingRelation.direction !== "owning" || !correspondingRelation.through) {
|
|
965
|
-
console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
|
|
966
|
-
return null;
|
|
967
|
-
}
|
|
968
|
-
console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
|
|
969
|
-
const through = correspondingRelation.through;
|
|
970
|
-
const result = {
|
|
971
|
-
table: through.table,
|
|
972
|
-
sourceColumn: through.targetColumn,
|
|
973
|
-
targetColumn: through.sourceColumn
|
|
974
|
-
};
|
|
975
|
-
console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
|
|
976
|
-
return result;
|
|
977
|
-
} catch (error) {
|
|
978
|
-
logger.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}'`, { error });
|
|
979
|
-
return null;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
898
|
* Build vector similarity search expressions for pgvector.
|
|
984
899
|
*
|
|
985
900
|
* Returns:
|
|
@@ -1056,7 +971,7 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1056
971
|
const joinPathRelationUpdates = [];
|
|
1057
972
|
const foreignKeys = /* @__PURE__ */ new Set();
|
|
1058
973
|
Object.values(resolvedRelations).forEach((relation) => {
|
|
1059
|
-
if (relation.
|
|
974
|
+
if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
|
|
1060
975
|
});
|
|
1061
976
|
for (const [key, value] of Object.entries(row)) {
|
|
1062
977
|
const property = properties[key];
|
|
@@ -1068,11 +983,11 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1068
983
|
if (property.type === "relation" && collection) {
|
|
1069
984
|
const relation = findRelation(resolvedRelations, key);
|
|
1070
985
|
if (relation) {
|
|
1071
|
-
if (relation.
|
|
986
|
+
if (relation.kind === "belongsTo") {
|
|
1072
987
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
1073
988
|
if (serializedValue !== void 0) result[relation.localKey] = serializedValue;
|
|
1074
989
|
continue;
|
|
1075
|
-
} else if (relation
|
|
990
|
+
} else if (hasForeignKeyOnTarget(relation)) {
|
|
1076
991
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
1077
992
|
inverseRelationUpdates.push({
|
|
1078
993
|
relationKey: key,
|
|
@@ -1080,7 +995,7 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1080
995
|
newValue: serializedValue
|
|
1081
996
|
});
|
|
1082
997
|
continue;
|
|
1083
|
-
} else if (relation.
|
|
998
|
+
} else if (relation.kind === "via") {
|
|
1084
999
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
1085
1000
|
if (relation.cardinality === "one") joinPathRelationUpdates.push({
|
|
1086
1001
|
relationKey: key,
|
|
@@ -1093,14 +1008,6 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1093
1008
|
newValue: serializedValue
|
|
1094
1009
|
});
|
|
1095
1010
|
continue;
|
|
1096
|
-
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
1097
|
-
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
1098
|
-
joinPathRelationUpdates.push({
|
|
1099
|
-
relationKey: key,
|
|
1100
|
-
relation,
|
|
1101
|
-
newTargetId: serializedValue
|
|
1102
|
-
});
|
|
1103
|
-
continue;
|
|
1104
1011
|
}
|
|
1105
1012
|
}
|
|
1106
1013
|
}
|
|
@@ -1186,7 +1093,7 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
1186
1093
|
for (const [propKey, property] of Object.entries(properties)) if (property.type === "relation" && !(propKey in result)) {
|
|
1187
1094
|
const relation = findRelation(resolvedRelations, propKey);
|
|
1188
1095
|
if (relation) {
|
|
1189
|
-
if (relation.
|
|
1096
|
+
if (relation.kind === "belongsTo" && relation.localKey in data) {
|
|
1190
1097
|
const fkValue = data[relation.localKey];
|
|
1191
1098
|
if (fkValue !== null && fkValue !== void 0) try {
|
|
1192
1099
|
const targetCollection = relation.target();
|
|
@@ -1194,7 +1101,7 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
1194
1101
|
} catch (e) {
|
|
1195
1102
|
logger.warn(`Could not resolve target collection for relation property: ${propKey}`, { error: e });
|
|
1196
1103
|
}
|
|
1197
|
-
} else if (relation
|
|
1104
|
+
} else if (hasForeignKeyOnTarget(relation) && db && registry) try {
|
|
1198
1105
|
const targetCollection = relation.target();
|
|
1199
1106
|
const targetTable = registry.getTable(getTableName$1(targetCollection));
|
|
1200
1107
|
const currentId = buildCompositeId(data, getPrimaryKeys(collection, registry));
|
|
@@ -1215,7 +1122,7 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
1215
1122
|
} catch (e) {
|
|
1216
1123
|
logger.warn(`Could not resolve inverse relation property: ${propKey}`, { error: e });
|
|
1217
1124
|
}
|
|
1218
|
-
else if (relation.
|
|
1125
|
+
else if (relation.kind === "via" && db && registry) try {
|
|
1219
1126
|
const targetCollection = relation.target();
|
|
1220
1127
|
const pks = getPrimaryKeys(collection, registry);
|
|
1221
1128
|
const currentId = buildCompositeId(data, pks);
|
|
@@ -1332,11 +1239,10 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
1332
1239
|
}
|
|
1333
1240
|
case "relation":
|
|
1334
1241
|
if (typeof value === "string" || typeof value === "number") {
|
|
1335
|
-
let relationDef = property.
|
|
1242
|
+
let relationDef = property.resolvedRelation;
|
|
1336
1243
|
if (!relationDef && propertyKey) relationDef = findRelation(resolveCollectionRelations(collection), propertyKey);
|
|
1337
|
-
if (!relationDef) relationDef = collection.relations?.find((rel) => rel.relationName === property.relationName);
|
|
1338
1244
|
if (!relationDef) {
|
|
1339
|
-
logger.warn(`
|
|
1245
|
+
logger.warn(`ResolvedRelation not defined in property for key: ${propertyKey || "unknown"}`);
|
|
1340
1246
|
return value;
|
|
1341
1247
|
}
|
|
1342
1248
|
try {
|
|
@@ -1436,7 +1342,7 @@ function normalizeScalarValues(data, properties, collection, resolvedRelations,
|
|
|
1436
1342
|
const result = {};
|
|
1437
1343
|
const internalFKColumns = /* @__PURE__ */ new Set();
|
|
1438
1344
|
Object.values(resolvedRelations).forEach((relation) => {
|
|
1439
|
-
if (relation.
|
|
1345
|
+
if (relation.kind === "belongsTo" && !properties[relation.localKey]) internalFKColumns.add(relation.localKey);
|
|
1440
1346
|
});
|
|
1441
1347
|
for (const [key, value] of Object.entries(data)) {
|
|
1442
1348
|
if (internalFKColumns.has(key)) {
|
|
@@ -1574,7 +1480,7 @@ var RelationService = class {
|
|
|
1574
1480
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
1575
1481
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1576
1482
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1577
|
-
if (relation.
|
|
1483
|
+
if (relation.kind === "via") {
|
|
1578
1484
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
1579
1485
|
let currentTable = parentTable;
|
|
1580
1486
|
for (const join of relation.joinPath) {
|
|
@@ -1689,8 +1595,8 @@ var RelationService = class {
|
|
|
1689
1595
|
* `tags` table and delete by primary key.
|
|
1690
1596
|
*/
|
|
1691
1597
|
async unlinkRelatedEntity(tx, hop, targetId) {
|
|
1598
|
+
if (!isManyToMany(hop.relation)) throw new Error(`Relation '${hop.relationKey}' has no junction table to unlink through`);
|
|
1692
1599
|
const through = hop.relation.through;
|
|
1693
|
-
if (!through) throw new Error(`Relation '${hop.relationKey}' has no junction table to unlink through`);
|
|
1694
1600
|
const junctionTable = this.registry.getTable(through.table);
|
|
1695
1601
|
if (!junctionTable) throw new Error(`Junction table not found: ${through.table}`);
|
|
1696
1602
|
const sourceJunctionColumn = junctionTable[through.sourceColumn];
|
|
@@ -1720,7 +1626,7 @@ var RelationService = class {
|
|
|
1720
1626
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1721
1627
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1722
1628
|
const parsedParentIds = parentIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
1723
|
-
if (relation.
|
|
1629
|
+
if (relation.kind === "via") {
|
|
1724
1630
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
1725
1631
|
let currentTable = parentTable;
|
|
1726
1632
|
for (const join of relation.joinPath) {
|
|
@@ -1749,7 +1655,7 @@ var RelationService = class {
|
|
|
1749
1655
|
}
|
|
1750
1656
|
return resultMap;
|
|
1751
1657
|
}
|
|
1752
|
-
if (relation.
|
|
1658
|
+
if (relation.kind === "belongsTo") {
|
|
1753
1659
|
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
1754
1660
|
const localKeyCol = parentTable[relation.localKey];
|
|
1755
1661
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
@@ -1783,7 +1689,7 @@ var RelationService = class {
|
|
|
1783
1689
|
}
|
|
1784
1690
|
return resultMap;
|
|
1785
1691
|
}
|
|
1786
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget
|
|
1692
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
1787
1693
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1788
1694
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
1789
1695
|
const results = await query;
|
|
@@ -1792,8 +1698,7 @@ var RelationService = class {
|
|
|
1792
1698
|
for (const row of results) {
|
|
1793
1699
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
1794
1700
|
let parentId;
|
|
1795
|
-
if (relation
|
|
1796
|
-
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
1701
|
+
if (hasForeignKeyOnTarget(relation)) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
1797
1702
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
1798
1703
|
}
|
|
1799
1704
|
return resultMap;
|
|
@@ -1816,7 +1721,7 @@ var RelationService = class {
|
|
|
1816
1721
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1817
1722
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1818
1723
|
const parsedParentIds = parentIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
1819
|
-
if (relation.
|
|
1724
|
+
if (relation.kind === "via") {
|
|
1820
1725
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
1821
1726
|
let currentTable = parentTable;
|
|
1822
1727
|
for (const join of relation.joinPath) {
|
|
@@ -1846,7 +1751,7 @@ var RelationService = class {
|
|
|
1846
1751
|
}
|
|
1847
1752
|
return resultMap;
|
|
1848
1753
|
}
|
|
1849
|
-
if (relation.
|
|
1754
|
+
if (relation.kind === "manyToMany") {
|
|
1850
1755
|
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
1851
1756
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
1852
1757
|
if (!junctionTable) {
|
|
@@ -1872,7 +1777,7 @@ var RelationService = class {
|
|
|
1872
1777
|
}
|
|
1873
1778
|
return resultMap;
|
|
1874
1779
|
}
|
|
1875
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget
|
|
1780
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
1876
1781
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1877
1782
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
1878
1783
|
const results = await query;
|
|
@@ -1881,9 +1786,7 @@ var RelationService = class {
|
|
|
1881
1786
|
for (const row of results) {
|
|
1882
1787
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
1883
1788
|
let parentId;
|
|
1884
|
-
if (relation
|
|
1885
|
-
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
1886
|
-
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
1789
|
+
if (hasForeignKeyOnTarget(relation)) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
1887
1790
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
1888
1791
|
const key = String(parentId);
|
|
1889
1792
|
const arr = resultMap.get(key) || [];
|
|
@@ -1903,7 +1806,7 @@ var RelationService = class {
|
|
|
1903
1806
|
if (!relation || relation.cardinality !== "many") continue;
|
|
1904
1807
|
const targetEntityIds = value && Array.isArray(value) ? value.map((rel) => rel.id) : [];
|
|
1905
1808
|
const targetCollection = relation.target();
|
|
1906
|
-
if (relation.
|
|
1809
|
+
if (relation.kind === "via") {
|
|
1907
1810
|
const parentTableName = getTableName$1(collection);
|
|
1908
1811
|
const targetTableName = getTableName$1(targetCollection);
|
|
1909
1812
|
let junctionTable = void 0;
|
|
@@ -1948,7 +1851,7 @@ var RelationService = class {
|
|
|
1948
1851
|
}));
|
|
1949
1852
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
1950
1853
|
}
|
|
1951
|
-
} else if (relation.
|
|
1854
|
+
} else if (relation.kind === "manyToMany") {
|
|
1952
1855
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
1953
1856
|
if (!junctionTable) {
|
|
1954
1857
|
logger.warn(`Junction table '${relation.through.table}' not found for relation '${key}' in collection '${collection.slug}'`);
|
|
@@ -1973,8 +1876,7 @@ var RelationService = class {
|
|
|
1973
1876
|
}));
|
|
1974
1877
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
1975
1878
|
}
|
|
1976
|
-
} else if (relation.
|
|
1977
|
-
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
1879
|
+
} else if (relation.cardinality === "many" && hasForeignKeyOnTarget(relation)) {
|
|
1978
1880
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
1979
1881
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
1980
1882
|
const targetIdInfo = targetPks[0];
|
|
@@ -2008,28 +1910,20 @@ var RelationService = class {
|
|
|
2008
1910
|
const targetIdInfo = targetPks[0];
|
|
2009
1911
|
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
2010
1912
|
const sourceIdInfo = sourcePks[0];
|
|
2011
|
-
if (relation.
|
|
1913
|
+
if (relation.kind === "via") {
|
|
2012
1914
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
2013
1915
|
continue;
|
|
2014
1916
|
}
|
|
2015
|
-
if (relation
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
targetColumn: targetRelation.through.sourceColumn
|
|
2023
|
-
};
|
|
2024
|
-
break;
|
|
2025
|
-
}
|
|
2026
|
-
if (junctionInfo) {
|
|
2027
|
-
await this.updateManyToManyInverseRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue, junctionInfo);
|
|
2028
|
-
continue;
|
|
2029
|
-
}
|
|
1917
|
+
if (isManyToMany(relation)) {
|
|
1918
|
+
await this.updateManyToManyInverseRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue, {
|
|
1919
|
+
table: relation.through.table,
|
|
1920
|
+
sourceColumn: relation.through.sourceColumn,
|
|
1921
|
+
targetColumn: relation.through.targetColumn
|
|
1922
|
+
});
|
|
1923
|
+
continue;
|
|
2030
1924
|
}
|
|
2031
|
-
if (!relation
|
|
2032
|
-
logger.warn(`
|
|
1925
|
+
if (!hasForeignKeyOnTarget(relation)) {
|
|
1926
|
+
logger.warn(`Relation '${relation.relationName}' has no column on the target to write. Skipping.`);
|
|
2033
1927
|
continue;
|
|
2034
1928
|
}
|
|
2035
1929
|
const foreignKeyColumn = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -2055,10 +1949,6 @@ var RelationService = class {
|
|
|
2055
1949
|
*/
|
|
2056
1950
|
async updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue) {
|
|
2057
1951
|
try {
|
|
2058
|
-
if (!relation.joinPath || relation.joinPath.length === 0) {
|
|
2059
|
-
logger.warn(`Inverse relation '${relation.relationName}' missing joinPath`);
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
1952
|
const sourceTableName = getTableName$1(sourceCollection);
|
|
2063
1953
|
const targetTableName = getTableName$1(targetCollection);
|
|
2064
1954
|
const intermediateTables = relation.joinPath.map((step) => step.table).filter((table) => table !== sourceTableName && table !== targetTableName);
|
|
@@ -2258,9 +2148,8 @@ var RelationService = class {
|
|
|
2258
2148
|
* to unwrap that same level back out.
|
|
2259
2149
|
*/
|
|
2260
2150
|
function isJunctionRelation(relation) {
|
|
2261
|
-
if (relation
|
|
2262
|
-
|
|
2263
|
-
return false;
|
|
2151
|
+
if (isManyToMany(relation)) return true;
|
|
2152
|
+
return relation.kind === "via" && relation.joinPath.length > 1;
|
|
2264
2153
|
}
|
|
2265
2154
|
/**
|
|
2266
2155
|
* Reach the target row inside a junction row.
|
|
@@ -2450,7 +2339,7 @@ function resolveNestedPath(path, registry) {
|
|
|
2450
2339
|
* such a path addresses the *link*, not the row.
|
|
2451
2340
|
*/
|
|
2452
2341
|
function isJunctionBackedRelation(relation) {
|
|
2453
|
-
return
|
|
2342
|
+
return relation.shared;
|
|
2454
2343
|
}
|
|
2455
2344
|
/**
|
|
2456
2345
|
* Reject a nested write whose final segment is a to-one relation.
|
|
@@ -2463,7 +2352,10 @@ function isJunctionBackedRelation(relation) {
|
|
|
2463
2352
|
* one.
|
|
2464
2353
|
*/
|
|
2465
2354
|
function assertWritableThrough(hop, path) {
|
|
2466
|
-
|
|
2355
|
+
const { relation } = hop;
|
|
2356
|
+
if (relation.writable && relation.cardinality === "many") return;
|
|
2357
|
+
if (!relation.writable) throw ApiError.badRequest(`"${path}" ends in '${hop.relationKey}', a \`via\` relation. Rebase will not infer how to write through an arbitrary join chain — write the row at "${hop.targetCollection.slug}" directly.`, "RELATION_NOT_WRITABLE");
|
|
2358
|
+
throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
|
|
2467
2359
|
}
|
|
2468
2360
|
//#endregion
|
|
2469
2361
|
//#region src/services/FetchService.ts
|
|
@@ -2502,7 +2394,7 @@ var FetchService = class {
|
|
|
2502
2394
|
let orderByField = table[orderBy];
|
|
2503
2395
|
if (!orderByField && collection) {
|
|
2504
2396
|
const property = collection.properties[orderBy];
|
|
2505
|
-
if (property && property.type === "relation" && "relation" in property && property.
|
|
2397
|
+
if (property && property.type === "relation" && "relation" in property && property.resolvedRelation?.kind === "belongsTo") orderByField = table[`${orderBy}_id`];
|
|
2506
2398
|
}
|
|
2507
2399
|
return orderByField;
|
|
2508
2400
|
}
|
|
@@ -2523,7 +2415,7 @@ var FetchService = class {
|
|
|
2523
2415
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
2524
2416
|
if (!shouldInclude(key)) continue;
|
|
2525
2417
|
const drizzleRelName = relation.relationName || key;
|
|
2526
|
-
if (relation.
|
|
2418
|
+
if (relation.kind === "via") continue;
|
|
2527
2419
|
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
2528
2420
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
2529
2421
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
@@ -2537,7 +2429,7 @@ var FetchService = class {
|
|
|
2537
2429
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
2538
2430
|
*/
|
|
2539
2431
|
getJunctionTargetRelationName(relation, _collection) {
|
|
2540
|
-
if (relation
|
|
2432
|
+
if (isManyToMany(relation)) return relation.through.targetColumn.replace(/_id$/, "_id");
|
|
2541
2433
|
return null;
|
|
2542
2434
|
}
|
|
2543
2435
|
/**
|
|
@@ -2547,7 +2439,7 @@ var FetchService = class {
|
|
|
2547
2439
|
*/
|
|
2548
2440
|
async resolveJoinPathRelations(row, collection, collectionPath, parsedId, _databaseId) {
|
|
2549
2441
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
2550
|
-
const promises = Object.entries(resolvedRelations).filter(([key, relation]) => relation.
|
|
2442
|
+
const promises = Object.entries(resolvedRelations).filter(([key, relation]) => relation.kind === "via").map(async ([key, relation]) => {
|
|
2551
2443
|
try {
|
|
2552
2444
|
const relatedRows = await this.relationService.fetchRelatedEntities(collectionPath, parsedId, key, { limit: relation.cardinality === "one" ? 1 : void 0 });
|
|
2553
2445
|
if (relation.cardinality === "one" && relatedRows.length > 0) {
|
|
@@ -2569,7 +2461,7 @@ var FetchService = class {
|
|
|
2569
2461
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
2570
2462
|
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
2571
2463
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
2572
|
-
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.
|
|
2464
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.kind === "via" && propertyKeys.has(key) && shouldInclude(key));
|
|
2573
2465
|
if (joinPathRelations.length === 0) return;
|
|
2574
2466
|
const parentIdOf = (row) => {
|
|
2575
2467
|
const address = buildCompositeId(row, idInfoArray);
|
|
@@ -2723,7 +2615,7 @@ var FetchService = class {
|
|
|
2723
2615
|
return flatRow;
|
|
2724
2616
|
} catch (e) {
|
|
2725
2617
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
2726
|
-
logger.error(`[FetchService]
|
|
2618
|
+
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
2727
2619
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
2728
2620
|
}
|
|
2729
2621
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
@@ -2774,7 +2666,7 @@ var FetchService = class {
|
|
|
2774
2666
|
return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
|
|
2775
2667
|
} catch (e) {
|
|
2776
2668
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
2777
|
-
logger.error(`[FetchService]
|
|
2669
|
+
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
2778
2670
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
2779
2671
|
}
|
|
2780
2672
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
@@ -2989,7 +2881,7 @@ var FetchService = class {
|
|
|
2989
2881
|
return restRows;
|
|
2990
2882
|
} catch (e) {
|
|
2991
2883
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
2992
|
-
logger.error(`[FetchService]
|
|
2884
|
+
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
2993
2885
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
2994
2886
|
}
|
|
2995
2887
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
@@ -3052,7 +2944,7 @@ var FetchService = class {
|
|
|
3052
2944
|
return restRow;
|
|
3053
2945
|
} catch (e) {
|
|
3054
2946
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
3055
|
-
logger.error(`[FetchService]
|
|
2947
|
+
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3056
2948
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
3057
2949
|
}
|
|
3058
2950
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
@@ -3200,7 +3092,7 @@ var FetchService = class {
|
|
|
3200
3092
|
if (parentIds.length === 0) return /* @__PURE__ */ new Map();
|
|
3201
3093
|
const relation = resolveCollectionRelations(getCollectionByPath(parentCollectionPath, this.registry))[relationKey];
|
|
3202
3094
|
if (!relation) {
|
|
3203
|
-
logger.warn(`[batchFetchManyRelatedRows]
|
|
3095
|
+
logger.warn(`[batchFetchManyRelatedRows] ResolvedRelation '${relationKey}' not found, skipping`);
|
|
3204
3096
|
return /* @__PURE__ */ new Map();
|
|
3205
3097
|
}
|
|
3206
3098
|
const entityMap = await this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
|
|
@@ -3425,7 +3317,7 @@ var PersistService = class {
|
|
|
3425
3317
|
assertWritableThrough(hop, collectionPath);
|
|
3426
3318
|
if (!await this.relationService.isRelated(hop, id)) throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to delete.`);
|
|
3427
3319
|
if (isJunctionBackedRelation(hop.relation)) {
|
|
3428
|
-
if (!hop.relation
|
|
3320
|
+
if (!isManyToMany(hop.relation)) throw ApiError.badRequest(`"${collectionPath}" reaches '${hop.targetCollection.slug}' through a multi-hop joinPath, so there is no single link to remove. Delete the row at "${hop.targetCollection.slug}" directly if that is what you meant.`, "RELATION_NOT_UNLINKABLE");
|
|
3429
3321
|
await this.relationService.unlinkRelatedEntity(this.db, hop, id);
|
|
3430
3322
|
return;
|
|
3431
3323
|
}
|
|
@@ -3461,16 +3353,13 @@ var PersistService = class {
|
|
|
3461
3353
|
* do, and first — stamped the parent's own foreign key onto the child row.
|
|
3462
3354
|
*/
|
|
3463
3355
|
resolveParentForeignKeyColumn(hop) {
|
|
3464
|
-
const { relation
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
return DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.to)[0];
|
|
3356
|
+
const { relation } = hop;
|
|
3357
|
+
switch (relation.kind) {
|
|
3358
|
+
case "hasOne":
|
|
3359
|
+
case "hasMany": return relation.foreignKeyOnTarget;
|
|
3360
|
+
case "via": return relation.joinPath.length === 1 ? DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to)[0] : void 0;
|
|
3361
|
+
default: return;
|
|
3471
3362
|
}
|
|
3472
|
-
if (relation.joinPath && relation.joinPath.length > 1) return;
|
|
3473
|
-
throw ApiError.badRequest(`Relation '${relationKey}' on '${hop.parentCollection.slug}' cannot be written through: it declares no \`foreignKeyOnTarget\` (the column on '${targetCollection.slug}' that records the parent) and no \`joinPath\`.`, "RELATION_NOT_WRITABLE");
|
|
3474
3363
|
}
|
|
3475
3364
|
/**
|
|
3476
3365
|
* Save an row (create or update)
|
|
@@ -3493,7 +3382,7 @@ var PersistService = class {
|
|
|
3493
3382
|
const parentPks = getPrimaryKeys(hop.parentCollection, this.registry);
|
|
3494
3383
|
return parseIdValues(hop.parentId, parentPks)[parentPks[0].fieldName];
|
|
3495
3384
|
};
|
|
3496
|
-
if (hop.relation
|
|
3385
|
+
if (isManyToMany(hop.relation)) junctionTableInfo = {
|
|
3497
3386
|
parentCollection: hop.parentCollection,
|
|
3498
3387
|
parentId: parentIdForWrite(),
|
|
3499
3388
|
relation: hop.relation,
|
|
@@ -5546,12 +5435,8 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5546
5435
|
break;
|
|
5547
5436
|
case "relation": {
|
|
5548
5437
|
const refProp = prop;
|
|
5549
|
-
const relation = findRelation(resolveCollectionRelations(collection), refProp.relationName ?? propName);
|
|
5550
|
-
if (!relation || relation.
|
|
5551
|
-
if (!relation.localKey) {
|
|
5552
|
-
logger.warn(`Could not generate column for owning relation '${relation.relationName}' on '${collection.name}': 'localKey' is not defined.`);
|
|
5553
|
-
return null;
|
|
5554
|
-
}
|
|
5438
|
+
const relation = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
|
|
5439
|
+
if (!relation || relation.kind !== "belongsTo") return null;
|
|
5555
5440
|
if (collection.properties[relation.localKey] && propName !== relation.localKey) return null;
|
|
5556
5441
|
let targetCollection;
|
|
5557
5442
|
try {
|
|
@@ -5645,33 +5530,17 @@ var generateSinglePolicyCode = (collection, rule, operation, policyName, resolve
|
|
|
5645
5530
|
*/
|
|
5646
5531
|
var computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
5647
5532
|
const fallback = rel.relationName ?? toSnakeCase(rel.target().slug);
|
|
5648
|
-
if (rel.
|
|
5533
|
+
if (rel.kind === "belongsTo") {
|
|
5649
5534
|
const normalisedKey = resolvePropertyKeyForColumn(sourceCollection, rel.localKey);
|
|
5650
5535
|
return `${getTableName$1(sourceCollection)}_${normalisedKey}`;
|
|
5651
5536
|
}
|
|
5652
|
-
if (rel.
|
|
5537
|
+
if (rel.kind === "hasMany" || rel.kind === "hasOne") try {
|
|
5653
5538
|
const targetCollection = rel.target();
|
|
5654
5539
|
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
5655
5540
|
return `${getTableName$1(targetCollection)}_${normalisedFK}`;
|
|
5656
5541
|
} catch {
|
|
5657
5542
|
return fallback;
|
|
5658
5543
|
}
|
|
5659
|
-
if (rel.direction === "inverse" && rel.cardinality === "one") {
|
|
5660
|
-
if (rel.foreignKeyOnTarget) try {
|
|
5661
|
-
const targetCollection = rel.target();
|
|
5662
|
-
const normalisedFK = resolvePropertyKeyForColumn(targetCollection, rel.foreignKeyOnTarget);
|
|
5663
|
-
return `${getTableName$1(targetCollection)}_${normalisedFK}`;
|
|
5664
|
-
} catch {
|
|
5665
|
-
return fallback;
|
|
5666
|
-
}
|
|
5667
|
-
try {
|
|
5668
|
-
const targetCollection = rel.target();
|
|
5669
|
-
const targetResolvedRelations = resolveCollectionRelations(targetCollection);
|
|
5670
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey && targetRel.target().slug === sourceCollection.slug);
|
|
5671
|
-
if (correspondingRelation && correspondingRelation.localKey) return `${getTableName$1(targetCollection)}_${correspondingRelation.localKey}`;
|
|
5672
|
-
} catch {}
|
|
5673
|
-
return fallback;
|
|
5674
|
-
}
|
|
5675
5544
|
return fallback;
|
|
5676
5545
|
};
|
|
5677
5546
|
var generateSchema = async (collections, stripPolicies = false) => {
|
|
@@ -5736,7 +5605,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5736
5605
|
const tableName = getTableName$1(collection);
|
|
5737
5606
|
if (tableName) allTablesToGenerate.set(tableName, { collection });
|
|
5738
5607
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5739
|
-
for (const relation of Object.values(resolvedRelations)) if (relation
|
|
5608
|
+
for (const relation of Object.values(resolvedRelations)) if (isManyToMany(relation)) {
|
|
5740
5609
|
const junctionTableName = relation.through.table;
|
|
5741
5610
|
if (!allTablesToGenerate.has(junctionTableName)) allTablesToGenerate.set(junctionTableName, {
|
|
5742
5611
|
collection: {
|
|
@@ -5751,7 +5620,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5751
5620
|
}
|
|
5752
5621
|
for (const [tableName, { collection, isJunction, relation, sourceCollection }] of allTablesToGenerate.entries()) {
|
|
5753
5622
|
const tableVarName = getTableVarName(tableName);
|
|
5754
|
-
if (isJunction && relation && sourceCollection && relation
|
|
5623
|
+
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
5755
5624
|
const targetCollection = relation.target();
|
|
5756
5625
|
const schema = (isPostgresCollectionConfig(targetCollection) ? targetCollection.schema : void 0) || (isPostgresCollectionConfig(sourceCollection) ? sourceCollection.schema : void 0);
|
|
5757
5626
|
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
@@ -5805,7 +5674,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5805
5674
|
const tableRelations = [];
|
|
5806
5675
|
if (isJunction) {
|
|
5807
5676
|
const relationInfo = Array.from(allTablesToGenerate.values()).find((v) => v.isJunction && getTableName$1(v.collection) === tableName);
|
|
5808
|
-
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && relationInfo.relation
|
|
5677
|
+
if (relationInfo && relationInfo.relation && relationInfo.sourceCollection && isManyToMany(relationInfo.relation)) {
|
|
5809
5678
|
const { relation, sourceCollection } = relationInfo;
|
|
5810
5679
|
const targetCollection = relation.target();
|
|
5811
5680
|
const sourceTableVar = getTableVarName(getTableName$1(sourceCollection));
|
|
@@ -5817,7 +5686,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5817
5686
|
let inverseRelationName = null;
|
|
5818
5687
|
try {
|
|
5819
5688
|
const targetRelations = resolveCollectionRelations(targetCollection);
|
|
5820
|
-
for (const [, targetRel] of Object.entries(targetRelations)) if (targetRel.
|
|
5689
|
+
for (const [, targetRel] of Object.entries(targetRelations)) if (targetRel.kind !== "belongsTo" && targetRel.cardinality === "many" && targetRel.relationName === owningRelationName) {
|
|
5821
5690
|
inverseRelationName = targetRel.relationName ?? null;
|
|
5822
5691
|
break;
|
|
5823
5692
|
}
|
|
@@ -5833,27 +5702,25 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5833
5702
|
const target = rel.target();
|
|
5834
5703
|
const targetTableVar = getTableVarName(getTableName$1(target));
|
|
5835
5704
|
const drizzleRelationName = computeSharedRelationName(rel, collection, collections);
|
|
5836
|
-
const deduplicationKey = `${drizzleRelationName}::${rel.
|
|
5705
|
+
const deduplicationKey = `${drizzleRelationName}::${rel.kind}`;
|
|
5837
5706
|
if (emittedRelationNames.has(deduplicationKey)) continue;
|
|
5838
5707
|
emittedRelationNames.add(deduplicationKey);
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5708
|
+
switch (rel.kind) {
|
|
5709
|
+
case "belongsTo":
|
|
5710
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${rel.localKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
5711
|
+
break;
|
|
5712
|
+
case "hasOne":
|
|
5713
|
+
tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
5714
|
+
break;
|
|
5715
|
+
case "hasMany":
|
|
5716
|
+
tableRelations.push(` "${relationKey}": many(${targetTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
5717
|
+
break;
|
|
5718
|
+
case "manyToMany": {
|
|
5845
5719
|
const junctionTableVar = getTableVarName(rel.through.table);
|
|
5846
5720
|
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
5847
|
-
|
|
5848
|
-
const targetResolvedRelations = resolveCollectionRelations(rel.target());
|
|
5849
|
-
const correspondingRelation = Object.values(targetResolvedRelations).find((targetRel) => targetRel.direction === "owning" && targetRel.cardinality === "many" && targetRel.through && targetRel.relationName === rel.inverseRelationName);
|
|
5850
|
-
if (correspondingRelation && correspondingRelation.through) {
|
|
5851
|
-
const junctionTableVar = getTableVarName(correspondingRelation.through.table);
|
|
5852
|
-
tableRelations.push(` "${relationKey}": many(${junctionTableVar}, { relationName: \"${drizzleRelationName}\" })`);
|
|
5853
|
-
} else logger.warn(`Could not find corresponding owning many-to-many relation for inverse relation '${relationKey}' on '${collection.name}'`);
|
|
5854
|
-
} catch (e) {
|
|
5855
|
-
logger.warn(`Could not resolve inverse many-to-many relation '${relationKey}'`, { error: e });
|
|
5721
|
+
break;
|
|
5856
5722
|
}
|
|
5723
|
+
case "via": break;
|
|
5857
5724
|
}
|
|
5858
5725
|
} catch (e) {
|
|
5859
5726
|
logger.warn(`Could not generate relation ${relationKey} for ${collection.name}`, { error: e });
|
|
@@ -5861,10 +5728,10 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5861
5728
|
for (const otherCollection of collections) {
|
|
5862
5729
|
if (otherCollection.slug === collection.slug) continue;
|
|
5863
5730
|
const otherRelations = resolveCollectionRelations(otherCollection);
|
|
5864
|
-
for (const [otherKey, otherRel] of Object.entries(otherRelations)) if (otherRel
|
|
5731
|
+
for (const [otherKey, otherRel] of Object.entries(otherRelations)) if (hasForeignKeyOnTarget(otherRel)) try {
|
|
5865
5732
|
if (otherRel.target().slug === collection.slug) {
|
|
5866
5733
|
const drizzleRelationName = computeSharedRelationName(otherRel, otherCollection, collections);
|
|
5867
|
-
const deduplicationKey = `${drizzleRelationName}::
|
|
5734
|
+
const deduplicationKey = `${drizzleRelationName}::belongsTo`;
|
|
5868
5735
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
5869
5736
|
const otherTableVar = getTableVarName(getTableName$1(otherCollection));
|
|
5870
5737
|
const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
|
|
@@ -5986,8 +5853,8 @@ function collectJunctionLinks(registry) {
|
|
|
5986
5853
|
continue;
|
|
5987
5854
|
}
|
|
5988
5855
|
for (const [relationKey, relation] of Object.entries(relations)) {
|
|
5989
|
-
|
|
5990
|
-
|
|
5856
|
+
if (!isManyToMany(relation)) continue;
|
|
5857
|
+
const through = relation.through;
|
|
5991
5858
|
const key = `${collection.slug}::${relationKey}::${through.table}`;
|
|
5992
5859
|
if (seen.has(key)) continue;
|
|
5993
5860
|
seen.add(key);
|
|
@@ -9080,8 +8947,8 @@ var PostgresCollectionRegistry = class extends CollectionRegistry {
|
|
|
9080
8947
|
*/
|
|
9081
8948
|
getRelationKeysForCollection(collectionPath) {
|
|
9082
8949
|
const collection = this.getCollectionByPath(collectionPath);
|
|
9083
|
-
if (!collection
|
|
9084
|
-
return
|
|
8950
|
+
if (!collection) return [];
|
|
8951
|
+
return Object.keys(resolveCollectionRelations(collection));
|
|
9085
8952
|
}
|
|
9086
8953
|
};
|
|
9087
8954
|
//#endregion
|
|
@@ -18032,6 +17899,178 @@ function formatBytes(bytes) {
|
|
|
18032
17899
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
18033
17900
|
}
|
|
18034
17901
|
//#endregion
|
|
17902
|
+
//#region src/collections/validate-relations.ts
|
|
17903
|
+
/**
|
|
17904
|
+
* Every name a column answers to: the key in the drizzle schema and the real
|
|
17905
|
+
* column name in Postgres. A relation may legitimately be written with either,
|
|
17906
|
+
* and reporting a working relation as broken is worse than not checking.
|
|
17907
|
+
*/
|
|
17908
|
+
function columnNames(table) {
|
|
17909
|
+
const names = /* @__PURE__ */ new Set();
|
|
17910
|
+
for (const [key, col] of Object.entries(getTableColumns(table))) {
|
|
17911
|
+
names.add(key);
|
|
17912
|
+
const dbName = col?.name;
|
|
17913
|
+
if (dbName) names.add(dbName);
|
|
17914
|
+
}
|
|
17915
|
+
return names;
|
|
17916
|
+
}
|
|
17917
|
+
var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
17918
|
+
/** `on.from` / `on.to` accept a single column or a composite tuple. */
|
|
17919
|
+
var asColumns = (value) => Array.isArray(value) ? value : [value];
|
|
17920
|
+
/**
|
|
17921
|
+
* Relations whose names do not resolve against the registered schema.
|
|
17922
|
+
*
|
|
17923
|
+
* Fails open wherever it cannot see enough to be sure — an unregistered source
|
|
17924
|
+
* table, a target belonging to another backend — because a false alarm here
|
|
17925
|
+
* costs more than a missed one: it would block boot on a working app.
|
|
17926
|
+
*/
|
|
17927
|
+
function findRelationDefects(collections, registry) {
|
|
17928
|
+
const defects = [];
|
|
17929
|
+
const registeredSlugs = new Set(registry.getCollections().map((c) => c.slug));
|
|
17930
|
+
for (const collection of collections) {
|
|
17931
|
+
const sourceTableName = getTableName$1(collection);
|
|
17932
|
+
const sourceTable = registry.getTable(sourceTableName);
|
|
17933
|
+
if (!sourceTable) continue;
|
|
17934
|
+
const sourceColumns = columnNames(sourceTable);
|
|
17935
|
+
const relations = resolveCollectionRelations(collection);
|
|
17936
|
+
for (const relation of Object.values(relations)) {
|
|
17937
|
+
const at = {
|
|
17938
|
+
collection: collection.slug,
|
|
17939
|
+
relationName: relation.relationName,
|
|
17940
|
+
kind: relation.kind
|
|
17941
|
+
};
|
|
17942
|
+
let targetCollection;
|
|
17943
|
+
try {
|
|
17944
|
+
targetCollection = relation.target();
|
|
17945
|
+
} catch (e) {
|
|
17946
|
+
defects.push({
|
|
17947
|
+
...at,
|
|
17948
|
+
problem: `its \`target()\` threw: ${e instanceof Error ? e.message : String(e)}`,
|
|
17949
|
+
fix: "a target thunk usually throws because of a circular import — make sure it is `() => otherCollection` and not evaluated at module load"
|
|
17950
|
+
});
|
|
17951
|
+
continue;
|
|
17952
|
+
}
|
|
17953
|
+
if (!registeredSlugs.has(targetCollection.slug)) continue;
|
|
17954
|
+
const targetTableName = getTableName$1(targetCollection);
|
|
17955
|
+
const targetTable = registry.getTable(targetTableName);
|
|
17956
|
+
if (!targetTable) {
|
|
17957
|
+
defects.push({
|
|
17958
|
+
...at,
|
|
17959
|
+
problem: `it points at collection \`${targetCollection.slug}\`, which has no table \`${targetTableName}\` in the schema`,
|
|
17960
|
+
fix: `create the \`${targetTableName}\` table, or correct \`table\` on the \`${targetCollection.slug}\` collection`
|
|
17961
|
+
});
|
|
17962
|
+
continue;
|
|
17963
|
+
}
|
|
17964
|
+
const targetColumns = columnNames(targetTable);
|
|
17965
|
+
switch (relation.kind) {
|
|
17966
|
+
case "belongsTo":
|
|
17967
|
+
if (!sourceColumns.has(relation.localKey)) defects.push({
|
|
17968
|
+
...at,
|
|
17969
|
+
problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
|
|
17970
|
+
fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
|
|
17971
|
+
});
|
|
17972
|
+
break;
|
|
17973
|
+
case "hasOne":
|
|
17974
|
+
case "hasMany":
|
|
17975
|
+
if (!targetColumns.has(relation.foreignKeyOnTarget)) defects.push({
|
|
17976
|
+
...at,
|
|
17977
|
+
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
17978
|
+
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
17979
|
+
});
|
|
17980
|
+
break;
|
|
17981
|
+
case "manyToMany": {
|
|
17982
|
+
const { table, sourceColumn, targetColumn } = relation.through;
|
|
17983
|
+
const junction = registry.getTable(table);
|
|
17984
|
+
if (!junction) {
|
|
17985
|
+
defects.push({
|
|
17986
|
+
...at,
|
|
17987
|
+
problem: `its junction table \`${table}\` does not exist`,
|
|
17988
|
+
fix: `create \`${table}\`, or name the real one with \`through: { table: "..." }\`. Note that an omitted \`through.table\` is derived from the two table names sorted and joined, so renaming a table changes it`
|
|
17989
|
+
});
|
|
17990
|
+
break;
|
|
17991
|
+
}
|
|
17992
|
+
const junctionColumns = columnNames(junction);
|
|
17993
|
+
for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) defects.push({
|
|
17994
|
+
...at,
|
|
17995
|
+
problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
|
|
17996
|
+
fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
17997
|
+
});
|
|
17998
|
+
break;
|
|
17999
|
+
}
|
|
18000
|
+
case "via": {
|
|
18001
|
+
if (relation.joinPath.length === 0) {
|
|
18002
|
+
defects.push({
|
|
18003
|
+
...at,
|
|
18004
|
+
problem: "its `joinPath` is empty, so it joins nothing",
|
|
18005
|
+
fix: "add at least one step, ending at the target's table"
|
|
18006
|
+
});
|
|
18007
|
+
break;
|
|
18008
|
+
}
|
|
18009
|
+
let prevName = sourceTableName;
|
|
18010
|
+
let prevColumns = sourceColumns;
|
|
18011
|
+
let broken = false;
|
|
18012
|
+
for (const [i, step] of relation.joinPath.entries()) {
|
|
18013
|
+
const stepTable = registry.getTable(step.table);
|
|
18014
|
+
if (!stepTable) {
|
|
18015
|
+
defects.push({
|
|
18016
|
+
...at,
|
|
18017
|
+
problem: `step ${i + 1} of its \`joinPath\` joins \`${step.table}\`, which is not a table in the schema`,
|
|
18018
|
+
fix: `correct \`joinPath[${i}].table\``
|
|
18019
|
+
});
|
|
18020
|
+
broken = true;
|
|
18021
|
+
break;
|
|
18022
|
+
}
|
|
18023
|
+
const stepColumns = columnNames(stepTable);
|
|
18024
|
+
for (const column of asColumns(step.on.from)) if (!prevColumns.has(column)) defects.push({
|
|
18025
|
+
...at,
|
|
18026
|
+
problem: `step ${i + 1} joins \`${prevName}.${column}\` → \`${step.table}\`, but \`${column}\` is not a column on \`${prevName}\``,
|
|
18027
|
+
fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote(prevColumns)}`
|
|
18028
|
+
});
|
|
18029
|
+
for (const column of asColumns(step.on.to)) if (!stepColumns.has(column)) defects.push({
|
|
18030
|
+
...at,
|
|
18031
|
+
problem: `step ${i + 1} joins into \`${step.table}.${column}\`, but \`${column}\` is not a column on \`${step.table}\``,
|
|
18032
|
+
fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote(stepColumns)}`
|
|
18033
|
+
});
|
|
18034
|
+
if (asColumns(step.on.from).length !== asColumns(step.on.to).length) defects.push({
|
|
18035
|
+
...at,
|
|
18036
|
+
problem: `step ${i + 1} compares ${asColumns(step.on.from).length} column(s) against ${asColumns(step.on.to).length}`,
|
|
18037
|
+
fix: `\`from\` and \`to\` must name the same number of columns in \`joinPath[${i}]\``
|
|
18038
|
+
});
|
|
18039
|
+
prevName = step.table;
|
|
18040
|
+
prevColumns = stepColumns;
|
|
18041
|
+
}
|
|
18042
|
+
if (!broken && prevName !== targetTableName) defects.push({
|
|
18043
|
+
...at,
|
|
18044
|
+
problem: `its \`joinPath\` ends at \`${prevName}\`, but it targets \`${targetCollection.slug}\` (table \`${targetTableName}\`)`,
|
|
18045
|
+
fix: `make the last step join \`${targetTableName}\`, or point \`target\` at the collection backed by \`${prevName}\``
|
|
18046
|
+
});
|
|
18047
|
+
break;
|
|
18048
|
+
}
|
|
18049
|
+
default: throw new Error(`Unhandled relation kind: ${JSON.stringify(relation)}`);
|
|
18050
|
+
}
|
|
18051
|
+
}
|
|
18052
|
+
}
|
|
18053
|
+
return defects;
|
|
18054
|
+
}
|
|
18055
|
+
/**
|
|
18056
|
+
* Fail boot on any relation that cannot resolve, listing all of them at once.
|
|
18057
|
+
*
|
|
18058
|
+
* Deliberately fatal rather than a warning. Every one of these produces an
|
|
18059
|
+
* empty result at query time and nothing else — an empty tab, an empty
|
|
18060
|
+
* `include`, a subcollection that looks like it has no rows. A server that
|
|
18061
|
+
* refuses to start is recoverable in a minute; a relation that quietly answers
|
|
18062
|
+
* "nothing" is the kind of bug found in production, weeks later, by a user
|
|
18063
|
+
* asking where their data went.
|
|
18064
|
+
*/
|
|
18065
|
+
function assertRelationsResolve(collections, registry) {
|
|
18066
|
+
const defects = findRelationDefects(collections, registry);
|
|
18067
|
+
if (defects.length === 0) return;
|
|
18068
|
+
const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
|
|
18069
|
+
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against the database schema.\n\nEach of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
|
|
18070
|
+
|
|
18071
|
+
` + lines.join("\n\n") + "\n");
|
|
18072
|
+
}
|
|
18073
|
+
//#endregion
|
|
18035
18074
|
//#region src/collections/buildRegistry.ts
|
|
18036
18075
|
/**
|
|
18037
18076
|
* Build the collection registry for a driver.
|
|
@@ -18056,8 +18095,163 @@ function buildCollectionRegistry(schema) {
|
|
|
18056
18095
|
if (schema.enums) registry.registerEnums(schema.enums);
|
|
18057
18096
|
if (schema.relations) registry.registerRelations(schema.relations);
|
|
18058
18097
|
warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
|
|
18098
|
+
assertRelationsResolve(registry.getCollections(), registry);
|
|
18059
18099
|
return registry;
|
|
18060
18100
|
}
|
|
18101
|
+
/** Key under which the version is stored in the auth schema's meta table. */
|
|
18102
|
+
var VERSION_KEY = "auth_schema_version";
|
|
18103
|
+
/**
|
|
18104
|
+
* Columns `refresh_tokens` must have for the current runtime's auth write path
|
|
18105
|
+
* to work. Checked by the health probe so a database that drifted *below* this
|
|
18106
|
+
* runtime is reported as unhealthy rather than discovered one failed login at a
|
|
18107
|
+
* time. Kept in step with the migration in `ensureAuthTablesExist`.
|
|
18108
|
+
*/
|
|
18109
|
+
var REQUIRED_REFRESH_TOKEN_COLUMNS = [
|
|
18110
|
+
"session_id",
|
|
18111
|
+
"revoked",
|
|
18112
|
+
"rotated_at",
|
|
18113
|
+
"session_started_at"
|
|
18114
|
+
];
|
|
18115
|
+
/**
|
|
18116
|
+
* A constraint whose *presence* means the database is still at version 1, in a
|
|
18117
|
+
* shape this runtime's rotation logic cannot write to: it makes two live tokens
|
|
18118
|
+
* of one rotating session collide.
|
|
18119
|
+
*/
|
|
18120
|
+
var RETIRED_REFRESH_TOKEN_CONSTRAINT = "unique_device_session";
|
|
18121
|
+
/**
|
|
18122
|
+
* Thrown when the database was migrated by a runtime newer than this one.
|
|
18123
|
+
*
|
|
18124
|
+
* Distinct class rather than a bare `Error` because `ensureAuthTablesExist`
|
|
18125
|
+
* wraps its migrations in a catch that deliberately swallows failures and
|
|
18126
|
+
* continues — every other problem there is better survived than crashed on.
|
|
18127
|
+
* This one is not, so the catch rethrows on this type specifically.
|
|
18128
|
+
*/
|
|
18129
|
+
var AuthSchemaVersionError = class extends Error {
|
|
18130
|
+
databaseVersion;
|
|
18131
|
+
runtimeVersion;
|
|
18132
|
+
constructor(databaseVersion, runtimeVersion) {
|
|
18133
|
+
super(`Auth schema version mismatch: the database is at version ${databaseVersion}, but this runtime understands version ${runtimeVersion}.\n\nA newer version of the framework has already migrated this database. Running this older runtime against it would boot cleanly and then fail every login and token refresh, because the auth schema it expects no longer exists.
|
|
18134
|
+
|
|
18135
|
+
Refusing to start. Deploy a framework version at or above the one that migrated this database, or restore the database from a backup taken before the upgrade.`);
|
|
18136
|
+
this.name = "AuthSchemaVersionError";
|
|
18137
|
+
this.databaseVersion = databaseVersion;
|
|
18138
|
+
this.runtimeVersion = runtimeVersion;
|
|
18139
|
+
}
|
|
18140
|
+
};
|
|
18141
|
+
/**
|
|
18142
|
+
* The schema the auth tables live in, derived exactly as `ensureAuthTablesExist`
|
|
18143
|
+
* derives it. Shared so the two cannot drift: a stamp written to one schema and
|
|
18144
|
+
* read from another would read as "never stamped" forever.
|
|
18145
|
+
*/
|
|
18146
|
+
function resolveAuthSchema(collection) {
|
|
18147
|
+
if (!collection) return "rebase";
|
|
18148
|
+
const usersSchema = "schema" in collection && typeof collection.schema === "string" ? collection.schema : "public";
|
|
18149
|
+
return usersSchema === "public" ? "rebase" : usersSchema;
|
|
18150
|
+
}
|
|
18151
|
+
/**
|
|
18152
|
+
* Read the stamped version, or `null` when the database has never been stamped.
|
|
18153
|
+
*
|
|
18154
|
+
* `null` is not an error and must not be treated as one: every database
|
|
18155
|
+
* provisioned before this file existed is unstamped, and so is every fresh one.
|
|
18156
|
+
* Uses `to_regclass` rather than selecting straight from the table so a missing
|
|
18157
|
+
* schema or table is a `null` rather than a thrown 42P01.
|
|
18158
|
+
*/
|
|
18159
|
+
async function readAuthSchemaVersion(db, authSchema) {
|
|
18160
|
+
const qualified = `"${authSchema}"."schema_meta"`;
|
|
18161
|
+
if (!(await db.execute(sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`)).rows[0]?.present) return null;
|
|
18162
|
+
const raw = (await db.execute(sql`
|
|
18163
|
+
SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}
|
|
18164
|
+
`)).rows[0]?.value;
|
|
18165
|
+
if (raw === void 0) return null;
|
|
18166
|
+
const parsed = Number.parseInt(raw, 10);
|
|
18167
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
18168
|
+
}
|
|
18169
|
+
/**
|
|
18170
|
+
* Refuse to run against a database a newer runtime has already migrated.
|
|
18171
|
+
*
|
|
18172
|
+
* Deliberately one-directional. A database *older* than this runtime is the
|
|
18173
|
+
* normal upgrade path — the migrations in `ensureAuthTablesExist` are about to
|
|
18174
|
+
* bring it forward, so it is not an error. Only the reverse is unrecoverable.
|
|
18175
|
+
*/
|
|
18176
|
+
async function assertAuthSchemaCompatible(db, authSchema) {
|
|
18177
|
+
const databaseVersion = await readAuthSchemaVersion(db, authSchema);
|
|
18178
|
+
if (databaseVersion !== null && databaseVersion > 2) throw new AuthSchemaVersionError(databaseVersion, 2);
|
|
18179
|
+
}
|
|
18180
|
+
/**
|
|
18181
|
+
* Record that this runtime's migrations have been applied.
|
|
18182
|
+
*
|
|
18183
|
+
* Called at the end of `ensureAuthTablesExist`, so a boot that failed partway
|
|
18184
|
+
* through leaves the older stamp in place and the next boot migrates again.
|
|
18185
|
+
*/
|
|
18186
|
+
async function stampAuthSchemaVersion(db, authSchema) {
|
|
18187
|
+
const qualified = `"${authSchema}"."schema_meta"`;
|
|
18188
|
+
await db.execute(sql`
|
|
18189
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (
|
|
18190
|
+
key TEXT PRIMARY KEY,
|
|
18191
|
+
value TEXT NOT NULL,
|
|
18192
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
18193
|
+
)
|
|
18194
|
+
`);
|
|
18195
|
+
await db.execute(sql`
|
|
18196
|
+
INSERT INTO ${sql.raw(qualified)} (key, value)
|
|
18197
|
+
VALUES (${VERSION_KEY}, ${String(2)})
|
|
18198
|
+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
|
18199
|
+
`);
|
|
18200
|
+
}
|
|
18201
|
+
/**
|
|
18202
|
+
* Check that the auth schema is one this runtime can actually write to.
|
|
18203
|
+
*
|
|
18204
|
+
* Two independent checks, because either alone has a blind spot:
|
|
18205
|
+
*
|
|
18206
|
+
* - The **stamp** catches a runtime older than the database. It is the precise
|
|
18207
|
+
* signal, but it is blind on every database provisioned before stamping
|
|
18208
|
+
* existed — which today is all of them.
|
|
18209
|
+
* - The **structure** catches a database older than the runtime, and works on
|
|
18210
|
+
* unstamped databases. It is what makes this useful immediately rather than
|
|
18211
|
+
* one upgrade cycle from now.
|
|
18212
|
+
*
|
|
18213
|
+
* Never throws: a probe that fails to run reports unhealthy with the reason, so
|
|
18214
|
+
* a broken check surfaces as a degraded health response rather than a 500 from
|
|
18215
|
+
* the health endpoint itself.
|
|
18216
|
+
*/
|
|
18217
|
+
async function probeAuthSchema(db, authSchema) {
|
|
18218
|
+
const problems = [];
|
|
18219
|
+
let databaseVersion = null;
|
|
18220
|
+
try {
|
|
18221
|
+
databaseVersion = await readAuthSchemaVersion(db, authSchema);
|
|
18222
|
+
if (databaseVersion !== null && databaseVersion > 2) problems.push(`database is at auth schema version ${databaseVersion}, this runtime understands 2 — it was migrated by a newer framework version`);
|
|
18223
|
+
const refreshTokens = `"${authSchema}"."refresh_tokens"`;
|
|
18224
|
+
if (!(await db.execute(sql`SELECT to_regclass(${refreshTokens}) IS NOT NULL AS present`)).rows[0]?.present) return {
|
|
18225
|
+
healthy: problems.length === 0,
|
|
18226
|
+
databaseVersion,
|
|
18227
|
+
runtimeVersion: 2,
|
|
18228
|
+
problems
|
|
18229
|
+
};
|
|
18230
|
+
const columns = await db.execute(sql`
|
|
18231
|
+
SELECT column_name FROM information_schema.columns
|
|
18232
|
+
WHERE table_schema = ${authSchema} AND table_name = 'refresh_tokens'
|
|
18233
|
+
`);
|
|
18234
|
+
const found = new Set(columns.rows.map((row) => row.column_name));
|
|
18235
|
+
const missing = REQUIRED_REFRESH_TOKEN_COLUMNS.filter((column) => !found.has(column));
|
|
18236
|
+
if (missing.length > 0) problems.push(`refresh_tokens is missing ${missing.join(", ")} — the auth migrations have not been applied to this database, so token rotation will fail`);
|
|
18237
|
+
if ((await db.execute(sql`
|
|
18238
|
+
SELECT 1 FROM pg_constraint c
|
|
18239
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
18240
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
18241
|
+
WHERE n.nspname = ${authSchema}
|
|
18242
|
+
AND t.relname = 'refresh_tokens'
|
|
18243
|
+
AND c.conname = ${RETIRED_REFRESH_TOKEN_CONSTRAINT}
|
|
18244
|
+
`)).rows.length > 0) problems.push(`refresh_tokens still carries ${RETIRED_REFRESH_TOKEN_CONSTRAINT} — concurrent token rotation for one session will fail on it`);
|
|
18245
|
+
} catch (error) {
|
|
18246
|
+
problems.push(`auth schema probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
18247
|
+
}
|
|
18248
|
+
return {
|
|
18249
|
+
healthy: problems.length === 0,
|
|
18250
|
+
databaseVersion,
|
|
18251
|
+
runtimeVersion: 2,
|
|
18252
|
+
problems
|
|
18253
|
+
};
|
|
18254
|
+
}
|
|
18061
18255
|
//#endregion
|
|
18062
18256
|
//#region src/auth/ensure-tables.ts
|
|
18063
18257
|
/**
|
|
@@ -18069,6 +18263,7 @@ function buildCollectionRegistry(schema) {
|
|
|
18069
18263
|
*/
|
|
18070
18264
|
async function ensureAuthTablesExist(db, collection) {
|
|
18071
18265
|
logger.info("🔍 Checking auth tables...");
|
|
18266
|
+
await assertAuthSchemaCompatible(db, resolveAuthSchema(collection));
|
|
18072
18267
|
try {
|
|
18073
18268
|
let usersTableName = "\"rebase\".\"users\"";
|
|
18074
18269
|
let userIdType = "TEXT";
|
|
@@ -18449,8 +18644,10 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
18449
18644
|
} catch (rlsReconcileError) {
|
|
18450
18645
|
logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
|
|
18451
18646
|
}
|
|
18647
|
+
await stampAuthSchemaVersion(db, authSchema);
|
|
18452
18648
|
logger.info("✅ Auth tables ready");
|
|
18453
18649
|
} catch (error) {
|
|
18650
|
+
if (error instanceof AuthSchemaVersionError) throw error;
|
|
18454
18651
|
logger.error("❌ Failed to create auth tables", { error });
|
|
18455
18652
|
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
18456
18653
|
}
|
|
@@ -20548,7 +20745,8 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20548
20745
|
userService,
|
|
20549
20746
|
roleService: userService,
|
|
20550
20747
|
emailService,
|
|
20551
|
-
authRepository
|
|
20748
|
+
authRepository,
|
|
20749
|
+
schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
|
|
20552
20750
|
};
|
|
20553
20751
|
},
|
|
20554
20752
|
async initializeHistory(config, driverResult) {
|
|
@@ -20576,7 +20774,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20576
20774
|
*/
|
|
20577
20775
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
20578
20776
|
const internals = driverResult.internals;
|
|
20579
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
20777
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-DGMYK0fr.js");
|
|
20580
20778
|
return { applied: (await ensureCollectionTables({ async query(text) {
|
|
20581
20779
|
const result = await internals.db.execute(sql.raw(text));
|
|
20582
20780
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|