@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.
@@ -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/relations.ts
376
- function sanitizeRelation(relation, sourceCollection, resolveCollection) {
377
- if (!relation.target) throw new Error("Relation is missing a `target` collection.");
378
- const rawTarget = relation.target;
379
- let targetCollection;
380
- if (typeof rawTarget === "string") {
381
- if (resolveCollection) targetCollection = resolveCollection(rawTarget);
382
- if (!targetCollection) targetCollection = {
383
- slug: rawTarget,
384
- name: rawTarget
385
- };
386
- } else if (typeof rawTarget === "function") {
387
- const evaluated = rawTarget();
388
- if (typeof evaluated === "string") {
389
- if (resolveCollection) targetCollection = resolveCollection(evaluated);
390
- if (!targetCollection) targetCollection = {
391
- slug: evaluated,
392
- name: evaluated
393
- };
394
- } else targetCollection = evaluated;
395
- } else if (rawTarget && typeof rawTarget === "object") targetCollection = rawTarget;
396
- if (!targetCollection) throw new Error("Relation is missing a valid `target` collection.");
397
- const newRelation = { ...relation };
398
- newRelation.target = () => {
399
- if (typeof rawTarget === "string") return resolveCollection && resolveCollection(rawTarget) || targetCollection;
400
- else if (typeof rawTarget === "function") {
401
- const evaluated = rawTarget();
402
- if (typeof evaluated === "string") return resolveCollection && resolveCollection(evaluated) || targetCollection;
403
- return evaluated;
404
- }
405
- return targetCollection;
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
- if (!newRelation.relationName) newRelation.relationName = toSnakeCase(targetCollection.slug);
408
- if (!newRelation.direction) if (newRelation.foreignKeyOnTarget) newRelation.direction = "inverse";
409
- else if (newRelation.through) newRelation.direction = "owning";
410
- else if (newRelation.cardinality === "many") newRelation.direction = "inverse";
411
- else newRelation.direction = "owning";
412
- if (!newRelation.joinPath) {
413
- const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);
414
- if (newRelation.cardinality === "one" && newRelation.direction === "owning") {
415
- if (!newRelation.localKey) newRelation.localKey = generateForeignKeyName(newRelation.relationName);
416
- } else if (newRelation.cardinality === "one" && newRelation.direction === "inverse") {
417
- if (!newRelation.foreignKeyOnTarget) {
418
- let foundForeignKey = false;
419
- try {
420
- const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
421
- for (const targetRel of targetRelations) if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) try {
422
- if (targetRel.target().slug === sourceCollection.slug) {
423
- newRelation.foreignKeyOnTarget = targetRel.localKey;
424
- foundForeignKey = true;
425
- break;
426
- }
427
- } catch (e) {
428
- continue;
429
- }
430
- } catch (e) {}
431
- if (!foundForeignKey) newRelation.foreignKeyOnTarget = generateForeignKeyName(newRelation.inverseRelationName ? toSnakeCase(newRelation.inverseRelationName) : sourceName);
432
- }
433
- } else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
434
- let isManyToManyInverse = false;
435
- if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) try {
436
- const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
437
- for (const targetRel of targetRelations) if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
438
- isManyToManyInverse = true;
439
- break;
440
- }
441
- if (!isManyToManyInverse && targetCollection.properties) for (const [propKey, prop] of Object.entries(targetCollection.properties)) {
442
- if (prop.type !== "relation") continue;
443
- const relProp = prop;
444
- if ((relProp.relationName || propKey) === newRelation.inverseRelationName && relProp.cardinality === "many" && (relProp.direction === "owning" || !relProp.direction)) {
445
- isManyToManyInverse = true;
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
- if (newRelation.cardinality === "one" && newRelation.direction === "owning" && !newRelation.localKey && !newRelation.joinPath) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'owning' one-to-one relation requires a 'localKey'. Check the relation config for '${newRelation.relationName}'`);
462
- if (newRelation.cardinality === "one" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-one relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
463
- if (newRelation.cardinality === "many" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath && !newRelation.inverseRelationName) throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-many relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
464
- return newRelation;
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 registeredRelationNames = /* @__PURE__ */ new Set();
474
- if (collection.relations) collection.relations.forEach((relation) => {
475
- try {
476
- const normalizedRelation = sanitizeRelation(relation, collection);
477
- const relationKey = normalizedRelation.relationName;
478
- if (relationKey) {
479
- relations[relationKey] = normalizedRelation;
480
- registeredRelationNames.add(relationKey);
481
- }
482
- } catch (e) {}
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
- function resolveRelationProperty(property, relations, propertyKey) {
620
- if (property.relation) return property;
621
- const name = property.relationName || propertyKey;
622
- const relation = name ? relations.find((rel) => rel.relationName === name) : void 0;
623
- if (!relation) throw Error(`Relation ${name ?? "(unnamed)"} not found`);
624
- return {
625
- ...property,
626
- relation
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
- function getSubcollections(collection) {
725
- if (collection.childCollections) return collection.childCollections() ?? [];
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 (getDataSourceCapabilities(collection.engine).supportsSubcollections && declaredSubcollections) return declaredSubcollections() ?? [];
728
- if (getDataSourceCapabilities(collection.engine).supportsRelations) {
729
- const resolvedRelations = resolveCollectionRelations(collection);
730
- return Object.values(resolvedRelations).filter((r) => r.cardinality === "many").map((r) => {
731
- const target = r.target();
732
- if (!target) return void 0;
733
- const relationKey = r.relationName || target.slug;
734
- let customName;
735
- if (collection.properties) {
736
- const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
737
- if (prop && prop[1].name) customName = prop[1].name;
738
- }
739
- const baseOverrides = { slug: relationKey };
740
- if (customName) {
741
- baseOverrides.name = customName;
742
- baseOverrides.singularName = customName;
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
- const targetWithOverrides = {
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.through) continue;
1628
- const targetCollection = typeof relation.target === "function" ? relation.target() : void 0;
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
- const extractedRelations = this.extractRelationsFromProperties(result.properties);
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], relations);
2121
+ for (const key in properties) newProperties[key] = this.normalizeProperty(key, properties[key], collection);
2148
2122
  return newProperties;
2149
2123
  }
2150
- normalizeProperty(key, property, relations) {
2124
+ normalizeProperty(key, property, collection) {
2151
2125
  const newProperty = { ...property };
2152
- if (newProperty.type === "map" && newProperty.properties) newProperty.properties = this.normalizeProperties(newProperty.properties, relations);
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, relations));
2156
- else arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations);
2157
- else if (arrayProp.oneOf && arrayProp.oneOf.properties) arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations);
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
- const name = relationProperty.relationName || key;
2164
- const relation = relations.find((r) => r.relationName === name);
2165
- if (relation) relationProperty.relation = relation;
2166
- else console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
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
- const targetRelationKey = relation.relationName || target.slug;
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.get(subcollection.slug) || this.normalizeCollection(subcollection);
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, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
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