@rebasepro/common 0.19.2-canary.g201749f → 0.19.2-canary.g2cd43e9

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.
@@ -19,20 +19,10 @@ export interface EntityDataOptions {
19
19
  */
20
20
  resolveCollection?: (slug: string) => {
21
21
  properties?: Record<string, unknown>;
22
+ relations?: unknown[];
23
+ slug?: string;
22
24
  } | undefined;
23
25
  }
24
- /**
25
- * Build a `RebaseData` object from a `DataDriver` using JavaScript Proxy.
26
- *
27
- * This is the key bridge: any property access like `data.products` returns
28
- * a `CollectionAccessor` backed by the underlying DataDriver, without
29
- * needing per-collection code generation.
30
- *
31
- * @example
32
- * const data = buildRebaseData(driver);
33
- * await data.products.create({ name: "Camera", price: 299 });
34
- * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
35
- */
36
26
  export declare function buildRebaseData(driver: DataDriver, options?: EntityDataOptions): RebaseData;
37
27
  /**
38
28
  * Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
package/dist/index.es.js CHANGED
@@ -5825,6 +5825,89 @@ function createPrimaryKeyResolver(options) {
5825
5825
  };
5826
5826
  }
5827
5827
  /**
5828
+ * Build the admin's view model out of the row the wire serves.
5829
+ *
5830
+ * The wire has ONE shape, for every consumer: flat columns, typed the way the
5831
+ * database typed them, and a relation rendered as the target's own columns (or
5832
+ * only its foreign key, when nothing asked for it). That is the REST contract,
5833
+ * what `find()` returns, what `listen()` pushes, and what the generated types
5834
+ * describe.
5835
+ *
5836
+ * The admin renders neither of those directly. Its date field requires a real
5837
+ * `Date` and rejects a string outright; its relation cells read `.data.values`
5838
+ * off a relation ref. Those requirements are the *admin's*, so they are met
5839
+ * here — in the browser, from the collection config the panel already has —
5840
+ * rather than by asking the server for a second wire shape.
5841
+ *
5842
+ * That second shape is what this replaces. Until 2026-09-09 the realtime wire
5843
+ * carried the view model and every other read carried flat rows, so `find()`
5844
+ * and `listen()` answered one query two ways; unifying the wire without doing
5845
+ * this conversion is what left every date cell reading "Invalid date value"
5846
+ * and every relation cell "Unexpected value".
5847
+ *
5848
+ * Values already in view-model form pass through untouched: a driver that
5849
+ * still sends `{ __type: "date" }` or a relation ref (the client revives both)
5850
+ * is served by the same walk.
5851
+ */
5852
+ function toViewModelValues(values, properties, collection, resolveCollection) {
5853
+ if (!properties) return values;
5854
+ const relations = collection ? resolveCollectionRelations(collection) : {};
5855
+ let out;
5856
+ const write = (key, value) => {
5857
+ out = out ?? { ...values };
5858
+ out[key] = value;
5859
+ };
5860
+ for (const [key, rawProperty] of Object.entries(properties)) {
5861
+ const property = rawProperty;
5862
+ if (!property) continue;
5863
+ if (!(key in values)) {
5864
+ const fkRelation = relations[key];
5865
+ const column = fkRelation && "localKey" in fkRelation ? fkRelation.localKey : void 0;
5866
+ const fk = column !== void 0 ? values[column] ?? values[toWireKey(column)] : void 0;
5867
+ const fkTarget = fkRelation?.targetSlug;
5868
+ if (fkTarget && (typeof fk === "string" || typeof fk === "number")) write(key, new EntityRelation(fk, fkTarget));
5869
+ continue;
5870
+ }
5871
+ const value = values[key];
5872
+ if (value === null || value === void 0) continue;
5873
+ const relation = relations[key];
5874
+ if (relation && (property.type === "relation" || property.of?.type === "relation" || property.type === "array")) {
5875
+ const target = relation.targetSlug;
5876
+ if (!target) continue;
5877
+ const targetProperties = resolveCollection?.(target)?.properties;
5878
+ const targetCollection = resolveCollection?.(target);
5879
+ const toRef = (item) => {
5880
+ if (item instanceof EntityRelation) return item;
5881
+ if (typeof item === "object" && item !== null && "__type" in item) return item;
5882
+ if (typeof item === "object" && item !== null) {
5883
+ const row = item;
5884
+ const keys = targetCollection ? resolvePrimaryKeys(targetCollection) : [];
5885
+ const id = keys.length > 0 ? buildCompositeId(row, keys) : row.id;
5886
+ if (id === void 0 || id === null || id === "") return item;
5887
+ return new EntityRelation(id, target, {
5888
+ id,
5889
+ path: target,
5890
+ values: toViewModelValues(row, targetProperties, targetCollection, resolveCollection)
5891
+ });
5892
+ }
5893
+ if (typeof item === "string" || typeof item === "number") return new EntityRelation(item, target);
5894
+ return item;
5895
+ };
5896
+ write(key, Array.isArray(value) ? value.map(toRef) : toRef(value));
5897
+ continue;
5898
+ }
5899
+ if (property.type === "date" && !(value instanceof Date)) {
5900
+ if (typeof value === "string" || typeof value === "number") {
5901
+ const date = new Date(value);
5902
+ write(key, isNaN(date.getTime()) ? null : date);
5903
+ }
5904
+ continue;
5905
+ }
5906
+ if (property.type === "map" && property.properties && typeof value === "object" && !Array.isArray(value)) write(key, toViewModelValues(value, property.properties, void 0, resolveCollection));
5907
+ }
5908
+ return out ?? values;
5909
+ }
5910
+ /**
5828
5911
  * Give a flat row the Entity view-model the admin renders.
5829
5912
  *
5830
5913
  * The address is *derived here* — it is not a column, and the row it came from
@@ -5835,12 +5918,12 @@ function createPrimaryKeyResolver(options) {
5835
5918
  * `primaryKeys` empty falls back to a literal `id` on the row: drivers other
5836
5919
  * than postgres still serve rows with one, and this keeps them working.
5837
5920
  */
