@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.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, policy, toCanonicalOp } from "@rebasepro/types";
1
+ import { ALL_WHERE_FILTER_OPS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, policy, toCanonicalOp } from "@rebasepro/types";
2
2
  import { deepClone, generateForeignKeyName, getIn, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
3
3
  import jsonLogic from "json-logic-js";
4
4
  import { deepEqual } from "fast-equals";
@@ -54,7 +54,7 @@ function getDefaultValueFortype(type) {
54
54
  else return null;
55
55
  }
56
56
  /**
57
- * Update the automatic values in an entity before save
57
+ * Update the automatic values in a entity before save
58
58
  * @group Driver
59
59
  */
60
60
  function updateDateAutoValues({ inputValues, properties, status, timestampNowValue }) {
@@ -66,7 +66,7 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
66
66
  }) ?? {};
67
67
  }
68
68
  /**
69
- * Add missing required fields, expected in the collection, to the values of an entity
69
+ * Add missing required fields, expected in the collection, to the values of a entity
70
70
  * @param values
71
71
  * @param properties
72
72
  * @group Driver
@@ -236,7 +236,7 @@ function getLocalChangesBackup(collection) {
236
236
  return collection.localChangesBackup;
237
237
  }
238
238
  /**
239
- * Returns the primary keys for an entity collection by inspecting the properties
239
+ * Returns the primary keys for a entity collection by inspecting the properties
240
240
  * and finding any properties with `isId`.
241
241
  * Fallbacks to `["id"]` if no properties are marked as `isId: true`.
242
242
  * @param collection
@@ -780,22 +780,56 @@ function withRoles(base, rule) {
780
780
  * {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
781
781
  * and the admin UI derive from the exact same expression.
782
782
  */
783
- function policyToPostgres(expr, collection) {
783
+ function policyToPostgres(expr, collection, options) {
784
+ return compile(expr, {
785
+ fieldCollection: collection,
786
+ fieldPrefix: "",
787
+ outerCollection: collection,
788
+ outerPrefix: "",
789
+ resolveCollection: options?.resolveCollection,
790
+ alias: { n: 0 }
791
+ });
792
+ }
793
+ function compile(expr, scope) {
784
794
  switch (expr.kind) {
785
795
  case "true": return "true";
786
796
  case "false": return "false";
787
- case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" AND ");
788
- case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" OR ");
797
+ case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" AND ");
798
+ case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" OR ");
789
799
  case "not":
790
800
  if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
791
- return `NOT (${policyToPostgres(expr.operand, collection)})`;
792
- case "compare": return `${operandToSql(expr.left, collection)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, collection)}`;
801
+ return `NOT (${compile(expr.operand, scope)})`;
802
+ case "compare": return `${operandToSql(expr.left, scope)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, scope)}`;
793
803
  case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
794
804
  case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
795
805
  case "authenticated": return "auth.uid() IS NOT NULL";
806
+ case "existsIn": return compileExistsIn(expr, scope);
796
807
  case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
797
808
  }
798
809
  }
810
+ /**
811
+ * Compiles `existsIn` to a correlated `EXISTS (SELECT 1 FROM <join> WHERE ...)`.
812
+ * Inside the subquery, `field` operands bind to the aliased join table and
813
+ * `outerField` operands bind to the (table-qualified) outer RLS row.
814
+ */
815
+ function compileExistsIn(expr, scope) {
816
+ const join = scope.resolveCollection?.(expr.collection);
817
+ const joinTable = join ? getTableName(join) : toSnakeCase(expr.collection);
818
+ const joinSchema = schemaOf(join) ?? schemaOf(scope.outerCollection) ?? "public";
819
+ const alias = `_ex${scope.alias.n++}`;
820
+ const outerTable = scope.outerCollection ? getTableName(scope.outerCollection) : void 0;
821
+ const outerSchema = schemaOf(scope.outerCollection) ?? "public";
822
+ const outerPrefix = outerTable ? `"${outerSchema}"."${outerTable}".` : "";
823
+ const innerScope = {
824
+ fieldCollection: join,
825
+ fieldPrefix: `"${alias}".`,
826
+ outerCollection: scope.outerCollection,
827
+ outerPrefix,
828
+ resolveCollection: scope.resolveCollection,
829
+ alias: scope.alias
830
+ };
831
+ return `EXISTS (SELECT 1 FROM "${joinSchema}"."${joinTable}" "${alias}" WHERE ${compile(expr.where, innerScope)})`;
832
+ }
799
833
  var COMPARE_SQL = {
800
834
  eq: "=",
801
835
  neq: "!=",
@@ -804,14 +838,18 @@ var COMPARE_SQL = {
804
838
  gt: ">",
805
839
  gte: ">="
806
840
  };
807
- function operandToSql(operand, collection) {
841
+ function operandToSql(operand, scope) {
808
842
  switch (operand.kind) {
809
- case "field": return resolveColumnName(operand.name, collection);
843
+ case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
844
+ case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
810
845
  case "literal": return quoteLiteral(operand.value);
811
846
  case "authUid": return "auth.uid()";
812
847
  case "authRoles": return "string_to_array(auth.roles(), ',')";
813
848
  }
814
849
  }
850
+ function schemaOf(collection) {
851
+ return collection?.schema || void 0;
852
+ }
815
853
  function resolveColumnName(propName, collection) {
816
854
  const prop = collection?.properties?.[propName];
817
855
  if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
@@ -854,6 +892,7 @@ function evaluatePolicy(expr, ctx) {
854
892
  return expr.roles.every((r) => r === "public" || userRoles.includes(r));
855
893
  }
856
894
  case "authenticated": return ctx.uid != null;
895
+ case "existsIn": return "unknown";
857
896
  case "raw": return "unknown";
858
897
  }
859
898
  }
@@ -891,6 +930,7 @@ function resolveOperand(operand, ctx) {
891
930
  known: true,
892
931
  value: ctx.entity.values[operand.name]
893
932
  };
933
+ case "outerField": return { known: false };
894
934
  }
