@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
package/dist/index.umd.js CHANGED
@@ -83,7 +83,7 @@
83
83
  else return null;
84
84
  }
85
85
  /**
86
- * Update the automatic values in an entity before save
86
+ * Update the automatic values in a entity before save
87
87
  * @group Driver
88
88
  */
89
89
  function updateDateAutoValues({ inputValues, properties, status, timestampNowValue }) {
@@ -95,7 +95,7 @@
95
95
  }) ?? {};
96
96
  }
97
97
  /**
98
- * Add missing required fields, expected in the collection, to the values of an entity
98
+ * Add missing required fields, expected in the collection, to the values of a entity
99
99
  * @param values
100
100
  * @param properties
101
101
  * @group Driver
@@ -265,7 +265,7 @@
265
265
  return collection.localChangesBackup;
266
266
  }
267
267
  /**
268
- * Returns the primary keys for an entity collection by inspecting the properties
268
+ * Returns the primary keys for a entity collection by inspecting the properties
269
269
  * and finding any properties with `isId`.
270
270
  * Fallbacks to `["id"]` if no properties are marked as `isId: true`.
271
271
  * @param collection
@@ -809,22 +809,56 @@
809
809
  * {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
810
810
  * and the admin UI derive from the exact same expression.
811
811
  */
812
- function policyToPostgres(expr, collection) {
812
+ function policyToPostgres(expr, collection, options) {
813
+ return compile(expr, {
814
+ fieldCollection: collection,
815
+ fieldPrefix: "",
816
+ outerCollection: collection,
817
+ outerPrefix: "",
818
+ resolveCollection: options?.resolveCollection,
819
+ alias: { n: 0 }
820
+ });
821
+ }
822
+ function compile(expr, scope) {
813
823
  switch (expr.kind) {
814
824
  case "true": return "true";
815
825
  case "false": return "false";
816
- case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" AND ");
817
- case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" OR ");
826
+ case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" AND ");
827
+ case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" OR ");
818
828
  case "not":
819
829
  if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
820
- return `NOT (${policyToPostgres(expr.operand, collection)})`;
821
- case "compare": return `${operandToSql(expr.left, collection)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, collection)}`;
830
+ return `NOT (${compile(expr.operand, scope)})`;
831
+ case "compare": return `${operandToSql(expr.left, scope)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, scope)}`;
822
832
  case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
823
833
  case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
824
834
  case "authenticated": return "auth.uid() IS NOT NULL";
835
+ case "existsIn": return compileExistsIn(expr, scope);
825
836
  case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
826
837
  }
827
838
  }
839
+ /**
840
+ * Compiles `existsIn` to a correlated `EXISTS (SELECT 1 FROM <join> WHERE ...)`.
841
+ * Inside the subquery, `field` operands bind to the aliased join table and
842
+ * `outerField` operands bind to the (table-qualified) outer RLS row.
843
+ */
844
+ function compileExistsIn(expr, scope) {
845
+ const join = scope.resolveCollection?.(expr.collection);
846
+ const joinTable = join ? getTableName(join) : (0, _rebasepro_utils.toSnakeCase)(expr.collection);
847
+ const joinSchema = schemaOf(join) ?? schemaOf(scope.outerCollection) ?? "public";
848
+ const alias = `_ex${scope.alias.n++}`;
849
+ const outerTable = scope.outerCollection ? getTableName(scope.outerCollection) : void 0;
850
+ const outerSchema = schemaOf(scope.outerCollection) ?? "public";
851
+ const outerPrefix = outerTable ? `"${outerSchema}"."${outerTable}".` : "";
852
+ const innerScope = {
853
+ fieldCollection: join,
854
+ fieldPrefix: `"${alias}".`,
855
+ outerCollection: scope.outerCollection,
856
+ outerPrefix,
857
+ resolveCollection: scope.resolveCollection,
858
+ alias: scope.alias
859
+ };
860
+ return `EXISTS (SELECT 1 FROM "${joinSchema}"."${joinTable}" "${alias}" WHERE ${compile(expr.where, innerScope)})`;
861
+ }
828
862
  var COMPARE_SQL = {
829
863
  eq: "=",
830
864
  neq: "!=",
@@ -833,14 +867,18 @@
833
867
  gt: ">",
834
868
  gte: ">="
835
869
  };
836
- function operandToSql(operand, collection) {
870
+ function operandToSql(operand, scope) {
837
871
  switch (operand.kind) {
838
- case "field": return resolveColumnName(operand.name, collection);
872
+ case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
873
+ case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
839
874
  case "literal": return quoteLiteral(operand.value);
840
875
  case "authUid": return "auth.uid()";
841
876
  case "authRoles": return "string_to_array(auth.roles(), ',')";
842
877
  }
843
878
  }
879
+ function schemaOf(collection) {
880
+ return collection?.schema || void 0;
881
+ }
844
882
  function resolveColumnName(propName, collection) {
845
883
  const prop = collection?.properties?.[propName];
846
884
  if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
@@ -883,6 +921,7 @@
883
921
  return expr.roles.every((r) => r === "public" || userRoles.includes(r));
884
922
  }
885
923
  case "authenticated": return ctx.uid != null;
924
+ case "existsIn": return "unknown";
886
925
  case "raw": return "unknown";
887
926
  }
888
927
  }
@@ -920,6 +959,7 @@
920
959
  known: true,
921
960
  value: ctx.entity.values[operand.name]
922
961
  };
962
+ case "outerField": return { known: false };
923
963
  }
924
964
  }