5838
- function rowToEntity(row, slug, primaryKeys = []) {
5921
+ function rowToEntity(row, slug, primaryKeys = [], toViewModel) {
5839
5922
  const { _matches, ...values } = row;
5840
5923
  return {
5841
5924
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
5842
5925
  path: slug,
5843
- values,
5926
+ values: toViewModel ? toViewModel(values) : values,
5844
5927
  ..._matches ? { searchMatches: _matches } : {}
5845
5928
  };
5846
5929
  }
@@ -5860,14 +5943,16 @@ function inlineEnvelope(envelope) {
5860
5943
  * Replace every relation envelope on a row with the target's flat columns.
5861
5944
  *
5862
5945
  * The SDK serves one relation shape — the inlined one (see
5863
- * {@link RestFetchService}) — and reads that come back through a *driver*
5864
- * method rather than the REST pipeline still carry envelopes. Realtime is the
5865
- * one such read left: there is no `listenForRest`, so the rows arrive shaped
5866
- * for the admin and are flattened here instead.
5946
+ * {@link RestFetchService}) — and Postgres now serves it on every read, so
5947
+ * against that driver this walk finds nothing to do. It stays for the drivers
5948
+ * whose own `fetchCollection` still answers with refs: a developer reading
5949
+ * through this accessor gets one shape whichever driver is underneath.
5867
5950
  *
5868
5951
  * Only applied where the REST pipeline is the contract (see `find`); a driver
5869
- * without a `restFetchService` keeps whatever it returns, so the admin's own
5870
- * path through {@link buildRebaseData} is untouched.
5952
+ * without a `restFetchService` keeps whatever it returns.
5953
+ *
5954
+ * Note this is NOT how the admin gets its view model — that is built in the
5955
+ * browser by {@link toViewModelValues}, from the same flat row.
5871
5956
  */
5872
5957
  function inlineRelationRefs(row) {
5873
5958
  let out;
@@ -5880,7 +5965,7 @@ function inlineRelationRefs(row) {
5880
5965
  }
5881
5966
  return out ?? row;
5882
5967
  }
5883
- function createDriverAccessor(driver, slug, getPks = () => []) {
5968
+ function createDriverAccessor(driver, slug, getPks = () => [], toViewModel) {
5884
5969
  const accessor = {
5885
5970
  async find(params) {
5886
5971
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
@@ -5929,7 +6014,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5929
6014
  const last = rows[rows.length - 1];
5930
6015
  const nextCursor = hasMore && last && driver.restFetchService?.cursorFor ? driver.restFetchService.cursorFor(slug, last, orderBy) : void 0;
5931
6016
  return {
5932
- data: rows.map((row) => rowToEntity(row, slug, getPks())),
6017
+ data: rows.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
5933
6018
  meta: {
5934
6019
  total,
5935
6020
  limit,
@@ -5945,7 +6030,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5945
6030
  path: slug,
5946
6031
  id
5947
6032
  });
5948
- return row ? rowToEntity(row, slug, getPks()) : void 0;
6033
+ return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
5949
6034
  },
5950
6035
  aggregate: driver.restFetchService?.aggregate ? async (params) => driver.restFetchService.aggregate(slug, {
5951
6036
  aggregates: params.select.map(toDriverAggregate),
@@ -5961,7 +6046,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5961
6046
  values: data,
5962
6047
  id,
5963
6048
  status: "new"
5964
- }), slug, getPks());
6049
+ }), slug, getPks(), toViewModel);
5965
6050
  },