895
935
  }
896
936
  function evaluateCompare(op, left, right, ctx) {
@@ -1102,7 +1142,7 @@ function resolveCollectionPathIds(path, allCollections) {
1102
1142
  } else {
1103
1143
  entityId = remainingPath;
1104
1144
  remainingPath = "";
1105
- 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.`);
1145
+ 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.`);
1106
1146
  }
1107
1147
  resolvedPathParts.push(entityId);
1108
1148
  currentCollections = getSubcollections(foundCollection);
@@ -1258,9 +1298,12 @@ function getParentReferencesFromPath(props) {
1258
1298
  //#endregion
1259
1299
  //#region src/util/builders.ts
1260
1300
  /**
1261
- * Identity function we use to defeat the type system of Typescript and build
1262
- * collection views with all its properties
1263
- * @param collection
1301
+ * @deprecated Use {@link defineCollection} instead it infers property
1302
+ * types automatically (autocomplete on `titleProperty`, `sort`,
1303
+ * `propertiesOrder`, callbacks) without manual generics.
1304
+ * `buildCollection` is kept for FireCMS migration compatibility and will
1305
+ * be removed before 1.0.
1306
+ *
1264
1307
  * @group Builder
1265
1308
  */
1266
1309
  function buildCollection(collection) {
@@ -1274,68 +1317,16 @@ function defineCollection(collection) {
1274
1317
  return collection;
1275
1318
  }
1276
1319
  /**
1277
- * Identity function we use to defeat the type system of Typescript and preserve
1278
- * the property keys.
1279
- * @param property
1320
+ * @deprecated Use plain typed property objects with {@link defineCollection}
1321
+ * instead — `defineCollection` infers property types automatically, making
1322
+ * this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
1323
+ * compatibility and will be removed before 1.0.
1324
+ *
1280
1325
  * @group Builder
1281
1326
  */
1282
1327
  function buildProperty(property) {
1283
1328
  return property;
1284
1329
  }
1285
- /**
1286
- * Identity function we use to defeat the type system of Typescript and preserve
1287
- * the properties keys.
1288
- * @param properties
1289
- * @group Builder
1290
- */
1291
- function buildProperties(properties) {
1292
- return properties;
1293
- }
1294
- /**
1295
- * Identity function we use to defeat the type system of Typescript and preserve
1296
- * the properties keys.
1297
- * @param propertiesOrBuilder
1298
- * @group Builder
1299
- */
1300
- function buildPropertiesOrBuilder(propertiesOrBuilder) {
1301
- return propertiesOrBuilder;
1302
- }
1303
- /**
1304
- * Identity function we use to defeat the type system of Typescript and preserve
1305
- * the properties keys.
1306
- * @param enumValues
1307
- * @group Builder
1308
- */
1309
- function buildEnum(enumValues) {
1310
- return enumValues;
1311
- }
1312
- /**
1313
- * Identity function we use to defeat the type system of Typescript and preserve
1314
- * the properties keys.
1315
- * @param enumValueConfig
1316
- * @group Builder
1317
- */
1318
- function buildEnumValueConfig(enumValueConfig) {
1319
- return enumValueConfig;
1320
- }
1321
- /**
1322
- * Identity function we use to defeat the type system of Typescript and preserve
1323
- * the properties keys.
1324
- * @param callbacks
1325
- * @group Builder
1326
- */
1327
- function buildEntityCallbacks(callbacks) {
1328
- return callbacks;
1329
- }
1330
- /**
1331
- * Identity function we use to defeat the type system of Typescript and build
1332
- * additional field delegates views with all its properties
1333
- * @param additionalFieldDelegate
1334
- * @group Builder
1335
- */
1336
- function buildAdditionalFieldDelegate(additionalFieldDelegate) {
1337
- return additionalFieldDelegate;
1338
- }
1339
1330
  //#endregion
1340
1331
  //#region src/util/storage.ts
1341
1332
  /**
@@ -1471,16 +1462,17 @@ async function processProperties(properties, values, previousValues, propsContex
1471
1462
  }
1472
1463
  /**
1473
1464
  * Helper function to extract field-level PropertyCallbacks from a properties schema
1474
- * and wrap them into an EntityCallbacks object recursively.
1465
+ * and wrap them into an CollectionCallbacks object recursively.
1475
1466
  */
1476
1467
  var buildPropertyCallbacks = (properties) => {
1477
1468
  if (!properties) return void 0;
1478
1469
  const propertyCallbacks = {};
1479
1470
  if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
1480
- const processedValues = await processProperties(properties, props.entity.values, props.entity.values, props, "afterRead");
1471
+ const row = props.row;
1472
+ const processedValues = await processProperties(properties, row, row, props, "afterRead");
1481
1473
  return {
1482
- ...props.entity,
1483
- values: processedValues
1474
+ ...props.row,
1475
+ ...processedValues
1484
1476
  };
1485
1477
  };
1486
1478
  if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
@@ -1667,6 +1659,84 @@ function applyEnumConditions(enumValues, conditions, context) {
1667
1659
  return result;
1668
1660
  }
1669
1661
  //#endregion
1662
+ //#region src/util/filter-operator-resolution.ts
1663
+ /**
1664
+ * Default operators offered per property type, before engine capabilities and
1665
+ * per-property narrowing are applied. These mirror what the built-in filter
1666
+ * fields can render.
1667
+ */
1668
+ var COMPARISON_OPS = [
1669
+ "==",
1670
+ "!=",
1671
+ ">",
1672
+ ">=",
1673
+ "<",
1674
+ "<="
1675
+ ];
1676
+ var NULL_CHECK_OPS = ["is-null", "is-not-null"];
1677
+ var MEMBERSHIP_OPS = ["in", "not-in"];
1678
+ var PATTERN_OPS = [
1679
+ "like",
1680
+ "ilike",
1681
+ "not-like",
1682
+ "not-ilike"
1683
+ ];
1684
+ var DEFAULT_OPS_BY_TYPE = {
1685
+ string: [
1686
+ ...COMPARISON_OPS,
1687
+ ...MEMBERSHIP_OPS,
1688
+ ...PATTERN_OPS,
1689
+ ...NULL_CHECK_OPS
1690
+ ],
1691
+ number: [
1692
+ ...COMPARISON_OPS,
1693
+ ...MEMBERSHIP_OPS,
1694
+ ...NULL_CHECK_OPS
1695
+ ],
1696
+ date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
1697
+ boolean: [
1698
+ "==",
1699
+ "!=",
1700
+ ...NULL_CHECK_OPS
1701
+ ],
1702
+ reference: [
1703
+ "==",
1704
+ "!=",
1705
+ ...MEMBERSHIP_OPS,
1706
+ ...NULL_CHECK_OPS
1707
+ ],
1708
+ relation: [
1709
+ "==",
1710
+ "!=",
1711
+ ...MEMBERSHIP_OPS,
1712
+ ...NULL_CHECK_OPS
1713
+ ]
1714
+ };
1715
+ /** Operators offered when the property is an *array of* a filterable type. */
1716
+ var ARRAY_OPS = ["array-contains", "array-contains-any"];
1717
+ /**
1718
+ * Resolve which filter operators the UI should offer for a property.
1719
+ *
1720
+ * The result is the **intersection** of three sets:
1721
+ * 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
1722
+ * (e.g. Firestore cannot run the LIKE family);
1723
+ * 2. what makes sense for the property type (e.g. no `>` on booleans);
1724
+ * 3. the developer's optional narrowing — `property.ui.filterOperators`.
1725
+ *
1726
+ * Returns an empty array when the property is not filterable (either by
1727
+ * type, or because the developer disabled it with `filterOperators: []`).
1728
+ *
1729
+ * @group Models
1730
+ */
1731
+ function resolveFilterOperators({ property, isArray, engine }) {
1732
+ const typeDefaults = isArray ? ARRAY_OPS : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
1733
+ if (typeDefaults.length === 0) return [];
1734
+ const engineOps = new Set(getDataSourceCapabilities(engine).filterOperators ?? ALL_WHERE_FILTER_OPS);
1735
+ const narrowing = property.ui?.filterOperators;
1736
+ const narrowingSet = narrowing !== void 0 ? new Set(narrowing) : void 0;
1737
+ return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
1738
+ }
1739
+ //#endregion
1670
1740
  //#region src/data/resolveDataSource.ts
1671
1741
  /**
1672
1742
  * Build a keyed registry from a list of {@link DataSourceDefinition}s.
@@ -1745,7 +1815,7 @@ var CollectionRegistry = class {
1745
1815
  rawCollectionsBySlug = /* @__PURE__ */ new Map();
1746
1816
  rawRootCollections = [];
1747
1817
  cachedRawCollectionsList = null;
1748
- lastRawInputSnapshot = null;
1818
+ lastRawInputEntity = null;
1749
1819
  constructor(collections, dataSources) {
1750
1820
  if (dataSources) this.dataSources = dataSources;
1751
1821
  if (collections) this.registerMultiple(collections);
@@ -1775,12 +1845,12 @@ var CollectionRegistry = class {
1775
1845
  * Returns true if the collections have changed, false otherwise.
1776
1846
  *
1777
1847
  * Idempotent: compares the raw input (before normalization) against a stored
1778
- * snapshot. Only re-normalizes and re-registers when the raw input actually changed.
1848
+ * entity. Only re-normalizes and re-registers when the raw input actually changed.
1779
1849
  * @param collections
1780
1850
  */
1781
1851
  registerMultiple(collections) {
1782
- const rawSnapshot = collections.map((c) => removeFunctions(c));
1783
- if (this.lastRawInputSnapshot && deepEqual(this.lastRawInputSnapshot, rawSnapshot)) return false;
1852
+ const rawEntity = collections.map((c) => removeFunctions(c));
1853
+ if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) return false;
1784
1854
  this.reset();
1785
1855
  collections.forEach((c) => {
1786
1856
  if (c.slug) this.collectionsBySlug.set(c.slug, c);
@@ -1804,7 +1874,7 @@ var CollectionRegistry = class {
1804
1874
  this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
1805
1875
  });
1806
1876
  });
1807
- this.lastRawInputSnapshot = rawSnapshot;
1877
+ this.lastRawInputEntity = rawEntity;
1808
1878
  return true;
1809
1879
  }
1810
1880
  register(collection, rawCollection) {
@@ -2224,7 +2294,7 @@ var QueryBuilder = class {
2224
2294
  * client.collection('users').orderBy('createdAt', 'desc').find()
2225
2295
  */
2226
2296
  orderBy(column, direction = "asc") {
2227
- this.params.orderBy = `${column}:${direction}`;
2297
+ this.params.orderBy = [column, direction];
2228
2298
  return this;
2229
2299
  }
2230
2300
  /**
@@ -2287,32 +2357,69 @@ var QueryBuilder = class {
2287
2357
  * PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
2288
2358
  * Everything else speaks `FilterValues` exclusively.
2289
2359
  *
2360
+ * Wire-format values are always strings — the wire format carries no type
2361
+ * metadata, so type coercion is the responsibility of the server-side data
2362
+ * driver which has access to the collection schema.
2363
+ *
2364
+ * Commas inside list values are backslash-escaped (`\,`), and literal
2365
+ * backslashes are escaped as `\\`.
2366
+ *
2290
2367
  * @module
2291
2368
  */
2292
2369
  /**
2293
- * Coerce a raw querystring value to its natural JS type.
2294
- * - `"true"` / `"false"` → boolean
2295
- * - `"null"` → null
2296
- * - Numeric strings → number
2297
- * - Everything else → string (unchanged)
2298
- */
2299
- function coerceValue(raw) {
2300
- if (raw === "true") return true;
2301
- if (raw === "false") return false;
2302
- if (raw === "null") return null;
2303
- if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
2304
- return raw;
2305
- }
2306
- /**
2307
2370
  * Serialize a JS value to its querystring representation.
2371
+ * `null` is serialized as the literal string `"null"`.
2308
2372
  */
2309
2373
  function stringifyValue(value) {
2310
2374
  if (value === null) return "null";
2311
- if (typeof value === "boolean") return String(value);
2312
2375
  return String(value);
2313
2376
  }
2314
2377
  /**
2315
- * Serialize a single condition tuple to a PostgREST dot-string.
2378
+ * Escape a single list item for the wire format.
2379
+ * `\` → `\\`, `,` → `\,`
2380
+ */
2381
+ function escapeListItem(value) {
2382
+ return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
2383
+ }
2384
+ /**
2385
+ * Unescape a single list item from the wire format.
2386
+ * `\\` → `\`, `\,` → `,`
2387
+ */
2388
+ function unescapeListItem(value) {
2389
+ let result = "";
2390
+ for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
2391
+ result += value[i + 1];
2392
+ i++;
2393
+ } else result += value[i];
2394
+ return result;
2395
+ }
2396
+ /**
2397
+ * Split a parenthesized list string on unescaped commas.
2398
+ * Input is the content between `(` and `)`.
2399
+ *
2400
+ * @example
2401
+ * splitListItems("admin,editor") // ["admin", "editor"]
2402
+ * splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
2403
+ */
2404
+ function splitListItems(inner) {
2405
+ const items = [];
2406
+ let current = "";
2407
+ for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
2408
+ current += inner[i] + inner[i + 1];
2409
+ i++;
2410
+ } else if (inner[i] === ",") {
2411
+ items.push(unescapeListItem(current));
2412
+ current = "";
2413
+ } else current += inner[i];
2414
+ items.push(unescapeListItem(current));
2415
+ return items;
2416
+ }
2417
+ var REST_OP_LOOKUP = REST_TO_CANONICAL;
2418
+ var CANONICAL_OP_LOOKUP = CANONICAL_TO_REST;
2419
+ /**
2420
+ * Serialize a single canonical condition tuple to a PostgREST dot-string.
2421
+ *
2422
+ * Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
2316
2423
  *
2317
2424
  * @example
2318
2425
  * serializeTuple(["==", "active"]) // "eq.active"
@@ -2320,22 +2427,20 @@ function stringifyValue(value) {
2320
2427
  * serializeTuple([">=", 18]) // "gte.18"
2321
2428
  */
2322
2429
  function serializeTuple(tuple) {
2323
- if (typeof tuple === "string") {
2324
- if (tuple.includes(".")) {
2325
- const dotIndex = tuple.indexOf(".");
2326
- if (REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
2327
- }
2328
- return tuple;
2329
- }
2330
- if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !CANONICAL_TO_REST[tuple[0]]) return `eq.${stringifyValue(tuple)}`;
2430
+ if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
2331
2431
  const [op, value] = tuple;
2332
- const restOp = CANONICAL_TO_REST[op];
2333
- if (Array.isArray(value)) return `${restOp}.(${value.map(stringifyValue).join(",")})`;
2432
+ if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
2433
+ const restOp = CANONICAL_OP_LOOKUP[op];
2434
+ if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`);
2435
+ if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
2334
2436
  return `${restOp}.${stringifyValue(value)}`;
2335
2437
  }
2336
2438
  /**
2337
- * Convert `FilterValues` to a PostgREST-style querystring record.
2439
+ * Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
2440
+ * querystring record.
2338
2441
  *
2442
+ * - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
2443
+ * - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
2339
2444
  * - Single conditions produce a string value.
2340
2445
  * - Multiple conditions on the same field produce a string array (repeated params).
2341
2446
  *
@@ -2345,11 +2450,19 @@ function serializeTuple(tuple) {
2345
2450
  *
2346
2451
  * serializeFilter({ age: [[">=", 18], ["<", 65]] })
2347
2452
  * // → { age: ["gte.18", "lt.65"] }
2453
+ *
2454
+ * // Pre-serialized strings pass through unchanged:
2455
+ * serializeFilter({ status: "eq.published" })
2456
+ * // → { status: "eq.published" }
2348
2457
  */
2349
2458
  function serializeFilter(filter) {
2350
2459
  const result = {};
2351
2460
  for (const [field, condition] of Object.entries(filter)) {
2352
2461
  if (condition === void 0) continue;
2462
+ if (typeof condition === "string") {
2463
+ result[field] = condition;
2464
+ continue;
2465
+ }
2353
2466
  if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
2354
2467
  else result[field] = serializeTuple(condition);
2355
2468
  }
@@ -2358,18 +2471,24 @@ function serializeFilter(filter) {
2358
2471
  /**
2359
2472
  * Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
2360
2473
  *
2361
- * If the string doesn't match a known operator prefix, falls back to
2474
+ * All values are returned as strings the wire format carries no type
2475
+ * metadata, so coercion is the data driver's responsibility.
2476
+ *
2477
+ * If the string doesn't match a known operator prefix, it falls back to
2362
2478
  * `["==", originalString]` (treating the whole string as an equality value).
2479
+ * This intentional defense handles values like `"user@host.com"` or
2480
+ * `"1.2.3"` that happen to contain dots.
2363
2481
  */
2364
2482
  function deserializeSingle(raw) {
2365
2483
  const dotIndex = raw.indexOf(".");
2366
- if (dotIndex === -1) return ["==", coerceValue(raw)];
2484
+ if (dotIndex === -1) return ["==", raw];
2367
2485
  const prefix = raw.substring(0, dotIndex);
2368
2486
  const rest = raw.substring(dotIndex + 1);
2369
- const canonicalOp = REST_TO_CANONICAL[prefix];
2487
+ const canonicalOp = REST_OP_LOOKUP[prefix];
2370
2488
  if (!canonicalOp) return ["==", raw];
2371
- if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, rest.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))];
2372
- return [canonicalOp, coerceValue(rest)];
2489
+ if (NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
2490
+ if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
2491
+ return [canonicalOp, rest];
2373
2492
  }
2374
2493
  /**
2375
2494
  * Convert a PostgREST-style querystring record to `FilterValues`.
@@ -2382,7 +2501,7 @@ function deserializeSingle(raw) {
2382
2501
  * // → { status: ["==", "active"] }
2383
2502
  *
2384
2503
  * deserializeFilter({ age: ["gte.18", "lt.65"] })
2385
- * // → { age: [[">=", 18], ["<", 65]] }
2504
+ * // → { age: [[">=", "18"], ["<", "65"]] }
2386
2505
  */
2387
2506
  function deserializeFilter(query) {
2388
2507
  const result = {};
@@ -2421,9 +2540,9 @@ function serializeLogicalCondition(cond) {
2421
2540
  const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
2422
2541
  return `${cond.type}(${inner})`;
2423
2542
  }
2424
- const restOp = CANONICAL_TO_REST[cond.operator] || "eq";
2543
+ const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
2425
2544
  if (Array.isArray(cond.value)) {
2426
- const items = cond.value.map(stringifyValue).join(",");
2545
+ const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
2427
2546
  return `${cond.column}.${restOp}.(${items})`;
2428
2547
  }
2429
2548
  return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
@@ -2471,99 +2590,115 @@ function deserializeLogicalCondition(str) {
2471
2590
  if (secondDot === -1) return {
2472
2591
  column,
2473
2592
  operator: "==",
2474
- value: coerceValue(rest)
2593
+ value: rest
2475
2594
  };
2476
2595
  const opStr = rest.substring(0, secondDot);
2477
- let valueStr = rest.substring(secondDot + 1);
2596
+ const valueStr = rest.substring(secondDot + 1);
2478
2597
  const operator = toCanonicalOp(opStr) ?? "==";
2479
2598
  if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
2480
2599
  column,
2481
2600
  operator,
2482
- value: valueStr.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))
2601
+ value: splitListItems(valueStr.slice(1, -1))
2483
2602
  };
2484
2603
  return {
2485
2604
  column,
2486
2605
  operator,
2487
- value: coerceValue(valueStr)
2606
+ value: valueStr
2488
2607
  };
2489
2608
  }
2490
2609
  //#endregion
2491
2610
  //#region src/data/buildRebaseData.ts
2492
2611
  /**
2493
- * Parse an orderBy string like "created_at:desc" into [field, direction].
2612
+ * Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
2613
+ * Mirrors the client SDK's rowToEntity conversion.
2494
2614
  */
2495
- function parseOrderBy(orderBy) {
2496
- if (!orderBy) return void 0;
2497
- const parts = orderBy.split(":");
2498
- return [parts[0], parts[1] || "asc"];
2615
+ function rowToEntity(row, slug) {
2616
+ return {
2617
+ id: row.id,
2618
+ path: slug,
2619
+ values: row
2620
+ };
2499
2621
  }
2500
2622
  function createDriverAccessor(driver, slug) {
2501
2623
  const accessor = {
2502
2624
  async find(params) {
2503
- const orderParsed = parseOrderBy(params?.orderBy);
2504
2625
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
2505
- const entities = await driver.fetchCollection({
2626
+ const limit = params?.limit ?? 20;
2627
+ const offset = params?.offset ?? 0;
2628
+ const fetchService = driver.restFetchService;
2629
+ const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
2630
+ filter,
2631
+ limit: params?.limit,
2632
+ offset: params?.offset,
2633
+ orderBy: params?.orderBy?.[0],
2634
+ order: params?.orderBy?.[1],
2635
+ searchString: params?.searchString
2636
+ }, params.include) : await driver.fetchCollection({
2506
2637
  path: slug,
2507
2638
  limit: params?.limit,
2508
2639
  offset: params?.offset,
2509
2640
  filter,
2510
- orderBy: orderParsed?.[0],
2511
- order: orderParsed?.[1],
2641
+ orderBy: params?.orderBy?.[0],
2642
+ order: params?.orderBy?.[1],
2512
2643
  searchString: params?.searchString
2513
2644
  });
2514
- const limit = params?.limit ?? 20;
2515
- const offset = params?.offset ?? 0;
2645
+ let total = rows.length + offset;
2646
+ let hasMore = rows.length >= limit;
2647
+ if (driver.count) {
2648
+ total = await driver.count({
2649
+ path: slug,
2650
+ filter
2651
+ });
2652
+ hasMore = offset + rows.length < total;
2653
+ }
2516
2654
  return {
2517
- data: entities,
2655
+ data: rows.map((row) => rowToEntity(row, slug)),
2518
2656
  meta: {
2519
- total: entities.length,
2657
+ total,
2520
2658
  limit,
2521
2659
  offset,
2522
- hasMore: entities.length >= limit
2660
+ hasMore
2523
2661
  }
2524
2662
  };
2525
2663
  },
2526
2664
  async findById(id) {
2527
- return driver.fetchEntity({
2665
+ const row = await driver.fetchOne({
2528
2666
  path: slug,
2529
- entityId: id
2667
+ id
2530
2668
  });
2669
+ return row ? rowToEntity(row, slug) : void 0;
2531
2670
  },
2532
2671
  async create(data, id) {
2533
- return driver.saveEntity({
2672
+ return rowToEntity(await driver.save({
2534
2673
  path: slug,
2535
2674
  values: data,
2536
- entityId: id,
2675
+ id,
2537
2676
  status: "new"
2538
- });
2677
+ }), slug);
2539
2678
  },
2540
2679
  async update(id, data) {
2541
- return driver.saveEntity({
2680
+ return rowToEntity(await driver.save({
2542
2681
  path: slug,
2543
2682
  values: data,
2544
- entityId: id,
2683
+ id,
2545
2684
  status: "existing"
2546
- });
2685
+ }), slug);
2547
2686
  },
2548
2687
  async delete(id) {
2549
- return driver.deleteEntity({ entity: {
2688
+ return driver.delete({ row: {
2550
2689
  id,
2551
2690
  path: slug,
2552
2691
  values: {}
2553
2692
  } });
2554
2693
  },
2555
- deleteAll: driver.deleteAll ? async () => {
2556
- return driver.deleteAll(slug);
2557
- } : void 0,
2558
- count: driver.countEntities ? async (params) => {
2694
+ count: driver.count ? async (params) => {
2559
2695
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
2560
- return driver.countEntities({
2696
+ return driver.count({
2561
2697
  path: slug,
2562
2698
  filter
2563
2699
  });
2564
2700
  } : void 0,
2565
2701
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
2566
- const orderParsed = parseOrderBy(params?.orderBy);
2567
2702
  const limit = params?.limit ?? 20;
2568
2703
  const offset = params?.offset ?? 0;
2569
2704
  return driver.listenCollection({
@@ -2571,12 +2706,12 @@ function createDriverAccessor(driver, slug) {
2571
2706
  limit: params?.limit,
2572
2707
  offset: params?.offset,
2573
2708
  filter: params?.where,
2574
- orderBy: orderParsed?.[0],
2575
- order: orderParsed?.[1],
2709
+ orderBy: params?.orderBy?.[0],
2710
+ order: params?.orderBy?.[1],
2576
2711
  searchString: params?.searchString,
2577
2712
  onUpdate: (entities) => {
2578
2713
  onUpdate({
2579
- data: entities,
2714
+ data: entities.map((row) => rowToEntity(row, slug)),
2580
2715
  meta: {
2581
2716
  total: entities.length,
2582
2717
  limit,
@@ -2588,11 +2723,11 @@ function createDriverAccessor(driver, slug) {
2588
2723
  onError
2589
2724
  });
2590
2725
  } : void 0,
2591
- listenById: driver.listenEntity ? (id, onUpdate, onError) => {
2592
- return driver.listenEntity({
2726
+ listenById: driver.listenOne ? (id, onUpdate, onError) => {
2727
+ return driver.listenOne({
2593
2728
  path: slug,
2594
- entityId: id,
2595
- onUpdate: (entity) => onUpdate(entity ?? void 0),
2729
+ id,
2730
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
2596
2731
  onError
2597
2732
  });
2598
2733
  } : void 0,
@@ -2629,7 +2764,7 @@ function createDriverAccessor(driver, slug) {
2629
2764
  * @example
2630
2765
  * const data = buildRebaseData(driver);
2631
2766
  * await data.products.create({ name: "Camera", price: 299 });
2632
- * const { data: items } = await data.products.find({ where: { status: "eq.published" } });
2767
+ * const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
2633
2768
  */
2634
2769
  function buildRebaseData(driver) {
2635
2770
  const cache = /* @__PURE__ */ new Map();
@@ -2648,6 +2783,230 @@ function buildRebaseData(driver) {
2648
2783
  return getAccessor(toSnakeCase(prop));
2649
2784
  } });
2650
2785
  }
2786
+ /**
2787
+ * Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
2788
+ * (id included) under `.values`, so this is just that payload.
2789
+ */
2790
+ function entityToRow(entity) {
2791
+ return entity.values;
2792
+ }
2793
+ /**
2794
+ * Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
2795
+ * but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
2796
+ * `FindResponse<M>`.
2797
+ */
2798
+ var SdkQueryBuilder = class {
2799
+ client;
2800
+ params = { where: {} };
2801
+ constructor(client) {
2802
+ this.client = client;
2803
+ }
2804
+ where(columnOrCondition, operator, value) {
2805
+ if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
2806
+ this.params.logical = columnOrCondition;
2807
+ return this;
2808
+ }
2809
+ if (!this.params.where) this.params.where = {};
2810
+ const column = columnOrCondition;
2811
+ const condition = [operator, value];
2812
+ const existing = this.params.where[column];
2813
+ if (existing === void 0) this.params.where[column] = condition;
2814
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
2815
+ else {
2816
+ let firstCondition;
2817
+ if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
2818
+ else firstCondition = ["==", existing];
2819
+ this.params.where[column] = [firstCondition, condition];
2820
+ }
2821
+ return this;
2822
+ }
2823
+ orderBy(column, direction = "asc") {
2824
+ this.params.orderBy = [column, direction];
2825
+ return this;
2826
+ }
2827
+ limit(count) {
2828
+ this.params.limit = count;
2829
+ return this;
2830
+ }
2831
+ offset(count) {
2832
+ this.params.offset = count;
2833
+ return this;
2834
+ }
2835
+ search(searchString) {
2836
+ this.params.searchString = searchString;
2837
+ return this;
2838
+ }
2839
+ include(...relations) {
2840
+ this.params.include = relations;
2841
+ return this;
2842
+ }
2843
+ async find() {
2844
+ return this.client.find(this.params);
2845
+ }
2846
+ async count() {
2847
+ return this.client.count ? this.client.count(this.params) : 0;
2848
+ }
2849
+ listen(onUpdate, onError) {
2850
+ if (!this.client.listen) throw new Error("Listen is only available when the driver supports realtime.");
2851
+ return this.client.listen(this.params, onUpdate, onError);
2852
+ }
2853
+ };
2854
+ /**
2855
+ * Wrap a Entity-shaped {@link CollectionAccessor} into a flat
2856
+ * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
2857
+ * so the backend SDK is byte-for-byte the same shape as the frontend client.
2858
+ */
2859
+ function toSdkCollectionClient(snap) {
2860
+ const client = {
2861
+ async find(params) {
2862
+ const res = await snap.find(params);
2863
+ return {
2864
+ data: res.data.map(entityToRow),
2865
+ meta: res.meta
2866
+ };
2867
+ },
2868
+ async findById(id) {
2869
+ const s = await snap.findById(id);
2870
+ return s ? entityToRow(s) : void 0;
2871
+ },
2872
+ async create(data, id) {
2873
+ return entityToRow(await snap.create(data, id));
2874
+ },
2875
+ async update(id, data) {
2876
+ return entityToRow(await snap.update(id, data));
2877
+ },
2878
+ delete(id) {
2879
+ return snap.delete(id);
2880
+ },
2881
+ count: snap.count ? (params) => snap.count(params) : void 0,
2882
+ listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
2883
+ data: res.data.map(entityToRow),
2884
+ meta: res.meta
2885
+ }), onError) : void 0,
2886
+ listenById: snap.listenById ? (id, onUpdate, onError) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError) : void 0,
2887
+ where(columnOrCondition, operator, value) {
2888
+ const builder = new SdkQueryBuilder(client);
2889
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
2890
+ return builder.where(columnOrCondition, operator, value);
2891
+ },
2892
+ orderBy: (column, direction) => new SdkQueryBuilder(client).orderBy(column, direction),
2893
+ limit: (count) => new SdkQueryBuilder(client).limit(count),
2894
+ offset: (count) => new SdkQueryBuilder(client).offset(count),
2895
+ search: (searchString) => new SdkQueryBuilder(client).search(searchString),
2896
+ include: (...relations) => new SdkQueryBuilder(client).include(...relations)
2897
+ };
2898
+ return client;
2899
+ }
2900
+ /**
2901
+ * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
2902
+ * {@link CollectionAccessor}. Every returned row is re-wrapped into the
2903
+ * `{ id, path, values }` view-model the admin CMS renders.
2904
+ */
2905
+ function toEntityAccessor(sdk, slug) {
2906
+ const accessor = {
2907
+ async find(params) {
2908
+ const res = await sdk.find(params);
2909
+ return {
2910
+ data: res.data.map((row) => rowToEntity(row, slug)),
2911
+ meta: res.meta
2912
+ };
2913
+ },
2914
+ async findById(id) {
2915
+ const row = await sdk.findById(id);
2916
+ return row ? rowToEntity(row, slug) : void 0;
2917
+ },
2918
+ async create(data, id) {
2919
+ return rowToEntity(await sdk.create(data, id), slug);
2920
+ },
2921
+ async update(id, data) {
2922
+ const row = await sdk.update(id, data);
2923
+ if (!row) throw new Error(`Update returned no data for id ${id}`);
2924
+ return rowToEntity(row, slug);
2925
+ },
2926
+ delete(id) {
2927
+ return sdk.delete(id);
2928
+ },
2929
+ count: sdk.count ? (params) => sdk.count(params) : void 0,
2930
+ listen: sdk.listen ? (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
2931
+ data: res.data.map((row) => rowToEntity(row, slug)),
2932
+ meta: res.meta
2933
+ }), onError) : void 0,
2934
+ listenById: sdk.listenById ? (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug) : void 0), onError) : void 0,
2935
+ where(columnOrCondition, operator, value) {
2936
+ const builder = new QueryBuilder(accessor);
2937
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
2938
+ return builder.where(columnOrCondition, operator, value);
2939
+ },
2940
+ orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
2941
+ limit: (count) => new QueryBuilder(accessor).limit(count),
2942
+ offset: (count) => new QueryBuilder(accessor).offset(count),
2943
+ search: (searchString) => new QueryBuilder(accessor).search(searchString),
2944
+ include: (...relations) => new QueryBuilder(accessor).include(...relations)
2945
+ };
2946
+ return accessor;
2947
+ }
2948
+ /**
2949
+ * Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
2950
+ *
2951
+ * This is the **CMS boundary**: the SDK client (`client.data`) returns flat
2952
+ * rows, but the admin renders the `Entity` view-model (`entity.values.*`).
2953
+ * `core/Rebase.tsx` wraps `client.data` through this before handing it to the
2954
+ * CMS `RebaseDataContext` — without it the admin renders rows with only their
2955
+ * `id`.
2956
+ */
2957
+ function wrapAsEntityData(sdkData) {
2958
+ const cache = /* @__PURE__ */ new Map();
2959
+ function getAccessor(slug) {
2960
+ let accessor = cache.get(slug);
2961
+ if (!accessor) {
2962
+ accessor = toEntityAccessor(sdkData.collection(slug), slug);
2963
+ cache.set(slug, accessor);
2964
+ }
2965
+ return accessor;
2966
+ }
2967
+ return new Proxy({ collection: getAccessor }, { get(_target, prop) {
2968
+ if (prop === "collection") return getAccessor;
2969
+ if (typeof prop === "symbol") return void 0;
2970
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
2971
+ return getAccessor(toSnakeCase(prop));
2972
+ } });
2973
+ }
2974
+ /**
2975
+ * Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
2976
+ *
2977
+ * Every collection accessor is adapted to return flat rows. Use this to derive
2978
+ * the flat SDK data layer (`context.data`) from an existing Entity data layer
2979
+ * — e.g. the admin routes its Entity data via `useData()` and exposes the
2980
+ * same routing as flat `context.data` for callbacks by wrapping it here.
2981
+ */
2982
+ function wrapAsSdkData(entityData) {
2983
+ const cache = /* @__PURE__ */ new Map();
2984
+ function getAccessor(slug) {
2985
+ let accessor = cache.get(slug);
2986
+ if (!accessor) {
2987
+ accessor = toSdkCollectionClient(entityData.collection(slug));
2988
+ cache.set(slug, accessor);
2989
+ }
2990
+ return accessor;
2991
+ }
2992
+ return new Proxy({ collection: getAccessor }, { get(_target, prop) {
2993
+ if (prop === "collection") return getAccessor;
2994
+ if (typeof prop === "symbol") return void 0;
2995
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
2996
+ return getAccessor(toSnakeCase(prop));
2997
+ } });
2998
+ }
2999
+ /**
3000
+ * Build a flat {@link RebaseSdkData} from a `DataDriver`.
3001
+ *
3002
+ * This is the developer-facing SDK data layer used by backend framework
3003
+ * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
3004
+ * identical in shape to the frontend SDK client — so the API is symmetric
3005
+ * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
3006
+ */
3007
+ function buildSdkData(driver) {
3008
+ return wrapAsSdkData(buildRebaseData(driver));
3009
+ }
2651
3010
  //#endregion
2652
3011
  //#region src/data/buildRoutedRebaseData.ts
2653
3012
  /**
@@ -2692,6 +3051,57 @@ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
2692
3051
  } });
2693
3052
  }
2694
3053
  //#endregion
3054
+ //#region src/data/sort-dialect.ts
3055
+ /**
3056
+ * Sort-order wire codec.
3057
+ *
3058
+ * This is the ONLY module that knows about the colon-delimited wire format
3059
+ * (`"field:direction"`) used in HTTP query parameters.
3060
+ * Everything else speaks {@link OrderByTuple} exclusively.
3061
+ *
3062
+ * Mirrors the filter architecture in `filter-dialect.ts`.
3063
+ *
3064
+ * @module
3065
+ */
3066
+ /**
3067
+ * Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
3068
+ *
3069
+ * **Runtime tolerance:** if the input is already a well-formed wire string
3070
+ * (from an untyped JS caller), it is returned unchanged.
3071
+ * This is undocumented tolerance, not public API — don't rely on it.
3072
+ *
3073
+ * @param orderBy - A canonical `[field, direction]` tuple, or at runtime
3074
+ * possibly a pre-serialized string (undocumented tolerance).
3075
+ * @returns The wire-format string, or `undefined` if the input is falsy.
3076
+ *
3077
+ * @remarks
3078
+ * Field names containing `:` are representable in the tuple form but
3079
+ * **not** on the wire — this is an inherent limitation of the colon-delimited
3080
+ * encoding and is not resolved here.
3081
+ */
3082
+ function serializeOrderBy(orderBy) {
3083
+ if (!orderBy) return void 0;
3084
+ if (typeof orderBy === "string") return orderBy;
3085
+ return `${orderBy[0]}:${orderBy[1]}`;
3086
+ }
3087
+ /**
3088
+ * Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
3089
+ *
3090
+ * Lenient parsing (matches existing server behaviour):
3091
+ * - Bare field name (no colon): `"name"` → `["name", "asc"]`
3092
+ * - Unknown direction: `"name:foo"` → `["name", "asc"]`
3093
+ * - Empty / falsy input: → `undefined`
3094
+ *
3095
+ * @param raw - The wire-format string from an HTTP query parameter.
3096
+ * @returns The canonical tuple, or `undefined` if the input is empty/falsy.
3097
+ */
3098
+ function deserializeOrderBy(raw) {
3099
+ if (!raw) return void 0;
3100
+ const idx = raw.indexOf(":");
3101
+ if (idx === -1) return [raw, "asc"];
3102
+ return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
3103
+ }
3104
+ //#endregion
2695
3105
  //#region src/table-classification.ts
2696
3106
  /** Schemas that are always considered Rebase-internal. */
2697
3107
  var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
@@ -2768,6 +3178,6 @@ async function detectJunctionTables(executeSql) {
2768
3178
  return junctionTables;
2769
3179
  }
2770
3180
  //#endregion
2771
- export { COLLECTION_PATH_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildAdditionalFieldDelegate, buildCollection, buildConditionContext, buildEntityCallbacks, buildEnum, buildEnumValueConfig, buildProperties, buildPropertiesOrBuilder, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, detectJunctionTables, enumToObjectEntries, evaluateCondition, evaluatePolicy, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getEnumVarName, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, policyToPostgres, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues };
3181
+ export { COLLECTION_PATH_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildCollection, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, enumToObjectEntries, evaluateCondition, evaluatePolicy, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getEnumVarName, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, policyToPostgres, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveFilterOperators, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
2772
3182
 
2773
3183
  //# sourceMappingURL=index.es.js.map