@rebasepro/common 0.23.0 → 0.24.1-canary.g19596ef

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.
Files changed (151) hide show
  1. package/README.md +13 -0
  2. package/dist/collections/CollectionRegistry.d.ts +72 -3
  3. package/dist/collections/CollectionRegistry.es.js +404 -0
  4. package/dist/collections/CollectionRegistry.es.js.map +1 -0
  5. package/dist/collections/auth-secrets.es.js +47 -0
  6. package/dist/collections/auth-secrets.es.js.map +1 -0
  7. package/dist/collections/default-collections.es.js +120 -0
  8. package/dist/collections/default-collections.es.js.map +1 -0
  9. package/dist/collections/engine-support.es.js +35 -0
  10. package/dist/collections/engine-support.es.js.map +1 -0
  11. package/dist/collections/field-access.d.ts +13 -0
  12. package/dist/collections/field-access.es.js +142 -0
  13. package/dist/collections/field-access.es.js.map +1 -0
  14. package/dist/collections/index.d.ts +1 -0
  15. package/dist/collections/soft-delete.es.js +34 -0
  16. package/dist/collections/soft-delete.es.js.map +1 -0
  17. package/dist/collections/unknown-field.d.ts +65 -0
  18. package/dist/collections/unknown-field.es.js +135 -0
  19. package/dist/collections/unknown-field.es.js.map +1 -0
  20. package/dist/data/buildRebaseData.d.ts +39 -2
  21. package/dist/data/buildRebaseData.es.js +1038 -0
  22. package/dist/data/buildRebaseData.es.js.map +1 -0
  23. package/dist/data/buildRoutedRebaseData.d.ts +6 -1
  24. package/dist/data/buildRoutedRebaseData.es.js +47 -0
  25. package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
  26. package/dist/data/call-context.d.ts +48 -0
  27. package/dist/data/call-context.es.js +195 -0
  28. package/dist/data/call-context.es.js.map +1 -0
  29. package/dist/data/collection-accessor.d.ts +54 -0
  30. package/dist/data/collection-accessor.es.js +101 -0
  31. package/dist/data/collection-accessor.es.js.map +1 -0
  32. package/dist/data/cursor.d.ts +1 -1
  33. package/dist/data/cursor.es.js +166 -0
  34. package/dist/data/cursor.es.js.map +1 -0
  35. package/dist/data/filter-conditions.es.js +24 -0
  36. package/dist/data/filter-conditions.es.js.map +1 -0
  37. package/dist/data/filter-dialect.d.ts +3 -3
  38. package/dist/data/filter-dialect.es.js +786 -0
  39. package/dist/data/filter-dialect.es.js.map +1 -0
  40. package/dist/data/include-spec.d.ts +2 -0
  41. package/dist/data/include-spec.es.js +273 -0
  42. package/dist/data/include-spec.es.js.map +1 -0
  43. package/dist/data/paginate.d.ts +8 -1
  44. package/dist/data/paginate.es.js +181 -0
  45. package/dist/data/paginate.es.js.map +1 -0
  46. package/dist/data/query_builder.d.ts +52 -4
  47. package/dist/data/query_builder.es.js +151 -0
  48. package/dist/data/query_builder.es.js.map +1 -0
  49. package/dist/data/resolveDataSource.es.js +87 -0
  50. package/dist/data/resolveDataSource.es.js.map +1 -0
  51. package/dist/data/row-etag.d.ts +46 -0
  52. package/dist/data/row-etag.es.js +64 -0
  53. package/dist/data/row-etag.es.js.map +1 -0
  54. package/dist/data/sort-dialect.es.js +236 -0
  55. package/dist/data/sort-dialect.es.js.map +1 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.es.js +59 -7235
  58. package/dist/table-classification.es.js +107 -0
  59. package/dist/table-classification.es.js.map +1 -0
  60. package/dist/util/auth-default-policies.es.js +184 -0
  61. package/dist/util/auth-default-policies.es.js.map +1 -0
  62. package/dist/util/builders.es.js +15 -0
  63. package/dist/util/builders.es.js.map +1 -0
  64. package/dist/util/callback-errors.es.js +164 -0
  65. package/dist/util/callback-errors.es.js.map +1 -0
  66. package/dist/util/callbacks.es.js +72 -0
  67. package/dist/util/callbacks.es.js.map +1 -0
  68. package/dist/util/collections.es.js +79 -0
  69. package/dist/util/collections.es.js.map +1 -0
  70. package/dist/util/common.es.js +7 -0
  71. package/dist/util/common.es.js.map +1 -0
  72. package/dist/util/compose-callbacks.d.ts +55 -0
  73. package/dist/util/compose-callbacks.es.js +154 -0
  74. package/dist/util/compose-callbacks.es.js.map +1 -0
  75. package/dist/util/conditions.es.js +100 -0
  76. package/dist/util/conditions.es.js.map +1 -0
  77. package/dist/util/copy.d.ts +33 -0
  78. package/dist/util/copy.es.js +74 -0
  79. package/dist/util/copy.es.js.map +1 -0
  80. package/dist/util/deprecation.d.ts +27 -0
  81. package/dist/util/deprecation.es.js +44 -0
  82. package/dist/util/deprecation.es.js.map +1 -0
  83. package/dist/util/download-url-cache.d.ts +122 -0
  84. package/dist/util/download-url-cache.es.js +187 -0
  85. package/dist/util/download-url-cache.es.js.map +1 -0
  86. package/dist/util/email.es.js +36 -0
  87. package/dist/util/email.es.js.map +1 -0
  88. package/dist/util/entities.d.ts +27 -2
  89. package/dist/util/entities.es.js +328 -0
  90. package/dist/util/entities.es.js.map +1 -0
  91. package/dist/util/enums.d.ts +10 -1
  92. package/dist/util/enums.es.js +37 -0
  93. package/dist/util/enums.es.js.map +1 -0
  94. package/dist/util/identity.es.js +138 -0
  95. package/dist/util/identity.es.js.map +1 -0
  96. package/dist/util/index.d.ts +8 -0
  97. package/dist/util/internal-tables.d.ts +10 -0
  98. package/dist/util/internal-tables.es.js +202 -0
  99. package/dist/util/internal-tables.es.js.map +1 -0
  100. package/dist/util/junction-policies.es.js +273 -0
  101. package/dist/util/junction-policies.es.js.map +1 -0
  102. package/dist/util/paths.es.js +26 -0
  103. package/dist/util/paths.es.js.map +1 -0
  104. package/dist/util/permissions.es.js +108 -0
  105. package/dist/util/permissions.es.js.map +1 -0
  106. package/dist/util/pg-column-to-property.es.js +303 -0
  107. package/dist/util/pg-column-to-property.es.js.map +1 -0
  108. package/dist/util/policy/compileRulePolicies.d.ts +32 -0
  109. package/dist/util/policy/compileRulePolicies.es.js +43 -0
  110. package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
  111. package/dist/util/policy/evaluatePolicy.es.js +104 -0
  112. package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
  113. package/dist/util/policy/index.d.ts +1 -0
  114. package/dist/util/policy/policyToPostgres.d.ts +24 -0
  115. package/dist/util/policy/policyToPostgres.es.js +593 -0
  116. package/dist/util/policy/policyToPostgres.es.js.map +1 -0
  117. package/dist/util/policy/policyToSecurityRule.es.js +79 -0
  118. package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
  119. package/dist/util/policy/securityRuleToConditions.es.js +48 -0
  120. package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
  121. package/dist/util/policy/sqlToPolicy.es.js +326 -0
  122. package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
  123. package/dist/util/relations.es.js +243 -0
  124. package/dist/util/relations.es.js.map +1 -0
  125. package/dist/util/resolutions.es.js +318 -0
  126. package/dist/util/resolutions.es.js.map +1 -0
  127. package/dist/util/resolve-relation.es.js +163 -0
  128. package/dist/util/resolve-relation.es.js.map +1 -0
  129. package/dist/util/resumable-upload-threshold.d.ts +15 -0
  130. package/dist/util/resumable-upload-threshold.es.js +20 -0
  131. package/dist/util/resumable-upload-threshold.es.js.map +1 -0
  132. package/dist/util/resumable-upload.d.ts +51 -0
  133. package/dist/util/resumable-upload.es.js +121 -0
  134. package/dist/util/resumable-upload.es.js.map +1 -0
  135. package/dist/util/sql-redaction.d.ts +21 -0
  136. package/dist/util/sql-redaction.es.js +107 -0
  137. package/dist/util/sql-redaction.es.js.map +1 -0
  138. package/dist/util/sql-rows.es.js +41 -0
  139. package/dist/util/sql-rows.es.js.map +1 -0
  140. package/dist/util/storage.d.ts +26 -4
  141. package/dist/util/storage.es.js +117 -0
  142. package/dist/util/storage.es.js.map +1 -0
  143. package/dist/util/string-column-length.es.js +31 -0
  144. package/dist/util/string-column-length.es.js.map +1 -0
  145. package/dist/util/tenant.es.js +201 -0
  146. package/dist/util/tenant.es.js.map +1 -0
  147. package/dist/util/untrusted-envelope.d.ts +25 -0
  148. package/dist/util/untrusted-envelope.es.js +40 -0
  149. package/dist/util/untrusted-envelope.es.js.map +1 -0
  150. package/package.json +5 -3
  151. package/dist/index.es.js.map +0 -1