925
965
  function evaluateCompare(op, left, right, ctx) {
@@ -1131,7 +1171,7 @@
1131
1171
  } else {
1132
1172
  entityId = remainingPath;
1133
1173
  remainingPath = "";
1134
- console.warn(`resolveCollectionPathIds: Path seems to end with an entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
1174
+ console.warn(`resolveCollectionPathIds: Path seems to end with a entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
1135
1175
  }
1136
1176
  resolvedPathParts.push(entityId);
1137
1177
  currentCollections = getSubcollections(foundCollection);
@@ -1287,9 +1327,12 @@
1287
1327
  //#endregion
1288
1328
  //#region src/util/builders.ts
1289
1329
  /**
1290
- * Identity function we use to defeat the type system of Typescript and build
1291
- * collection views with all its properties
1292
- * @param collection
1330
+ * @deprecated Use {@link defineCollection} instead it infers property
1331
+ * types automatically (autocomplete on `titleProperty`, `sort`,
1332
+ * `propertiesOrder`, callbacks) without manual generics.
1333
+ * `buildCollection` is kept for FireCMS migration compatibility and will
1334
+ * be removed before 1.0.
1335
+ *
1293
1336
  * @group Builder
1294
1337
  */
1295
1338
  function buildCollection(collection) {
@@ -1303,68 +1346,16 @@
1303
1346
  return collection;
1304
1347
  }
1305
1348
  /**
1306
- * Identity function we use to defeat the type system of Typescript and preserve
1307
- * the property keys.
1308
- * @param property
1349
+ * @deprecated Use plain typed property objects with {@link defineCollection}
1350
+ * instead — `defineCollection` infers property types automatically, making
1351
+ * this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
1352
+ * compatibility and will be removed before 1.0.
1353
+ *
1309
1354
  * @group Builder
1310
1355
  */
1311
1356
  function buildProperty(property) {
1312
1357
  return property;
1313
1358
  }
1314
- /**
1315
- * Identity function we use to defeat the type system of Typescript and preserve
1316
- * the properties keys.
1317
- * @param properties
1318
- * @group Builder
1319
- */
1320
- function buildProperties(properties) {
1321
- return properties;
1322
- }
1323
- /**
1324
- * Identity function we use to defeat the type system of Typescript and preserve
1325
- * the properties keys.
1326
- * @param propertiesOrBuilder
1327
- * @group Builder
1328
- */
1329
- function buildPropertiesOrBuilder(propertiesOrBuilder) {
1330
- return propertiesOrBuilder;
1331
- }
1332
- /**
1333
- * Identity function we use to defeat the type system of Typescript and preserve
1334
- * the properties keys.
1335
- * @param enumValues
1336
- * @group Builder
1337
- */
1338
- function buildEnum(enumValues) {
1339
- return enumValues;
1340
- }
1341
- /**
1342
- * Identity function we use to defeat the type system of Typescript and preserve
1343
- * the properties keys.
1344
- * @param enumValueConfig
1345
- * @group Builder
1346
- */
1347
- function buildEnumValueConfig(enumValueConfig) {
1348
- return enumValueConfig;
1349
- }
1350
- /**
1351
- * Identity function we use to defeat the type system of Typescript and preserve
1352
- * the properties keys.
1353
- * @param callbacks
1354
- * @group Builder
1355
- */
1356
- function buildEntityCallbacks(callbacks) {
1357
- return callbacks;
1358
- }
1359
- /**
1360
- * Identity function we use to defeat the type system of Typescript and build
1361
- * additional field delegates views with all its properties
1362
- * @param additionalFieldDelegate
1363
- * @group Builder
1364
- */
1365
- function buildAdditionalFieldDelegate(additionalFieldDelegate) {
1366
- return additionalFieldDelegate;
1367
- }
1368
1359
  //#endregion
1369
1360
  //#region src/util/storage.ts
1370
1361
  /**
@@ -1500,16 +1491,17 @@
1500
1491
  }
1501
1492
  /**
1502
1493
  * Helper function to extract field-level PropertyCallbacks from a properties schema
1503
- * and wrap them into an EntityCallbacks object recursively.
1494
+ * and wrap them into an CollectionCallbacks object recursively.
1504
1495
  */
1505
1496
  var buildPropertyCallbacks = (properties) => {
1506
1497
  if (!properties) return void 0;
1507
1498
  const propertyCallbacks = {};
1508
1499
  if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
1509
- const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1500
+ const row = props.row;
1501
+ const processedValues = await processProperties(properties, row, row, props, "afterRead");
1510
1502
  return {
1511
- ...props.entity,
1512
- values: processedValues
1503
+ ...props.row,
1504
+ ...processedValues
1513
1505
  };
1514
1506
  };
1515
1507
  if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
@@ -1696,6 +1688,84 @@
1696
1688
  return result;
1697
1689
  }
1698
1690
  //#endregion
