@rebasepro/common 0.7.0 → 0.9.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 -4
- package/dist/collections/CollectionRegistry.d.ts +30 -15
- package/dist/collections/default-collections.d.ts +255 -2
- package/dist/data/buildRebaseData.d.ts +30 -2
- package/dist/data/buildRoutedRebaseData.d.ts +14 -9
- package/dist/data/filter-dialect.d.ts +75 -0
- package/dist/data/query_builder.d.ts +4 -4
- package/dist/data/resolveDataSource.d.ts +8 -8
- package/dist/data/sort-dialect.d.ts +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +1125 -299
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1138 -303
- package/dist/index.umd.js.map +1 -1
- package/dist/util/builders.d.ts +52 -42
- package/dist/util/callbacks.d.ts +8 -3
- package/dist/util/collections.d.ts +4 -4
- package/dist/util/entities.d.ts +2 -2
- package/dist/util/filter-operator-resolution.d.ts +32 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/navigation_from_path.d.ts +4 -4
- package/dist/util/navigation_utils.d.ts +3 -3
- package/dist/util/parent_references_from_path.d.ts +2 -2
- package/dist/util/permissions.d.ts +30 -6
- package/dist/util/policy/evaluatePolicy.d.ts +31 -0
- package/dist/util/policy/index.d.ts +3 -0
- package/dist/util/policy/policyToPostgres.d.ts +22 -0
- package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
- package/dist/util/policy/sqlToPolicy.d.ts +20 -0
- package/dist/util/references.d.ts +2 -2
- package/dist/util/relations.d.ts +5 -5
- package/dist/util/resolutions.d.ts +2 -2
- package/dist/util/storage.d.ts +26 -1
- package/package.json +13 -13
- package/src/collections/CollectionRegistry.ts +92 -61
- package/src/collections/default-collections.ts +4 -4
- package/src/data/buildRebaseData.ts +336 -172
- package/src/data/buildRoutedRebaseData.ts +22 -16
- package/src/data/filter-dialect.ts +403 -0
- package/src/data/query_builder.ts +19 -10
- package/src/data/resolveDataSource.ts +10 -10
- package/src/data/sort-dialect.ts +56 -0
- package/src/index.ts +2 -0
- package/src/util/builders.ts +87 -84
- package/src/util/callbacks.ts +15 -8
- package/src/util/collections.ts +4 -4
- package/src/util/entities.ts +4 -4
- package/src/util/filter-operator-resolution.ts +81 -0
- package/src/util/index.ts +2 -0
- package/src/util/navigation_from_path.ts +4 -4
- package/src/util/navigation_utils.ts +8 -8
- package/src/util/parent_references_from_path.ts +3 -3
- package/src/util/permissions.test.ts +7 -5
- package/src/util/permissions.ts +90 -163
- package/src/util/policy/evaluatePolicy.ts +152 -0
- package/src/util/policy/index.ts +3 -0
- package/src/util/policy/policyToPostgres.ts +165 -0
- package/src/util/policy/securityRuleToConditions.ts +67 -0
- package/src/util/policy/sqlToPolicy.ts +88 -0
- package/src/util/references.ts +3 -3
- package/src/util/relations.ts +19 -20
- package/src/util/resolutions.ts +11 -11
- package/src/util/storage.ts +34 -1
|
@@ -1,48 +1,78 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrayProperty,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
CollectionCallbacks,
|
|
4
|
+
EngineProperties,
|
|
5
|
+
CollectionConfig,
|
|
6
|
+
getDataSourceCapabilities,
|
|
7
|
+
getDeclaredSubcollections,
|
|
6
8
|
NumberProperty,
|
|
7
9
|
Properties,
|
|
8
10
|
Property,
|
|
9
11
|
Relation,
|
|
10
12
|
RelationProperty,
|
|
11
|
-
StringProperty
|
|
12
|
-
getDataSourceCapabilities
|
|
13
|
+
StringProperty
|
|
13
14
|
} from "@rebasepro/types";
|
|
14
15
|
import { deepEqual } from "fast-equals";
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
enumToObjectEntries,
|
|
19
|
+
findRelation,
|
|
20
|
+
getSubcollections,
|
|
21
|
+
getTableName,
|
|
22
|
+
resolveCollectionRelations,
|
|
23
|
+
sanitizeRelation
|
|
24
|
+
} from "../util";
|
|
25
|
+
import { deepClone, mergeDeep, removeFunctions } from "@rebasepro/utils";
|
|
26
|
+
import { DataSourceRegistry, resolveDataSource } from "../data/resolveDataSource";
|
|
19
27
|
|
|
20
28
|
export class CollectionRegistry {
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* Declared data sources, used during normalization to resolve each
|
|
24
32
|
* collection's engine (so `dataSource`-only collections get the right
|
|
25
|
-
* capabilities). Empty by default
|
|
33
|
+
* capabilities). Empty by default.
|
|
26
34
|
*/
|
|
27
35
|
private dataSources: DataSourceRegistry = {};
|
|
28
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
|
+
|
|
29
59
|
// Normalized runtime layer (used by Data Grid / UI)
|
|
30
|
-
private collectionsByTableName = new Map<string,
|
|
31
|
-
private collectionsBySlug = new Map<string,
|
|
32
|
-
private rootCollections:
|
|
33
|
-
private cachedCollectionsList:
|
|
60
|
+
private collectionsByTableName = new Map<string, CollectionConfig>();
|
|
61
|
+
private collectionsBySlug = new Map<string, CollectionConfig>();
|
|
62
|
+
private rootCollections: CollectionConfig[] = [];
|
|
63
|
+
private cachedCollectionsList: CollectionConfig[] | null = null;
|
|
34
64
|
|
|
35
65
|
// Raw configuration layer (used by Collection Editor AST generator)
|
|
36
|
-
private rawCollectionsByTableName = new Map<string,
|
|
37
|
-
private rawCollectionsBySlug = new Map<string,
|
|
38
|
-
private rawRootCollections:
|
|
39
|
-
private cachedRawCollectionsList:
|
|
66
|
+
private rawCollectionsByTableName = new Map<string, CollectionConfig>();
|
|
67
|
+
private rawCollectionsBySlug = new Map<string, CollectionConfig>();
|
|
68
|
+
private rawRootCollections: CollectionConfig[] = [];
|
|
69
|
+
private cachedRawCollectionsList: CollectionConfig[] | null = null;
|
|
40
70
|
|
|
41
|
-
//
|
|
71
|
+
// Entity of raw input for idempotency check — compared BEFORE normalization
|
|
42
72
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
43
|
-
private
|
|
73
|
+
private lastRawInputEntity: ReturnType<typeof removeFunctions>[] | null = null;
|
|
44
74
|
|
|
45
|
-
constructor(collections?:
|
|
75
|
+
constructor(collections?: CollectionConfig[], dataSources?: DataSourceRegistry) {
|
|
46
76
|
if (dataSources) this.dataSources = dataSources;
|
|
47
77
|
if (collections) {
|
|
48
78
|
this.registerMultiple(collections);
|
|
@@ -77,15 +107,15 @@ export class CollectionRegistry {
|
|
|
77
107
|
* Returns true if the collections have changed, false otherwise.
|
|
78
108
|
*
|
|
79
109
|
* Idempotent: compares the raw input (before normalization) against a stored
|
|
80
|
-
*
|
|
110
|
+
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
81
111
|
* @param collections
|
|
82
112
|
*/
|
|
83
|
-
registerMultiple(collections:
|
|
113
|
+
registerMultiple(collections: CollectionConfig[]): boolean {
|
|
84
114
|
// Compare raw input BEFORE normalization to detect actual changes.
|
|
85
115
|
// This avoids the old issue where normalization creates new objects
|
|
86
116
|
// that always fail deep-equal even when the source data is identical.
|
|
87
|
-
const
|
|
88
|
-
if (this.
|
|
117
|
+
const rawEntity = collections.map(c => removeFunctions(c));
|
|
118
|
+
if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) {
|
|
89
119
|
return false;
|
|
90
120
|
}
|
|
91
121
|
|
|
@@ -133,13 +163,13 @@ export class CollectionRegistry {
|
|
|
133
163
|
}
|
|
134
164
|
});
|
|
135
165
|
|
|
136
|
-
// Store the
|
|
137
|
-
this.
|
|
166
|
+
// Store the entity for future comparisons
|
|
167
|
+
this.lastRawInputEntity = rawEntity;
|
|
138
168
|
|
|
139
169
|
return true;
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
register(collection:
|
|
172
|
+
register(collection: CollectionConfig, rawCollection?: CollectionConfig) {
|
|
143
173
|
const raw = rawCollection ? deepClone(rawCollection) : deepClone(collection);
|
|
144
174
|
|
|
145
175
|
this.rootCollections.push(collection);
|
|
@@ -148,7 +178,7 @@ export class CollectionRegistry {
|
|
|
148
178
|
this._registerRecursively(collection, raw);
|
|
149
179
|
}
|
|
150
180
|
|
|
151
|
-
private _registerRecursively(collection:
|
|
181
|
+
private _registerRecursively(collection: CollectionConfig, rawCollection: CollectionConfig) {
|
|
152
182
|
if (this.collectionsByTableName.has(getTableName(collection))) {
|
|
153
183
|
return;
|
|
154
184
|
}
|
|
@@ -177,31 +207,30 @@ export class CollectionRegistry {
|
|
|
177
207
|
}
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
public normalizeCollection(collection:
|
|
210
|
+
public normalizeCollection(collection: CollectionConfig): CollectionConfig {
|
|
181
211
|
// Work on a shallow copy to avoid mutating the caller's reference.
|
|
182
212
|
// This is critical for idempotency (the raw input must not be changed)
|
|
183
213
|
// and for preventing mutation of module-level collection singletons.
|
|
184
|
-
const result = { ...collection } as
|
|
185
|
-
|
|
186
|
-
// 0.
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (engine) (result as { driver?: string }).driver = engine;
|
|
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;
|
|
197
226
|
}
|
|
198
227
|
|
|
199
228
|
// 1. Extract relations from properties that have inline config (target set)
|
|
200
229
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
201
230
|
|
|
202
231
|
// 2. Merge with manual relations[] (manual entries win on name conflict)
|
|
203
|
-
const relResult = result
|
|
204
|
-
const manualRelations = getDataSourceCapabilities(result.
|
|
232
|
+
const relResult = result;
|
|
233
|
+
const manualRelations = getDataSourceCapabilities(result.engine).supportsRelations ? (relResult.relations ?? []) : [];
|
|
205
234
|
const mergedRelationsRaw = [...extractedRelations];
|
|
206
235
|
for (const manual of manualRelations) {
|
|
207
236
|
const name = manual.relationName;
|
|
@@ -227,7 +256,7 @@ export class CollectionRegistry {
|
|
|
227
256
|
// foreignKeyOnTarget, etc.) are populated. Without this the
|
|
228
257
|
// property.relation stamp is missing junction-table metadata and
|
|
229
258
|
// the backend cannot fetch many-to-many data.
|
|
230
|
-
if (getDataSourceCapabilities(result.
|
|
259
|
+
if (getDataSourceCapabilities(result.engine).supportsRelations) {
|
|
231
260
|
mergedRelations = mergedRelationsRaw.map(r => {
|
|
232
261
|
try {
|
|
233
262
|
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
@@ -244,13 +273,15 @@ export class CollectionRegistry {
|
|
|
244
273
|
|
|
245
274
|
// 4. Normalize properties (which stamps relation on each property)
|
|
246
275
|
const properties: Properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
247
|
-
result.properties = properties;
|
|
276
|
+
result.properties = properties as EngineProperties;
|
|
248
277
|
|
|
249
278
|
// Populate childCollections from driver-specific fields
|
|
250
279
|
if (!result.childCollections) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
280
|
+
const capabilities = getDataSourceCapabilities(result.engine);
|
|
281
|
+
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
282
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) {
|
|
283
|
+
result.childCollections = declaredSubcollections;
|
|
284
|
+
} else if (capabilities.supportsRelations && relResult.relations) {
|
|
254
285
|
const manyRelations = relResult.relations.filter((r: Relation) => r.cardinality === "many");
|
|
255
286
|
if (manyRelations.length > 0) {
|
|
256
287
|
result.childCollections = () => manyRelations.map((r: Relation) => {
|
|
@@ -347,7 +378,7 @@ export class CollectionRegistry {
|
|
|
347
378
|
return newProperty;
|
|
348
379
|
}
|
|
349
380
|
|
|
350
|
-
get(path: string):
|
|
381
|
+
get(path: string): CollectionConfig | undefined {
|
|
351
382
|
// First try slug lookup
|
|
352
383
|
const bySlug = this.collectionsBySlug.get(path);
|
|
353
384
|
if (bySlug) return bySlug;
|
|
@@ -367,7 +398,7 @@ export class CollectionRegistry {
|
|
|
367
398
|
* Gets the pristine, un-normalized collection exactly as it was provided.
|
|
368
399
|
* Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
|
|
369
400
|
*/
|
|
370
|
-
getRaw(path: string):
|
|
401
|
+
getRaw(path: string): CollectionConfig | undefined {
|
|
371
402
|
const bySlug = this.rawCollectionsBySlug.get(path);
|
|
372
403
|
if (bySlug) return bySlug;
|
|
373
404
|
|
|
@@ -385,7 +416,7 @@ export class CollectionRegistry {
|
|
|
385
416
|
* Get collection by resolving multi-segment paths through relations
|
|
386
417
|
* e.g., "authors/70/posts" resolves to the posts collection
|
|
387
418
|
*/
|
|
388
|
-
getCollectionByPath(collectionPath: string):
|
|
419
|
+
getCollectionByPath(collectionPath: string): CollectionConfig | undefined {
|
|
389
420
|
// Handle simple single collection path
|
|
390
421
|
if (!collectionPath.includes("/")) {
|
|
391
422
|
return this.get(collectionPath);
|
|
@@ -411,8 +442,8 @@ export class CollectionRegistry {
|
|
|
411
442
|
const relationKey = pathSegments[i];
|
|
412
443
|
|
|
413
444
|
// Get relations for current collection
|
|
414
|
-
if (!getDataSourceCapabilities(currentCollection.
|
|
415
|
-
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses
|
|
445
|
+
if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) {
|
|
446
|
+
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
416
447
|
}
|
|
417
448
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
418
449
|
const relation = findRelation(resolvedRelations, relationKey);
|
|
@@ -436,14 +467,14 @@ export class CollectionRegistry {
|
|
|
436
467
|
return currentCollection;
|
|
437
468
|
}
|
|
438
469
|
|
|
439
|
-
getCollections():
|
|
470
|
+
getCollections(): CollectionConfig[] {
|
|
440
471
|
if (!this.cachedCollectionsList) {
|
|
441
472
|
this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
442
473
|
}
|
|
443
474
|
return this.cachedCollectionsList;
|
|
444
475
|
}
|
|
445
476
|
|
|
446
|
-
getRawCollections():
|
|
477
|
+
getRawCollections(): CollectionConfig[] {
|
|
447
478
|
if (!this.cachedRawCollectionsList) {
|
|
448
479
|
this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
449
480
|
}
|
|
@@ -455,9 +486,9 @@ export class CollectionRegistry {
|
|
|
455
486
|
* information about the collections and entity IDs along the path
|
|
456
487
|
*/
|
|
457
488
|
resolvePathToCollections(path: string): {
|
|
458
|
-
collections:
|
|
489
|
+
collections: CollectionConfig[],
|
|
459
490
|
entityIds: (string | number)[],
|
|
460
|
-
finalCollection:
|
|
491
|
+
finalCollection: CollectionConfig
|
|
461
492
|
} {
|
|
462
493
|
const pathSegments = path.split("/").filter(p => p);
|
|
463
494
|
|
|
@@ -469,7 +500,7 @@ export class CollectionRegistry {
|
|
|
469
500
|
throw new Error(`Invalid collection path: ${path}. It must have an odd number of segments.`);
|
|
470
501
|
}
|
|
471
502
|
|
|
472
|
-
const collections:
|
|
503
|
+
const collections: CollectionConfig[] = [];
|
|
473
504
|
const entityIds: (string | number)[] = [];
|
|
474
505
|
|
|
475
506
|
// Start with the first collection
|
|
@@ -488,12 +519,12 @@ export class CollectionRegistry {
|
|
|
488
519
|
|
|
489
520
|
if (i + 1 < pathSegments.length) {
|
|
490
521
|
const subcollectionSlug = pathSegments[i + 1];
|
|
491
|
-
const subcollections:
|
|
522
|
+
const subcollections: CollectionConfig[] | undefined = getSubcollections(currentCollection);
|
|
492
523
|
if (!subcollections || subcollections.length === 0) {
|
|
493
524
|
throw new Error(`No subcollections found for ${currentCollection.slug} in path: ${path}`);
|
|
494
525
|
}
|
|
495
526
|
|
|
496
|
-
const subcollection:
|
|
527
|
+
const subcollection: CollectionConfig | undefined = subcollections.find(c => c.slug === subcollectionSlug);
|
|
497
528
|
if (!subcollection) {
|
|
498
529
|
throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug}`);
|
|
499
530
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { defineCollection } from "../util/builders";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default users collection.
|
|
@@ -7,7 +7,7 @@ import type { PostgresCollection } from "@rebasepro/types";
|
|
|
7
7
|
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
8
8
|
* override by defining their own collection with `slug: "users"`.
|
|
9
9
|
*/
|
|
10
|
-
export const defaultUsersCollection
|
|
10
|
+
export const defaultUsersCollection = defineCollection({
|
|
11
11
|
name: "Users",
|
|
12
12
|
singularName: "User",
|
|
13
13
|
slug: "users",
|
|
@@ -48,7 +48,7 @@ unique: true }
|
|
|
48
48
|
name: "Photo URL",
|
|
49
49
|
type: "string",
|
|
50
50
|
columnName: "photo_url",
|
|
51
|
-
url: "image"
|
|
51
|
+
ui: { url: "image" }
|
|
52
52
|
},
|
|
53
53
|
roles: {
|
|
54
54
|
name: "Roles",
|
|
@@ -120,4 +120,4 @@ disabled: { hidden: true } }
|
|
|
120
120
|
},
|
|
121
121
|
listProperties: ["displayName", "email", "roles", "createdAt"],
|
|
122
122
|
propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
|
|
123
|
-
};
|
|
123
|
+
});
|