@rebasepro/common 0.23.0 → 0.24.1-canary.g56af4a4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collections/CollectionRegistry.d.ts +72 -3
- package/dist/collections/CollectionRegistry.es.js +404 -0
- package/dist/collections/CollectionRegistry.es.js.map +1 -0
- package/dist/collections/auth-secrets.es.js +47 -0
- package/dist/collections/auth-secrets.es.js.map +1 -0
- package/dist/collections/default-collections.es.js +120 -0
- package/dist/collections/default-collections.es.js.map +1 -0
- package/dist/collections/engine-support.es.js +35 -0
- package/dist/collections/engine-support.es.js.map +1 -0
- package/dist/collections/field-access.d.ts +13 -0
- package/dist/collections/field-access.es.js +142 -0
- package/dist/collections/field-access.es.js.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/soft-delete.es.js +34 -0
- package/dist/collections/soft-delete.es.js.map +1 -0
- package/dist/collections/unknown-field.d.ts +65 -0
- package/dist/collections/unknown-field.es.js +135 -0
- package/dist/collections/unknown-field.es.js.map +1 -0
- package/dist/data/buildRebaseData.d.ts +27 -2
- package/dist/data/buildRebaseData.es.js +1026 -0
- package/dist/data/buildRebaseData.es.js.map +1 -0
- package/dist/data/buildRoutedRebaseData.d.ts +6 -1
- package/dist/data/buildRoutedRebaseData.es.js +47 -0
- package/dist/data/buildRoutedRebaseData.es.js.map +1 -0
- package/dist/data/collection-accessor.d.ts +54 -0
- package/dist/data/collection-accessor.es.js +101 -0
- package/dist/data/collection-accessor.es.js.map +1 -0
- package/dist/data/cursor.d.ts +1 -1
- package/dist/data/cursor.es.js +166 -0
- package/dist/data/cursor.es.js.map +1 -0
- package/dist/data/filter-conditions.es.js +24 -0
- package/dist/data/filter-conditions.es.js.map +1 -0
- package/dist/data/filter-dialect.d.ts +3 -3
- package/dist/data/filter-dialect.es.js +786 -0
- package/dist/data/filter-dialect.es.js.map +1 -0
- package/dist/data/include-spec.d.ts +2 -0
- package/dist/data/include-spec.es.js +273 -0
- package/dist/data/include-spec.es.js.map +1 -0
- package/dist/data/paginate.d.ts +8 -1
- package/dist/data/paginate.es.js +181 -0
- package/dist/data/paginate.es.js.map +1 -0
- package/dist/data/query_builder.d.ts +52 -4
- package/dist/data/query_builder.es.js +151 -0
- package/dist/data/query_builder.es.js.map +1 -0
- package/dist/data/resolveDataSource.es.js +87 -0
- package/dist/data/resolveDataSource.es.js.map +1 -0
- package/dist/data/row-etag.d.ts +46 -0
- package/dist/data/row-etag.es.js +64 -0
- package/dist/data/row-etag.es.js.map +1 -0
- package/dist/data/sort-dialect.es.js +236 -0
- package/dist/data/sort-dialect.es.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +56 -7235
- package/dist/table-classification.es.js +107 -0
- package/dist/table-classification.es.js.map +1 -0
- package/dist/util/auth-default-policies.es.js +184 -0
- package/dist/util/auth-default-policies.es.js.map +1 -0
- package/dist/util/builders.es.js +15 -0
- package/dist/util/builders.es.js.map +1 -0
- package/dist/util/callback-errors.es.js +164 -0
- package/dist/util/callback-errors.es.js.map +1 -0
- package/dist/util/callbacks.es.js +72 -0
- package/dist/util/callbacks.es.js.map +1 -0
- package/dist/util/collections.es.js +79 -0
- package/dist/util/collections.es.js.map +1 -0
- package/dist/util/common.es.js +7 -0
- package/dist/util/common.es.js.map +1 -0
- package/dist/util/conditions.es.js +100 -0
- package/dist/util/conditions.es.js.map +1 -0
- package/dist/util/copy.d.ts +33 -0
- package/dist/util/copy.es.js +74 -0
- package/dist/util/copy.es.js.map +1 -0
- package/dist/util/download-url-cache.d.ts +122 -0
- package/dist/util/download-url-cache.es.js +187 -0
- package/dist/util/download-url-cache.es.js.map +1 -0
- package/dist/util/email.es.js +36 -0
- package/dist/util/email.es.js.map +1 -0
- package/dist/util/entities.d.ts +27 -2
- package/dist/util/entities.es.js +328 -0
- package/dist/util/entities.es.js.map +1 -0
- package/dist/util/enums.d.ts +10 -1
- package/dist/util/enums.es.js +37 -0
- package/dist/util/enums.es.js.map +1 -0
- package/dist/util/identity.es.js +138 -0
- package/dist/util/identity.es.js.map +1 -0
- package/dist/util/index.d.ts +6 -0
- package/dist/util/internal-tables.d.ts +10 -0
- package/dist/util/internal-tables.es.js +202 -0
- package/dist/util/internal-tables.es.js.map +1 -0
- package/dist/util/junction-policies.es.js +273 -0
- package/dist/util/junction-policies.es.js.map +1 -0
- package/dist/util/paths.es.js +26 -0
- package/dist/util/paths.es.js.map +1 -0
- package/dist/util/permissions.es.js +108 -0
- package/dist/util/permissions.es.js.map +1 -0
- package/dist/util/pg-column-to-property.es.js +303 -0
- package/dist/util/pg-column-to-property.es.js.map +1 -0
- package/dist/util/policy/compileRulePolicies.d.ts +32 -0
- package/dist/util/policy/compileRulePolicies.es.js +43 -0
- package/dist/util/policy/compileRulePolicies.es.js.map +1 -0
- package/dist/util/policy/evaluatePolicy.es.js +104 -0
- package/dist/util/policy/evaluatePolicy.es.js.map +1 -0
- package/dist/util/policy/index.d.ts +1 -0
- package/dist/util/policy/policyToPostgres.d.ts +24 -0
- package/dist/util/policy/policyToPostgres.es.js +593 -0
- package/dist/util/policy/policyToPostgres.es.js.map +1 -0
- package/dist/util/policy/policyToSecurityRule.es.js +79 -0
- package/dist/util/policy/policyToSecurityRule.es.js.map +1 -0
- package/dist/util/policy/securityRuleToConditions.es.js +48 -0
- package/dist/util/policy/securityRuleToConditions.es.js.map +1 -0
- package/dist/util/policy/sqlToPolicy.es.js +326 -0
- package/dist/util/policy/sqlToPolicy.es.js.map +1 -0
- package/dist/util/relations.es.js +243 -0
- package/dist/util/relations.es.js.map +1 -0
- package/dist/util/resolutions.es.js +318 -0
- package/dist/util/resolutions.es.js.map +1 -0
- package/dist/util/resolve-relation.es.js +163 -0
- package/dist/util/resolve-relation.es.js.map +1 -0
- package/dist/util/resumable-upload-threshold.d.ts +15 -0
- package/dist/util/resumable-upload-threshold.es.js +20 -0
- package/dist/util/resumable-upload-threshold.es.js.map +1 -0
- package/dist/util/resumable-upload.d.ts +51 -0
- package/dist/util/resumable-upload.es.js +121 -0
- package/dist/util/resumable-upload.es.js.map +1 -0
- package/dist/util/sql-redaction.d.ts +21 -0
- package/dist/util/sql-redaction.es.js +107 -0
- package/dist/util/sql-redaction.es.js.map +1 -0
- package/dist/util/sql-rows.es.js +41 -0
- package/dist/util/sql-rows.es.js.map +1 -0
- package/dist/util/storage.d.ts +26 -4
- package/dist/util/storage.es.js +117 -0
- package/dist/util/storage.es.js.map +1 -0
- package/dist/util/string-column-length.es.js +31 -0
- package/dist/util/string-column-length.es.js.map +1 -0
- package/dist/util/tenant.es.js +201 -0
- package/dist/util/tenant.es.js.map +1 -0
- package/dist/util/untrusted-envelope.d.ts +25 -0
- package/dist/util/untrusted-envelope.es.js +40 -0
- package/dist/util/untrusted-envelope.es.js.map +1 -0
- package/package.json +5 -3
- package/dist/index.es.js.map +0 -1
|
@@ -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
|
-
|
|
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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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defaultUsersCollection } from "./default-collections.es.js";
|
|
2
|
+
//#region src/collections/auth-secrets.ts
|
|
3
|
+
/** Is this collection the one the auth subsystem stores users in? */
|
|
4
|
+
function isAuthCollection(collection) {
|
|
5
|
+
const auth = collection.auth;
|
|
6
|
+
if (auth === true) return true;
|
|
7
|
+
return typeof auth === "object" && auth !== null && "enabled" in auth && auth.enabled === true;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The column names, and property keys, the default users collection marks
|
|
11
|
+
* `excludeFromApi`.
|
|
12
|
+
*
|
|
13
|
+
* Both spellings, because a redeclaration is free to rename the property: one
|
|
14
|
+
* project's copy says `password_hash` where the default says `passwordHash`, and
|
|
15
|
+
* the column is the same either way. Read off {@link defaultUsersCollection}, so
|
|
16
|
+
* a secret added there is covered on the same commit.
|
|
17
|
+
*/
|
|
18
|
+
function defaultSecretNames() {
|
|
19
|
+
const properties = defaultUsersCollection.properties;
|
|
20
|
+
const names = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
22
|
+
if (!property?.excludeFromApi) continue;
|
|
23
|
+
names.add(key);
|
|
24
|
+
if (property.columnName) names.add(property.columnName);
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The keys of the properties on `collection` that hold an auth secret and do not
|
|
30
|
+
* carry `excludeFromApi`. Empty for any collection that is not the user store: a
|
|
31
|
+
* `password_hash` column elsewhere — a CRM importing hashes, say — is that
|
|
32
|
+
* project's data, not this rule's business.
|
|
33
|
+
*/
|
|
34
|
+
function authSecretsMissingExclusion(collection) {
|
|
35
|
+
if (!collection.properties || !isAuthCollection(collection)) return [];
|
|
36
|
+
const secrets = defaultSecretNames();
|
|
37
|
+
const missing = [];
|
|
38
|
+
for (const [key, property] of Object.entries(collection.properties)) {
|
|
39
|
+
if (!property || property.excludeFromApi) continue;
|
|
40
|
+
if (secrets.has(key) || secrets.has(property.columnName ?? key)) missing.push(key);
|
|
41
|
+
}
|
|
42
|
+
return missing;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { authSecretsMissingExclusion };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=auth-secrets.es.js.map
|