@rebasepro/common 0.8.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 (53) hide show
  1. package/README.md +4 -4
  2. package/dist/collections/CollectionRegistry.d.ts +16 -16
  3. package/dist/collections/default-collections.d.ts +1 -1
  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 +18 -4
  7. package/dist/data/query_builder.d.ts +1 -1
  8. package/dist/data/resolveDataSource.d.ts +1 -1
  9. package/dist/data/sort-dialect.d.ts +41 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.es.js +569 -159
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +573 -163
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/util/builders.d.ts +19 -56
  16. package/dist/util/callbacks.d.ts +3 -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 +1 -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 +6 -6
  25. package/dist/util/policy/policyToPostgres.d.ts +14 -2
  26. package/dist/util/references.d.ts +2 -2
  27. package/dist/util/relations.d.ts +5 -5
  28. package/dist/util/resolutions.d.ts +2 -2
  29. package/package.json +3 -3
  30. package/src/collections/CollectionRegistry.ts +36 -36
  31. package/src/data/buildRebaseData.ts +332 -57
  32. package/src/data/buildRoutedRebaseData.ts +22 -16
  33. package/src/data/filter-dialect.ts +145 -60
  34. package/src/data/query_builder.ts +11 -2
  35. package/src/data/resolveDataSource.ts +1 -1
  36. package/src/data/sort-dialect.ts +56 -0
  37. package/src/index.ts +1 -0
  38. package/src/util/builders.ts +25 -99
  39. package/src/util/callbacks.ts +8 -8
  40. package/src/util/collections.ts +4 -4
  41. package/src/util/entities.ts +4 -4
  42. package/src/util/filter-operator-resolution.ts +81 -0
  43. package/src/util/index.ts +1 -0
  44. package/src/util/navigation_from_path.ts +4 -4
  45. package/src/util/navigation_utils.ts +8 -8
  46. package/src/util/parent_references_from_path.ts +3 -3
  47. package/src/util/permissions.test.ts +2 -2
  48. package/src/util/permissions.ts +7 -7
  49. package/src/util/policy/evaluatePolicy.ts +6 -0
  50. package/src/util/policy/policyToPostgres.ts +90 -10
  51. package/src/util/references.ts +2 -2
  52. package/src/util/relations.ts +12 -12
  53. package/src/util/resolutions.ts +5 -5
@@ -1,13 +1,17 @@
1
1
  import {
2
- DataDriver,
3
- RebaseData,
4
2
  CollectionAccessor,
5
- FindParams,
6
- FindResponse,
3
+ DataDriver,
7
4
  Entity,
8
5
  EntityValues,
9
- WhereFilterOp,
6
+ FindParams,
7
+ FindResponse,
8
+ FindResult,
10
9
  LogicalCondition,
10
+ RebaseData,
11
+ RebaseSdkData,
12
+ SDKCollectionClient,
13
+ SDKQueryBuilderInterface,
14
+ WhereFilterOp,
11
15
  WhereValue
12
16
  } from "@rebasepro/types";
13
17
  import { toSnakeCase } from "@rebasepro/utils";
@@ -15,14 +19,15 @@ import { QueryBuilder } from "./query_builder";
15
19
  import { deserializeFilter } from "./filter-dialect";
16
20
 
17
21
  /**
18
- * Parse an orderBy string like "created_at:desc" into [field, direction].
22
+ * Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
23
+ * Mirrors the client SDK's rowToEntity conversion.
19
24
  */
