@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.
Files changed (63) hide show
  1. package/README.md +4 -4
  2. package/dist/collections/CollectionRegistry.d.ts +30 -15
  3. package/dist/collections/default-collections.d.ts +255 -2
  4. package/dist/data/buildRebaseData.d.ts +30 -2
  5. package/dist/data/buildRoutedRebaseData.d.ts +14 -9
  6. package/dist/data/filter-dialect.d.ts +75 -0
  7. package/dist/data/query_builder.d.ts +4 -4
  8. package/dist/data/resolveDataSource.d.ts +8 -8
  9. package/dist/data/sort-dialect.d.ts +41 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.es.js +1125 -299
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +1138 -303
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/util/builders.d.ts +52 -42
  16. package/dist/util/callbacks.d.ts +8 -3
  17. package/dist/util/collections.d.ts +4 -4
  18. package/dist/util/entities.d.ts +2 -2
  19. package/dist/util/filter-operator-resolution.d.ts +32 -0
  20. package/dist/util/index.d.ts +2 -0
  21. package/dist/util/navigation_from_path.d.ts +4 -4
  22. package/dist/util/navigation_utils.d.ts +3 -3
  23. package/dist/util/parent_references_from_path.d.ts +2 -2
  24. package/dist/util/permissions.d.ts +30 -6
  25. package/dist/util/policy/evaluatePolicy.d.ts +31 -0
  26. package/dist/util/policy/index.d.ts +3 -0
  27. package/dist/util/policy/policyToPostgres.d.ts +22 -0
  28. package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
  29. package/dist/util/policy/sqlToPolicy.d.ts +20 -0
  30. package/dist/util/references.d.ts +2 -2
  31. package/dist/util/relations.d.ts +5 -5
  32. package/dist/util/resolutions.d.ts +2 -2
  33. package/dist/util/storage.d.ts +26 -1
  34. package/package.json +13 -13
  35. package/src/collections/CollectionRegistry.ts +92 -61
  36. package/src/collections/default-collections.ts +4 -4
  37. package/src/data/buildRebaseData.ts +336 -172
  38. package/src/data/buildRoutedRebaseData.ts +22 -16
  39. package/src/data/filter-dialect.ts +403 -0
  40. package/src/data/query_builder.ts +19 -10
  41. package/src/data/resolveDataSource.ts +10 -10
  42. package/src/data/sort-dialect.ts +56 -0
  43. package/src/index.ts +2 -0
  44. package/src/util/builders.ts +87 -84
  45. package/src/util/callbacks.ts +15 -8
  46. package/src/util/collections.ts +4 -4
  47. package/src/util/entities.ts +4 -4
  48. package/src/util/filter-operator-resolution.ts +81 -0
  49. package/src/util/index.ts +2 -0
  50. package/src/util/navigation_from_path.ts +4 -4
  51. package/src/util/navigation_utils.ts +8 -8
  52. package/src/util/parent_references_from_path.ts +3 -3
  53. package/src/util/permissions.test.ts +7 -5
  54. package/src/util/permissions.ts +90 -163
  55. package/src/util/policy/evaluatePolicy.ts +152 -0
  56. package/src/util/policy/index.ts +3 -0
  57. package/src/util/policy/policyToPostgres.ts +165 -0
  58. package/src/util/policy/securityRuleToConditions.ts +67 -0
  59. package/src/util/policy/sqlToPolicy.ts +88 -0
  60. package/src/util/references.ts +3 -3
  61. package/src/util/relations.ts +19 -20
  62. package/src/util/resolutions.ts +11 -11
  63. package/src/util/storage.ts +34 -1
@@ -1,48 +1,78 @@
1
1
  import {
2
2
  ArrayProperty,
3
- CollectionWithRelations,
4
- CollectionWithSubcollections,
5
- EntityCollection,
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 { enumToObjectEntries, getSubcollections, getTableName, resolveCollectionRelations, findRelation, sanitizeRelation } from "../util";
17
- import { removeFunctions, mergeDeep, deepClone } from "@rebasepro/utils";
18
- import { resolveDataSource, DataSourceRegistry } from "../data/resolveDataSource";
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 → behaviour keys off `driver` as before.
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, EntityCollection>();
31
- private collectionsBySlug = new Map<string, EntityCollection>();
32
- private rootCollections: EntityCollection[] = [];
33
- private cachedCollectionsList: EntityCollection[] | null = null;
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, EntityCollection>();
37
- private rawCollectionsBySlug = new Map<string, EntityCollection>();
38
- private rawRootCollections: EntityCollection[] = [];
39
- private cachedRawCollectionsList: EntityCollection[] | null = null;
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
- // Snapshot of raw input for idempotency check — compared BEFORE normalization
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 lastRawInputSnapshot: ReturnType<typeof removeFunctions>[] | null = null;
73
+ private lastRawInputEntity: ReturnType<typeof removeFunctions>[] | null = null;
44
74
 
45
- constructor(collections?: EntityCollection[], dataSources?: DataSourceRegistry) {
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
- * snapshot. Only re-normalizes and re-registers when the raw input actually changed.
110
+ * entity. Only re-normalizes and re-registers when the raw input actually changed.
81
111
  * @param collections
82
112
  */
83
- registerMultiple(collections: EntityCollection[]): boolean {
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 rawSnapshot = collections.map(c => removeFunctions(c));
88
- if (this.lastRawInputSnapshot && deepEqual(this.lastRawInputSnapshot, rawSnapshot)) {
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 snapshot for future comparisons
137
- this.lastRawInputSnapshot = rawSnapshot;
166
+ // Store the entity for future comparisons
167
+ this.lastRawInputEntity = rawEntity;
138
168
 
139
169
  return true;
140
170
  }
141
171
 
142
- register(collection: EntityCollection, rawCollection?: EntityCollection) {
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: EntityCollection, rawCollection: EntityCollection) {
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: EntityCollection): EntityCollection {
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 EntityCollection;
185
-
186
- // 0. For `dataSource`-only collections (no explicit `driver`), resolve
187
- // the engine from the data-source registry and stamp it as `driver`
188
- // on the normalized copy, so downstream capability lookups (which
189
- // read `driver`) are correct. Surgical on purpose: collections that
190
- // already set `driver`, and plain default collections (no driver,
191
- // no dataSource), are left untouched. Only the normalized layer is
192
- // affected — the raw layer used by the collection editor keeps the
193
- // author's original fields.
194
- if (result.dataSource && !result.driver) {
195
- const engine = resolveDataSource(result, this.dataSources).engine;
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 as CollectionWithRelations;
204
- const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? (relResult.relations ?? []) : [];
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.driver).supportsRelations) {
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
- if (getDataSourceCapabilities(result.driver).supportsSubcollections && (result as CollectionWithSubcollections).subcollections) {
252
- result.childCollections = (result as CollectionWithSubcollections).subcollections;
253
- } else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
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): EntityCollection | undefined {
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): EntityCollection | undefined {
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): EntityCollection | undefined {
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.driver).supportsRelations) {
415
- throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
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(): EntityCollection[] {
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(): EntityCollection[] {
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: EntityCollection[],
489
+ collections: CollectionConfig[],
459
490
  entityIds: (string | number)[],
460
- finalCollection: EntityCollection
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: EntityCollection[] = [];
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: EntityCollection[] | undefined = getSubcollections(currentCollection);
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: EntityCollection | undefined = subcollections.find(c => c.slug === subcollectionSlug);
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 type { PostgresCollection } from "@rebasepro/types";
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: PostgresCollection = {
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
+ });