package/README.md CHANGED
@@ -35,6 +35,19 @@ This package has no React dependency — it's pure TypeScript and can be used in
35
35
  | `and(...conditions)` | Create an AND logical condition |
36
36
  | `cond(column, op, value)` | Create a single filter condition |
37
37
 
38
+ ### Callbacks
39
+
40
+ | Export | Description |
41
+ |---|---|
42
+ | `composeCallbacks(...sets)` | Combines several sets of collection callbacks into one, run in order, the way the backend combines its global, collection and property tiers. Also exported from `@rebasepro/cms-types` and `@rebasepro/server`. New in 0.25. |
43
+ | `createCallContext(partial)` | Builds the `context` a collection callback receives, so a test can call a hook directly; a missing `client`, `data` or `storageSource` throws on first use, naming itself. New in 0.25. |
44
+
45
+ ### Deprecation
46
+
47
+ | Export | Description |
48
+ |---|---|
49
+ | `warnDeprecated(key, message)` | Prints `[Rebase] Deprecated: <message>` once per process per key. What Rebase's own deprecated aliases call, and usable by a plugin or library for its own. New in 0.25. |
50
+
38
51
  ### Collections
39
52
 
40
53
  | Export | Description |
@@ -24,6 +24,13 @@ export declare class CollectionRegistry {
24
24
  getGlobalCallbacks(): CollectionCallbacks | undefined;
25
25
  private collectionsByTableName;
26
26
  private collectionsBySlug;
27
+ /**
28
+ * Root collections whose driver stores them under a `path` other than
29
+ * their slug, by that path. A list, because the path is only unique within
30
+ * a data source: two collections in different databases may both be stored
31
+ * at `customer`.
32
+ */
33
+ private collectionsByDataPath;
27
34
  private rootCollections;
28
35
  private cachedCollectionsList;
29
36
  private rawCollectionsByTableName;
@@ -49,11 +56,61 @@ export declare class CollectionRegistry {
49
56
  */
50
57
  registerMultiple(collections: CollectionConfig[]): boolean;
51
58
  register(collection: CollectionConfig, rawCollection?: CollectionConfig): void;
59
+ /**
60
+ * Index a root collection by the path its driver stores it under, when
61
+ * that is not its slug. Only roots: a subcollection's `path` is relative to
62
+ * the record it hangs off, so it names nothing on its own.
63
+ */
64
+ private indexDataPath;
52
65
  private _registerRecursively;
53
66
  normalizeCollection(collection: CollectionConfig): CollectionConfig;
54
67
  private normalizeProperties;
55
68
  private normalizeProperty;
56
- get(path: string): CollectionConfig | undefined;
69
+ /**
70
+ * The collection registered under `path`: by slug, by slug with hyphens
71
+ * for underscores, by table name, and last by the `path` a root Firestore
72
+ * or MongoDB collection declares for its driver.
73
+ *
74
+ * One string can name two collections. Declaring `slug: "fs_diagnosis",
75
+ * path: "diagnosis"` is how a Firestore collection sits beside a Postgres
76
+ * collection whose slug is `diagnosis` — and a reference read back from
77
+ * Firestore carries `diagnosis`. The order above settles it in favour of the
78
+ * slug unless `preferredDriver` is given: then the first collection whose
79
+ * data source or engine is `preferredDriver` wins. Pass a reference's
80
+ * `driver`, or the data source of the record the path was read from.
81
+ */
82
+ get(path: string, preferredDriver?: string): CollectionConfig | undefined;
83
+ /** Every collection {@link get} could answer `path` with, in its order. */
84
+ private candidatesFor;
85
+ /**
86
+ * The path a collection's rows are stored under, for the path the admin
87
+ * addresses it by.
88
+ *
89
+ * The admin addresses a collection by its slug — in routes, in
90
+ * `data.collection(...)`, in the `path` of every entity it lists — because
91
+ * the slug is what is unique. A Firestore or MongoDB collection may declare
92
+ * a `path` of its own ({@link getCollectionDataPath}), and its driver has to
93
+ * be handed that one, under every record and subcollection too:
94
+ * `fs_diagnosis/abc/locales` is stored at `diagnosis/abc/locales`.
95
+ *
96
+ * A path that names no registered collection, or runs only through
97
+ * collections stored under their slugs, comes back unchanged.
98
+ */
99
+ resolveDataPath(path: string): string;
100
+ /**
101
+ * The path the admin addresses a collection by, for a path its rows are
102
+ * stored under: the inverse of {@link resolveDataPath}, for a reference
103
+ * read back from its driver.
104
+ *
105
+ * The root is the longest leading run of segments naming a collection, and
106
+ * becomes its slug when the run is where that collection is stored. Each
107
+ * subcollection after a record id is matched by where it is stored too. A
108
+ * path that already names its collections by slug comes back unchanged.
109
+ *
110
+ * `preferredDriver` decides between a collection stored at the path and one
111
+ * whose slug it is, as in {@link get}.
112
+ */
113
+ resolveCollectionPath(path: string, preferredDriver?: string): string;
57
114
  /**
58
115
  * Gets the pristine, un-normalized collection exactly as it was provided.
59
116
  * Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
@@ -68,9 +125,21 @@ export declare class CollectionRegistry {
68
125
  getRawCollections(): CollectionConfig[];
69
126
  /**
70
127
  * Resolves a multi-segment path like "products/123/locales" and returns
71
- * information about the collections and entity IDs along the path
128
+ * information about the collections and entity IDs along the path.
129
+ *
130
+ * A slug may contain slashes (`content/de-DE/podcasts`), so the root is
131
+ * the longest leading run of segments naming a registered collection, and
132
+ * each subcollection is matched the same way among its parent's.
133
+ *
134
+ * @param options.allowRecordPath accept a path that ends at a record
135
+ * (`…/{id}`) and resolve it to the collection holding that record, with the
136
+ * id last in `entityIds`. Without it such a path is an error. Only the
137
+ * registry can tell the two shapes apart, because only it knows where each
138
+ * slug ends.
72
139
  */
73
- resolvePathToCollections(path: string): {
140
+ resolvePathToCollections(path: string, options?: {
141
+ allowRecordPath?: boolean;
142
+ }): {
74
143
  collections: CollectionConfig[];
75
144
  entityIds: (string | number)[];
76
145
  finalCollection: CollectionConfig;
@@ -0,0 +1,404 @@
1
+ import { enumToObjectEntries } from "../util/enums.es.js";
2
+ import { resolveRelation } from "../util/resolve-relation.es.js";
3
+ import { findRelation, getTableName, resolveCollectionRelations } from "../util/relations.es.js";
4
+ import { getSubcollections } from "../util/resolutions.es.js";
5
+ import { authSecretsMissingExclusion } from "./auth-secrets.es.js";
6
+ import { resolveDataSource } from "../data/resolveDataSource.es.js";
7
+ import { getCollectionDataPath, getDataSourceCapabilities } from "@rebasepro/types";
8
+ import { deepClone, removeFunctions } from "@rebasepro/utils";
9
+ import { deepEqual } from "fast-equals";
10
+ //#region src/collections/CollectionRegistry.ts
11
+ var CollectionRegistry = class {
12
+ /**
13
+ * Declared data sources, used during normalization to resolve each
14
+ * collection's engine (so `dataSource`-only collections get the right
15
+ * capabilities). Empty by default.
16
+ */
17
+ dataSources = {};
18
+ /**
19
+ * Global lifecycle callbacks applied to every collection.
20
+ * Runs on all data paths (REST, WebSocket, `rebase.dataAsAdmin`).
21
+ * Execution order: global → collection → property callbacks.
22
+ */
23
+ _globalCallbacks;
24
+ /**
25
+ * Set global lifecycle callbacks that apply to every collection.
26
+ * Typically called once during backend initialization.
27
+ */
28
+ setGlobalCallbacks(callbacks) {
29
+ this._globalCallbacks = callbacks;
30
+ }
31
+ /**
32
+ * Get the currently registered global callbacks, if any.
33
+ */
34
+ getGlobalCallbacks() {
35
+ return this._globalCallbacks;
36
+ }
37
+ collectionsByTableName = /* @__PURE__ */ new Map();
38
+ collectionsBySlug = /* @__PURE__ */ new Map();
39
+ /**
40
+ * Root collections whose driver stores them under a `path` other than
41
+ * their slug, by that path. A list, because the path is only unique within
42
+ * a data source: two collections in different databases may both be stored
43
+ * at `customer`.
44
+ */
45
+ collectionsByDataPath = /* @__PURE__ */ new Map();
46
+ rootCollections = [];
47
+ cachedCollectionsList = null;
48
+ rawCollectionsByTableName = /* @__PURE__ */ new Map();
49
+ rawCollectionsBySlug = /* @__PURE__ */ new Map();
50
+ rawRootCollections = [];
51
+ cachedRawCollectionsList = null;
52
+ lastRawInputEntity = null;
53
+ constructor(collections, dataSources) {
54
+ if (dataSources) this.dataSources = dataSources;
55
+ if (collections) this.registerMultiple(collections);
56
+ }
57
+ /**
58
+ * Provide the declared data sources used to resolve each collection's
59
+ * engine during normalization. Set this before registering collections.
60
+ * Returns true if the registry changed (callers may re-register).
61
+ */
62
+ setDataSources(dataSources) {
63
+ if (deepEqual(this.dataSources, dataSources)) return false;
64
+ this.dataSources = dataSources ?? {};
65
+ return true;
66
+ }
67
+ reset() {
68
+ this.collectionsByTableName.clear();
69
+ this.collectionsBySlug.clear();
70
+ this.collectionsByDataPath.clear();
71
+ this.rootCollections = [];
72
+ this.cachedCollectionsList = null;
73
+ this.rawCollectionsByTableName.clear();
74
+ this.rawCollectionsBySlug.clear();
75
+ this.rawRootCollections = [];
76
+ this.cachedRawCollectionsList = null;
77
+ }
78
+ /**
79
+ * Registers a collection and its subcollections recursively.
80
+ * Returns true if the collections have changed, false otherwise.
81
+ *
82
+ * Idempotent: compares the raw input (before normalization) against a stored
83
+ * entity. Only re-normalizes and re-registers when the raw input actually changed.
84
+ * @param collections
85
+ */
86
+ registerMultiple(collections) {
87
+ const rawEntity = collections.map((c) => removeFunctions(c));
88
+ if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) return false;
89
+ this.reset();
90
+ collections.forEach((c) => {
91
+ if (c.slug) this.collectionsBySlug.set(c.slug, c);
92
+ this.collectionsByTableName.set(getTableName(c), c);
93
+ });
94
+ const normalizedCollections = collections.map((c) => this.normalizeCollection({ ...c }));
95
+ normalizedCollections.forEach((c, index) => {
96
+ const raw = deepClone(collections[index]);
97
+ this.rootCollections.push(c);
98
+ this.rawRootCollections.push(raw);
99
+ const normalized = this.normalizeCollection(c);
100
+ this.collectionsByTableName.set(getTableName(normalized), normalized);
101
+ this.rawCollectionsByTableName.set(getTableName(raw), raw);
102
+ if (normalized.slug) this.collectionsBySlug.set(normalized.slug, normalized);
103
+ this.indexDataPath(normalized);
104
+ if (raw.slug) this.rawCollectionsBySlug.set(raw.slug, raw);
105
+ });
106
+ normalizedCollections.forEach((c) => {
107
+ const subcollections = getSubcollections(c);
108
+ if (subcollections && subcollections.length > 0) subcollections.forEach((subCollection) => {
109
+ if (!subCollection) return;
110
+ this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
111
+ });
112
+ });
113
+ this.lastRawInputEntity = rawEntity;
114
+ return true;
115
+ }
116
+ register(collection, rawCollection) {
117
+ const raw = rawCollection ? deepClone(rawCollection) : deepClone(collection);
118
+ this.rootCollections.push(collection);
119
+ this.rawRootCollections.push(raw);
120
+ this._registerRecursively(collection, raw);
121
+ const registered = this.collectionsByTableName.get(getTableName(collection));
122
+ if (registered) this.indexDataPath(registered);
123
+ }
124
+ /**
125
+ * Index a root collection by the path its driver stores it under, when
126
+ * that is not its slug. Only roots: a subcollection's `path` is relative to
127
+ * the record it hangs off, so it names nothing on its own.
128
+ */
129
+ indexDataPath(collection) {
130
+ const dataPath = getCollectionDataPath(collection);
131
+ if (!dataPath || dataPath === collection.slug) return;
132
+ const indexed = this.collectionsByDataPath.get(dataPath) ?? [];
133
+ if (!indexed.includes(collection)) this.collectionsByDataPath.set(dataPath, [...indexed, collection]);
134
+ }
135
+ _registerRecursively(collection, rawCollection) {
136
+ if (this.collectionsByTableName.has(getTableName(collection))) return;
137
+ const normalizedCollection = this.normalizeCollection(collection);
138
+ this.collectionsByTableName.set(getTableName(normalizedCollection), normalizedCollection);
139
+ this.rawCollectionsByTableName.set(getTableName(rawCollection), rawCollection);
140
+ if (normalizedCollection.slug) this.collectionsBySlug.set(normalizedCollection.slug, normalizedCollection);
141
+ if (rawCollection.slug) this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
142
+ const subcollections = getSubcollections(normalizedCollection);
143
+ if (subcollections && subcollections.length > 0) subcollections.forEach((subCollection) => {
144
+ if (!subCollection) return;
145
+ this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
146
+ });
147
+ }
148
+ normalizeCollection(collection) {
149
+ const result = { ...collection };
150
+ {
151
+ const resolved = resolveDataSource(result, this.dataSources);
152
+ if (!result.dataSource) result.dataSource = resolved.key;
153
+ if (!result.engine) result.engine = resolved.engine;
154
+ }
155
+ const properties = this.normalizeProperties(result.properties, result);
156
+ result.properties = properties;
157
+ for (const key of authSecretsMissingExclusion(result)) properties[key].excludeFromApi = true;
158
+ return result;
159
+ }
160
+ normalizeProperties(properties, collection) {
161
+ const newProperties = {};
162
+ for (const key in properties) newProperties[key] = this.normalizeProperty(key, properties[key], collection);
163
+ return newProperties;
164
+ }
165
+ normalizeProperty(key, property, collection) {
166
+ const newProperty = { ...property };
167
+ if (newProperty.type === "map" && newProperty.properties) newProperty.properties = this.normalizeProperties(newProperty.properties, collection);
168
+ else if (newProperty.type === "array") {
169
+ const arrayProp = newProperty;
170
+ if (arrayProp.of) if (Array.isArray(arrayProp.of)) arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, collection));
171
+ else arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, collection);
172
+ else if (arrayProp.oneOf && arrayProp.oneOf.properties) arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, collection);
173
+ } else if ((newProperty.type === "string" || newProperty.type === "number") && newProperty.enum) {
174
+ const stringOrNumberProperty = newProperty;
175
+ if (Array.isArray(stringOrNumberProperty.enum)) stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum);
176
+ else if (typeof stringOrNumberProperty.enum === "object") stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];
177
+ } else if (newProperty.type === "relation") {
178
+ const relationProperty = newProperty;
179
+ if (relationProperty.relation) relationProperty.resolvedRelation = resolveRelation(relationProperty.relation, collection, key);
180
+ else {
181
+ const declared = resolveCollectionRelations(collection)[key];
182
+ if (declared) relationProperty.resolvedRelation = declared;
183
+ else console.warn(`Relation property '${key}' on '${collection.slug}' names no relation: it has no \`relation\` block, and the collection's \`relations\` array has no entry called '${key}'. The field will render no picker, generate no foreign key, and return nothing from \`include()\`.`);
184
+ }
185
+ }
186
+ return newProperty;
187
+ }
188
+ /**
189
+ * The collection registered under `path`: by slug, by slug with hyphens
190
+ * for underscores, by table name, and last by the `path` a root Firestore
191
+ * or MongoDB collection declares for its driver.
192
+ *
193
+ * One string can name two collections. Declaring `slug: "fs_diagnosis",
194
+ * path: "diagnosis"` is how a Firestore collection sits beside a Postgres
195
+ * collection whose slug is `diagnosis` — and a reference read back from
196
+ * Firestore carries `diagnosis`. The order above settles it in favour of the
197
+ * slug unless `preferredDriver` is given: then the first collection whose
198
+ * data source or engine is `preferredDriver` wins. Pass a reference's
199
+ * `driver`, or the data source of the record the path was read from.
200
+ */
201
+ get(path, preferredDriver) {
202
+ if (preferredDriver) {
203
+ const preferred = this.candidatesFor(path).find((collection) => collection.dataSource === preferredDriver || collection.engine === preferredDriver);
204
+ if (preferred) return preferred;
205
+ }
206
+ const bySlug = this.collectionsBySlug.get(path);
207
+ if (bySlug) return bySlug;
208
+ if (path.includes("-")) {
209
+ const normalized = path.replace(/-/g, "_");
210
+ const byNormalized = this.collectionsBySlug.get(normalized);
211
+ if (byNormalized) return byNormalized;
212
+ }
213
+ const byTableName = this.collectionsByTableName.get(path);
214
+ if (byTableName) return byTableName;
215
+ return this.collectionsByDataPath.get(path)?.[0];
216
+ }
217
+ /** Every collection {@link get} could answer `path` with, in its order. */
218
+ candidatesFor(path) {
219
+ const candidates = [
220
+ this.collectionsBySlug.get(path),
221
+ path.includes("-") ? this.collectionsBySlug.get(path.replace(/-/g, "_")) : void 0,
222
+ this.collectionsByTableName.get(path),
223
+ ...this.collectionsByDataPath.get(path) ?? []
224
+ ];
225
+ return candidates.filter((collection, index) => collection !== void 0 && candidates.indexOf(collection) === index);
226
+ }
227
+ /**
228
+ * The path a collection's rows are stored under, for the path the admin
229
+ * addresses it by.
230
+ *
231
+ * The admin addresses a collection by its slug — in routes, in
232
+ * `data.collection(...)`, in the `path` of every entity it lists — because
233
+ * the slug is what is unique. A Firestore or MongoDB collection may declare
234
+ * a `path` of its own ({@link getCollectionDataPath}), and its driver has to
235
+ * be handed that one, under every record and subcollection too:
236
+ * `fs_diagnosis/abc/locales` is stored at `diagnosis/abc/locales`.
237
+ *
238
+ * A path that names no registered collection, or runs only through
239
+ * collections stored under their slugs, comes back unchanged.
240
+ */
241
+ resolveDataPath(path) {
242
+ let resolved;
243
+ const exact = this.get(path);
244
+ if (exact) resolved = {
245
+ collections: [exact],
246
+ entityIds: []
247
+ };
248
+ else try {
249
+ resolved = this.resolvePathToCollections(path);
250
+ } catch {
251
+ return path;
252
+ }
253
+ const { collections, entityIds } = resolved;
254
+ if (collections.every((collection) => getCollectionDataPath(collection) === collection.slug)) return path;
255
+ return collections.map((collection, index) => index < entityIds.length ? `${getCollectionDataPath(collection)}/${entityIds[index]}` : getCollectionDataPath(collection)).join("/");
256
+ }
257
+ /**
258
+ * The path the admin addresses a collection by, for a path its rows are
259
+ * stored under: the inverse of {@link resolveDataPath}, for a reference
260
+ * read back from its driver.
261
+ *
262
+ * The root is the longest leading run of segments naming a collection, and
263
+ * becomes its slug when the run is where that collection is stored. Each
264
+ * subcollection after a record id is matched by where it is stored too. A
265
+ * path that already names its collections by slug comes back unchanged.
266
+ *
267
+ * `preferredDriver` decides between a collection stored at the path and one
268
+ * whose slug it is, as in {@link get}.
269
+ */
270
+ resolveCollectionPath(path, preferredDriver) {
271
+ const segments = path.split("/").filter(Boolean);
272
+ for (let length = segments.length; length > 0; length--) {
273
+ const prefix = segments.slice(0, length).join("/");
274
+ const root = this.get(prefix, preferredDriver);
275
+ if (!root) continue;
276
+ const translated = [getCollectionDataPath(root) === prefix ? root.slug : prefix];
277
+ let current = root;
278
+ let rest = segments.slice(length);
279
+ while (rest.length >= 2) {
280
+ const [entityId, storedAt, ...after] = rest;
281
+ const child = getSubcollections(current).find((subcollection) => getCollectionDataPath(subcollection) === storedAt);
282
+ if (!child) break;
283
+ translated.push(entityId, child.slug);
284
+ current = child;
285
+ rest = after;
286
+ }
287
+ const collectionPath = [...translated, ...rest].join("/");
288
+ return collectionPath === segments.join("/") ? path : collectionPath;
289
+ }
290
+ return path;
291
+ }
292
+ /**
293
+ * Gets the pristine, un-normalized collection exactly as it was provided.
294
+ * Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
295
+ */
296
+ getRaw(path) {
297
+ const bySlug = this.rawCollectionsBySlug.get(path);
298
+ if (bySlug) return bySlug;
299
+ if (path.includes("-")) {
300
+ const normalized = path.replace(/-/g, "_");
301
+ const byNormalized = this.rawCollectionsBySlug.get(normalized);
302
+ if (byNormalized) return byNormalized;
303
+ }
304
+ return this.rawCollectionsByTableName.get(path);
305
+ }
306
+ /**
307
+ * Get collection by resolving multi-segment paths through relations
308
+ * e.g., "authors/70/posts" resolves to the posts collection
309
+ */
310
+ getCollectionByPath(collectionPath) {
311
+ if (!collectionPath.includes("/")) return this.get(collectionPath);
312
+ const pathSegments = collectionPath.split("/").filter((p) => p);
313
+ if (pathSegments.length < 3 || pathSegments.length % 2 === 0) throw new Error(`Invalid relation path: ${collectionPath}. Expected format: collection/id/relation or collection/id/relation/id/relation`);
314
+ const rootCollectionPath = pathSegments[0];
315
+ let currentCollection = this.get(rootCollectionPath);
316
+ if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
317
+ for (let i = 2; i < pathSegments.length; i += 2) {
318
+ const relationKey = pathSegments[i];
319
+ if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
320
+ const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
321
+ if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
322
+ const target = relation.target();
323
+ currentCollection = this.collectionsByTableName.get(getTableName(target)) ?? this.normalizeCollection(target);
324
+ if (i + 1 < pathSegments.length) {}
325
+ }
326
+ return currentCollection;
327
+ }
328
+ getCollections() {
329
+ if (!this.cachedCollectionsList) this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
330
+ return this.cachedCollectionsList;
331
+ }
332
+ getRawCollections() {
333
+ if (!this.cachedRawCollectionsList) this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
334
+ return this.cachedRawCollectionsList;
335
+ }
336
+ /**
337
+ * Resolves a multi-segment path like "products/123/locales" and returns
338
+ * information about the collections and entity IDs along the path.
339
+ *
340
+ * A slug may contain slashes (`content/de-DE/podcasts`), so the root is
341
+ * the longest leading run of segments naming a registered collection, and
342
+ * each subcollection is matched the same way among its parent's.
343
+ *
344
+ * @param options.allowRecordPath accept a path that ends at a record
345
+ * (`…/{id}`) and resolve it to the collection holding that record, with the
346
+ * id last in `entityIds`. Without it such a path is an error. Only the
347
+ * registry can tell the two shapes apart, because only it knows where each
348
+ * slug ends.
349
+ */
350
+ resolvePathToCollections(path, options) {
351
+ const pathSegments = path.split("/").filter((p) => p);
352
+ if (pathSegments.length === 0) throw new Error(`Invalid path: ${path}`);
353
+ const root = splitAtLeadingCollection(pathSegments, (candidate) => this.get(candidate));
354
+ if (!root) throw new Error(`Unknown collection path or slug: ${path}`);
355
+ let currentCollection = root.collection;
356
+ const collections = [currentCollection];
357
+ const entityIds = [];
358
+ let rest = root.rest;
359
+ while (rest.length > 0) {
360
+ const [entityId, ...afterEntity] = rest;
361
+ entityIds.push(entityId);
362
+ if (afterEntity.length === 0) {
363
+ if (options?.allowRecordPath) break;
364
+ throw new Error(`Invalid collection path: ${path}. It ends at the record '${entityId}' of '${currentCollection.slug}', not at a collection.`);
365
+ }
366
+ const subcollections = getSubcollections(currentCollection);
367
+ if (!subcollections || subcollections.length === 0) throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
368
+ const child = splitAtLeadingCollection(afterEntity, (candidate) => subcollections.find((c) => c.slug === candidate));
369
+ if (!child) throw new Error(`Subcollection '${afterEntity[0]}' not found in ${currentCollection.slug}`);
370
+ currentCollection = this.normalizeCollection(child.collection);
371
+ collections.push(currentCollection);
372
+ rest = child.rest;
373
+ }
374
+ return {
375
+ collections,
376
+ entityIds,
377
+ finalCollection: currentCollection
378
+ };
379
+ }
380
+ };
381
+ /**
382
+ * Split path segments at the collection they start with.
383
+ *
384
+ * A slug may contain slashes, and several drivers need it to: a Firestore
385
+ * collection partitioned by locale is declared as `content/de-DE/podcasts`,
386
+ * and that string is its *name*. So the collection a path starts with is not
387
+ * its first segment but the longest run of whole leading segments that names
388
+ * one, and what follows it is `entityId/subcollection/…`.
389
+ *
390
+ * Whole segments only, so `content` never matches the start of `contents/1`.
391
+ */
392
+ function splitAtLeadingCollection(segments, find) {
393
+ for (let length = segments.length; length > 0; length--) {
394
+ const collection = find(segments.slice(0, length).join("/"));
395
+ if (collection) return {
396
+ collection,
397
+ rest: segments.slice(length)
398
+ };
399
+ }
400
+ }
401
+ //#endregion
402
+ export { CollectionRegistry };
403
+
404
+ //# sourceMappingURL=CollectionRegistry.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollectionRegistry.es.js","names":[],"sources":["../../src/collections/CollectionRegistry.ts"],"sourcesContent":["import {\n ArrayProperty,\n CollectionCallbacks,\n EngineProperties,\n CollectionConfig,\n getCollectionDataPath,\n getDataSourceCapabilities,\n getDeclaredSubcollections,\n NumberProperty,\n Properties,\n Property,\n Relation,\n RelationProperty,\n StringProperty\n} from \"@rebasepro/types\";\nimport { deepEqual } from \"fast-equals\";\n\nimport {\n enumToObjectEntries,\n findRelation,\n getSubcollections,\n getTableName,\n resolveCollectionRelations,\n resolveRelation\n} from \"../util\";\nimport { deepClone, mergeDeep, removeFunctions } from \"@rebasepro/utils\";\nimport { DataSourceRegistry, resolveDataSource } from \"../data/resolveDataSource\";\nimport { authSecretsMissingExclusion } from \"./auth-secrets\";\n\nexport class CollectionRegistry {\n\n /**\n * Declared data sources, used during normalization to resolve each\n * collection's engine (so `dataSource`-only collections get the right\n * capabilities). Empty by default.\n */\n private dataSources: DataSourceRegistry = {};\n\n /**\n * Global lifecycle callbacks applied to every collection.\n * Runs on all data paths (REST, WebSocket, `rebase.dataAsAdmin`).\n * Execution order: global → collection → property callbacks.\n */\n private _globalCallbacks?: CollectionCallbacks;\n\n /**\n * Set global lifecycle callbacks that apply to every collection.\n * Typically called once during backend initialization.\n */\n setGlobalCallbacks(callbacks: CollectionCallbacks): void {\n this._globalCallbacks = callbacks;\n }\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): CollectionCallbacks | undefined {\n return this._globalCallbacks;\n }\n\n // Normalized runtime layer (used by Data Grid / UI)\n private collectionsByTableName = new Map<string, CollectionConfig>();\n private collectionsBySlug = new Map<string, CollectionConfig>();\n /**\n * Root collections whose driver stores them under a `path` other than\n * their slug, by that path. A list, because the path is only unique within\n * a data source: two collections in different databases may both be stored\n * at `customer`.\n */\n private collectionsByDataPath = new Map<string, CollectionConfig[]>();\n private rootCollections: CollectionConfig[] = [];\n private cachedCollectionsList: CollectionConfig[] | null = null;\n\n // Raw configuration layer (used by Collection Editor AST generator)\n private rawCollectionsByTableName = new Map<string, CollectionConfig>();\n private rawCollectionsBySlug = new Map<string, CollectionConfig>();\n private rawRootCollections: CollectionConfig[] = [];\n private cachedRawCollectionsList: CollectionConfig[] | null = null;\n\n // Entity of raw input for idempotency check — compared BEFORE normalization\n // to avoid the issue where normalization creates new objects that always fail equality.\n private lastRawInputEntity: ReturnType<typeof removeFunctions>[] | null = null;\n\n constructor(collections?: CollectionConfig[], dataSources?: DataSourceRegistry) {\n if (dataSources) this.dataSources = dataSources;\n if (collections) {\n this.registerMultiple(collections);\n }\n }\n\n /**\n * Provide the declared data sources used to resolve each collection's\n * engine during normalization. Set this before registering collections.\n * Returns true if the registry changed (callers may re-register).\n */\n setDataSources(dataSources: DataSourceRegistry): boolean {\n if (deepEqual(this.dataSources, dataSources)) return false;\n this.dataSources = dataSources ?? {};\n return true;\n }\n\n reset() {\n this.collectionsByTableName.clear();\n this.collectionsBySlug.clear();\n this.collectionsByDataPath.clear();\n this.rootCollections = [];\n this.cachedCollectionsList = null;\n\n this.rawCollectionsByTableName.clear();\n this.rawCollectionsBySlug.clear();\n this.rawRootCollections = [];\n this.cachedRawCollectionsList = null;\n }\n\n /**\n * Registers a collection and its subcollections recursively.\n * Returns true if the collections have changed, false otherwise.\n *\n * Idempotent: compares the raw input (before normalization) against a stored\n * entity. Only re-normalizes and re-registers when the raw input actually changed.\n * @param collections\n */\n registerMultiple(collections: CollectionConfig[]): boolean {\n // Compare raw input BEFORE normalization to detect actual changes.\n // This avoids the old issue where normalization creates new objects\n // that always fail deep-equal even when the source data is identical.\n const rawEntity = collections.map(c => removeFunctions(c));\n if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) {\n return false;\n }\n\n this.reset();\n // Phase 0: Populate maps with raw collections first for string target resolution\n collections.forEach((c) => {\n if (c.slug) {\n this.collectionsBySlug.set(c.slug, c);\n }\n this.collectionsByTableName.set(getTableName(c), c);\n });\n\n const normalizedCollections = collections.map(c => this.normalizeCollection({ ...c }));\n\n // Phase 1: Register all top-level collections first (without recursion).\n // This ensures that injected entityViews (e.g. History tab) are preserved.\n // Without this, _registerRecursively could register a relation-target collection\n // (e.g. Tags from Posts.relations) using the raw module object (without injected views)\n // before the top-level Tags collection (with injected views) gets its turn.\n normalizedCollections.forEach((c, index) => {\n const raw = deepClone(collections[index]);\n this.rootCollections.push(c);\n this.rawRootCollections.push(raw);\n\n const normalized = this.normalizeCollection(c);\n this.collectionsByTableName.set(getTableName(normalized), normalized);\n this.rawCollectionsByTableName.set(getTableName(raw), raw);\n if (normalized.slug) {\n this.collectionsBySlug.set(normalized.slug, normalized);\n }\n this.indexDataPath(normalized);\n if (raw.slug) {\n this.rawCollectionsBySlug.set(raw.slug, raw);\n }\n });\n\n // Phase 2: Now recurse into subcollections (relations, etc.)\n normalizedCollections.forEach((c) => {\n const subcollections = getSubcollections(c);\n if (subcollections && subcollections.length > 0) {\n subcollections.forEach((subCollection) => {\n if (!subCollection) return;\n // Spread to avoid mutating the original target() return value\n this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));\n });\n }\n });\n\n // Store the entity for future comparisons\n this.lastRawInputEntity = rawEntity;\n\n return true;\n }\n\n register(collection: CollectionConfig, rawCollection?: CollectionConfig) {\n const raw = rawCollection ? deepClone(rawCollection) : deepClone(collection);\n\n this.rootCollections.push(collection);\n this.rawRootCollections.push(raw);\n\n this._registerRecursively(collection, raw);\n\n const registered = this.collectionsByTableName.get(getTableName(collection));\n if (registered) this.indexDataPath(registered);\n }\n\n /**\n * Index a root collection by the path its driver stores it under, when\n * that is not its slug. Only roots: a subcollection's `path` is relative to\n * the record it hangs off, so it names nothing on its own.\n */\n private indexDataPath(collection: CollectionConfig) {\n const dataPath = getCollectionDataPath(collection);\n if (!dataPath || dataPath === collection.slug) return;\n const indexed = this.collectionsByDataPath.get(dataPath) ?? [];\n if (!indexed.includes(collection)) {\n this.collectionsByDataPath.set(dataPath, [...indexed, collection]);\n }\n }\n\n private _registerRecursively(collection: CollectionConfig, rawCollection: CollectionConfig) {\n if (this.collectionsByTableName.has(getTableName(collection))) {\n return;\n }\n\n const normalizedCollection = this.normalizeCollection(collection);\n this.collectionsByTableName.set(getTableName(normalizedCollection), normalizedCollection);\n this.rawCollectionsByTableName.set(getTableName(rawCollection), rawCollection);\n\n if (normalizedCollection.slug) {\n this.collectionsBySlug.set(normalizedCollection.slug, normalizedCollection);\n }\n if (rawCollection.slug) {\n this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);\n }\n\n // Use the normalized collection for subcollection discovery so that\n // both inline-extracted and explicit relations are considered.\n const subcollections = getSubcollections(normalizedCollection);\n\n if (subcollections && subcollections.length > 0) {\n subcollections.forEach((subCollection) => {\n if (!subCollection) return;\n // Spread to avoid mutating the original target() return value\n this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));\n });\n }\n }\n\n public normalizeCollection(collection: CollectionConfig): CollectionConfig {\n // Work on a shallow copy to avoid mutating the caller's reference.\n // This is critical for idempotency (the raw input must not be changed)\n // and for preventing mutation of module-level collection singletons.\n const result = { ...collection } as CollectionConfig;\n\n // 0. Resolve and stamp `dataSource` and `engine` on the normalized copy.\n // After this block every normalized collection has both fields set,\n // so downstream code can read them directly without calling\n // `resolveDataSource()`. Only the normalized layer is affected —\n // the raw layer used by the collection editor keeps the author's\n // original fields.\n {\n const resolved = resolveDataSource(result, this.dataSources);\n if (!result.dataSource) (result as { dataSource?: string }).dataSource = resolved.key;\n if (!result.engine) (result as { engine?: string }).engine = resolved.engine;\n }\n\n // Relations are left exactly as authored.\n //\n // This used to hoist every inline relation property into\n // `collection.relations`, merge it with the declared ones, and run each\n // through `sanitizeRelation` — a pass that guessed at missing fields and\n // fell back to the raw relation when it threw. `resolveCollectionRelations`\n // now reads both sources itself and defaults deterministically, so there\n // is nothing to hoist, nothing to merge and nothing to guess.\n //\n // The hoisting also had a defect worth not reinstating: it flattened\n // relations declared inside a `map` up to the collection's top level,\n // where they became child-view tabs keyed by the inner property key.\n\n // Stamp each relation property with its resolved relation.\n const properties: Properties = this.normalizeProperties(result.properties, result);\n result.properties = properties as EngineProperties;\n\n // The user store's secret columns are excluded in every registry, not\n // only in the config the server loaded. A redeclared `users` collection\n // that omits `excludeFromApi` reaches the panel as written, and a form\n // built from it submits `passwordHash: null` with every new user — a\n // write the server refuses. The properties are this call's own copies,\n // so setting the flag here leaves the caller's collection untouched.\n for (const key of authSecretsMissingExclusion(result)) {\n properties[key].excludeFromApi = true;\n }\n\n // `childCollections` is deliberately NOT populated here.\n //\n // It used to be, from the same many-relations `getEntityChildViews`\n // reads — but stamped with the *target's* slug rather than the relation\n // key, and then cached onto the collection, so the registry's version\n // shadowed the correct one for every consumer downstream. Deriving on\n // read leaves one implementation and keeps `childCollections` meaning\n // what it documents: a custom driver's explicit override.\n return result;\n }\n\n private normalizeProperties(properties: Properties, collection: CollectionConfig): Properties {\n const newProperties: Properties = {};\n for (const key in properties) {\n newProperties[key] = this.normalizeProperty(key, properties[key], collection);\n }\n return newProperties;\n }\n\n private normalizeProperty(key: string, property: Property, collection: CollectionConfig): Property {\n const newProperty = { ...property };\n\n if (newProperty.type === \"map\" && newProperty.properties) {\n newProperty.properties = this.normalizeProperties(newProperty.properties, collection);\n } else if (newProperty.type === \"array\") {\n // Cast to get a properly typed mutable reference\n const arrayProp = newProperty as ArrayProperty;\n if (arrayProp.of) {\n if (Array.isArray(arrayProp.of)) {\n (arrayProp as { of: Property | Property[] }).of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, collection));\n } else {\n arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, collection);\n }\n } else if (arrayProp.oneOf && arrayProp.oneOf.properties) {\n arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, collection);\n }\n } else if ((newProperty.type === \"string\" || newProperty.type === \"number\") && newProperty.enum) {\n const stringOrNumberProperty = newProperty as StringProperty | NumberProperty;\n if (Array.isArray(stringOrNumberProperty.enum)) {\n // A plain list of values (`[\"draft\", \"published\"]`, from a JS\n // config) becomes `{ id, label }` entries here, once, so no\n // reader downstream meets a bare string.\n stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum);\n } else if (typeof stringOrNumberProperty.enum === \"object\") {\n stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];\n }\n } else if (newProperty.type === \"relation\") {\n const relationProperty = newProperty as RelationProperty;\n\n // A property either declares its link inline, or names one the\n // collection declares. Resolve the first directly; look the second\n // up by name. Either way the property carries the fully-defaulted\n // relation, so no consumer has to re-derive it.\n if (relationProperty.relation) {\n relationProperty.resolvedRelation = resolveRelation(relationProperty.relation, collection, key);\n } else {\n const declared = resolveCollectionRelations(collection)[key];\n if (declared) {\n relationProperty.resolvedRelation = declared;\n } else {\n // The boot validator refuses this shape outright, naming the\n // property and both ways to fix it — see\n // `checkRelationPropertiesResolve` in @rebasepro/server. This\n // stays as the second line, for the registries built outside\n // a validated boot: the panel's, and the collection editor's\n // preview of a config being written.\n //\n // Still `console.warn`. There is no logger below\n // @rebasepro/server, and this package runs in the browser as\n // well as on the server, so acquiring one is a design\n // decision rather than a substitution.\n console.warn(\n `Relation property '${key}' on '${collection.slug}' names no relation: it has no ` +\n \"`relation` block, and the collection's `relations` array has no entry called \" +\n `'${key}'. The field will render no picker, generate no foreign key, and return ` +\n \"nothing from `include()`.\"\n );\n }\n }\n }\n\n return newProperty;\n }\n\n /**\n * The collection registered under `path`: by slug, by slug with hyphens\n * for underscores, by table name, and last by the `path` a root Firestore\n * or MongoDB collection declares for its driver.\n *\n * One string can name two collections. Declaring `slug: \"fs_diagnosis\",\n * path: \"diagnosis\"` is how a Firestore collection sits beside a Postgres\n * collection whose slug is `diagnosis` — and a reference read back from\n * Firestore carries `diagnosis`. The order above settles it in favour of the\n * slug unless `preferredDriver` is given: then the first collection whose\n * data source or engine is `preferredDriver` wins. Pass a reference's\n * `driver`, or the data source of the record the path was read from.\n */\n get(path: string, preferredDriver?: string): CollectionConfig | undefined {\n if (preferredDriver) {\n const preferred = this.candidatesFor(path)\n .find(collection => collection.dataSource === preferredDriver || collection.engine === preferredDriver);\n if (preferred) return preferred;\n }\n\n // First try slug lookup\n const bySlug = this.collectionsBySlug.get(path);\n if (bySlug) return bySlug;\n\n // Fallback: normalize hyphens → underscores (URLs use kebab-case, slugs use snake_case)\n if (path.includes(\"-\")) {\n const normalized = path.replace(/-/g, \"_\");\n const byNormalized = this.collectionsBySlug.get(normalized);\n if (byNormalized) return byNormalized;\n }\n\n // Fallback to table name lookup\n const byTableName = this.collectionsByTableName.get(path);\n if (byTableName) return byTableName;\n\n // Last, the path a driver stores a root collection under\n return this.collectionsByDataPath.get(path)?.[0];\n }\n\n /** Every collection {@link get} could answer `path` with, in its order. */\n private candidatesFor(path: string): CollectionConfig[] {\n const candidates = [\n this.collectionsBySlug.get(path),\n path.includes(\"-\") ? this.collectionsBySlug.get(path.replace(/-/g, \"_\")) : undefined,\n this.collectionsByTableName.get(path),\n ...(this.collectionsByDataPath.get(path) ?? [])\n ];\n return candidates.filter((collection, index): collection is CollectionConfig =>\n collection !== undefined && candidates.indexOf(collection) === index);\n }\n\n /**\n * The path a collection's rows are stored under, for the path the admin\n * addresses it by.\n *\n * The admin addresses a collection by its slug — in routes, in\n * `data.collection(...)`, in the `path` of every entity it lists — because\n * the slug is what is unique. A Firestore or MongoDB collection may declare\n * a `path` of its own ({@link getCollectionDataPath}), and its driver has to\n * be handed that one, under every record and subcollection too:\n * `fs_diagnosis/abc/locales` is stored at `diagnosis/abc/locales`.\n *\n * A path that names no registered collection, or runs only through\n * collections stored under their slugs, comes back unchanged.\n */\n resolveDataPath(path: string): string {\n let resolved: { collections: CollectionConfig[], entityIds: (string | number)[] };\n // The whole path first: a slug may contain slashes.\n const exact = this.get(path);\n if (exact) {\n resolved = { collections: [exact], entityIds: [] };\n } else {\n try {\n resolved = this.resolvePathToCollections(path);\n } catch {\n return path;\n }\n }\n\n const { collections, entityIds } = resolved;\n if (collections.every(collection => getCollectionDataPath(collection) === collection.slug)) {\n return path;\n }\n return collections\n .map((collection, index) => index < entityIds.length\n ? `${getCollectionDataPath(collection)}/${entityIds[index]}`\n : getCollectionDataPath(collection))\n .join(\"/\");\n }\n\n /**\n * The path the admin addresses a collection by, for a path its rows are\n * stored under: the inverse of {@link resolveDataPath}, for a reference\n * read back from its driver.\n *\n * The root is the longest leading run of segments naming a collection, and\n * becomes its slug when the run is where that collection is stored. Each\n * subcollection after a record id is matched by where it is stored too. A\n * path that already names its collections by slug comes back unchanged.\n *\n * `preferredDriver` decides between a collection stored at the path and one\n * whose slug it is, as in {@link get}.\n */\n resolveCollectionPath(path: string, preferredDriver?: string): string {\n const segments = path.split(\"/\").filter(Boolean);\n for (let length = segments.length; length > 0; length--) {\n const prefix = segments.slice(0, length).join(\"/\");\n const root = this.get(prefix, preferredDriver);\n if (!root) continue;\n\n // Reached by its slug, its table name or an alias, a root keeps\n // what it was called.\n const translated = [getCollectionDataPath(root) === prefix ? root.slug : prefix];\n let current: CollectionConfig = root;\n let rest = segments.slice(length);\n while (rest.length >= 2) {\n const [entityId, storedAt, ...after] = rest;\n const child = getSubcollections(current).find(subcollection => getCollectionDataPath(subcollection) === storedAt);\n if (!child) break;\n translated.push(entityId, child.slug);\n current = child;\n rest = after;\n }\n\n const collectionPath = [...translated, ...rest].join(\"/\");\n return collectionPath === segments.join(\"/\") ? path : collectionPath;\n }\n return path;\n }\n\n /**\n * Gets the pristine, un-normalized collection exactly as it was provided.\n * Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.\n */\n getRaw(path: string): CollectionConfig | undefined {\n const bySlug = this.rawCollectionsBySlug.get(path);\n if (bySlug) return bySlug;\n\n // Fallback: normalize hyphens → underscores (URLs use kebab-case, slugs use snake_case)\n if (path.includes(\"-\")) {\n const normalized = path.replace(/-/g, \"_\");\n const byNormalized = this.rawCollectionsBySlug.get(normalized);\n if (byNormalized) return byNormalized;\n }\n\n return this.rawCollectionsByTableName.get(path);\n }\n\n /**\n * Get collection by resolving multi-segment paths through relations\n * e.g., \"authors/70/posts\" resolves to the posts collection\n */\n getCollectionByPath(collectionPath: string): CollectionConfig | undefined {\n // Handle simple single collection path\n if (!collectionPath.includes(\"/\")) {\n return this.get(collectionPath);\n }\n\n // Handle multi-segment paths by resolving through relations\n const pathSegments = collectionPath.split(\"/\").filter(p => p);\n\n if (pathSegments.length < 3 || pathSegments.length % 2 === 0) {\n throw new Error(`Invalid relation path: ${collectionPath}. Expected format: collection/id/relation or collection/id/relation/id/relation`);\n }\n\n // Start with the root collection\n const rootCollectionPath = pathSegments[0];\n let currentCollection = this.get(rootCollectionPath);\n\n if (!currentCollection) {\n throw new Error(`Root collection not found: ${rootCollectionPath}`);\n }\n\n // Navigate through the path using relations\n for (let i = 2; i < pathSegments.length; i += 2) {\n const relationKey = pathSegments[i];\n\n // Get relations for current collection\n if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) {\n throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);\n }\n const resolvedRelations = resolveCollectionRelations(currentCollection);\n const relation = findRelation(resolvedRelations, relationKey);\n\n if (!relation) {\n throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);\n }\n\n // Move to the target collection.\n //\n // By the relation's own target, never by a slug lookup on its\n // *name*: `this.get(relation.relationName)` searches the global slug\n // map, so a relation named `people` that targets `notes` resolved to\n // an unrelated root collection called `people` — and a nested write\n // then ran that collection's callbacks against its properties.\n // The registered instance is preferred, matched by table, to pick up\n // whatever normalization and injection it received.\n const target = relation.target();\n currentCollection = this.collectionsByTableName.get(getTableName(target))\n ?? this.normalizeCollection(target);\n\n // If there are more segments, continue navigation\n if (i + 1 < pathSegments.length) {\n // Skip entity ID segment\n }\n }\n\n return currentCollection;\n }\n\n getCollections(): CollectionConfig[] {\n if (!this.cachedCollectionsList) {\n this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());\n }\n return this.cachedCollectionsList;\n }\n\n getRawCollections(): CollectionConfig[] {\n if (!this.cachedRawCollectionsList) {\n this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());\n }\n return this.cachedRawCollectionsList;\n }\n\n /**\n * Resolves a multi-segment path like \"products/123/locales\" and returns\n * information about the collections and entity IDs along the path.\n *\n * A slug may contain slashes (`content/de-DE/podcasts`), so the root is\n * the longest leading run of segments naming a registered collection, and\n * each subcollection is matched the same way among its parent's.\n *\n * @param options.allowRecordPath accept a path that ends at a record\n * (`…/{id}`) and resolve it to the collection holding that record, with the\n * id last in `entityIds`. Without it such a path is an error. Only the\n * registry can tell the two shapes apart, because only it knows where each\n * slug ends.\n */\n resolvePathToCollections(path: string, options?: { allowRecordPath?: boolean }): {\n collections: CollectionConfig[],\n entityIds: (string | number)[],\n finalCollection: CollectionConfig\n } {\n const pathSegments = path.split(\"/\").filter(p => p);\n\n if (pathSegments.length === 0) {\n throw new Error(`Invalid path: ${path}`);\n }\n\n // Not `pathSegments[0]`: for `medico/v2.0.0/joints/j1/movements` that\n // looked for a root collection called `medico`, threw, and left every\n // subcollection under a slashed slug unreachable.\n const root = splitAtLeadingCollection(pathSegments, (candidate) => this.get(candidate));\n if (!root) {\n throw new Error(`Unknown collection path or slug: ${path}`);\n }\n\n let currentCollection = root.collection;\n const collections: CollectionConfig[] = [currentCollection];\n const entityIds: (string | number)[] = [];\n\n // What follows a collection is (entityId, subcollection) pairs, so a\n // collection path is one whose *remainder* pairs up. Counting the whole\n // path's segments instead is off by one for every slug with an odd\n // number of slashes: `content/podcasts` and everything under it read\n // as a record path.\n let rest = root.rest;\n while (rest.length > 0) {\n const [entityId, ...afterEntity] = rest;\n entityIds.push(entityId);\n\n if (afterEntity.length === 0) {\n if (options?.allowRecordPath) break;\n throw new Error(`Invalid collection path: ${path}. It ends at the record '${entityId}' of '${currentCollection.slug}', not at a collection.`);\n }\n\n const subcollections: CollectionConfig[] | undefined = getSubcollections(currentCollection);\n if (!subcollections || subcollections.length === 0) {\n throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);\n }\n\n const child = splitAtLeadingCollection(afterEntity, (candidate) => subcollections.find(c => c.slug === candidate));\n if (!child) {\n throw new Error(`Subcollection '${afterEntity[0]}' not found in ${currentCollection.slug}`);\n }\n // The child as resolved, not whatever root collection happens to\n // share its slug. Re-looking it up globally both risked the wrong\n // collection and discarded the relation's `overrides`, which are\n // applied when the child view is built.\n currentCollection = this.normalizeCollection(child.collection);\n collections.push(currentCollection);\n rest = child.rest;\n }\n\n return {\n collections,\n entityIds,\n finalCollection: currentCollection\n };\n }\n\n}\n\n/**\n * Split path segments at the collection they start with.\n *\n * A slug may contain slashes, and several drivers need it to: a Firestore\n * collection partitioned by locale is declared as `content/de-DE/podcasts`,\n * and that string is its *name*. So the collection a path starts with is not\n * its first segment but the longest run of whole leading segments that names\n * one, and what follows it is `entityId/subcollection/…`.\n *\n * Whole segments only, so `content` never matches the start of `contents/1`.\n */\nfunction splitAtLeadingCollection(\n segments: string[],\n find: (candidate: string) => CollectionConfig | undefined\n): { collection: CollectionConfig; rest: string[] } | undefined {\n for (let length = segments.length; length > 0; length--) {\n const collection = find(segments.slice(0, length).join(\"/\"));\n if (collection) {\n return {\n collection,\n rest: segments.slice(length)\n };\n }\n }\n return undefined;\n}\n"],"mappings":"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"}