5966
6051
  createMany: driver.saveMany ? async (data, options) => {
5967
6052
  return (await driver.saveMany({
@@ -5969,7 +6054,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5969
6054
  rows: data,
5970
6055
  upsert: options?.upsert,
5971
6056
  onConflict: options?.onConflict
5972
- })).map((row) => rowToEntity(row, slug, getPks()));
6057
+ })).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
5973
6058
  } : void 0,
5974
6059
  async update(id, data) {
5975
6060
  return rowToEntity(await driver.save({
@@ -5977,7 +6062,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5977
6062
  values: data,
5978
6063
  id,
5979
6064
  status: "existing"
5980
- }), slug, getPks());
6065
+ }), slug, getPks(), toViewModel);
5981
6066
  },
5982
6067
  async delete(id) {
5983
6068
  return driver.delete({ row: {
@@ -5993,7 +6078,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5993
6078
  id: u.id,
5994
6079
  values: u.data
5995
6080
  }))
5996
- })).map((row) => rowToEntity(row, slug, getPks()));
6081
+ })).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
5997
6082
  } : void 0,
5998
6083
  deleteMany: driver.deleteMany ? async (ids) => {
5999
6084
  await driver.deleteMany({
@@ -6025,7 +6110,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6025
6110
  vectorSearch: params?.vectorSearch,
6026
6111
  onUpdate: (entities) => {
6027
6112
  onUpdate({
6028
- data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
6113
+ data: entities.map((row) => rowToEntity(normalize(row), slug, getPks(), toViewModel)),
6029
6114
  meta: {
6030
6115
  total: offset + entities.length,
6031
6116
  limit,
@@ -6042,7 +6127,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6042
6127
  return driver.listenOne({
6043
6128
  path: slug,
6044
6129
  id,
6045
- onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks()) : void 0),
6130
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks(), toViewModel) : void 0),
6046
6131
  onError
6047
6132
  });
6048
6133
  } : void 0,
@@ -6084,13 +6169,32 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6084
6169
  * await data.products.create({ name: "Camera", price: 299 });
6085
6170
  * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
6086
6171
  */
6172
+ /**
6173
+ * The view-model converter for one collection, or `undefined` when there is no
6174
+ * collection config to build it from.
6175
+ *
6176
+ * Absent is the honest answer for every consumer that is not the admin: the
6177
+ * flat SDK derives itself from this same layer (`buildSdkData`) and must keep
6178
+ * the wire's own types, and it registers no collection resolver.
6179
+ */
6180
+ function createViewModelConverter(options) {
6181
+ if (!options?.resolveCollection) return () => void 0;
6182
+ return function converterFor(slug) {
6183
+ return (values) => {
6184
+ const collection = options.resolveCollection?.(slug);
6185
+ if (!collection) return values;
6186
+ return toViewModelValues(values, collection.properties, collection, options.resolveCollection);
6187
+ };
6188
+ };
6189
+ }
6087
6190
  function buildRebaseData(driver, options) {
6088
6191
  const cache = /* @__PURE__ */ new Map();
6089
6192
  const primaryKeysFor = createPrimaryKeyResolver(options);
6193
+ const viewModelFor = createViewModelConverter(options);
6090
6194
  function getAccessor(slug) {
6091
6195
  let accessor = cache.get(slug);
6092
6196
  if (!accessor) {
6093
- accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug));
6197
+ accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug), viewModelFor(slug));
6094
6198
  cache.set(slug, accessor);
6095
6199
  }