1691
+ //#region src/util/filter-operator-resolution.ts
1692
+ /**
1693
+ * Default operators offered per property type, before engine capabilities and
1694
+ * per-property narrowing are applied. These mirror what the built-in filter
1695
+ * fields can render.
1696
+ */
1697
+ var COMPARISON_OPS = [
1698
+ "==",
1699
+ "!=",
1700
+ ">",
1701
+ ">=",
1702
+ "<",
1703
+ "<="
1704
+ ];
1705
+ var NULL_CHECK_OPS = ["is-null", "is-not-null"];
1706
+ var MEMBERSHIP_OPS = ["in", "not-in"];
1707
+ var PATTERN_OPS = [
1708
+ "like",
1709
+ "ilike",
1710
+ "not-like",
1711
+ "not-ilike"
1712
+ ];
1713
+ var DEFAULT_OPS_BY_TYPE = {
1714
+ string: [
1715
+ ...COMPARISON_OPS,
1716
+ ...MEMBERSHIP_OPS,
1717
+ ...PATTERN_OPS,
1718
+ ...NULL_CHECK_OPS
1719
+ ],
1720
+ number: [
1721
+ ...COMPARISON_OPS,
1722
+ ...MEMBERSHIP_OPS,
1723
+ ...NULL_CHECK_OPS
1724
+ ],
1725
+ date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
1726
+ boolean: [
1727
+ "==",
1728
+ "!=",
1729
+ ...NULL_CHECK_OPS
1730
+ ],
1731
+ reference: [
1732
+ "==",
1733
+ "!=",
1734
+ ...MEMBERSHIP_OPS,
1735
+ ...NULL_CHECK_OPS
1736
+ ],
1737
+ relation: [
1738
+ "==",
1739
+ "!=",
1740
+ ...MEMBERSHIP_OPS,
1741
+ ...NULL_CHECK_OPS
1742
+ ]
1743
+ };
1744
+ /** Operators offered when the property is an *array of* a filterable type. */
1745
+ var ARRAY_OPS = ["array-contains", "array-contains-any"];
1746
+ /**
1747
+ * Resolve which filter operators the UI should offer for a property.
1748
+ *
1749
+ * The result is the **intersection** of three sets:
1750
+ * 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
1751
+ * (e.g. Firestore cannot run the LIKE family);
1752
+ * 2. what makes sense for the property type (e.g. no `>` on booleans);
1753
+ * 3. the developer's optional narrowing — `property.ui.filterOperators`.
1754
+ *
1755
+ * Returns an empty array when the property is not filterable (either by
1756
+ * type, or because the developer disabled it with `filterOperators: []`).
1757
+ *
1758
+ * @group Models
1759
+ */
1760
+ function resolveFilterOperators({ property, isArray, engine }) {
1761
+ const typeDefaults = isArray ? ARRAY_OPS : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
1762
+ if (typeDefaults.length === 0) return [];
1763
+ const engineOps = new Set((0, _rebasepro_types.getDataSourceCapabilities)(engine).filterOperators ?? _rebasepro_types.ALL_WHERE_FILTER_OPS);
1764
+ const narrowing = property.ui?.filterOperators;
1765
+ const narrowingSet = narrowing !== void 0 ? new Set(narrowing) : void 0;
1766
+ return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
1767
+ }
1768
+ //#endregion
1699
1769
  //#region src/data/resolveDataSource.ts
1700
1770
  /**
1701
1771
  * Build a keyed registry from a list of {@link DataSourceDefinition}s.
@@ -1774,7 +1844,7 @@
1774
1844
  rawCollectionsBySlug = /* @__PURE__ */ new Map();
1775
1845
  rawRootCollections = [];
1776
1846
  cachedRawCollectionsList = null;
1777
- lastRawInputSnapshot = null;
1847
+ lastRawInputEntity = null;
1778
1848
  constructor(collections, dataSources) {
1779
1849
  if (dataSources) this.dataSources = dataSources;
1780
1850
  if (collections) this.registerMultiple(collections);
@@ -1804,12 +1874,12 @@
1804
1874
  * Returns true if the collections have changed, false otherwise.
1805
1875
  *
1806
1876
  * Idempotent: compares the raw input (before normalization) against a stored
1807
- * snapshot. Only re-normalizes and re-registers when the raw input actually changed.
1877
+ * entity. Only re-normalizes and re-registers when the raw input actually changed.
1808
1878
  * @param collections
1809
1879
  */
1810
1880
  registerMultiple(collections) {
1811
- const rawSnapshot = collections.map((c) => (0, _rebasepro_utils.removeFunctions)(c));
1812
- if (this.lastRawInputSnapshot && (0, fast_equals.deepEqual)(this.lastRawInputSnapshot, rawSnapshot)) return false;
1881
+ const rawEntity = collections.map((c) => (0, _rebasepro_utils.removeFunctions)(c));
1882
+ if (this.lastRawInputEntity && (0, fast_equals.deepEqual)(this.lastRawInputEntity, rawEntity)) return false;
1813
1883
  this.reset();
1814
1884
  collections.forEach((c) => {
1815
1885
  if (c.slug) this.collectionsBySlug.set(c.slug, c);
@@ -1833,7 +1903,7 @@
1833
1903
  this._registerRecursively(this.normalizeCollection({ ...subCollection }), (0, _rebasepro_utils.deepClone)(subCollection));
1834
1904
  });
1835
1905
  });
1836
- this.lastRawInputSnapshot = rawSnapshot;
1906
+ this.lastRawInputEntity = rawEntity;
1837
1907
  return true;
1838
1908
  }
