@rebasepro/common 0.17.3 → 0.18.0
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/README.md +4 -0
- package/dist/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArrayProperty,
|
|
3
|
-
CollectionCallbacks,
|
|
4
|
-
EngineProperties,
|
|
5
|
-
CollectionConfig,
|
|
6
|
-
getDataSourceCapabilities,
|
|
7
|
-
getDeclaredSubcollections,
|
|
8
|
-
NumberProperty,
|
|
9
|
-
Properties,
|
|
10
|
-
Property,
|
|
11
|
-
Relation,
|
|
12
|
-
RelationProperty,
|
|
13
|
-
StringProperty
|
|
14
|
-
} from "@rebasepro/types";
|
|
15
|
-
import { deepEqual } from "fast-equals";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
enumToObjectEntries,
|
|
19
|
-
findRelation,
|
|
20
|
-
getSubcollections,
|
|
21
|
-
getTableName,
|
|
22
|
-
resolveCollectionRelations,
|
|
23
|
-
resolveRelation
|
|
24
|
-
} from "../util";
|
|
25
|
-
import { deepClone, mergeDeep, removeFunctions } from "@rebasepro/utils";
|
|
26
|
-
import { DataSourceRegistry, resolveDataSource } from "../data/resolveDataSource";
|
|
27
|
-
|
|
28
|
-
export class CollectionRegistry {
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Declared data sources, used during normalization to resolve each
|
|
32
|
-
* collection's engine (so `dataSource`-only collections get the right
|
|
33
|
-
* capabilities). Empty by default.
|
|
34
|
-
*/
|
|
35
|
-
private dataSources: DataSourceRegistry = {};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Global lifecycle callbacks applied to every collection.
|
|
39
|
-
* Runs on all data paths (REST, WebSocket, `rebase.data`).
|
|
40
|
-
* Execution order: global → collection → property callbacks.
|
|
41
|
-
*/
|
|
42
|
-
private _globalCallbacks?: CollectionCallbacks;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Set global lifecycle callbacks that apply to every collection.
|
|
46
|
-
* Typically called once during backend initialization.
|
|
47
|
-
*/
|
|
48
|
-
setGlobalCallbacks(callbacks: CollectionCallbacks): void {
|
|
49
|
-
this._globalCallbacks = callbacks;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the currently registered global callbacks, if any.
|
|
54
|
-
*/
|
|
55
|
-
getGlobalCallbacks(): CollectionCallbacks | undefined {
|
|
56
|
-
return this._globalCallbacks;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Normalized runtime layer (used by Data Grid / UI)
|
|
60
|
-
private collectionsByTableName = new Map<string, CollectionConfig>();
|
|
61
|
-
private collectionsBySlug = new Map<string, CollectionConfig>();
|
|
62
|
-
private rootCollections: CollectionConfig[] = [];
|
|
63
|
-
private cachedCollectionsList: CollectionConfig[] | null = null;
|
|
64
|
-
|
|
65
|
-
// Raw configuration layer (used by Collection Editor AST generator)
|
|
66
|
-
private rawCollectionsByTableName = new Map<string, CollectionConfig>();
|
|
67
|
-
private rawCollectionsBySlug = new Map<string, CollectionConfig>();
|
|
68
|
-
private rawRootCollections: CollectionConfig[] = [];
|
|
69
|
-
private cachedRawCollectionsList: CollectionConfig[] | null = null;
|
|
70
|
-
|
|
71
|
-
// Entity of raw input for idempotency check — compared BEFORE normalization
|
|
72
|
-
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
73
|
-
private lastRawInputEntity: ReturnType<typeof removeFunctions>[] | null = null;
|
|
74
|
-
|
|
75
|
-
constructor(collections?: CollectionConfig[], dataSources?: DataSourceRegistry) {
|
|
76
|
-
if (dataSources) this.dataSources = dataSources;
|
|
77
|
-
if (collections) {
|
|
78
|
-
this.registerMultiple(collections);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Provide the declared data sources used to resolve each collection's
|
|
84
|
-
* engine during normalization. Set this before registering collections.
|
|
85
|
-
* Returns true if the registry changed (callers may re-register).
|
|
86
|
-
*/
|
|
87
|
-
setDataSources(dataSources: DataSourceRegistry): boolean {
|
|
88
|
-
if (deepEqual(this.dataSources, dataSources)) return false;
|
|
89
|
-
this.dataSources = dataSources ?? {};
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
reset() {
|
|
94
|
-
this.collectionsByTableName.clear();
|
|
95
|
-
this.collectionsBySlug.clear();
|
|
96
|
-
this.rootCollections = [];
|
|
97
|
-
this.cachedCollectionsList = null;
|
|
98
|
-
|
|
99
|
-
this.rawCollectionsByTableName.clear();
|
|
100
|
-
this.rawCollectionsBySlug.clear();
|
|
101
|
-
this.rawRootCollections = [];
|
|
102
|
-
this.cachedRawCollectionsList = null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Registers a collection and its subcollections recursively.
|
|
107
|
-
* Returns true if the collections have changed, false otherwise.
|
|
108
|
-
*
|
|
109
|
-
* Idempotent: compares the raw input (before normalization) against a stored
|
|
110
|
-
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
111
|
-
* @param collections
|
|
112
|
-
*/
|
|
113
|
-
registerMultiple(collections: CollectionConfig[]): boolean {
|
|
114
|
-
// Compare raw input BEFORE normalization to detect actual changes.
|
|
115
|
-
// This avoids the old issue where normalization creates new objects
|
|
116
|
-
// that always fail deep-equal even when the source data is identical.
|
|
117
|
-
const rawEntity = collections.map(c => removeFunctions(c));
|
|
118
|
-
if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.reset();
|
|
123
|
-
// Phase 0: Populate maps with raw collections first for string target resolution
|
|
124
|
-
collections.forEach((c) => {
|
|
125
|
-
if (c.slug) {
|
|
126
|
-
this.collectionsBySlug.set(c.slug, c);
|
|
127
|
-
}
|
|
128
|
-
this.collectionsByTableName.set(getTableName(c), c);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const normalizedCollections = collections.map(c => this.normalizeCollection({ ...c }));
|
|
132
|
-
|
|
133
|
-
// Phase 1: Register all top-level collections first (without recursion).
|
|
134
|
-
// This ensures that injected entityViews (e.g. History tab) are preserved.
|
|
135
|
-
// Without this, _registerRecursively could register a relation-target collection
|
|
136
|
-
// (e.g. Tags from Posts.relations) using the raw module object (without injected views)
|
|
137
|
-
// before the top-level Tags collection (with injected views) gets its turn.
|
|
138
|
-
normalizedCollections.forEach((c, index) => {
|
|
139
|
-
const raw = deepClone(collections[index]);
|
|
140
|
-
this.rootCollections.push(c);
|
|
141
|
-
this.rawRootCollections.push(raw);
|
|
142
|
-
|
|
143
|
-
const normalized = this.normalizeCollection(c);
|
|
144
|
-
this.collectionsByTableName.set(getTableName(normalized), normalized);
|
|
145
|
-
this.rawCollectionsByTableName.set(getTableName(raw), raw);
|
|
146
|
-
if (normalized.slug) {
|
|
147
|
-
this.collectionsBySlug.set(normalized.slug, normalized);
|
|
148
|
-
}
|
|
149
|
-
if (raw.slug) {
|
|
150
|
-
this.rawCollectionsBySlug.set(raw.slug, raw);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Phase 2: Now recurse into subcollections (relations, etc.)
|
|
155
|
-
normalizedCollections.forEach((c) => {
|
|
156
|
-
const subcollections = getSubcollections(c);
|
|
157
|
-
if (subcollections && subcollections.length > 0) {
|
|
158
|
-
subcollections.forEach((subCollection) => {
|
|
159
|
-
if (!subCollection) return;
|
|
160
|
-
// Spread to avoid mutating the original target() return value
|
|
161
|
-
this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Store the entity for future comparisons
|
|
167
|
-
this.lastRawInputEntity = rawEntity;
|
|
168
|
-
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
register(collection: CollectionConfig, rawCollection?: CollectionConfig) {
|
|
173
|
-
const raw = rawCollection ? deepClone(rawCollection) : deepClone(collection);
|
|
174
|
-
|
|
175
|
-
this.rootCollections.push(collection);
|
|
176
|
-
this.rawRootCollections.push(raw);
|
|
177
|
-
|
|
178
|
-
this._registerRecursively(collection, raw);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private _registerRecursively(collection: CollectionConfig, rawCollection: CollectionConfig) {
|
|
182
|
-
if (this.collectionsByTableName.has(getTableName(collection))) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const normalizedCollection = this.normalizeCollection(collection);
|
|
187
|
-
this.collectionsByTableName.set(getTableName(normalizedCollection), normalizedCollection);
|
|
188
|
-
this.rawCollectionsByTableName.set(getTableName(rawCollection), rawCollection);
|
|
189
|
-
|
|
190
|
-
if (normalizedCollection.slug) {
|
|
191
|
-
this.collectionsBySlug.set(normalizedCollection.slug, normalizedCollection);
|
|
192
|
-
}
|
|
193
|
-
if (rawCollection.slug) {
|
|
194
|
-
this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Use the normalized collection for subcollection discovery so that
|
|
198
|
-
// both inline-extracted and explicit relations are considered.
|
|
199
|
-
const subcollections = getSubcollections(normalizedCollection);
|
|
200
|
-
|
|
201
|
-
if (subcollections && subcollections.length > 0) {
|
|
202
|
-
subcollections.forEach((subCollection) => {
|
|
203
|
-
if (!subCollection) return;
|
|
204
|
-
// Spread to avoid mutating the original target() return value
|
|
205
|
-
this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
public normalizeCollection(collection: CollectionConfig): CollectionConfig {
|
|
211
|
-
// Work on a shallow copy to avoid mutating the caller's reference.
|
|
212
|
-
// This is critical for idempotency (the raw input must not be changed)
|
|
213
|
-
// and for preventing mutation of module-level collection singletons.
|
|
214
|
-
const result = { ...collection } as CollectionConfig;
|
|
215
|
-
|
|
216
|
-
// 0. Resolve and stamp `dataSource` and `engine` on the normalized copy.
|
|
217
|
-
// After this block every normalized collection has both fields set,
|
|
218
|
-
// so downstream code can read them directly without calling
|
|
219
|
-
// `resolveDataSource()`. Only the normalized layer is affected —
|
|
220
|
-
// the raw layer used by the collection editor keeps the author's
|
|
221
|
-
// original fields.
|
|
222
|
-
{
|
|
223
|
-
const resolved = resolveDataSource(result, this.dataSources);
|
|
224
|
-
if (!result.dataSource) (result as { dataSource?: string }).dataSource = resolved.key;
|
|
225
|
-
if (!result.engine) (result as { engine?: string }).engine = resolved.engine;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Relations are left exactly as authored.
|
|
229
|
-
//
|
|
230
|
-
// This used to hoist every inline relation property into
|
|
231
|
-
// `collection.relations`, merge it with the declared ones, and run each
|
|
232
|
-
// through `sanitizeRelation` — a pass that guessed at missing fields and
|
|
233
|
-
// fell back to the raw relation when it threw. `resolveCollectionRelations`
|
|
234
|
-
// now reads both sources itself and defaults deterministically, so there
|
|
235
|
-
// is nothing to hoist, nothing to merge and nothing to guess.
|
|
236
|
-
//
|
|
237
|
-
// The hoisting also had a defect worth not reinstating: it flattened
|
|
238
|
-
// relations declared inside a `map` up to the collection's top level,
|
|
239
|
-
// where they became child-view tabs keyed by the inner property key.
|
|
240
|
-
|
|
241
|
-
// Stamp each relation property with its resolved relation.
|
|
242
|
-
const properties: Properties = this.normalizeProperties(result.properties, result);
|
|
243
|
-
result.properties = properties as EngineProperties;
|
|
244
|
-
|
|
245
|
-
// `childCollections` is deliberately NOT populated here.
|
|
246
|
-
//
|
|
247
|
-
// It used to be, from the same many-relations `getEntityChildViews`
|
|
248
|
-
// reads — but stamped with the *target's* slug rather than the relation
|
|
249
|
-
// key, and then cached onto the collection, so the registry's version
|
|
250
|
-
// shadowed the correct one for every consumer downstream. Deriving on
|
|
251
|
-
// read leaves one implementation and keeps `childCollections` meaning
|
|
252
|
-
// what it documents: a custom driver's explicit override.
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private normalizeProperties(properties: Properties, collection: CollectionConfig): Properties {
|
|
257
|
-
const newProperties: Properties = {};
|
|
258
|
-
for (const key in properties) {
|
|
259
|
-
newProperties[key] = this.normalizeProperty(key, properties[key], collection);
|
|
260
|
-
}
|
|
261
|
-
return newProperties;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private normalizeProperty(key: string, property: Property, collection: CollectionConfig): Property {
|
|
265
|
-
const newProperty = { ...property };
|
|
266
|
-
|
|
267
|
-
if (newProperty.type === "map" && newProperty.properties) {
|
|
268
|
-
newProperty.properties = this.normalizeProperties(newProperty.properties, collection);
|
|
269
|
-
} else if (newProperty.type === "array") {
|
|
270
|
-
// Cast to get a properly typed mutable reference
|
|
271
|
-
const arrayProp = newProperty as ArrayProperty;
|
|
272
|
-
if (arrayProp.of) {
|
|
273
|
-
if (Array.isArray(arrayProp.of)) {
|
|
274
|
-
(arrayProp as { of: Property | Property[] }).of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, collection));
|
|
275
|
-
} else {
|
|
276
|
-
arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, collection);
|
|
277
|
-
}
|
|
278
|
-
} else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
|
|
279
|
-
arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, collection);
|
|
280
|
-
}
|
|
281
|
-
} else if ((newProperty.type === "string" || newProperty.type === "number") && newProperty.enum) {
|
|
282
|
-
const stringOrNumberProperty = newProperty as StringProperty | NumberProperty;
|
|
283
|
-
if (typeof stringOrNumberProperty.enum === "object" && !Array.isArray(stringOrNumberProperty.enum)) {
|
|
284
|
-
stringOrNumberProperty.enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];
|
|
285
|
-
}
|
|
286
|
-
} else if (newProperty.type === "relation") {
|
|
287
|
-
const relationProperty = newProperty as RelationProperty;
|
|
288
|
-
|
|
289
|
-
// A property either declares its link inline, or names one the
|
|
290
|
-
// collection declares. Resolve the first directly; look the second
|
|
291
|
-
// up by name. Either way the property carries the fully-defaulted
|
|
292
|
-
// relation, so no consumer has to re-derive it.
|
|
293
|
-
if (relationProperty.relation) {
|
|
294
|
-
relationProperty.resolvedRelation = resolveRelation(relationProperty.relation, collection, key);
|
|
295
|
-
} else {
|
|
296
|
-
const declared = resolveCollectionRelations(collection)[key];
|
|
297
|
-
if (declared) {
|
|
298
|
-
relationProperty.resolvedRelation = declared;
|
|
299
|
-
} else {
|
|
300
|
-
console.warn(
|
|
301
|
-
`Relation property '${key}' on '${collection.slug}' declares no \`relation\`, and the ` +
|
|
302
|
-
"collection has no relation of that name."
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return newProperty;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
get(path: string): CollectionConfig | undefined {
|
|
312
|
-
// First try slug lookup
|
|
313
|
-
const bySlug = this.collectionsBySlug.get(path);
|
|
314
|
-
if (bySlug) return bySlug;
|
|
315
|
-
|
|
316
|
-
// Fallback: normalize hyphens → underscores (URLs use kebab-case, slugs use snake_case)
|
|
317
|
-
if (path.includes("-")) {
|
|
318
|
-
const normalized = path.replace(/-/g, "_");
|
|
319
|
-
const byNormalized = this.collectionsBySlug.get(normalized);
|
|
320
|
-
if (byNormalized) return byNormalized;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Fallback to table name lookup
|
|
324
|
-
return this.collectionsByTableName.get(path);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Gets the pristine, un-normalized collection exactly as it was provided.
|
|
329
|
-
* Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
|
|
330
|
-
*/
|
|
331
|
-
getRaw(path: string): CollectionConfig | undefined {
|
|
332
|
-
const bySlug = this.rawCollectionsBySlug.get(path);
|
|
333
|
-
if (bySlug) return bySlug;
|
|
334
|
-
|
|
335
|
-
// Fallback: normalize hyphens → underscores (URLs use kebab-case, slugs use snake_case)
|
|
336
|
-
if (path.includes("-")) {
|
|
337
|
-
const normalized = path.replace(/-/g, "_");
|
|
338
|
-
const byNormalized = this.rawCollectionsBySlug.get(normalized);
|
|
339
|
-
if (byNormalized) return byNormalized;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return this.rawCollectionsByTableName.get(path);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Get collection by resolving multi-segment paths through relations
|
|
347
|
-
* e.g., "authors/70/posts" resolves to the posts collection
|
|
348
|
-
*/
|
|
349
|
-
getCollectionByPath(collectionPath: string): CollectionConfig | undefined {
|
|
350
|
-
// Handle simple single collection path
|
|
351
|
-
if (!collectionPath.includes("/")) {
|
|
352
|
-
return this.get(collectionPath);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Handle multi-segment paths by resolving through relations
|
|
356
|
-
const pathSegments = collectionPath.split("/").filter(p => p);
|
|
357
|
-
|
|
358
|
-
if (pathSegments.length < 3 || pathSegments.length % 2 === 0) {
|
|
359
|
-
throw new Error(`Invalid relation path: ${collectionPath}. Expected format: collection/id/relation or collection/id/relation/id/relation`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Start with the root collection
|
|
363
|
-
const rootCollectionPath = pathSegments[0];
|
|
364
|
-
let currentCollection = this.get(rootCollectionPath);
|
|
365
|
-
|
|
366
|
-
if (!currentCollection) {
|
|
367
|
-
throw new Error(`Root collection not found: ${rootCollectionPath}`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Navigate through the path using relations
|
|
371
|
-
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
372
|
-
const relationKey = pathSegments[i];
|
|
373
|
-
|
|
374
|
-
// Get relations for current collection
|
|
375
|
-
if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) {
|
|
376
|
-
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
377
|
-
}
|
|
378
|
-
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
379
|
-
const relation = findRelation(resolvedRelations, relationKey);
|
|
380
|
-
|
|
381
|
-
if (!relation) {
|
|
382
|
-
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Move to the target collection.
|
|
386
|
-
//
|
|
387
|
-
// By the relation's own target, never by a slug lookup on its
|
|
388
|
-
// *name*: `this.get(relation.relationName)` searches the global slug
|
|
389
|
-
// map, so a relation named `people` that targets `notes` resolved to
|
|
390
|
-
// an unrelated root collection called `people` — and a nested write
|
|
391
|
-
// then ran that collection's callbacks against its properties.
|
|
392
|
-
// The registered instance is preferred, matched by table, to pick up
|
|
393
|
-
// whatever normalization and injection it received.
|
|
394
|
-
const target = relation.target();
|
|
395
|
-
currentCollection = this.collectionsByTableName.get(getTableName(target))
|
|
396
|
-
?? this.normalizeCollection(target);
|
|
397
|
-
|
|
398
|
-
// If there are more segments, continue navigation
|
|
399
|
-
if (i + 1 < pathSegments.length) {
|
|
400
|
-
// Skip entity ID segment
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return currentCollection;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
getCollections(): CollectionConfig[] {
|
|
408
|
-
if (!this.cachedCollectionsList) {
|
|
409
|
-
this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
410
|
-
}
|
|
411
|
-
return this.cachedCollectionsList;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
getRawCollections(): CollectionConfig[] {
|
|
415
|
-
if (!this.cachedRawCollectionsList) {
|
|
416
|
-
this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
417
|
-
}
|
|
418
|
-
return this.cachedRawCollectionsList;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Resolves a multi-segment path like "products/123/locales" and returns
|
|
423
|
-
* information about the collections and entity IDs along the path
|
|
424
|
-
*/
|
|
425
|
-
resolvePathToCollections(path: string): {
|
|
426
|
-
collections: CollectionConfig[],
|
|
427
|
-
entityIds: (string | number)[],
|
|
428
|
-
finalCollection: CollectionConfig
|
|
429
|
-
} {
|
|
430
|
-
const pathSegments = path.split("/").filter(p => p);
|
|
431
|
-
|
|
432
|
-
if (pathSegments.length === 0) {
|
|
433
|
-
throw new Error(`Invalid path: ${path}`);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (pathSegments.length % 2 !== 1) {
|
|
437
|
-
throw new Error(`Invalid collection path: ${path}. It must have an odd number of segments.`);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const collections: CollectionConfig[] = [];
|
|
441
|
-
const entityIds: (string | number)[] = [];
|
|
442
|
-
|
|
443
|
-
// Start with the first collection
|
|
444
|
-
let currentCollection = this.get(pathSegments[0]);
|
|
445
|
-
|
|
446
|
-
if (!currentCollection) {
|
|
447
|
-
throw new Error(`Unknown collection path or slug: ${pathSegments[0]}`);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
collections.push(currentCollection);
|
|
451
|
-
|
|
452
|
-
// Process the rest of the path in pairs (entityId, subcollectionSlug)
|
|
453
|
-
for (let i = 1; i < pathSegments.length; i += 2) {
|
|
454
|
-
const entityId = pathSegments[i];
|
|
455
|
-
entityIds.push(entityId);
|
|
456
|
-
|
|
457
|
-
if (i + 1 < pathSegments.length) {
|
|
458
|
-
const subcollectionSlug = pathSegments[i + 1];
|
|
459
|
-
const subcollections: CollectionConfig[] | undefined = getSubcollections(currentCollection);
|
|
460
|
-
if (!subcollections || subcollections.length === 0) {
|
|
461
|
-
throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const subcollection: CollectionConfig | undefined = subcollections.find(c => c.slug === subcollectionSlug);
|
|
465
|
-
if (!subcollection) {
|
|
466
|
-
throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
467
|
-
}
|
|
468
|
-
// The child as resolved, not whatever root collection happens to
|
|
469
|
-
// share its slug. Re-looking it up globally both risked the wrong
|
|
470
|
-
// collection and discarded the relation's `overrides`, which are
|
|
471
|
-
// applied when the child view is built.
|
|
472
|
-
currentCollection = this.normalizeCollection(subcollection);
|
|
473
|
-
collections.push(currentCollection);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
collections,
|
|
479
|
-
entityIds,
|
|
480
|
-
finalCollection: currentCollection
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { defineCollection } from "../util/builders";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Default users collection.
|
|
5
|
-
*
|
|
6
|
-
* Prepended to the developer's collections array by the admin and server.
|
|
7
|
-
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
8
|
-
* override by defining their own collection with `slug: "users"`.
|
|
9
|
-
*
|
|
10
|
-
* Schema only — no `admin` block. This package is on the backend's dependency path,
|
|
11
|
-
* where that field does not exist: `@rebasepro/cms-types` adds it by declaration
|
|
12
|
-
* merging, and a BaaS install never installs that. The scaffolded
|
|
13
|
-
* `config/collections/users.ts` carries the presentation for projects that want this
|
|
14
|
-
* collection in their panel, which is also where it is editable.
|
|
15
|
-
*/
|
|
16
|
-
export const defaultUsersCollection = defineCollection({
|
|
17
|
-
name: "Users",
|
|
18
|
-
singularName: "User",
|
|
19
|
-
slug: "users",
|
|
20
|
-
auth: true,
|
|
21
|
-
table: "users",
|
|
22
|
-
schema: "rebase",
|
|
23
|
-
securityRules: [
|
|
24
|
-
{ operation: "select",
|
|
25
|
-
roles: ["admin"] },
|
|
26
|
-
{ operations: ["insert", "update", "delete"],
|
|
27
|
-
roles: ["admin"] }
|
|
28
|
-
],
|
|
29
|
-
properties: {
|
|
30
|
-
id: {
|
|
31
|
-
name: "ID",
|
|
32
|
-
type: "string",
|
|
33
|
-
isId: "uuid"
|
|
34
|
-
},
|
|
35
|
-
email: {
|
|
36
|
-
name: "Email",
|
|
37
|
-
type: "string",
|
|
38
|
-
validation: { required: true,
|
|
39
|
-
unique: true }
|
|
40
|
-
},
|
|
41
|
-
displayName: {
|
|
42
|
-
name: "Name",
|
|
43
|
-
type: "string",
|
|
44
|
-
columnName: "display_name",
|
|
45
|
-
validation: { required: true }
|
|
46
|
-
},
|
|
47
|
-
photoURL: {
|
|
48
|
-
name: "Photo URL",
|
|
49
|
-
type: "string",
|
|
50
|
-
columnName: "photo_url"
|
|
51
|
-
},
|
|
52
|
-
roles: {
|
|
53
|
-
name: "Roles",
|
|
54
|
-
type: "array",
|
|
55
|
-
columnType: "text[]",
|
|
56
|
-
of: {
|
|
57
|
-
name: "Role",
|
|
58
|
-
type: "string",
|
|
59
|
-
enum: {
|
|
60
|
-
admin: "Admin",
|
|
61
|
-
editor: "Editor",
|
|
62
|
-
viewer: "Viewer"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
passwordHash: {
|
|
67
|
-
name: "Password Hash",
|
|
68
|
-
type: "string",
|
|
69
|
-
columnName: "password_hash",
|
|
70
|
-
excludeFromApi: true
|
|
71
|
-
},
|
|
72
|
-
emailVerified: {
|
|
73
|
-
name: "Email Verified",
|
|
74
|
-
type: "boolean",
|
|
75
|
-
columnName: "email_verified",
|
|
76
|
-
defaultValue: false
|
|
77
|
-
},
|
|
78
|
-
emailVerificationToken: {
|
|
79
|
-
name: "Email Verification Token",
|
|
80
|
-
type: "string",
|
|
81
|
-
columnName: "email_verification_token",
|
|
82
|
-
excludeFromApi: true
|
|
83
|
-
},
|
|
84
|
-
emailVerificationSentAt: {
|
|
85
|
-
name: "Email Verification Sent At",
|
|
86
|
-
type: "date",
|
|
87
|
-
columnName: "email_verification_sent_at"
|
|
88
|
-
},
|
|
89
|
-
metadata: {
|
|
90
|
-
name: "Metadata",
|
|
91
|
-
type: "map",
|
|
92
|
-
keyValue: true,
|
|
93
|
-
properties: {},
|
|
94
|
-
defaultValue: {}
|
|
95
|
-
},
|
|
96
|
-
createdAt: {
|
|
97
|
-
name: "Created At",
|
|
98
|
-
type: "date",
|
|
99
|
-
columnName: "created_at",
|
|
100
|
-
autoValue: "on_create"
|
|
101
|
-
},
|
|
102
|
-
updatedAt: {
|
|
103
|
-
name: "Updated At",
|
|
104
|
-
type: "date",
|
|
105
|
-
columnName: "updated_at",
|
|
106
|
-
autoValue: "on_update"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
});
|
package/src/collections/index.ts
DELETED