6096
6200
  return accessor;
@@ -6355,29 +6459,29 @@ function toSdkCollectionClient(snap, slug = "collection") {
6355
6459
  * {@link CollectionAccessor}. Every returned row is re-wrapped into the
6356
6460
  * `{ id, path, values }` view-model the admin panel renders.
6357
6461
  */
6358
- function toEntityAccessor(sdk, slug, getPks = () => []) {
6462
+ function toEntityAccessor(sdk, slug, getPks = () => [], toViewModel) {
6359
6463
  const accessor = {
6360
6464
  async find(params) {
6361
6465
  const res = await sdk.find(params);
6362
6466
  return {
6363
- data: res.data.map((row) => rowToEntity(row, slug, getPks())),
6467
+ data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
6364
6468
  meta: res.meta
6365
6469
  };
6366
6470
  },
6367
6471
  async findById(id) {
6368
6472
  const row = await sdk.findById(id);
6369
- return row ? rowToEntity(row, slug, getPks()) : void 0;
6473
+ return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
6370
6474
  },
6371
6475
  async create(data, id) {
6372
- return rowToEntity(await sdk.create(data, id), slug, getPks());
6476
+ return rowToEntity(await sdk.create(data, id), slug, getPks(), toViewModel);
6373
6477
  },
6374
6478
  createMany: sdk.createMany ? async (data, options) => {
6375
- return (await sdk.createMany(data, options)).map((row) => rowToEntity(row, slug, getPks()));
6479
+ return (await sdk.createMany(data, options)).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
6376
6480
  } : void 0,
6377
6481
  async update(id, data) {
6378
6482
  const row = await sdk.update(id, data);
6379
6483
  if (!row) throw new Error(`Update returned no data for id ${id}`);
6380
- return rowToEntity(row, slug, getPks());
6484
+ return rowToEntity(row, slug, getPks(), toViewModel);
6381
6485
  },
6382
6486
  delete(id) {
6383
6487
  return sdk.delete(id);
@@ -6385,10 +6489,10 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
6385
6489
  count: isUnsupported(sdk.count) ? void 0 : (params) => sdk.count(params),
6386
6490
  aggregate: isUnsupported(sdk.aggregate) ? void 0 : (params) => sdk.aggregate(params),
6387
6491
  listen: isUnsupported(sdk.listen) ? void 0 : (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
6388
- data: res.data.map((row) => rowToEntity(row, slug, getPks())),
6492
+ data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
6389
6493
  meta: res.meta
6390
6494
  }), onError),
6391
- listenById: isUnsupported(sdk.listenById) ? void 0 : (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug, getPks()) : void 0), onError),
6495
+ listenById: isUnsupported(sdk.listenById) ? void 0 : (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0), onError),
6392
6496
  where(columnOrCondition, operator, value) {
6393
6497
  const builder = new QueryBuilder(accessor);
6394
6498
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
@@ -6424,10 +6528,11 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
6424
6528
  function wrapAsEntityData(sdkData, options) {
6425
6529
  const cache = /* @__PURE__ */ new Map();
6426
6530
  const primaryKeysFor = createPrimaryKeyResolver(options);
6531
+ const viewModelFor = createViewModelConverter(options);
6427
6532
  function getAccessor(slug) {
6428
6533
  let accessor = cache.get(slug);
6429
6534
  if (!accessor) {
6430
- accessor = toEntityAccessor(sdkData.collection(slug), slug, () => primaryKeysFor(slug));
6535
+ accessor = toEntityAccessor(sdkData.collection(slug), slug, () => primaryKeysFor(slug), viewModelFor(slug));
6431
6536
  cache.set(slug, accessor);
6432
6537
  }
6433
6538
  return accessor;