1839
1909
  register(collection, rawCollection) {
@@ -2253,7 +2323,7 @@
2253
2323
  * client.collection('users').orderBy('createdAt', 'desc').find()
2254
2324
  */
2255
2325
  orderBy(column, direction = "asc") {
2256
- this.params.orderBy = `${column}:${direction}`;
2326
+ this.params.orderBy = [column, direction];
2257
2327
  return this;
2258
2328
  }
2259
2329
  /**
@@ -2316,32 +2386,69 @@
2316
2386
  * PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
2317
2387
  * Everything else speaks `FilterValues` exclusively.
2318
2388
  *
2389
+ * Wire-format values are always strings — the wire format carries no type
2390
+ * metadata, so type coercion is the responsibility of the server-side data
2391
+ * driver which has access to the collection schema.
2392
+ *
2393
+ * Commas inside list values are backslash-escaped (`\,`), and literal
2394
+ * backslashes are escaped as `\\`.
2395
+ *
2319
2396
  * @module
2320
2397
  */
2321
2398
  /**
2322
- * Coerce a raw querystring value to its natural JS type.
2323
- * - `"true"` / `"false"` → boolean
2324
- * - `"null"` → null
2325
- * - Numeric strings → number
2326
- * - Everything else → string (unchanged)
2327
- */
2328
- function coerceValue(raw) {
2329
- if (raw === "true") return true;
2330
- if (raw === "false") return false;
2331
- if (raw === "null") return null;
2332
- if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
2333
- return raw;
2334
- }
2335
- /**
2336
2399
  * Serialize a JS value to its querystring representation.
2400
+ * `null` is serialized as the literal string `"null"`.
2337
2401
  */
2338
2402
  function stringifyValue(value) {
2339
2403
  if (value === null) return "null";
2340
- if (typeof value === "boolean") return String(value);
2341
2404
  return String(value);
2342
2405
  }
2343
2406
  /**
2344
- * Serialize a single condition tuple to a PostgREST dot-string.
2407
+ * Escape a single list item for the wire format.
2408
+ * `\` → `\\`, `,` → `\,`
2409
+ */
2410
+ function escapeListItem(value) {
2411
+ return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
2412
+ }
2413
+ /**
2414
+ * Unescape a single list item from the wire format.
2415
+ * `\\` → `\`, `\,` → `,`
2416
+ */
2417
+ function unescapeListItem(value) {
2418
+ let result = "";
2419
+ for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
2420
+ result += value[i + 1];
2421
+ i++;
2422
+ } else result += value[i];
2423
+ return result;
2424
+ }
2425
+ /**
2426
+ * Split a parenthesized list string on unescaped commas.
2427
+ * Input is the content between `(` and `)`.
2428
+ *
2429
+ * @example
2430
+ * splitListItems("admin,editor") // ["admin", "editor"]
2431
+ * splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
2432
+ */
2433
+ function splitListItems(inner) {
2434
+ const items = [];
2435
+ let current = "";
2436
+ for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
2437
+ current += inner[i] + inner[i + 1];
2438
+ i++;
2439
+ } else if (inner[i] === ",") {
2440
+ items.push(unescapeListItem(current));
2441
+ current = "";
2442
+ } else current += inner[i];
2443
+ items.push(unescapeListItem(current));
2444
+ return items;
2445
+ }
2446
+ var REST_OP_LOOKUP = _rebasepro_types.REST_TO_CANONICAL;
2447
+ var CANONICAL_OP_LOOKUP = _rebasepro_types.CANONICAL_TO_REST;
2448
+ /**
2449
+ * Serialize a single canonical condition tuple to a PostgREST dot-string.
2450
+ *
2451
+ * Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
2345
2452
  *
2346
2453
  * @example
2347
2454
  * serializeTuple(["==", "active"]) // "eq.active"
@@ -2349,22 +2456,20 @@
2349
2456
  * serializeTuple([">=", 18]) // "gte.18"
2350
2457
  */
