@rebasepro/common 0.19.2-canary.gef769df → 0.20.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.
@@ -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
@@ -10,10 +10,28 @@ var DEFAULT_ONE_OF_VALUE = "value";
10
10
  function isPropertyBuilder(property) {
11
11
  return typeof property?.dynamicProps === "function";
12
12
  }
13
+ /**
14
+ * What a form opens with: a value for every property it can write.
15
+ *
16
+ * `excludeFromApi` columns are left out, and that is the whole of the rule —
17
+ * they are not part of the API surface in either direction, so there is nothing
18
+ * for a form to open showing and nothing it may send back. Including them was
19
+ * not cosmetic: the baseline is what gets submitted, so a new record carried
20
+ * `passwordHash: null` and `emailVerificationToken: null` into the create, and
21
+ * the server refused the whole write with "these columns are the server's to
22
+ * set" — the users collection could not be added to from the panel at all. The
23
+ * fields were invisible on screen (`admin.disabled.hidden`), which is what made
24
+ * the error read as being about the roles the operator *had* just edited.
25
+ *
26
+ * Server-side defaulting does not come through here: `applyDefaultValuesOnCreate`
27
+ * asks each property for its own default, so an excluded column with a declared
28
+ * `defaultValue` is still filled in on an in-process write.
29
+ */
13
30
  function getDefaultValuesFor(properties) {
14
31
  if (!properties) return {};
15
32
  return Object.entries(properties).map(([key, property]) => {
16
33
  if (!property) return {};
34
+ if (property.excludeFromApi) return {};
17
35
  const value = getDefaultValueFor(property);
18
36
  return value === void 0 ? {} : { [key]: value };
19
37
  }).reduce((a, b) => ({
@@ -124,18 +142,18 @@ function updateUserAutoValues({ inputValues, properties, status, uid }) {
124
142
  function applyDefaultValuesOnCreate(values, properties) {
125
143
  if (!properties) return values ?? {};
126
144
  const result = { ...values ?? {} };
127
- const defaults = getDefaultValuesFor(properties);
128
145
  for (const [key, property] of Object.entries(properties)) {
129
146
  if (!property) continue;
130
147
  if (!declaresDefault(property)) continue;
148
+ const defaultValue = getDefaultValueFor(property);
131
149
  if (result[key] !== void 0) {
132
150
  if (property.type === "map" && property.defaultValue === void 0 && isPlainObject(result[key])) result[key] = {
133
- ...defaults[key] ?? {},
151
+ ...defaultValue ?? {},
134
152
  ...result[key]
135
153
  };
136
154
  continue;
137
155
  }
138
- if (defaults[key] !== void 0) result[key] = defaults[key];
156
+ if (defaultValue !== void 0) result[key] = defaultValue;
139
157
  }
140
158
  return result;
141
159
  }
@@ -5825,6 +5843,89 @@ function createPrimaryKeyResolver(options) {
5825
5843
  };
5826
5844
  }
5827
5845
  /**
5846
+ * Build the admin's view model out of the row the wire serves.
5847
+ *
5848
+ * The wire has ONE shape, for every consumer: flat columns, typed the way the
5849
+ * database typed them, and a relation rendered as the target's own columns (or
5850
+ * only its foreign key, when nothing asked for it). That is the REST contract,
5851
+ * what `find()` returns, what `listen()` pushes, and what the generated types
5852
+ * describe.
5853
+ *
5854
+ * The admin renders neither of those directly. Its date field requires a real
5855
+ * `Date` and rejects a string outright; its relation cells read `.data.values`
5856
+ * off a relation ref. Those requirements are the *admin's*, so they are met
5857
+ * here — in the browser, from the collection config the panel already has —
5858
+ * rather than by asking the server for a second wire shape.
5859
+ *
5860
+ * That second shape is what this replaces. Until 2026-09-09 the realtime wire
5861
+ * carried the view model and every other read carried flat rows, so `find()`
5862
+ * and `listen()` answered one query two ways; unifying the wire without doing
5863
+ * this conversion is what left every date cell reading "Invalid date value"
5864
+ * and every relation cell "Unexpected value".
5865
+ *
5866
+ * Values already in view-model form pass through untouched: a driver that
5867
+ * still sends `{ __type: "date" }` or a relation ref (the client revives both)
5868
+ * is served by the same walk.
5869
+ */
5870
+ function toViewModelValues(values, properties, collection, resolveCollection) {
5871
+ if (!properties) return values;
5872
+ const relations = collection ? resolveCollectionRelations(collection) : {};
5873
+ let out;
5874
+ const write = (key, value) => {
5875
+ out = out ?? { ...values };
5876
+ out[key] = value;
5877
+ };
5878
+ for (const [key, rawProperty] of Object.entries(properties)) {
5879
+ const property = rawProperty;
5880
+ if (!property) continue;
5881
+ if (!(key in values)) {
5882
+ const fkRelation = relations[key];
5883
+ const column = fkRelation && "localKey" in fkRelation ? fkRelation.localKey : void 0;
5884
+ const fk = column !== void 0 ? values[column] ?? values[toWireKey(column)] : void 0;
5885
+ const fkTarget = fkRelation?.targetSlug;
5886
+ if (fkTarget && (typeof fk === "string" || typeof fk === "number")) write(key, new EntityRelation(fk, fkTarget));
5887
+ continue;
5888
+ }
5889
+ const value = values[key];
5890
+ if (value === null || value === void 0) continue;
5891
+ const relation = relations[key];
5892
+ if (relation && (property.type === "relation" || property.of?.type === "relation" || property.type === "array")) {
5893
+ const target = relation.targetSlug;
5894
+ if (!target) continue;
5895
+ const targetProperties = resolveCollection?.(target)?.properties;
5896
+ const targetCollection = resolveCollection?.(target);
5897
+ const toRef = (item) => {
5898
+ if (item instanceof EntityRelation) return item;
5899
+ if (typeof item === "object" && item !== null && "__type" in item) return item;
5900
+ if (typeof item === "object" && item !== null) {
5901
+ const row = item;
5902
+ const keys = targetCollection ? resolvePrimaryKeys(targetCollection) : [];
5903
+ const id = keys.length > 0 ? buildCompositeId(row, keys) : row.id;
5904
+ if (id === void 0 || id === null || id === "") return item;
5905
+ return new EntityRelation(id, target, {
5906
+ id,
5907
+ path: target,
5908
+ values: toViewModelValues(row, targetProperties, targetCollection, resolveCollection)
5909
+ });
5910
+ }
5911
+ if (typeof item === "string" || typeof item === "number") return new EntityRelation(item, target);
5912
+ return item;
5913
+ };
5914
+ write(key, Array.isArray(value) ? value.map(toRef) : toRef(value));
5915
+ continue;
5916
+ }
5917
+ if (property.type === "date" && !(value instanceof Date)) {
5918
+ if (typeof value === "string" || typeof value === "number") {
5919
+ const date = new Date(value);
5920
+ write(key, isNaN(date.getTime()) ? null : date);
5921
+ }
5922
+ continue;
5923
+ }
5924
+ if (property.type === "map" && property.properties && typeof value === "object" && !Array.isArray(value)) write(key, toViewModelValues(value, property.properties, void 0, resolveCollection));
5925
+ }
5926
+ return out ?? values;
5927
+ }
5928
+ /**
5828
5929
  * Give a flat row the Entity view-model the admin renders.
5829
5930
  *
5830
5931
  * The address is *derived here* — it is not a column, and the row it came from
@@ -5835,12 +5936,12 @@ function createPrimaryKeyResolver(options) {
5835
5936
  * `primaryKeys` empty falls back to a literal `id` on the row: drivers other
5836
5937
  * than postgres still serve rows with one, and this keeps them working.
5837
5938
  */
5838
- function rowToEntity(row, slug, primaryKeys = []) {
5939
+ function rowToEntity(row, slug, primaryKeys = [], toViewModel) {
5839
5940
  const { _matches, ...values } = row;
5840
5941
  return {
5841
5942
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
5842
5943
  path: slug,
5843
- values,
5944
+ values: toViewModel ? toViewModel(values) : values,
5844
5945
  ..._matches ? { searchMatches: _matches } : {}
5845
5946
  };
5846
5947
  }
@@ -5860,14 +5961,16 @@ function inlineEnvelope(envelope) {
5860
5961
  * Replace every relation envelope on a row with the target's flat columns.
5861
5962
  *
5862
5963
  * 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.
5964
+ * {@link RestFetchService}) — and Postgres now serves it on every read, so
5965
+ * against that driver this walk finds nothing to do. It stays for the drivers
5966
+ * whose own `fetchCollection` still answers with refs: a developer reading
5967
+ * through this accessor gets one shape whichever driver is underneath.
5867
5968
  *
5868
5969
  * 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.
5970
+ * without a `restFetchService` keeps whatever it returns.
5971
+ *
5972
+ * Note this is NOT how the admin gets its view model — that is built in the
5973
+ * browser by {@link toViewModelValues}, from the same flat row.
5871
5974
  */
5872
5975
  function inlineRelationRefs(row) {
5873
5976
  let out;
@@ -5880,7 +5983,7 @@ function inlineRelationRefs(row) {
5880
5983
  }
5881
5984
  return out ?? row;
5882
5985
  }
5883
- function createDriverAccessor(driver, slug, getPks = () => []) {
5986
+ function createDriverAccessor(driver, slug, getPks = () => [], toViewModel) {
5884
5987
  const accessor = {
5885
5988
  async find(params) {
5886
5989
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
@@ -5929,7 +6032,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5929
6032
  const last = rows[rows.length - 1];
5930
6033
  const nextCursor = hasMore && last && driver.restFetchService?.cursorFor ? driver.restFetchService.cursorFor(slug, last, orderBy) : void 0;
5931
6034
  return {
5932
- data: rows.map((row) => rowToEntity(row, slug, getPks())),
6035
+ data: rows.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
5933
6036
  meta: {
5934
6037
  total,
5935
6038
  limit,
@@ -5945,7 +6048,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5945
6048
  path: slug,
5946
6049
  id
5947
6050
  });
5948
- return row ? rowToEntity(row, slug, getPks()) : void 0;
6051
+ return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
5949
6052
  },
5950
6053
  aggregate: driver.restFetchService?.aggregate ? async (params) => driver.restFetchService.aggregate(slug, {
5951
6054
  aggregates: params.select.map(toDriverAggregate),
@@ -5961,7 +6064,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5961
6064
  values: data,
5962
6065
  id,
5963
6066
  status: "new"
5964
- }), slug, getPks());
6067
+ }), slug, getPks(), toViewModel);
5965
6068
  },
5966
6069
  createMany: driver.saveMany ? async (data, options) => {
5967
6070
  return (await driver.saveMany({
@@ -5969,7 +6072,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5969
6072
  rows: data,
5970
6073
  upsert: options?.upsert,
5971
6074
  onConflict: options?.onConflict
5972
- })).map((row) => rowToEntity(row, slug, getPks()));
6075
+ })).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
5973
6076
  } : void 0,
