@rebasepro/common 0.10.1-canary.ff9ccd6 → 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/collections/CollectionRegistry.d.ts +0 -6
- package/dist/index.es.js +226 -252
- package/dist/index.es.js.map +1 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/relations.d.ts +30 -9
- package/dist/util/resolutions.d.ts +34 -2
- package/dist/util/resolve-relation.d.ts +19 -0
- package/package.json +3 -3
- package/src/collections/CollectionRegistry.ts +63 -122
- package/src/data/buildRebaseData.ts +12 -12
- package/src/data/query_builder.ts +7 -2
- package/src/util/index.ts +1 -0
- package/src/util/junction-policies.ts +5 -3
- package/src/util/relations.ts +46 -284
- package/src/util/resolutions.ts +118 -51
- package/src/util/resolve-relation.ts +134 -0
|
@@ -51,12 +51,6 @@ export declare class CollectionRegistry {
|
|
|
51
51
|
register(collection: CollectionConfig, rawCollection?: CollectionConfig): void;
|
|
52
52
|
private _registerRecursively;
|
|
53
53
|
normalizeCollection(collection: CollectionConfig): CollectionConfig;
|
|
54
|
-
/**
|
|
55
|
-
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
56
|
-
* This allows developers to define relations directly on properties without a separate
|
|
57
|
-
* `relations[]` entry on the collection.
|
|
58
|
-
*/
|
|
59
|
-
private extractRelationsFromProperties;
|
|
60
54
|
private normalizeProperties;
|
|
61
55
|
private normalizeProperty;
|
|
62
56
|
get(path: string): CollectionConfig | undefined;
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isPostgresCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
1
|
+
import { ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
2
2
|
import { deepClone, generateForeignKeyName, getIn, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
import jsonLogic from "json-logic-js";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
@@ -372,151 +372,143 @@ function fullPathToCollectionSegments(path) {
|
|
|
372
372
|
return path.split("/").filter((e, i) => i % 2 === 0);
|
|
373
373
|
}
|
|
374
374
|
//#endregion
|
|
375
|
-
//#region src/util/
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
375
|
+
//#region src/util/resolve-relation.ts
|
|
376
|
+
/**
|
|
377
|
+
* Fill in a relation's defaults.
|
|
378
|
+
*
|
|
379
|
+
* This replaces `sanitizeRelation`, which had to work out *which kind of link
|
|
380
|
+
* you meant* from whichever optional fields happened to be set — 194 lines of
|
|
381
|
+
* it, including a pass that inspected the target collection's own relations to
|
|
382
|
+
* decide whether a `many`/`inverse` pair was a one-to-many or the far side of a
|
|
383
|
+
* many-to-many, wrapped in a `try/catch` that fell through to the wrong answer
|
|
384
|
+
* when it could not tell. Two consumers running that logic at different moments
|
|
385
|
+
* could reach different conclusions about the same relation.
|
|
386
|
+
*
|
|
387
|
+
* With the kind declared there is nothing to work out. What remains is
|
|
388
|
+
* defaulting — a table name, a column name — which is deterministic, depends
|
|
389
|
+
* only on the relation and its two endpoints, and cannot fail. That is why this
|
|
390
|
+
* function returns rather than throws, and why it needs no cache to be
|
|
391
|
+
* consistent.
|
|
392
|
+
*/
|
|
393
|
+
function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
394
|
+
const target = relation.target;
|
|
395
|
+
if (typeof target !== "function") throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has no \`target\`. Give it a thunk: \`target: () => otherCollection\`.`);
|
|
396
|
+
const targetCollection = target();
|
|
397
|
+
if (!targetCollection?.slug) throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has a \`target\` that did not resolve to a collection.`);
|
|
398
|
+
const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);
|
|
399
|
+
const shared = {
|
|
400
|
+
relationName,
|
|
401
|
+
target,
|
|
402
|
+
targetSlug: targetCollection.slug,
|
|
403
|
+
onUpdate: relation.onUpdate,
|
|
404
|
+
onDelete: relation.onDelete,
|
|
405
|
+
overrides: relation.overrides,
|
|
406
|
+
validation: relation.validation
|
|
406
407
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
408
|
+
const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);
|
|
409
|
+
switch (relation.kind) {
|
|
410
|
+
case "belongsTo": return {
|
|
411
|
+
...shared,
|
|
412
|
+
kind: "belongsTo",
|
|
413
|
+
cardinality: "one",
|
|
414
|
+
writable: true,
|
|
415
|
+
shared: false,
|
|
416
|
+
localKey: relation.localKey ?? generateForeignKeyName(relationName)
|
|
417
|
+
};
|
|
418
|
+
case "hasOne": return {
|
|
419
|
+
...shared,
|
|
420
|
+
kind: "hasOne",
|
|
421
|
+
cardinality: "one",
|
|
422
|
+
writable: true,
|
|
423
|
+
shared: false,
|
|
424
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
425
|
+
};
|
|
426
|
+
case "hasMany": return {
|
|
427
|
+
...shared,
|
|
428
|
+
kind: "hasMany",
|
|
429
|
+
cardinality: "many",
|
|
430
|
+
writable: true,
|
|
431
|
+
shared: false,
|
|
432
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
433
|
+
};
|
|
434
|
+
case "manyToMany": {
|
|
435
|
+
const sourceTable = getTableName(sourceCollection);
|
|
436
|
+
const targetTable = getTableName(targetCollection);
|
|
437
|
+
return {
|
|
438
|
+
...shared,
|
|
439
|
+
kind: "manyToMany",
|
|
440
|
+
cardinality: "many",
|
|
441
|
+
writable: true,
|
|
442
|
+
shared: true,
|
|
443
|
+
through: {
|
|
444
|
+
table: relation.through?.table ?? [sourceTable, targetTable].sort().join("_"),
|
|
445
|
+
sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),
|
|
446
|
+
targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName)
|
|
448
447
|
}
|
|
449
|
-
} catch (e) {}
|
|
450
|
-
if (!isManyToManyInverse && !newRelation.foreignKeyOnTarget) newRelation.foreignKeyOnTarget = generateForeignKeyName(sourceName);
|
|
451
|
-
} else if (newRelation.cardinality === "many" && newRelation.direction === "owning") {
|
|
452
|
-
const sourceTableName = getTableName(sourceCollection);
|
|
453
|
-
const targetTableName = getTableName(targetCollection);
|
|
454
|
-
newRelation.through = {
|
|
455
|
-
table: newRelation.through?.table ?? [sourceTableName, targetTableName].sort().join("_"),
|
|
456
|
-
sourceColumn: newRelation.through?.sourceColumn ?? generateForeignKeyName(sourceName),
|
|
457
|
-
targetColumn: newRelation.through?.targetColumn ?? generateForeignKeyName(newRelation.relationName)
|
|
458
448
|
};
|
|
459
449
|
}
|
|
450
|
+
case "via": return {
|
|
451
|
+
...shared,
|
|
452
|
+
kind: "via",
|
|
453
|
+
cardinality: relation.cardinality,
|
|
454
|
+
writable: false,
|
|
455
|
+
shared: true,
|
|
456
|
+
joinPath: relation.joinPath
|
|
457
|
+
};
|
|
458
|
+
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
460
459
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/util/relations.ts
|
|
463
|
+
/**
|
|
464
|
+
* Whether the target rows are shared with other parents — a many-to-many, or a
|
|
465
|
+
* multi-hop `via` chain.
|
|
466
|
+
*
|
|
467
|
+
* Decides what a write "through" the relation may touch: a shared target
|
|
468
|
+
* belongs to every parent that links it, so the parent owns the *link* and not
|
|
469
|
+
* the row. The backend enforces that (an unlink rather than a delete) and the
|
|
470
|
+
* admin renders it (remove-from-parent rather than delete).
|
|
471
|
+
*
|
|
472
|
+
* Now a field on the resolved relation rather than a re-derivation, so both
|
|
473
|
+
* sides read the same answer instead of each computing one.
|
|
474
|
+
*/
|
|
475
|
+
function isJunctionBackedRelation(relation) {
|
|
476
|
+
return relation.shared;
|
|
465
477
|
}
|
|
466
478
|
/** WeakMap cache — same collection instance always yields the same relation map. */
|
|
467
479
|
var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
480
|
+
/**
|
|
481
|
+
* Every relation a collection declares, keyed by the name it is addressed by.
|
|
482
|
+
*
|
|
483
|
+
* A relation reaches the map from either of two places — the collection's
|
|
484
|
+
* `relations` array, or a `relation` property that declares one inline — and is
|
|
485
|
+
* keyed by its resolved `relationName`, which is what a nested path segment,
|
|
486
|
+
* an `include` key and an admin tab all match against.
|
|
487
|
+
*
|
|
488
|
+
* Resolution no longer swallows failures. It used to wrap each relation in a
|
|
489
|
+
* `try/catch` that dropped anything it could not work out, so a
|
|
490
|
+
* mis-declared relation silently vanished instead of being reported; with the
|
|
491
|
+
* kind declared, the only remaining failure is a `target` that does not resolve,
|
|
492
|
+
* which is worth hearing about.
|
|
493
|
+
*/
|
|
468
494
|
function resolveCollectionRelations(collection) {
|
|
469
495
|
const cached = _resolvedRelationsCache.get(collection);
|
|
470
496
|
if (cached) return cached;
|
|
471
497
|
if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
|
|
472
498
|
const relations = {};
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
});
|
|
484
|
-
if (collection.properties) Object.entries(collection.properties).forEach(([propKey, prop]) => {
|
|
485
|
-
const relation = resolvePropertyRelation({
|
|
486
|
-
propertyKey: propKey,
|
|
487
|
-
property: prop,
|
|
488
|
-
sourceCollection: collection
|
|
489
|
-
});
|
|
490
|
-
if (relation) {
|
|
491
|
-
if (relations[propKey]) return;
|
|
492
|
-
if (!relation.relationName) relation.relationName = propKey;
|
|
493
|
-
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
494
|
-
relations[propKey] = normalizedRelation;
|
|
495
|
-
registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
499
|
+
for (const relation of collection.relations ?? []) {
|
|
500
|
+
const resolved = resolveRelation(relation, collection);
|
|
501
|
+
relations[resolved.relationName] = resolved;
|
|
502
|
+
}
|
|
503
|
+
for (const [propertyKey, property] of Object.entries(collection.properties ?? {})) {
|
|
504
|
+
if (property?.type !== "relation") continue;
|
|
505
|
+
const declared = property.relation;
|
|
506
|
+
if (!declared || relations[propertyKey]) continue;
|
|
507
|
+
relations[propertyKey] = resolveRelation(declared, collection, propertyKey);
|
|
508
|
+
}
|
|
498
509
|
_resolvedRelationsCache.set(collection, relations);
|
|
499
510
|
return relations;
|
|
500
511
|
}
|
|
501
|
-
function resolvePropertyRelation({ propertyKey, property, sourceCollection }) {
|
|
502
|
-
if (property.type !== "relation") return void 0;
|
|
503
|
-
const relProp = property;
|
|
504
|
-
if (relProp.target) return {
|
|
505
|
-
relationName: relProp.relationName || propertyKey,
|
|
506
|
-
target: relProp.target,
|
|
507
|
-
cardinality: relProp.cardinality || "one",
|
|
508
|
-
direction: relProp.direction || "owning",
|
|
509
|
-
inverseRelationName: relProp.inverseRelationName,
|
|
510
|
-
localKey: relProp.localKey,
|
|
511
|
-
foreignKeyOnTarget: relProp.foreignKeyOnTarget,
|
|
512
|
-
through: relProp.through,
|
|
513
|
-
joinPath: relProp.joinPath,
|
|
514
|
-
onUpdate: relProp.onUpdate,
|
|
515
|
-
onDelete: relProp.onDelete,
|
|
516
|
-
overrides: relProp.overrides
|
|
517
|
-
};
|
|
518
|
-
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
519
|
-
}
|
|
520
512
|
function getTableName(collection) {
|
|
521
513
|
if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
522
514
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -616,15 +608,21 @@ function resolveProperty(props) {
|
|
|
616
608
|
}
|
|
617
609
|
return resolvedProperty;
|
|
618
610
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
611
|
+
/**
|
|
612
|
+
* The resolved relation a relation property refers to.
|
|
613
|
+
*
|
|
614
|
+
* Normalization stamps `resolvedRelation` onto the property, so this is usually
|
|
615
|
+
* a field read. It falls back to resolving from the collection for properties
|
|
616
|
+
* that never went through the registry — a preview, or a form rendered straight
|
|
617
|
+
* from an authored config.
|
|
618
|
+
*/
|
|
619
|
+
function resolveRelationProperty(property, collection, propertyKey) {
|
|
620
|
+
if (property.resolvedRelation) return property.resolvedRelation;
|
|
621
|
+
if (property.relation) return resolveRelation(property.relation, collection, propertyKey);
|
|
622
|
+
const name = propertyKey ?? "";
|
|
623
|
+
const declared = resolveCollectionRelations(collection)[name];
|
|
624
|
+
if (!declared) throw Error(`Relation property '${name || "(unnamed)"}' on '${collection.slug}' declares no \`relation\`, and the collection has no relation of that name.`);
|
|
625
|
+
return declared;
|
|
628
626
|
}
|
|
629
627
|
/**
|
|
630
628
|
* Resolve enum aliases for a string or number property
|
|
@@ -721,34 +719,79 @@ function resolveEnumValues(input) {
|
|
|
721
719
|
else if (Array.isArray(input)) return input;
|
|
722
720
|
else return;
|
|
723
721
|
}
|
|
724
|
-
|
|
725
|
-
|
|
722
|
+
/**
|
|
723
|
+
* The lists rendered inside an entity view of `collection` — its tabs.
|
|
724
|
+
*
|
|
725
|
+
* The single derivation. There used to be two that disagreed: this one, and a
|
|
726
|
+
* copy in `CollectionRegistry.normalizeCollection` that stamped each child with
|
|
727
|
+
* the *target collection's* slug instead of the relation key. Since the
|
|
728
|
+
* registry ran first and cached its answer onto `childCollections`, its version
|
|
729
|
+
* was the one that won, and the frontend addressed child listings by a segment
|
|
730
|
+
* the backend could not resolve.
|
|
731
|
+
*
|
|
732
|
+
* Order of precedence:
|
|
733
|
+
* 1. `childCollections` — the explicit escape hatch for custom drivers.
|
|
734
|
+
* 2. `subcollections` on an engine that has real containment (Firestore).
|
|
735
|
+
* 3. many-relations on an engine that has relations (SQL).
|
|
736
|
+
*/
|
|
737
|
+
function getEntityChildViews(collection) {
|
|
738
|
+
const asSubcollections = (collections) => collections.filter(Boolean).map((child) => ({
|
|
739
|
+
key: child.slug,
|
|
740
|
+
collection: child,
|
|
741
|
+
source: { kind: "subcollection" }
|
|
742
|
+
}));
|
|
743
|
+
if (collection.childCollections) return asSubcollections(collection.childCollections() ?? []);
|
|
744
|
+
const capabilities = getDataSourceCapabilities(collection.engine);
|
|
726
745
|
const declaredSubcollections = getDeclaredSubcollections(collection);
|
|
727
|
-
if (
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) return asSubcollections(declaredSubcollections() ?? []);
|
|
747
|
+
if (!capabilities.supportsRelations) return [];
|
|
748
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
749
|
+
const views = [];
|
|
750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
751
|
+
for (const [relationKey, relation] of Object.entries(resolvedRelations)) {
|
|
752
|
+
if (relation.cardinality !== "many") continue;
|
|
753
|
+
const identity = relation.relationName ?? relationKey;
|
|
754
|
+
if (seen.has(identity)) continue;
|
|
755
|
+
let target;
|
|
756
|
+
try {
|
|
757
|
+
target = relation.target();
|
|
758
|
+
} catch {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (!target) continue;
|
|
762
|
+
seen.add(identity);
|
|
763
|
+
const customName = Object.entries(collection.properties ?? {}).find(([propKey, p]) => p.type === "relation" && (p.relation?.relationName ?? propKey) === identity)?.[1]?.name;
|
|
764
|
+
const base = {
|
|
765
|
+
...target,
|
|
766
|
+
slug: relationKey,
|
|
767
|
+
...customName ? {
|
|
768
|
+
name: customName,
|
|
769
|
+
singularName: customName
|
|
770
|
+
} : {}
|
|
771
|
+
};
|
|
772
|
+
views.push({
|
|
773
|
+
key: relationKey,
|
|
774
|
+
collection: relation.overrides ? mergeDeep(base, relation.overrides) : base,
|
|
775
|
+
source: {
|
|
776
|
+
kind: "relation",
|
|
777
|
+
relationKey,
|
|
778
|
+
mode: isJunctionBackedRelation(relation) ? "linked" : "owned",
|
|
779
|
+
targetSlug: target.slug
|
|
743
780
|
}
|
|
744
|
-
|
|
745
|
-
...target,
|
|
746
|
-
...baseOverrides
|
|
747
|
-
};
|
|
748
|
-
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
749
|
-
}).filter((c) => Boolean(c));
|
|
781
|
+
});
|
|
750
782
|
}
|
|
751
|
-
return
|
|
783
|
+
return views;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The child views of `collection` as bare collections.
|
|
787
|
+
*
|
|
788
|
+
* The flattened view of {@link getEntityChildViews}, for navigation code that
|
|
789
|
+
* only needs to match a path segment against a slug. Anything that cares *what
|
|
790
|
+
* kind* of list it is showing — chiefly the admin, which must not offer a
|
|
791
|
+
* global delete on a shared row — should read the views instead.
|
|
792
|
+
*/
|
|
793
|
+
function getSubcollections(collection) {
|
|
794
|
+
return getEntityChildViews(collection).map((view) => view.collection);
|
|
752
795
|
}
|
|
753
796
|
//#endregion
|
|
754
797
|
//#region src/util/policy/sqlToPolicy.ts
|
|
@@ -1624,8 +1667,8 @@ function resolveJunctionSpecs(collections) {
|
|
|
1624
1667
|
for (const collection of collections) {
|
|
1625
1668
|
const resolved = resolveCollectionRelations(collection);
|
|
1626
1669
|
for (const relation of Object.values(resolved)) {
|
|
1627
|
-
if (!relation
|
|
1628
|
-
const targetCollection =
|
|
1670
|
+
if (!isManyToMany(relation)) continue;
|
|
1671
|
+
const targetCollection = relation.target();
|
|
1629
1672
|
if (!targetCollection) continue;
|
|
1630
1673
|
const rawName = relation.through.table;
|
|
1631
1674
|
const table = rawName.includes(".") ? rawName.split(".").pop() : rawName;
|
|
@@ -2070,100 +2113,33 @@ var CollectionRegistry = class {
|
|
|
2070
2113
|
if (!result.dataSource) result.dataSource = resolved.key;
|
|
2071
2114
|
if (!result.engine) result.engine = resolved.engine;
|
|
2072
2115
|
}
|
|
2073
|
-
|
|
2074
|
-
const relResult = result;
|
|
2075
|
-
const manualRelations = getDataSourceCapabilities(result.engine).supportsRelations ? relResult.relations ?? [] : [];
|
|
2076
|
-
const mergedRelationsRaw = [...extractedRelations];
|
|
2077
|
-
for (const manual of manualRelations) {
|
|
2078
|
-
const name = manual.relationName;
|
|
2079
|
-
if (!name) mergedRelationsRaw.push(manual);
|
|
2080
|
-
else {
|
|
2081
|
-
const existingIndex = mergedRelationsRaw.findIndex((r) => r.relationName === name);
|
|
2082
|
-
if (existingIndex === -1) mergedRelationsRaw.push(manual);
|
|
2083
|
-
else mergedRelationsRaw[existingIndex] = {
|
|
2084
|
-
...manual,
|
|
2085
|
-
...mergedRelationsRaw[existingIndex]
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
let mergedRelations = mergedRelationsRaw;
|
|
2090
|
-
if (getDataSourceCapabilities(result.engine).supportsRelations) {
|
|
2091
|
-
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
2092
|
-
try {
|
|
2093
|
-
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
2094
|
-
} catch {
|
|
2095
|
-
return r;
|
|
2096
|
-
}
|
|
2097
|
-
});
|
|
2098
|
-
relResult.relations = mergedRelations;
|
|
2099
|
-
}
|
|
2100
|
-
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
2101
|
-
if (!result.childCollections) {
|
|
2102
|
-
const capabilities = getDataSourceCapabilities(result.engine);
|
|
2103
|
-
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
2104
|
-
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
2105
|
-
else if (capabilities.supportsRelations && relResult.relations) {
|
|
2106
|
-
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
2107
|
-
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
2108
|
-
const target = r.target();
|
|
2109
|
-
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2116
|
+
result.properties = this.normalizeProperties(result.properties, result);
|
|
2113
2117
|
return result;
|
|
2114
2118
|
}
|
|
2115
|
-
|
|
2116
|
-
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
2117
|
-
* This allows developers to define relations directly on properties without a separate
|
|
2118
|
-
* `relations[]` entry on the collection.
|
|
2119
|
-
*/
|
|
2120
|
-
extractRelationsFromProperties(properties) {
|
|
2121
|
-
const relations = [];
|
|
2122
|
-
for (const [key, property] of Object.entries(properties)) if (property.type === "relation") {
|
|
2123
|
-
const relProp = property;
|
|
2124
|
-
const target = relProp.target ?? relProp.relation?.target;
|
|
2125
|
-
if (target) {
|
|
2126
|
-
const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
|
|
2127
|
-
relations.push({
|
|
2128
|
-
relationName,
|
|
2129
|
-
target,
|
|
2130
|
-
cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
|
|
2131
|
-
direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
|
|
2132
|
-
inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
|
|
2133
|
-
localKey: relProp.localKey ?? relProp.relation?.localKey,
|
|
2134
|
-
foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
|
|
2135
|
-
through: relProp.through ?? relProp.relation?.through,
|
|
2136
|
-
joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
|
|
2137
|
-
onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
|
|
2138
|
-
onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
|
|
2139
|
-
overrides: relProp.overrides ?? relProp.relation?.overrides
|
|
2140
|
-
});
|
|
2141
|
-
}
|
|
2142
|
-
} else if (property.type === "map" && property.properties) relations.push(...this.extractRelationsFromProperties(property.properties));
|
|
2143
|
-
return relations;
|
|
2144
|
-
}
|
|
2145
|
-
normalizeProperties(properties, relations) {
|
|
2119
|
+
normalizeProperties(properties, collection) {
|
|
2146
2120
|
const newProperties = {};
|
|
2147
|
-
for (const key in properties) newProperties[key] = this.normalizeProperty(key, properties[key],
|
|
2121
|
+
for (const key in properties) newProperties[key] = this.normalizeProperty(key, properties[key], collection);
|
|
2148
2122
|
return newProperties;
|
|
2149
2123
|
}
|
|
2150
|
-
normalizeProperty(key, property,
|
|
2124
|
+
normalizeProperty(key, property, collection) {
|
|
2151
2125
|
const newProperty = { ...property };
|
|
2152
|
-
if (newProperty.type === "map" && newProperty.properties) newProperty.properties = this.normalizeProperties(newProperty.properties,
|
|
2126
|
+
if (newProperty.type === "map" && newProperty.properties) newProperty.properties = this.normalizeProperties(newProperty.properties, collection);
|
|
2153
2127
|
else if (newProperty.type === "array") {
|
|
2154
2128
|
const arrayProp = newProperty;
|
|
2155
|
-
if (arrayProp.of) if (Array.isArray(arrayProp.of)) arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p,
|
|
2156
|
-
else arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of,
|
|
2157
|
-
else if (arrayProp.oneOf && arrayProp.oneOf.properties) arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties,
|
|
2129
|
+
if (arrayProp.of) if (Array.isArray(arrayProp.of)) arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, collection));
|
|
2130
|
+
else arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, collection);
|
|
2131
|
+
else if (arrayProp.oneOf && arrayProp.oneOf.properties) arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, collection);
|
|
2158
2132
|
} else if ((newProperty.type === "string" || newProperty.type === "number") && newProperty.enum) {
|
|
2159
2133
|
const stringOrNumberProperty = newProperty;
|
|
2160
2134
|
if (typeof stringOrNumberProperty.enum === "object" && !Array.isArray(stringOrNumberProperty.enum)) stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];
|
|
2161
2135
|
} else if (newProperty.type === "relation") {
|
|
2162
2136
|
const relationProperty = newProperty;
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2137
|
+
if (relationProperty.relation) relationProperty.resolvedRelation = resolveRelation(relationProperty.relation, collection, key);
|
|
2138
|
+
else {
|
|
2139
|
+
const declared = resolveCollectionRelations(collection)[key];
|
|
2140
|
+
if (declared) relationProperty.resolvedRelation = declared;
|
|
2141
|
+
else console.warn(`Relation property '${key}' on '${collection.slug}' declares no \`relation\`, and the collection has no relation of that name.`);
|
|
2142
|
+
}
|
|
2167
2143
|
}
|
|
2168
2144
|
return newProperty;
|
|
2169
2145
|
}
|
|
@@ -2208,9 +2184,7 @@ var CollectionRegistry = class {
|
|
|
2208
2184
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2209
2185
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
2210
2186
|
const target = relation.target();
|
|
2211
|
-
|
|
2212
|
-
const targetSlug = relation.overrides?.slug ?? targetRelationKey;
|
|
2213
|
-
currentCollection = this.get(targetSlug) || this.normalizeCollection(target);
|
|
2187
|
+
currentCollection = this.collectionsByTableName.get(getTableName(target)) ?? this.normalizeCollection(target);
|
|
2214
2188
|
if (i + 1 < pathSegments.length) {}
|
|
2215
2189
|
}
|
|
2216
2190
|
return currentCollection;
|
|
@@ -2245,7 +2219,7 @@ var CollectionRegistry = class {
|
|
|
2245
2219
|
if (!subcollections || subcollections.length === 0) throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
2246
2220
|
const subcollection = subcollections.find((c) => c.slug === subcollectionSlug);
|
|
2247
2221
|
if (!subcollection) throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
2248
|
-
currentCollection = this.
|
|
2222
|
+
currentCollection = this.normalizeCollection(subcollection);
|
|
2249
2223
|
collections.push(currentCollection);
|
|
2250
2224
|
}
|
|
2251
2225
|
}
|
|
@@ -3356,6 +3330,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
3356
3330
|
return junctionTables;
|
|
3357
3331
|
}
|
|
3358
3332
|
//#endregion
|
|
3359
|
-
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, and, buildCollection, buildCompositeId, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isPropertyBuilder, isRebaseInternalTable, normalizeToEntityRelation, or, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum,
|
|
3333
|
+
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, and, buildCollection, buildCompositeId, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeToEntityRelation, or, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
3360
3334
|
|
|
3361
3335
|
//# sourceMappingURL=index.es.js.map
|