2351
2458
  function serializeTuple(tuple) {
2352
- if (typeof tuple === "string") {
2353
- if (tuple.includes(".")) {
2354
- const dotIndex = tuple.indexOf(".");
2355
- if (_rebasepro_types.REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
2356
- }
2357
- return tuple;
2358
- }
2359
- if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !_rebasepro_types.CANONICAL_TO_REST[tuple[0]]) return `eq.${stringifyValue(tuple)}`;
2459
+ if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
2360
2460
  const [op, value] = tuple;
2361
- const restOp = _rebasepro_types.CANONICAL_TO_REST[op];
2362
- if (Array.isArray(value)) return `${restOp}.(${value.map(stringifyValue).join(",")})`;
2461
+ if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
2462
+ const restOp = CANONICAL_OP_LOOKUP[op];
2463
+ if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(_rebasepro_types.CANONICAL_TO_REST).join(", ")}`);
2464
+ if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
2363
2465
  return `${restOp}.${stringifyValue(value)}`;
2364
2466
  }
2365
2467
  /**
2366
- * Convert `FilterValues` to a PostgREST-style querystring record.
2468
+ * Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
2469
+ * querystring record.
2367
2470
  *
2471
+ * - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
2472
+ * - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
2368
2473
  * - Single conditions produce a string value.
2369
2474
  * - Multiple conditions on the same field produce a string array (repeated params).
2370
2475
  *
@@ -2374,11 +2479,19 @@
2374
2479
  *
2375
2480
  * serializeFilter({ age: [[">=", 18], ["<", 65]] })
2376
2481
  * // → { age: ["gte.18", "lt.65"] }
2482
+ *
2483
+ * // Pre-serialized strings pass through unchanged:
2484
+ * serializeFilter({ status: "eq.published" })
2485
+ * // → { status: "eq.published" }
2377
2486
  */
2378
2487
  function serializeFilter(filter) {
2379
2488
  const result = {};
2380
2489
  for (const [field, condition] of Object.entries(filter)) {
2381
2490
  if (condition === void 0) continue;
2491
+ if (typeof condition === "string") {
2492
+ result[field] = condition;
2493
+ continue;
2494
+ }
2382
2495
  if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
2383
2496
  else result[field] = serializeTuple(condition);
2384
2497
  }
@@ -2387,18 +2500,24 @@
2387
2500
  /**
2388
2501
  * Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
2389
2502
  *
2390
- * If the string doesn't match a known operator prefix, falls back to
2503
+ * All values are returned as strings the wire format carries no type
2504
+ * metadata, so coercion is the data driver's responsibility.
2505
+ *
2506
+ * If the string doesn't match a known operator prefix, it falls back to
2391
2507
  * `["==", originalString]` (treating the whole string as an equality value).
2508
+ * This intentional defense handles values like `"user@host.com"` or
2509
+ * `"1.2.3"` that happen to contain dots.
2392
2510
  */
2393
2511
  function deserializeSingle(raw) {
2394
2512
  const dotIndex = raw.indexOf(".");
2395
- if (dotIndex === -1) return ["==", coerceValue(raw)];
2513
+ if (dotIndex === -1) return ["==", raw];
2396
2514
  const prefix = raw.substring(0, dotIndex);
2397
2515
  const rest = raw.substring(dotIndex + 1);
2398
- const canonicalOp = _rebasepro_types.REST_TO_CANONICAL[prefix];
2516
+ const canonicalOp = REST_OP_LOOKUP[prefix];
2399
2517
  if (!canonicalOp) return ["==", raw];
2400
- if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, rest.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))];
2401
- return [canonicalOp, coerceValue(rest)];
2518
+ if (_rebasepro_types.NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
2519
+ if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
2520
+ return [canonicalOp, rest];
2402
2521
  }
2403
2522
  /**
2404
2523
  * Convert a PostgREST-style querystring record to `FilterValues`.
@@ -2411,7 +2530,7 @@
2411
2530
  * // → { status: ["==", "active"] }
2412
2531
  *
2413
2532
  * deserializeFilter({ age: ["gte.18", "lt.65"] })
2414
- * // → { age: [[">=", 18], ["<", 65]] }
2533
+ * // → { age: [[">=", "18"], ["<", "65"]] }
2415
2534
  */
2416
2535
  function deserializeFilter(query) {
2417
2536
  const result = {};
@@ -2450,9 +2569,9 @@
2450
2569
  const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
2451
2570
  return `${cond.type}(${inner})`;
2452
2571
  }
2453
- const restOp = _rebasepro_types.CANONICAL_TO_REST[cond.operator] || "eq";
2572
+ const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
2454
2573
  if (Array.isArray(cond.value)) {
2455
- const items = cond.value.map(stringifyValue).join(",");
2574
+ const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
2456
2575
  return `${cond.column}.${restOp}.(${items})`;
2457
2576
  }
2458
2577
  return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
@@ -2500,99 +2619,115 @@
2500
2619
  if (secondDot === -1) return {
2501
2620
  column,
2502
2621
  operator: "==",
2503
- value: coerceValue(rest)
2622
+ value: rest
2504
2623
  };
2505
2624
  const opStr = rest.substring(0, secondDot);
2506
- let valueStr = rest.substring(secondDot + 1);
2625
+ const valueStr = rest.substring(secondDot + 1);
2507
2626
  const operator = (0, _rebasepro_types.toCanonicalOp)(opStr) ?? "==";
2508
2627
  if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
2509
2628
  column,
2510
2629
  operator,
2511
- value: valueStr.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))
2630
+ value: splitListItems(valueStr.slice(1, -1))
2512
2631
  };
2513
2632
  return {
2514
2633
  column,
2515
2634
  operator,
2516
- value: coerceValue(valueStr)
2635
+ value: valueStr
2517
2636
  };
2518
2637
  }
2519
2638
  //#endregion
2520
2639
  //#region src/data/buildRebaseData.ts
2521
2640
  /**
2522
- * Parse an orderBy string like "created_at:desc" into [field, direction].
2641
+ * Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
2642
+ * Mirrors the client SDK's rowToEntity conversion.
2523
2643
  */
2524
- function parseOrderBy(orderBy) {
2525
- if (!orderBy) return void 0;
2526
- const parts = orderBy.split(":");
2527
- return [parts[0], parts[1] || "asc"];
2644
+ function rowToEntity(row, slug) {
2645
+ return {
2646
+ id: row.id,
2647
+ path: slug,
2648
+ values: row
2649
+ };
2528
2650
  }
2529
2651
  function createDriverAccessor(driver, slug) {
2530
2652
  const accessor = {
2531
2653
  async find(params) {
2532
- const orderParsed = parseOrderBy(params?.orderBy);
2533
2654
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
2534
- const entities = await driver.fetchCollection({
2655
+ const limit = params?.limit ?? 20;
2656
+ const offset = params?.offset ?? 0;
2657
+ const fetchService = driver.restFetchService;
2658
+ const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
2659
+ filter,
2660
+ limit: params?.limit,
2661
+ offset: params?.offset,
2662
+ orderBy: params?.orderBy?.[0],
2663
+ order: params?.orderBy?.[1],
2664
+ searchString: params?.searchString
2665
+ }, params.include) : await driver.fetchCollection({
2535
2666
  path: slug,
2536
2667
  limit: params?.limit,
2537
2668
  offset: params?.offset,
2538
2669
  filter,
2539
- orderBy: orderParsed?.[0],
2540
- order: orderParsed?.[1],
2670
+ orderBy: params?.orderBy?.[0],
2671
+ order: params?.orderBy?.[1],
2541
2672
  searchString: params?.searchString
2542
2673
  });
2543
- const limit = params?.limit ?? 20;
2544
- const offset = params?.offset ?? 0;
2674
+ let total = rows.length + offset;
2675
+ let hasMore = rows.length >= limit;
2676
+ if (driver.count) {
2677
+ total = await driver.count({
2678
+ path: slug,
2679
+ filter
2680
+ });
2681
+ hasMore = offset + rows.length < total;
2682
+ }
2545
2683
  return {
2546
- data: entities,
2684
+ data: rows.map((row) => rowToEntity(row, slug)),
2547
2685
  meta: {
2548
- total: entities.length,
2686
+ total,
2549
2687
  limit,
2550
2688
  offset,
2551
- hasMore: entities.length >= limit
2689
+ hasMore
2552
2690
  }
2553
2691
  };
2554
2692
  },
2555
2693
  async findById(id) {
2556
- return driver.fetchEntity({
2694
+ const row = await driver.fetchOne({
2557
2695
  path: slug,
2558
- entityId: id
2696
+ id
2559
2697
  });
2698
+ return row ? rowToEntity(row, slug) : void 0;
2560
2699
  },
2561
2700
  async create(data, id) {
2562
- return driver.saveEntity({
2701
+ return rowToEntity(await driver.save({
2563
2702
  path: slug,
2564
2703
  values: data,
2565
- entityId: id,
2704
+ id,
2566
2705
  status: "new"
2567
- });
2706
+ }), slug);
2568
2707
  },
2569
2708
  async update(id, data) {
2570
- return driver.saveEntity({
2709
+ return rowToEntity(await driver.save({
2571
2710
  path: slug,
2572
2711
  values: data,
2573
- entityId: id,
2712
+ id,
2574
2713
  status: "existing"
2575
- });
2714
+ }), slug);
2576
2715
  },
2577
2716
  async delete(id) {
2578
- return driver.deleteEntity({ entity: {
2717
+ return driver.delete({ row: {
2579
2718
  id,
2580
2719
  path: slug,
2581
2720
  values: {}
2582
2721
  } });
2583
2722
  },
2584
- deleteAll: driver.deleteAll ? async () => {
2585
- return driver.deleteAll(slug);
2586
- } : void 0,
2587
- count: driver.countEntities ? async (params) => {
2723
+ count: driver.count ? async (params) => {
2588
2724
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
2589
- return driver.countEntities({
2725
+ return driver.count({
2590
2726
  path: slug,
2591
2727
  filter
2592
2728
  });
2593
2729
  } : void 0,
2594
2730
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
2595
- const orderParsed = parseOrderBy(params?.orderBy);
2596
2731
  const limit = params?.limit ?? 20;
2597
2732
  const offset = params?.offset ?? 0;
2598
2733
  return driver.listenCollection({
@@ -2600,12 +2735,12 @@
2600
2735
  limit: params?.limit,
2601
2736
  offset: params?.offset,
2602
2737
  filter: params?.where,
2603
- orderBy: orderParsed?.[0],
2604
- order: orderParsed?.[1],
2738
+ orderBy: params?.orderBy?.[0],
2739
+ order: params?.orderBy?.[1],
2605
2740
  searchString: params?.searchString,
2606
2741
  onUpdate: (entities) => {
2607
2742
  onUpdate({
2608
- data: entities,
2743
+ data: entities.map((row) => rowToEntity(row, slug)),
2609
2744
  meta: {
2610
2745
  total: entities.length,
2611
2746
  limit,
@@ -2617,11 +2752,11 @@
2617
2752
  onError
2618
2753
  });
2619
2754
  } : void 0,
2620
- listenById: driver.listenEntity ? (id, onUpdate, onError) => {
2621
- return driver.listenEntity({
2755
+ listenById: driver.listenOne ? (id, onUpdate, onError) => {
2756
+ return driver.listenOne({
2622
2757
  path: slug,
2623
- entityId: id,
2624
- onUpdate: (entity) => onUpdate(entity ?? void 0),
2758
+ id,
2759
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
2625
2760
  onError
2626
2761
  });
2627
2762
  } : void 0,
@@ -2658,7 +2793,7 @@
2658
2793
  * @example
2659
2794
  * const data = buildRebaseData(driver);
2660
2795
  * await data.products.create({ name: "Camera", price: 299 });
2661
- * const { data: items } = await data.products.find({ where: { status: "eq.published" } });
2796
+ * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
2662
2797
  */
2663
2798
  function buildRebaseData(driver) {
2664
2799
  const cache = /* @__PURE__ */ new Map();
@@ -2677,6 +2812,230 @@
2677
2812
  return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
2678
2813
  } });
2679
2814
  }
2815
+ /**
2816
+ * Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
2817
+ * (id included) under `.values`, so this is just that payload.
2818
+ */
2819
+ function entityToRow(entity) {
2820
+ return entity.values;
2821
+ }
2822
+ /**
2823
+ * Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
2824
+ * but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
2825
+ * `FindResponse<M>`.
2826
+ */
2827
+ var SdkQueryBuilder = class {
2828
+ client;
2829
+ params = { where: {} };
2830
+ constructor(client) {
2831
+ this.client = client;
2832
+ }
2833
+ where(columnOrCondition, operator, value) {
2834
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
2835
+ this.params.logical = columnOrCondition;
2836
+ return this;
2837
+ }
2838
+ if (!this.params.where) this.params.where = {};
2839
+ const column = columnOrCondition;
2840
+ const condition = [operator, value];
2841
+ const existing = this.params.where[column];
2842
+ if (existing === void 0) this.params.where[column] = condition;
2843
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
2844
+ else {
2845
+ let firstCondition;
2846
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
2847
+ else firstCondition = ["==", existing];
2848
+ this.params.where[column] = [firstCondition, condition];
2849
+ }
2850
+ return this;
2851
+ }
2852
+ orderBy(column, direction = "asc") {
2853
+ this.params.orderBy = [column, direction];
2854
+ return this;
2855
+ }
2856
+ limit(count) {
2857
+ this.params.limit = count;
2858
+ return this;
2859
+ }
2860
+ offset(count) {
2861
+ this.params.offset = count;
2862
+ return this;
2863
+ }
2864
+ search(searchString) {
2865
+ this.params.searchString = searchString;
2866
+ return this;
2867
+ }
2868
+ include(...relations) {
2869
+ this.params.include = relations;
2870
+ return this;
2871
+ }
2872
+ async find() {
2873
+ return this.client.find(this.params);
2874
+ }
2875
+ async count() {
2876
+ return this.client.count ? this.client.count(this.params) : 0;
2877
+ }
2878
+ listen(onUpdate, onError) {
2879
+ if (!this.client.listen) throw new Error("Listen is only available when the driver supports realtime.");
2880
+ return this.client.listen(this.params, onUpdate, onError);
2881
+ }
2882
+ };
2883
+ /**
2884
+ * Wrap a Entity-shaped {@link CollectionAccessor} into a flat
2885
+ * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
2886
+ * so the backend SDK is byte-for-byte the same shape as the frontend client.
2887
+ */
2888
+ function toSdkCollectionClient(snap) {
2889
+ const client = {
2890
+ async find(params) {
2891
+ const res = await snap.find(params);
2892
+ return {
2893
+ data: res.data.map(entityToRow),
2894
+ meta: res.meta
2895
+ };
2896
+ },
2897
+ async findById(id) {
2898
+ const s = await snap.findById(id);
2899
+ return s ? entityToRow(s) : void 0;
2900
+ },
2901
+ async create(data, id) {
2902
+ return entityToRow(await snap.create(data, id));
2903
+ },
2904
+ async update(id, data) {
2905
+ return entityToRow(await snap.update(id, data));
2906
+ },
2907
+ delete(id) {
2908
+ return snap.delete(id);
2909
+ },
2910
+ count: snap.count ? (params) => snap.count(params) : void 0,
2911
+ listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
2912
+ data: res.data.map(entityToRow),
2913
+ meta: res.meta
2914
+ }), onError) : void 0,
2915
+ listenById: snap.listenById ? (id, onUpdate, onError) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError) : void 0,
2916
+ where(columnOrCondition, operator, value) {
2917
+ const builder = new SdkQueryBuilder(client);
2918
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
2919
+ return builder.where(columnOrCondition, operator, value);
2920
+ },
2921
+ orderBy: (column, direction) => new SdkQueryBuilder(client).orderBy(column, direction),
2922
+ limit: (count) => new SdkQueryBuilder(client).limit(count),
2923
+ offset: (count) => new SdkQueryBuilder(client).offset(count),
2924
+ search: (searchString) => new SdkQueryBuilder(client).search(searchString),
2925
+ include: (...relations) => new SdkQueryBuilder(client).include(...relations)
2926
+ };
2927
+ return client;
2928
+ }
2929
+ /**
2930
+ * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
2931
+ * {@link CollectionAccessor}. Every returned row is re-wrapped into the
2932
+ * `{ id, path, values }` view-model the admin CMS renders.
2933
+ */
2934
+ function toEntityAccessor(sdk, slug) {
2935
+ const accessor = {
2936
+ async find(params) {
2937
+ const res = await sdk.find(params);
2938
+ return {
2939
+ data: res.data.map((row) => rowToEntity(row, slug)),
2940
+ meta: res.meta
2941
+ };
2942
+ },
2943
+ async findById(id) {
2944
+ const row = await sdk.findById(id);
2945
+ return row ? rowToEntity(row, slug) : void 0;
2946
+ },
2947
+ async create(data, id) {
2948
+ return rowToEntity(await sdk.create(data, id), slug);
2949
+ },
2950
+ async update(id, data) {
2951
+ const row = await sdk.update(id, data);
2952
+ if (!row) throw new Error(`Update returned no data for id ${id}`);
2953
+ return rowToEntity(row, slug);
2954
+ },
2955
+ delete(id) {
2956
+ return sdk.delete(id);
2957
+ },
2958
+ count: sdk.count ? (params) => sdk.count(params) : void 0,
2959
+ listen: sdk.listen ? (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
2960
+ data: res.data.map((row) => rowToEntity(row, slug)),
2961
+ meta: res.meta
2962
+ }), onError) : void 0,
2963
+ listenById: sdk.listenById ? (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug) : void 0), onError) : void 0,
2964
+ where(columnOrCondition, operator, value) {
2965
+ const builder = new QueryBuilder(accessor);
2966
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
2967
+ return builder.where(columnOrCondition, operator, value);
2968
+ },
2969
+ orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
2970
+ limit: (count) => new QueryBuilder(accessor).limit(count),
2971
+ offset: (count) => new QueryBuilder(accessor).offset(count),
2972
+ search: (searchString) => new QueryBuilder(accessor).search(searchString),
2973
+ include: (...relations) => new QueryBuilder(accessor).include(...relations)
2974
+ };
2975
+ return accessor;
2976
+ }
2977
+ /**
2978
+ * Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
2979
+ *
2980
+ * This is the **CMS boundary**: the SDK client (`client.data`) returns flat
2981
+ * rows, but the admin renders the `Entity` view-model (`entity.values.*`).
2982
+ * `core/Rebase.tsx` wraps `client.data` through this before handing it to the
2983
+ * CMS `RebaseDataContext` — without it the admin renders rows with only their
2984
+ * `id`.
2985
+ */
2986
+ function wrapAsEntityData(sdkData) {
2987
+ const cache = /* @__PURE__ */ new Map();
2988
+ function getAccessor(slug) {
2989
+ let accessor = cache.get(slug);
2990
+ if (!accessor) {
2991
+ accessor = toEntityAccessor(sdkData.collection(slug), slug);
2992
+ cache.set(slug, accessor);
2993
+ }
2994
+ return accessor;
2995
+ }
2996
+ return new Proxy({ collection: getAccessor }, { get(_target, prop) {
2997
+ if (prop === "collection") return getAccessor;
2998
+ if (typeof prop === "symbol") return void 0;
2999
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
3000
+ return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
3001
+ } });
3002
+ }
3003
+ /**
3004
+ * Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
3005
+ *
3006
+ * Every collection accessor is adapted to return flat rows. Use this to derive
3007
+ * the flat SDK data layer (`context.data`) from an existing Entity data layer
3008
+ * — e.g. the admin routes its Entity data via `useData()` and exposes the
3009
+ * same routing as flat `context.data` for callbacks by wrapping it here.
3010
+ */
3011
+ function wrapAsSdkData(entityData) {
3012
+ const cache = /* @__PURE__ */ new Map();
3013
+ function getAccessor(slug) {
3014
+ let accessor = cache.get(slug);
3015
+ if (!accessor) {
3016
+ accessor = toSdkCollectionClient(entityData.collection(slug));
3017
+ cache.set(slug, accessor);
3018
+ }
3019
+ return accessor;
3020
+ }
3021
+ return new Proxy({ collection: getAccessor }, { get(_target, prop) {
3022
+ if (prop === "collection") return getAccessor;
3023
+ if (typeof prop === "symbol") return void 0;
3024
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
3025
+ return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
3026
+ } });
3027
+ }
3028
+ /**
3029
+ * Build a flat {@link RebaseSdkData} from a `DataDriver`.
3030
+ *
3031
+ * This is the developer-facing SDK data layer used by backend framework
3032
+ * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
3033
+ * identical in shape to the frontend SDK client — so the API is symmetric
3034
+ * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
3035
+ */
3036
+ function buildSdkData(driver) {
3037
+ return wrapAsSdkData(buildRebaseData(driver));
3038
+ }
2680
3039
  //#endregion
2681
3040
  //#region src/data/buildRoutedRebaseData.ts
2682
3041
  /**
@@ -2721,6 +3080,57 @@
2721
3080
  } });
2722
3081
  }
2723
3082
  //#endregion
3083
+ //#region src/data/sort-dialect.ts
3084
+ /**
3085
+ * Sort-order wire codec.
3086
+ *
3087
+ * This is the ONLY module that knows about the colon-delimited wire format
3088
+ * (`"field:direction"`) used in HTTP query parameters.
3089
+ * Everything else speaks {@link OrderByTuple} exclusively.
3090
+ *
3091
+ * Mirrors the filter architecture in `filter-dialect.ts`.
3092
+ *
3093
+ * @module
3094
+ */
3095
+ /**
3096
+ * Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
3097
+ *
3098
+ * **Runtime tolerance:** if the input is already a well-formed wire string
3099
+ * (from an untyped JS caller), it is returned unchanged.
3100
+ * This is undocumented tolerance, not public API — don't rely on it.
3101
+ *
3102
+ * @param orderBy - A canonical `[field, direction]` tuple, or at runtime
3103
+ * possibly a pre-serialized string (undocumented tolerance).
3104
+ * @returns The wire-format string, or `undefined` if the input is falsy.
3105
+ *
3106
+ * @remarks
3107
+ * Field names containing `:` are representable in the tuple form but
3108
+ * **not** on the wire — this is an inherent limitation of the colon-delimited
3109
+ * encoding and is not resolved here.
3110
+ */
3111
+ function serializeOrderBy(orderBy) {
3112
+ if (!orderBy) return void 0;
3113
+ if (typeof orderBy === "string") return orderBy;
3114
+ return `${orderBy[0]}:${orderBy[1]}`;
3115
+ }
3116
+ /**
3117
+ * Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
3118
+ *
3119
+ * Lenient parsing (matches existing server behaviour):
3120
+ * - Bare field name (no colon): `"name"` → `["name", "asc"]`
3121
+ * - Unknown direction: `"name:foo"` → `["name", "asc"]`
3122
+ * - Empty / falsy input: → `undefined`
3123
+ *
3124
+ * @param raw - The wire-format string from an HTTP query parameter.
3125
+ * @returns The canonical tuple, or `undefined` if the input is empty/falsy.
3126
+ */
3127
+ function deserializeOrderBy(raw) {
3128
+ if (!raw) return void 0;
3129
+ const idx = raw.indexOf(":");
3130
+ if (idx === -1) return [raw, "asc"];
3131
+ return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
3132
+ }
3133
+ //#endregion
2724
3134
  //#region src/table-classification.ts
2725
3135
  /** Schemas that are always considered Rebase-internal. */
2726
3136
  var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
@@ -2808,18 +3218,13 @@
2808
3218
  exports.addInitialSlash = addInitialSlash;
2809
3219
  exports.and = and;
2810
3220
  exports.applyPropertyConditions = applyPropertyConditions;
2811
- exports.buildAdditionalFieldDelegate = buildAdditionalFieldDelegate;
2812
3221
  exports.buildCollection = buildCollection;
2813
3222
  exports.buildConditionContext = buildConditionContext;
2814
- exports.buildEntityCallbacks = buildEntityCallbacks;
2815
- exports.buildEnum = buildEnum;
2816
- exports.buildEnumValueConfig = buildEnumValueConfig;
2817
- exports.buildProperties = buildProperties;
2818
- exports.buildPropertiesOrBuilder = buildPropertiesOrBuilder;
2819
3223
  exports.buildProperty = buildProperty;
2820
3224
  exports.buildPropertyCallbacks = buildPropertyCallbacks;
2821
3225
  exports.buildRebaseData = buildRebaseData;
2822
3226
  exports.buildRoutedRebaseData = buildRoutedRebaseData;
3227
+ exports.buildSdkData = buildSdkData;
2823
3228
  exports.canCreateEntity = canCreateEntity;
2824
3229
  exports.canDeleteEntity = canDeleteEntity;
2825
3230
  exports.canEditEntity = canEditEntity;
@@ -2834,6 +3239,7 @@
2834
3239
  exports.defineCollection = defineCollection;
2835
3240
  exports.deserializeFilter = deserializeFilter;
2836
3241
  exports.deserializeLogicalCondition = deserializeLogicalCondition;
3242
+ exports.deserializeOrderBy = deserializeOrderBy;
2837
3243
  exports.detectJunctionTables = detectJunctionTables;
2838
3244
  exports.enumToObjectEntries = enumToObjectEntries;
2839
3245
  exports.evaluateCondition = evaluateCondition;
@@ -2877,6 +3283,7 @@
2877
3283
  exports.resolveDataSource = resolveDataSource;
2878
3284
  exports.resolveDefaultSelectedView = resolveDefaultSelectedView;
2879
3285
  exports.resolveEnumValues = resolveEnumValues;
3286
+ exports.resolveFilterOperators = resolveFilterOperators;
2880
3287
  exports.resolveProperties = resolveProperties;
2881
3288
  exports.resolveProperty = resolveProperty;
2882
3289
  exports.resolvePropertyEnum = resolvePropertyEnum;
@@ -2891,11 +3298,14 @@
2891
3298
  exports.segmentsToStrippedPath = segmentsToStrippedPath;
2892
3299
  exports.serializeFilter = serializeFilter;
2893
3300
  exports.serializeLogicalCondition = serializeLogicalCondition;
3301
+ exports.serializeOrderBy = serializeOrderBy;
2894
3302
  exports.sortProperties = sortProperties;
2895
3303
  exports.stripCollectionPath = stripCollectionPath;
2896
3304
  exports.traverseValueProperty = traverseValueProperty;
2897
3305
  exports.traverseValuesProperties = traverseValuesProperties;
2898
3306
  exports.updateDateAutoValues = updateDateAutoValues;
3307
+ exports.wrapAsEntityData = wrapAsEntityData;
3308
+ exports.wrapAsSdkData = wrapAsSdkData;
2899
3309
  });
2900
3310
 
2901
3311
  //# sourceMappingURL=index.umd.js.map