5974
6077
  async update(id, data) {
5975
6078
  return rowToEntity(await driver.save({
@@ -5977,7 +6080,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5977
6080
  values: data,
5978
6081
  id,
5979
6082
  status: "existing"
5980
- }), slug, getPks());
6083
+ }), slug, getPks(), toViewModel);
5981
6084
  },
5982
6085
  async delete(id) {
5983
6086
  return driver.delete({ row: {
@@ -5993,7 +6096,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
5993
6096
  id: u.id,
5994
6097
  values: u.data
5995
6098
  }))
5996
- })).map((row) => rowToEntity(row, slug, getPks()));
6099
+ })).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
5997
6100
  } : void 0,
5998
6101
  deleteMany: driver.deleteMany ? async (ids) => {
5999
6102
  await driver.deleteMany({
@@ -6025,7 +6128,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6025
6128
  vectorSearch: params?.vectorSearch,
6026
6129
  onUpdate: (entities) => {
6027
6130
  onUpdate({
6028
- data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
6131
+ data: entities.map((row) => rowToEntity(normalize(row), slug, getPks(), toViewModel)),
6029
6132
  meta: {
6030
6133
  total: offset + entities.length,
6031
6134
  limit,
@@ -6042,7 +6145,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6042
6145
  return driver.listenOne({
6043
6146
  path: slug,
6044
6147
  id,
6045
- onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks()) : void 0),
6148
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks(), toViewModel) : void 0),
6046
6149
  onError
6047
6150
  });
6048
6151
  } : void 0,
@@ -6084,13 +6187,32 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
6084
6187
  * await data.products.create({ name: "Camera", price: 299 });
6085
6188
  * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
6086
6189
  */
6190
+ /**
6191
+ * The view-model converter for one collection, or `undefined` when there is no
6192
+ * collection config to build it from.
6193
+ *
6194
+ * Absent is the honest answer for every consumer that is not the admin: the
6195
+ * flat SDK derives itself from this same layer (`buildSdkData`) and must keep
6196
+ * the wire's own types, and it registers no collection resolver.
6197
+ */
6198
+ function createViewModelConverter(options) {
6199
+ if (!options?.resolveCollection) return () => void 0;
6200
+ return function converterFor(slug) {
6201
+ return (values) => {
6202
+ const collection = options.resolveCollection?.(slug);
6203
+ if (!collection) return values;
6204
+ return toViewModelValues(values, collection.properties, collection, options.resolveCollection);
6205
+ };
6206
+ };
6207
+ }
6087
6208
  function buildRebaseData(driver, options) {
6088
6209
  const cache = /* @__PURE__ */ new Map();
6089
6210
  const primaryKeysFor = createPrimaryKeyResolver(options);
6211
+ const viewModelFor = createViewModelConverter(options);
6090
6212
  function getAccessor(slug) {
6091
6213
  let accessor = cache.get(slug);
6092
6214
  if (!accessor) {
6093
- accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug));
6215
+ accessor = createDriverAccessor(driver, slug, () => primaryKeysFor(slug), viewModelFor(slug));
6094
6216
  cache.set(slug, accessor);
6095
6217
  }
6096
6218
  return accessor;
@@ -6355,29 +6477,29 @@ function toSdkCollectionClient(snap, slug = "collection") {
6355
6477
  * {@link CollectionAccessor}. Every returned row is re-wrapped into the
6356
6478
  * `{ id, path, values }` view-model the admin panel renders.
6357
6479
  */
6358
- function toEntityAccessor(sdk, slug, getPks = () => []) {
6480
+ function toEntityAccessor(sdk, slug, getPks = () => [], toViewModel) {
6359
6481
  const accessor = {
6360
6482
  async find(params) {
6361
6483
  const res = await sdk.find(params);
6362
6484
  return {
6363
- data: res.data.map((row) => rowToEntity(row, slug, getPks())),
6485
+ data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
6364
6486
  meta: res.meta
6365
6487
  };
6366
6488
  },
6367
6489
  async findById(id) {
6368
6490
  const row = await sdk.findById(id);
6369
- return row ? rowToEntity(row, slug, getPks()) : void 0;
6491
+ return row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0;
6370
6492
  },
6371
6493
  async create(data, id) {
6372
- return rowToEntity(await sdk.create(data, id), slug, getPks());
6494
+ return rowToEntity(await sdk.create(data, id), slug, getPks(), toViewModel);
6373
6495
  },
6374
6496
  createMany: sdk.createMany ? async (data, options) => {
6375
- return (await sdk.createMany(data, options)).map((row) => rowToEntity(row, slug, getPks()));
6497
+ return (await sdk.createMany(data, options)).map((row) => rowToEntity(row, slug, getPks(), toViewModel));
6376
6498
  } : void 0,
6377
6499
  async update(id, data) {
6378
6500
  const row = await sdk.update(id, data);
6379
6501
  if (!row) throw new Error(`Update returned no data for id ${id}`);
6380
- return rowToEntity(row, slug, getPks());
6502
+ return rowToEntity(row, slug, getPks(), toViewModel);
6381
6503
  },
6382
6504
  delete(id) {
6383
6505
  return sdk.delete(id);
@@ -6385,10 +6507,10 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
6385
6507
  count: isUnsupported(sdk.count) ? void 0 : (params) => sdk.count(params),
6386
6508
  aggregate: isUnsupported(sdk.aggregate) ? void 0 : (params) => sdk.aggregate(params),
6387
6509
  listen: isUnsupported(sdk.listen) ? void 0 : (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
6388
- data: res.data.map((row) => rowToEntity(row, slug, getPks())),
6510
+ data: res.data.map((row) => rowToEntity(row, slug, getPks(), toViewModel)),
6389
6511
  meta: res.meta
6390
6512
  }), onError),
6391
- listenById: isUnsupported(sdk.listenById) ? void 0 : (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug, getPks()) : void 0), onError),
6513
+ listenById: isUnsupported(sdk.listenById) ? void 0 : (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug, getPks(), toViewModel) : void 0), onError),
6392
6514
  where(columnOrCondition, operator, value) {
6393
6515
  const builder = new QueryBuilder(accessor);
6394
6516
  if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
@@ -6424,10 +6546,11 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
6424
6546
  function wrapAsEntityData(sdkData, options) {
6425
6547
  const cache = /* @__PURE__ */ new Map();
6426
6548
  const primaryKeysFor = createPrimaryKeyResolver(options);
6549
+ const viewModelFor = createViewModelConverter(options);
6427
6550
  function getAccessor(slug) {
6428
6551
  let accessor = cache.get(slug);
6429
6552
  if (!accessor) {
6430
- accessor = toEntityAccessor(sdkData.collection(slug), slug, () => primaryKeysFor(slug));
6553
+ accessor = toEntityAccessor(sdkData.collection(slug), slug, () => primaryKeysFor(slug), viewModelFor(slug));
6431
6554
  cache.set(slug, accessor);
6432
6555
  }
6433
6556
  return accessor;