20
- function parseOrderBy(orderBy?: string): [string, "asc" | "desc"] | undefined {
21
- if (!orderBy) return undefined;
22
- const parts = orderBy.split(":");
23
- const field = parts[0];
24
- const direction = (parts[1] as "asc" | "desc") || "asc";
25
- return [field, direction];
25
+ function rowToEntity<M extends Record<string, unknown>>(row: Record<string, unknown>, slug: string): Entity<M> {
26
+ return {
27
+ id: row.id as string | number,
28
+ path: slug,
29
+ values: row as EntityValues<M>
30
+ };
26
31
  }
27
32
 
28
33
  function createDriverAccessor<M extends Record<string, unknown> = Record<string, unknown>>(
@@ -31,73 +36,87 @@ function createDriverAccessor<M extends Record<string, unknown> = Record<string,
31
36
  ): CollectionAccessor<M> {
32
37
  const accessor: CollectionAccessor<M> = {
33
38
  async find(params?: FindParams): Promise<FindResponse<M>> {
34
- const orderParsed = parseOrderBy(params?.orderBy);
35
39
  // Ensure filters are in canonical [op, value] format even if passed as PostgREST strings
36
- const filter = params?.where ? deserializeFilter(params.where as any) : undefined;
37
-
38
- const entities = await driver.fetchCollection<M>({
39
- path: slug,
40
- limit: params?.limit,
41
- offset: params?.offset,
42
- filter,
43
- orderBy: orderParsed?.[0],
44
- order: orderParsed?.[1],
45
- searchString: params?.searchString
46
- });
40
+ const filter = params?.where ? deserializeFilter(params.where as Record<string, unknown>) : undefined;
47
41
  const limit = params?.limit ?? 20;
48
42
  const offset = params?.offset ?? 0;
43
+
44
+ // Use the RestFetchService for include-aware queries when available
45
+ const fetchService = driver.restFetchService;
46
+ const rows = (fetchService && params?.include && params.include.length > 0)
47
+ ? await fetchService.fetchCollectionForRest(
48
+ slug,
49
+ {
50
+ filter,
51
+ limit: params?.limit,
52
+ offset: params?.offset,
53
+ orderBy: params?.orderBy?.[0],
54
+ order: params?.orderBy?.[1],
55
+ searchString: params?.searchString
56
+ },
57
+ params.include
58
+ )
59
+ : await driver.fetchCollection<M>({
60
+ path: slug,
61
+ limit: params?.limit,
62
+ offset: params?.offset,
63
+ filter,
64
+ orderBy: params?.orderBy?.[0],
65
+ order: params?.orderBy?.[1],
66
+ searchString: params?.searchString
67
+ });
68
+
69
+ // Compute real total when count is available
70
+ let total = rows.length + offset;
71
+ let hasMore = rows.length >= limit;
72
+ if (driver.count) {
73
+ total = await driver.count({ path: slug, filter });
74
+ hasMore = offset + rows.length < total;
75
+ }
76
+
49
77
  return {
50
- data: entities,
51
- meta: {
52
- total: entities.length,
53
- limit,
54
- offset,
55
- hasMore: entities.length >= limit
56
- }
78
+ data: rows.map((row: Record<string, unknown>) => rowToEntity<M>(row, slug)),
79
+ meta: { total, limit, offset, hasMore }
57
80
  };
58
81
  },
59
82
 
60
83
  async findById(id: string | number): Promise<Entity<M> | undefined> {
61
- return driver.fetchEntity<M>({ path: slug,
62
- entityId: id });
84
+ const row = await driver.fetchOne<M>({ path: slug, id: id });
85
+ return row ? rowToEntity<M>(row, slug) : undefined;
63
86
  },
64
87
 
65
88
  async create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>> {
66
- return driver.saveEntity<M>({
89
+ const row = await driver.save<M>({
67
90
  path: slug,
68
91
  values: data,
69
- entityId: id,
92
+ id: id,
70
93
  status: "new"
71
94
  });
95
+ return rowToEntity<M>(row, slug);
72
96
  },
73
97
 
74
98
  async update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>> {
75
- return driver.saveEntity<M>({
99
+ const row = await driver.save<M>({
76
100
  path: slug,
77
101
  values: data,
78
- entityId: id,
102
+ id: id,
79
103
  status: "existing"
80
104
  });
105
+ return rowToEntity<M>(row, slug);
81
106
  },
82
107
 
83
108
  async delete(id: string | number): Promise<void> {
84
- return driver.deleteEntity({
85
- entity: { id,
109
+ return driver.delete({
110
+ row: { id,
86
111
  path: slug,
87
112
  values: {} as Record<string, unknown> }
88
113
  });
89
114
  },
90
115
 
91
- deleteAll: driver.deleteAll
92
- ? async (): Promise<void> => {
93
- return driver.deleteAll!(slug);
94
- }
95
- : undefined,
96
-
97
- count: driver.countEntities
116
+ count: driver.count
98
117
  ? async (params?: FindParams): Promise<number> => {
99
- const filter = params?.where ? deserializeFilter(params.where as any) : undefined;
100
- return driver.countEntities!({
118
+ const filter = params?.where ? deserializeFilter(params.where as Record<string, unknown>) : undefined;
119
+ return driver.count!({
101
120
  path: slug,
102
121
  filter
103
122
  });
@@ -106,7 +125,6 @@ values: {} as Record<string, unknown> }
106
125
 
107
126
  listen: driver.listenCollection
108
127
  ? (params: FindParams | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void) => {
109
- const orderParsed = parseOrderBy(params?.orderBy);
110
128
  const limit = params?.limit ?? 20;
111
129
  const offset = params?.offset ?? 0;
112
130
  return driver.listenCollection!<M>({
@@ -114,12 +132,12 @@ values: {} as Record<string, unknown> }
114
132
  limit: params?.limit,
115
133
  offset: params?.offset,
116
134
  filter: params?.where,
117
- orderBy: orderParsed?.[0],
118
- order: orderParsed?.[1],
135
+ orderBy: params?.orderBy?.[0],
136
+ order: params?.orderBy?.[1],
119
137
  searchString: params?.searchString,
120
138
  onUpdate: (entities) => {
121
139
  onUpdate({
122
- data: entities,
140
+ data: entities.map((row: Record<string, unknown>) => rowToEntity<M>(row, slug)),
123
141
  meta: {
124
142
  total: entities.length,
125
143
  limit,
@@ -132,12 +150,12 @@ values: {} as Record<string, unknown> }
132
150
  });
133
151
  } : undefined,
134
152
 
135
- listenById: driver.listenEntity
153
+ listenById: driver.listenOne
136
154
  ? (id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void) => {
137
- return driver.listenEntity!<M>({
155
+ return driver.listenOne!<M>({
138
156
  path: slug,
139
- entityId: id,
140
- onUpdate: (entity) => onUpdate(entity ?? undefined),
157
+ id: id,
158
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity<M>(entity, slug) : undefined),
141
159
  onError
142
160
  });
143
161
  } : undefined,
@@ -180,7 +198,7 @@ values: {} as Record<string, unknown> }
180
198
  * @example
181
199
  * const data = buildRebaseData(driver);
182
200
  * await data.products.create({ name: "Camera", price: 299 });
183
- * const { data: items } = await data.products.find({ where: { status: "eq.published" } });
201
+ * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
184
202
  */
185
203
  export function buildRebaseData(driver: DataDriver): RebaseData {
186
204
  const cache = new Map<string, CollectionAccessor>();
@@ -212,3 +230,260 @@ export function buildRebaseData(driver: DataDriver): RebaseData {
212
230
  }
213
231
  });
214
232
  }
233
+
234
+ // =============================================================================
235
+ // SDK data — flat rows (symmetric with the frontend SDK client)
236
+ // =============================================================================
237
+
238
+ /**
239
+ * Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
240
+ * (id included) under `.values`, so this is just that payload.
241
+ */
242
+ function entityToRow<M extends Record<string, unknown>>(entity: Entity<M>): M {
243
+ return entity.values as unknown as M;
244
+ }
245
+
246
+ /**
247
+ * Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
248
+ * but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
249
+ * `FindResponse<M>`.
250
+ */
251
+ class SdkQueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements SDKQueryBuilderInterface<M> {
252
+ private params: FindParams = { where: {} };
253
+
254
+ constructor(private client: SDKCollectionClient<M>) {}
255
+
256
+ where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
257
+ where(logicalCondition: LogicalCondition): this;
258
+ where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown): this {
259
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
260
+ this.params.logical = columnOrCondition as LogicalCondition;
261
+ return this;
262
+ }
263
+ if (!this.params.where) this.params.where = {};
264
+ const column = columnOrCondition as string;
265
+ const condition: [WhereFilterOp, unknown] = [operator!, value];
266
+ const existing = this.params.where[column];
267
+ if (existing === undefined) {
268
+ this.params.where[column] = condition;
269
+ } else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) {
270
+ (this.params.where[column] as [WhereFilterOp, unknown][]).push(condition);
271
+ } else {
272
+ let firstCondition: [WhereFilterOp, unknown];
273
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") {
274
+ firstCondition = existing as [WhereFilterOp, unknown];
275
+ } else {
276
+ firstCondition = ["==", existing];
277
+ }
278
+ this.params.where[column] = [firstCondition, condition];
279
+ }
280
+ return this;
281
+ }
282
+
283
+ orderBy(column: keyof M & string, direction: "asc" | "desc" = "asc"): this {
284
+ this.params.orderBy = [column, direction];
285
+ return this;
286
+ }
287
+
288
+ limit(count: number): this { this.params.limit = count; return this; }
289
+ offset(count: number): this { this.params.offset = count; return this; }
290
+ search(searchString: string): this { this.params.searchString = searchString; return this; }
291
+ include(...relations: string[]): this { this.params.include = relations; return this; }
292
+
293
+ async find(): Promise<FindResult<M>> {
294
+ return this.client.find(this.params);
295
+ }
296
+
297
+ async count(): Promise<number> {
298
+ return this.client.count ? this.client.count(this.params) : 0;
299
+ }
300
+
301
+ listen(onUpdate: (data: FindResult<M>) => void, onError?: (error: Error) => void): () => void {
302
+ if (!this.client.listen) {
303
+ throw new Error("Listen is only available when the driver supports realtime.");
304
+ }
305
+ return this.client.listen(this.params, onUpdate, onError);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Wrap a Entity-shaped {@link CollectionAccessor} into a flat
311
+ * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
312
+ * so the backend SDK is byte-for-byte the same shape as the frontend client.
313
+ */
314
+ function toSdkCollectionClient<M extends Record<string, unknown>>(
315
+ snap: CollectionAccessor<M>
316
+ ): SDKCollectionClient<M> {
317
+ const client: SDKCollectionClient<M> = {
318
+ async find(params?: FindParams): Promise<FindResult<M>> {
319
+ const res = await snap.find(params);
320
+ return { data: res.data.map(entityToRow), meta: res.meta };
321
+ },
322
+ async findById(id: string | number): Promise<M | undefined> {
323
+ const s = await snap.findById(id);
324
+ return s ? entityToRow(s) : undefined;
325
+ },
326
+ async create(data: Partial<M>, id?: string | number): Promise<M> {
327
+ return entityToRow(await snap.create(data as Partial<EntityValues<M>>, id));
328
+ },
329
+ async update(id: string | number, data: Partial<M>): Promise<M> {
330
+ return entityToRow(await snap.update(id, data as Partial<EntityValues<M>>));
331
+ },
332
+ delete(id: string | number): Promise<void> {
333
+ return snap.delete(id);
334
+ },
335
+ count: snap.count ? (params?: FindParams) => snap.count!(params) : undefined,
336
+ listen: snap.listen
337
+ ? (params: FindParams | undefined, onUpdate: (r: FindResult<M>) => void, onError?: (e: Error) => void) =>
338
+ snap.listen!(params, (res) => onUpdate({ data: res.data.map(entityToRow), meta: res.meta }), onError)
339
+ : undefined,
340
+ listenById: snap.listenById
341
+ ? (id: string | number, onUpdate: (r: M | undefined) => void, onError?: (e: Error) => void) =>
342
+ snap.listenById!(id, (s) => onUpdate(s ? entityToRow(s) : undefined), onError)
343
+ : undefined,
344
+ where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown) {
345
+ const builder = new SdkQueryBuilder<M>(client);
346
+ if (typeof columnOrCondition === "object") {
347
+ return builder.where(columnOrCondition);
348
+ }
349
+ return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
350
+ },
351
+ orderBy: (column: keyof M & string, direction?: "asc" | "desc") => new SdkQueryBuilder<M>(client).orderBy(column, direction),
352
+ limit: (count: number) => new SdkQueryBuilder<M>(client).limit(count),
353
+ offset: (count: number) => new SdkQueryBuilder<M>(client).offset(count),
354
+ search: (searchString: string) => new SdkQueryBuilder<M>(client).search(searchString),
355
+ include: (...relations: string[]) => new SdkQueryBuilder<M>(client).include(...relations)
356
+ };
357
+ return client;
358
+ }
359
+
360
+ /**
361
+ * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
362
+ * {@link CollectionAccessor}. Every returned row is re-wrapped into the
363
+ * `{ id, path, values }` view-model the admin CMS renders.
364
+ */
365
+ function toEntityAccessor<M extends Record<string, unknown>>(
366
+ sdk: SDKCollectionClient<M>,
367
+ slug: string
368
+ ): CollectionAccessor<M> {
369
+ const accessor: CollectionAccessor<M> = {
370
+ async find(params?: FindParams): Promise<FindResponse<M>> {
371
+ const res = await sdk.find(params);
372
+ return { data: res.data.map((row) => rowToEntity<M>(row, slug)), meta: res.meta };
373
+ },
374
+ async findById(id: string | number): Promise<Entity<M> | undefined> {
375
+ const row = await sdk.findById(id);
376
+ return row ? rowToEntity<M>(row, slug) : undefined;
377
+ },
378
+ async create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>> {
379
+ return rowToEntity<M>(await sdk.create(data as Partial<M>, id), slug);
380
+ },
381
+ async update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>> {
382
+ const row = await sdk.update(id, data as Partial<M>);
383
+ if (!row) throw new Error(`Update returned no data for id ${id}`);
384
+ return rowToEntity<M>(row, slug);
385
+ },
386
+ delete(id: string | number): Promise<void> {
387
+ return sdk.delete(id);
388
+ },
389
+ count: sdk.count ? (params?: FindParams) => sdk.count!(params) : undefined,
390
+ listen: sdk.listen
391
+ ? (params: FindParams | undefined, onUpdate: (r: FindResponse<M>) => void, onError?: (e: Error) => void) =>
392
+ sdk.listen!(params, (res) => onUpdate({ data: res.data.map((row) => rowToEntity<M>(row, slug)), meta: res.meta }), onError)
393
+ : undefined,
394
+ listenById: sdk.listenById
395
+ ? (id: string | number, onUpdate: (s: Entity<M> | undefined) => void, onError?: (e: Error) => void) =>
396
+ sdk.listenById!(id, (row) => onUpdate(row ? rowToEntity<M>(row, slug) : undefined), onError)
397
+ : undefined,
398
+ where(columnOrCondition: string | LogicalCondition, operator?: WhereFilterOp, value?: unknown) {
399
+ const builder = new QueryBuilder<M>(accessor);
400
+ if (typeof columnOrCondition === "object") {
401
+ return builder.where(columnOrCondition);
402
+ }
403
+ return builder.where(columnOrCondition as keyof M & string, operator!, value as WhereValue<M[keyof M & string]>);
404
+ },
405
+ orderBy: (column: keyof M & string, direction?: "asc" | "desc") => new QueryBuilder<M>(accessor).orderBy(column, direction),
406
+ limit: (count: number) => new QueryBuilder<M>(accessor).limit(count),
407
+ offset: (count: number) => new QueryBuilder<M>(accessor).offset(count),
408
+ search: (searchString: string) => new QueryBuilder<M>(accessor).search(searchString),
409
+ include: (...relations: string[]) => new QueryBuilder<M>(accessor).include(...relations)
410
+ };
411
+ return accessor;
412
+ }
413
+
414
+ /**
415
+ * Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
416
+ *
417
+ * This is the **CMS boundary**: the SDK client (`client.data`) returns flat
418
+ * rows, but the admin renders the `Entity` view-model (`entity.values.*`).
419
+ * `core/Rebase.tsx` wraps `client.data` through this before handing it to the
420
+ * CMS `RebaseDataContext` — without it the admin renders rows with only their
421
+ * `id`.
422
+ */
423
+ export function wrapAsEntityData(sdkData: RebaseSdkData): RebaseData {
424
+ const cache = new Map<string, CollectionAccessor>();
425
+
426
+ function getAccessor(slug: string): CollectionAccessor {
427
+ let accessor = cache.get(slug);
428
+ if (!accessor) {
429
+ accessor = toEntityAccessor(sdkData.collection(slug), slug);
430
+ cache.set(slug, accessor);
431
+ }
432
+ return accessor;
433
+ }
434
+
435
+ const target = { collection: getAccessor } as RebaseData;
436
+
437
+ return new Proxy(target, {
438
+ get(_target, prop: string | symbol) {
439
+ if (prop === "collection") return getAccessor;
440
+ if (typeof prop === "symbol") return undefined;
441
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return undefined;
442
+ return getAccessor(toSnakeCase(prop));
443
+ }
444
+ });
445
+ }
446
+
447
+ /**
448
+ * Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
449
+ *
450
+ * Every collection accessor is adapted to return flat rows. Use this to derive
451
+ * the flat SDK data layer (`context.data`) from an existing Entity data layer
452
+ * — e.g. the admin routes its Entity data via `useData()` and exposes the
453
+ * same routing as flat `context.data` for callbacks by wrapping it here.
454
+ */
455
+ export function wrapAsSdkData(entityData: RebaseData): RebaseSdkData {
456
+ const cache = new Map<string, SDKCollectionClient>();
457
+
458
+ function getAccessor(slug: string): SDKCollectionClient {
459
+ let accessor = cache.get(slug);
460
+ if (!accessor) {
461
+ accessor = toSdkCollectionClient(entityData.collection(slug));
462
+ cache.set(slug, accessor);
463
+ }
464
+ return accessor;
465
+ }
466
+
467
+ const target = { collection: getAccessor } as RebaseSdkData;
468
+
469
+ return new Proxy(target, {
470
+ get(_target, prop: string | symbol) {
471
+ if (prop === "collection") return getAccessor;
472
+ if (typeof prop === "symbol") return undefined;
473
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return undefined;
474
+ return getAccessor(toSnakeCase(prop));
475
+ }
476
+ });
477
+ }
478
+
479
+ /**
480
+ * Build a flat {@link RebaseSdkData} from a `DataDriver`.
481
+ *
482
+ * This is the developer-facing SDK data layer used by backend framework
483
+ * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
484
+ * identical in shape to the frontend SDK client — so the API is symmetric
485
+ * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
486
+ */
487
+ export function buildSdkData(driver: DataDriver): RebaseSdkData {
488
+ return wrapAsSdkData(buildRebaseData(driver));
489
+ }
@@ -1,24 +1,30 @@
1
- import { RebaseData, CollectionAccessor } from "@rebasepro/types";
1
+ import { RebaseData, RebaseSdkData } from "@rebasepro/types";
2
2
  import { toSnakeCase } from "@rebasepro/utils";
3
3
 
4
+ /**
5
+ * The two data-layer shapes that can be routed: the Entity-shaped admin
6
+ * {@link RebaseData} or the flat SDK {@link RebaseSdkData}. Both expose a
7
+ * `.collection(slug)` accessor, which is all the router needs.
8
+ */
9
+ export type RoutableData = RebaseData | RebaseSdkData;
10
+
4
11
  /**
5
12
  * Parameters for {@link buildRoutedRebaseData}.
6
13
  */
7
- export interface RoutedRebaseDataParams {
14
+ export interface RoutedRebaseDataParams<T extends RoutableData = RebaseData> {
8
15
  /**
9
16
  * The default data source. Handles every collection that does not
10
17
  * resolve to an entry in `sources` (i.e. server-transport collections,
11
18
  * which ride the Rebase client).
12
19
  */
13
- defaultData: RebaseData;
20
+ defaultData: T;
14
21
 
15
22
  /**
16
- * Per-data-source {@link RebaseData} instances for direct and custom
17
- * transports, keyed by data-source key (e.g. `"analytics"`). Server-
18
- * mediated sources are not listed here — they fall through to
19
- * `defaultData`.
23
+ * Per-data-source instances for direct and custom transports, keyed by
24
+ * data-source key (e.g. `"analytics"`). Server-mediated sources are not
25
+ * listed here — they fall through to `defaultData`.
20
26
  */
21
- sources: Record<string, RebaseData>;
27
+ sources: Record<string, T>;
22
28
 
23
29
  /**
24
30
  * Resolve the data-source key for a given collection slug or path.
@@ -55,11 +61,11 @@ export interface RoutedRebaseDataParams {
55
61
  * await data.products.find(); // → default (server / Postgres)
56
62
  * await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
57
63
  */
58
- export function buildRoutedRebaseData({
64
+ export function buildRoutedRebaseData<T extends RoutableData = RebaseData>({
59
65
  defaultData,
60
66
  sources,
61
67
  resolveKey
62
- }: RoutedRebaseDataParams): RebaseData {
68
+ }: RoutedRebaseDataParams<T>): T {
63
69
 
64
70
  // Fast path: nothing to route → return the default untouched (preserves
65
71
  // referential identity for effect dependencies).
@@ -67,21 +73,21 @@ export function buildRoutedRebaseData({
67
73
  return defaultData;
68
74
  }
69
75
 
70
- function resolve(slugOrPath: string): RebaseData {
76
+ function resolve(slugOrPath: string): T {
71
77
  const key = resolveKey(slugOrPath);
72
78
  if (key && sources[key]) return sources[key];
73
79
  return defaultData;
74
80
  }
75
81
 
76
- function getAccessor(slugOrPath: string): CollectionAccessor {
77
- return resolve(slugOrPath).collection(slugOrPath);
82
+ function getAccessor(slugOrPath: string) {
83
+ return (resolve(slugOrPath) as RoutableData).collection(slugOrPath);
78
84
  }
79
85
 
80
86
  const target = {
81
87
  collection: getAccessor
82
- } as RebaseData;
88
+ } as unknown as T;
83
89
 
84
- return new Proxy(target, {
90
+ return new Proxy(target as object, {
85
91
  get(_target, prop: string | symbol) {
86
92
  if (prop === "collection") return getAccessor;
87
93
  // Ignore Symbol properties (e.g. Symbol.toPrimitive, Symbol.iterator)
@@ -93,5 +99,5 @@ export function buildRoutedRebaseData({
93
99
  // buildRebaseData so dynamic access routes consistently.
94
100
  return getAccessor(toSnakeCase(prop));
95
101
  }
96
- });
102
+ }) as T;
97
103
  }