@rebasepro/common 0.7.0 → 0.8.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 (42) hide show
  1. package/dist/collections/CollectionRegistry.d.ts +17 -2
  2. package/dist/collections/default-collections.d.ts +255 -2
  3. package/dist/data/filter-dialect.d.ts +61 -0
  4. package/dist/data/query_builder.d.ts +4 -4
  5. package/dist/data/resolveDataSource.d.ts +7 -7
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +604 -188
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +611 -186
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/util/builders.d.ts +48 -1
  12. package/dist/util/callbacks.d.ts +6 -1
  13. package/dist/util/index.d.ts +1 -0
  14. package/dist/util/permissions.d.ts +26 -2
  15. package/dist/util/policy/evaluatePolicy.d.ts +31 -0
  16. package/dist/util/policy/index.d.ts +3 -0
  17. package/dist/util/policy/policyToPostgres.d.ts +10 -0
  18. package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
  19. package/dist/util/policy/sqlToPolicy.d.ts +20 -0
  20. package/dist/util/storage.d.ts +26 -1
  21. package/package.json +13 -13
  22. package/src/collections/CollectionRegistry.ts +59 -28
  23. package/src/collections/default-collections.ts +4 -4
  24. package/src/data/buildRebaseData.ts +9 -120
  25. package/src/data/filter-dialect.ts +318 -0
  26. package/src/data/query_builder.ts +10 -10
  27. package/src/data/resolveDataSource.ts +9 -9
  28. package/src/index.ts +1 -0
  29. package/src/util/builders.ts +78 -1
  30. package/src/util/callbacks.ts +8 -1
  31. package/src/util/index.ts +1 -0
  32. package/src/util/permissions.test.ts +5 -3
  33. package/src/util/permissions.ts +85 -158
  34. package/src/util/policy/evaluatePolicy.ts +146 -0
  35. package/src/util/policy/index.ts +3 -0
  36. package/src/util/policy/policyToPostgres.ts +85 -0
  37. package/src/util/policy/securityRuleToConditions.ts +67 -0
  38. package/src/util/policy/sqlToPolicy.ts +88 -0
  39. package/src/util/references.ts +1 -1
  40. package/src/util/relations.ts +8 -9
  41. package/src/util/resolutions.ts +6 -6
  42. package/src/util/storage.ts +34 -1
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, getDataSourceCapabilities } from "@rebasepro/types";
1
+ import { CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, 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";
@@ -336,7 +336,7 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
336
336
  if (!newRelation.foreignKeyOnTarget) {
337
337
  let foundForeignKey = false;
338
338
  try {
339
- const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
339
+ const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
340
340
  for (const targetRel of targetRelations) if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) try {
341
341
  if (targetRel.target().slug === sourceCollection.slug) {
342
342
  newRelation.foreignKeyOnTarget = targetRel.localKey;
@@ -352,7 +352,7 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
352
352
  } else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
353
353
  let isManyToManyInverse = false;
354
354
  if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) try {
355
- const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
355
+ const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
356
356
  for (const targetRel of targetRelations) if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
357
357
  isManyToManyInverse = true;
358
358
  break;
@@ -387,11 +387,10 @@ var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
387
387
  function resolveCollectionRelations(collection) {
388
388
  const cached = _resolvedRelationsCache.get(collection);
389
389
  if (cached) return cached;
390
- if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
391
- const relCollection = collection;
390
+ if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
392
391
  const relations = {};
393
392
  const registeredRelationNames = /* @__PURE__ */ new Set();
394
- if (relCollection.relations) relCollection.relations.forEach((relation) => {
393
+ if (collection.relations) collection.relations.forEach((relation) => {
395
394
  try {
396
395
  const normalizedRelation = sanitizeRelation(relation, collection);
397
396
  const relationKey = normalizedRelation.relationName;
@@ -438,7 +437,7 @@ function resolvePropertyRelation({ propertyKey, property, sourceCollection }) {
438
437
  console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
439
438
  }
440
439
  function getTableName(collection) {
441
- if (getDataSourceCapabilities(collection.driver).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
440
+ if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
442
441
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
443
442
  }
444
443
  function getTableVarName(tableName) {
@@ -643,8 +642,9 @@ function resolveEnumValues(input) {
643
642
  }
644
643
  function getSubcollections(collection) {
645
644
  if (collection.childCollections) return collection.childCollections() ?? [];
646
- if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) return collection.subcollections() ?? [];
647
- if (getDataSourceCapabilities(collection.driver).supportsRelations) {
645
+ const declaredSubcollections = getDeclaredSubcollections(collection);
646
+ if (getDataSourceCapabilities(collection.engine).supportsSubcollections && declaredSubcollections) return declaredSubcollections() ?? [];
647
+ if (getDataSourceCapabilities(collection.engine).supportsRelations) {
648
648
  const resolvedRelations = resolveCollectionRelations(collection);
649
649
  return Object.values(resolvedRelations).filter((r) => r.cardinality === "many").map((r) => {
650
650
  const target = r.target();
@@ -670,113 +670,339 @@ function getSubcollections(collection) {
670
670
  return [];
671
671
  }
672
672
  //#endregion
673
- //#region src/util/permissions.ts
674
- function evaluateAST(sqlString, auth, entity) {
675
- if (!entity) return true;
676
- let cleanedSQL = sqlString.trim();
677
- while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
678
- let openCount = 0;
679
- let isEnclosing = true;
680
- for (let i = 0; i < cleanedSQL.length - 1; i++) {
681
- if (cleanedSQL[i] === "(") openCount++;
682
- else if (cleanedSQL[i] === ")") openCount--;
683
- if (openCount === 0) {
684
- isEnclosing = false;
685
- break;
686
- }
673
+ //#region src/util/policy/sqlToPolicy.ts
674
+ /**
675
+ * A tiny, regex-based SQL "parser" for security rules.
676
+ *
677
+ * This is NOT a full SQL parser. It is designed to handle the subset of SQL
678
+ * commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
679
+ * optimistic client-side UI decision.
680
+ *
681
+ * It handles:
682
+ * - `field = 'literal'`
683
+ * - `field != 'literal'`
684
+ * - `field = current_setting('app.user_id')`
685
+ * - `A AND B`
686
+ * - `true`
687
+ * - `IN (...)` (as optimistic true)
688
+ *
689
+ * For anything it doesn't understand, it returns a `raw` expression, which
690
+ * the evaluator treats as "unknown" (and usually optimistic true).
691
+ */
692
+ function sqlToPolicy(sql) {
693
+ const trimmed = sql.trim();
694
+ if (trimmed.toLowerCase() === "true") return policy.true();
695
+ if (trimmed.toLowerCase() === "false") return policy.false();
696
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
697
+ if (overlapMatch) {
698
+ const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
699
+ return policy.rolesOverlap(roles);
700
+ }
701
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
702
+ if (containMatch) {
703
+ const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
704
+ return policy.rolesContain(roles);
705
+ }
706
+ if (trimmed.toUpperCase().includes(" OR ")) {
707
+ const parts = trimmed.split(/ OR /i);
708
+ return policy.or(...parts.map(sqlToPolicy));
709
+ }
710
+ if (trimmed.toUpperCase().includes(" AND ")) {
711
+ const parts = trimmed.split(/ AND /i);
712
+ return policy.and(...parts.map(sqlToPolicy));
713
+ }
714
+ const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
715
+ if (match) {
716
+ const [, leftStr, op, rightStr] = match;
717
+ const left = parseOperand(leftStr.trim());
718
+ const right = parseOperand(rightStr.trim());
719
+ if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
720
+ }
721
+ return policy.raw(sql);
722
+ }
723
+ function parseOperand(str) {
724
+ if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
725
+ const stringMatch = str.match(/^'(.+)'$/);
726
+ if (stringMatch) return policy.literal(stringMatch[1]);
727
+ if (/^\w+$/.test(str)) return policy.field(str);
728
+ return null;
729
+ }
730
+ //#endregion
731
+ //#region src/util/policy/securityRuleToConditions.ts
732
+ /**
733
+ * Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
734
+ * structured `condition`/`check`, and raw `using`/`withCheck` — into a single
735
+ * normalized {@link PolicyExpression} pair.
736
+ *
737
+ * **This is the linchpin against drift:** both the Postgres DDL generators and
738
+ * the client-side evaluator consume this one function, so there is exactly one
739
+ * definition of what a rule means. In particular, application `roles` are folded
740
+ * into the expression here (AND'd with the base condition, matching how Postgres
741
+ * generates the clause) rather than being handled separately by each consumer.
742
+ */
743
+ function securityRuleToConditions(rule) {
744
+ return {
745
+ usingExpr: withRoles(baseUsing(rule), rule),
746
+ withCheckExpr: withRoles(baseWithCheck(rule), rule)
747
+ };
748
+ }
749
+ function baseUsing(rule) {
750
+ if (rule.condition) return rule.condition;
751
+ if (rule.using != null) return sqlToPolicy(rule.using);
752
+ if (rule.access === "public") return policy.true();
753
+ if (rule.ownerField) return policy.compare(policy.field(rule.ownerField), "eq", policy.authUid());
754
+ return null;
755
+ }
756
+ function baseWithCheck(rule) {
757
+ if (rule.check) return rule.check;
758
+ if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
759
+ return baseUsing(rule);
760
+ }
761
+ /**
762
+ * AND the base condition with an application-role check, or produce a roles-only
763
+ * condition when there is no base. Mirrors the Postgres generator so that a
764
+ * role-scoped restrictive rule denies exactly the same set of users on both
765
+ * sides.
766
+ */
767
+ function withRoles(base, rule) {
768
+ if (!rule.roles || rule.roles.length === 0) return base;
769
+ const rolesExpr = policy.rolesOverlap(rule.roles);
770
+ if (rule.mode === "restrictive") return base ? policy.or(policy.not(rolesExpr), base) : policy.not(rolesExpr);
771
+ return base ? policy.and(base, rolesExpr) : rolesExpr;
772
+ }
773
+ //#endregion
774
+ //#region src/util/policy/policyToPostgres.ts
775
+ /**
776
+ * Compiles a {@link PolicyExpression} to a PostgreSQL boolean SQL string,
777
+ * suitable for a `USING (...)` / `WITH CHECK (...)` clause.
778
+ *
779
+ * This is one of the two consumers of the shared policy model (the other being
780
+ * {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
781
+ * and the admin UI derive from the exact same expression.
782
+ */
783
+ function policyToPostgres(expr, collection) {
784
+ switch (expr.kind) {
785
+ case "true": return "true";
786
+ 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 ");
789
+ case "not":
790
+ 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)}`;
793
+ case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
794
+ case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
795
+ case "authenticated": return "auth.uid() IS NOT NULL";
796
+ case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
797
+ }
798
+ }
799
+ var COMPARE_SQL = {
800
+ eq: "=",
801
+ neq: "!=",
802
+ lt: "<",
803
+ lte: "<=",
804
+ gt: ">",
805
+ gte: ">="
806
+ };
807
+ function operandToSql(operand, collection) {
808
+ switch (operand.kind) {
809
+ case "field": return resolveColumnName(operand.name, collection);
810
+ case "literal": return quoteLiteral(operand.value);
811
+ case "authUid": return "auth.uid()";
812
+ case "authRoles": return "string_to_array(auth.roles(), ',')";
813
+ }
814
+ }
815
+ function resolveColumnName(propName, collection) {
816
+ const prop = collection?.properties?.[propName];
817
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
818
+ return toSnakeCase(propName);
819
+ }
820
+ function quoteLiteral(value) {
821
+ if (value === null) return "NULL";
822
+ if (typeof value === "boolean") return value ? "true" : "false";
823
+ if (typeof value === "number") return String(value);
824
+ return `'${value.replace(/'/g, "''")}'`;
825
+ }
826
+ /** Sorted, single-quoted `ARRAY['a','b']` — matches the generators' output. */
827
+ function rolesArraySql(roles) {
828
+ return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
829
+ }
830
+ //#endregion
831
+ //#region src/util/policy/evaluatePolicy.ts
832
+ /**
833
+ * Evaluates a {@link PolicyExpression} against a user + row, using three-valued
834
+ * (Kleene) logic so that `"unknown"` sub-results propagate soundly.
835
+ *
836
+ * This is the JavaScript twin of {@link policyToPostgres}: both derive from the
837
+ * same expression, so the admin UI matches database enforcement by construction
838
+ * for every non-raw rule.
839
+ */
840
+ function evaluatePolicy(expr, ctx) {
841
+ switch (expr.kind) {
842
+ case "true": return true;
843
+ case "false": return false;
844
+ case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
845
+ case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
846
+ case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
847
+ case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
848
+ case "rolesOverlap": {
849
+ const userRoles = ctx.roles ?? [];
850
+ return expr.roles.some((r) => r === "public" || userRoles.includes(r));
687
851
  }
688
- if (isEnclosing) cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
689
- else break;
690
- }
691
- const splitByTopLevel = (str, delimiter) => {
692
- const parts = [];
693
- let current = "";
694
- let openCount = 0;
695
- let i = 0;
696
- while (i < str.length) {
697
- if (str[i] === "(") openCount++;
698
- else if (str[i] === ")") openCount--;
699
- if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
700
- parts.push(current);
701
- current = "";
702
- i += delimiter.length;
703
- } else {
704
- current += str[i];
705
- i++;
706
- }
852
+ case "rolesContain": {
853
+ const userRoles = ctx.roles ?? [];
854
+ return expr.roles.every((r) => r === "public" || userRoles.includes(r));
707
855
  }
708
- parts.push(current);
709
- return parts;
710
- };
711
- const orParts = splitByTopLevel(cleanedSQL, " OR ");
712
- if (orParts.length > 1) return orParts.some((part) => evaluateAST(part, auth, entity));
713
- const andParts = splitByTopLevel(cleanedSQL, " AND ");
714
- if (andParts.length > 1) return andParts.every((part) => evaluateAST(part, auth, entity));
715
- const upperSQL = cleanedSQL.toUpperCase();
716
- if (upperSQL.includes(" IN ") || upperSQL.includes(" EXISTS ")) return true;
717
- const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
718
- if (roleIntersectMatch && roleIntersectMatch[1]) {
719
- const requiredRoles = roleIntersectMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
720
- const userRoles = auth.user?.roles || [];
721
- return requiredRoles.some((r) => userRoles.includes(r));
722
- }
723
- const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
724
- if (roleContainMatch && roleContainMatch[1]) {
725
- const requiredRoles = roleContainMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
726
- const userRoles = auth.user?.roles || [];
727
- return requiredRoles.every((r) => userRoles.includes(r));
728
- }
729
- const pattern1 = /* @__PURE__ */ new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
730
- const pattern2 = /* @__PURE__ */ new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
731
- const match1 = cleanedSQL.match(pattern1);
732
- if (match1 && match1[1]) return entity.values[match1[1]] === auth.user?.uid;
733
- const match2 = cleanedSQL.match(pattern2);
734
- if (match2 && match2[1]) return entity.values[match2[1]] === auth.user?.uid;
735
- const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
736
- if (simpleEqualityMatch) {
737
- const field = simpleEqualityMatch[1];
738
- const operator = simpleEqualityMatch[2];
739
- const value = simpleEqualityMatch[3];
740
- const entityValue = entity.values[field];
741
- if (operator === "=") return entityValue === value;
742
- if (operator === "!=") return entityValue !== value;
856
+ case "authenticated": return ctx.uid != null;
857
+ case "raw": return "unknown";
743
858
  }
859
+ }
860
+ function kleeneAnd$1(values) {
861
+ if (values.some((v) => v === false)) return false;
862
+ if (values.some((v) => v === "unknown")) return "unknown";
744
863
  return true;
745
864
  }
746
- function evaluateRule(rule, auth, entity) {
747
- if (rule.access === "public") return true;
748
- if (rule.ownerField) {
749
- if (!entity) {} else if (entity.values[rule.ownerField] !== auth.user?.uid) return false;
865
+ function kleeneOr(values) {
866
+ if (values.some((v) => v === true)) return true;
867
+ if (values.some((v) => v === "unknown")) return "unknown";
868
+ return false;
869
+ }
870
+ function kleeneNot(value) {
871
+ if (value === "unknown") return "unknown";
872
+ return !value;
873
+ }
874
+ function resolveOperand(operand, ctx) {
875
+ switch (operand.kind) {
876
+ case "literal": return {
877
+ known: true,
878
+ value: operand.value
879
+ };
880
+ case "authUid": return {
881
+ known: true,
882
+ value: ctx.uid ?? null
883
+ };
884
+ case "authRoles": return {
885
+ known: true,
886
+ value: ctx.roles ?? []
887
+ };
888
+ case "field":
889
+ if (!ctx.entity) return { known: false };
890
+ return {
891
+ known: true,
892
+ value: ctx.entity.values[operand.name]
893
+ };
750
894
  }
751
- if (rule.using && !evaluateAST(rule.using, auth, entity)) return false;
752
- if (rule.withCheck && !evaluateAST(rule.withCheck, auth, entity)) return false;
895
+ }
896
+ function evaluateCompare(op, left, right, ctx) {
897
+ const l = resolveOperand(left, ctx);
898
+ const r = resolveOperand(right, ctx);
899
+ if (!l.known || !r.known) return "unknown";
900
+ const a = l.value;
901
+ const b = r.value;
902
+ if (a === null || b === null) {
903
+ if (op === "eq") return false;
904
+ if (op === "neq") return true;
905
+ return "unknown";
906
+ }
907
+ if (op === "eq") return a === b;
908
+ if (op === "neq") return a !== b;
909
+ if (typeof a === "string" && typeof b === "string") {
910
+ if (op === "lt") return a < b;
911
+ if (op === "lte") return a <= b;
912
+ if (op === "gt") return a > b;
913
+ if (op === "gte") return a >= b;
914
+ }
915
+ if (typeof a === "number" && typeof b === "number") {
916
+ if (op === "lt") return a < b;
917
+ if (op === "lte") return a <= b;
918
+ if (op === "gt") return a > b;
919
+ if (op === "gte") return a >= b;
920
+ }
921
+ if (typeof a === "bigint" && typeof b === "bigint") {
922
+ if (op === "lt") return a < b;
923
+ if (op === "lte") return a <= b;
924
+ if (op === "gt") return a > b;
925
+ if (op === "gte") return a >= b;
926
+ }
927
+ return "unknown";
928
+ }
929
+ //#endregion
930
+ //#region src/util/permissions.ts
931
+ /** Combine clause results with AND under three-valued (Kleene) logic. */
932
+ function kleeneAnd(values) {
933
+ if (values.some((v) => v === false)) return false;
934
+ if (values.some((v) => v === "unknown")) return "unknown";
753
935
  return true;
754
936
  }
755
- function checkOperation(collection, authContext, entity, targetOperation) {
756
- const securityRules = getDataSourceCapabilities(collection.driver).supportsRLS ? collection.securityRules : void 0;
937
+ /** The operations a rule covers, mirroring the Postgres generator's resolution. */
938
+ function ruleOperations(rule) {
939
+ return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
940
+ }
941
+ function ruleApplies(rule, targetOperation) {
942
+ const ops = ruleOperations(rule);
943
+ return ops.includes(targetOperation) || ops.includes("all");
944
+ }
945
+ /**
946
+ * Evaluate a single rule for one operation, returning a tri-state.
947
+ *
948
+ * A `null` clause (the rule contributes no condition for a required clause)
949
+ * denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
950
+ * in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
951
+ * INSERT/UPDATE; both must pass for UPDATE.
952
+ */
953
+ function evaluateRuleForOperation(rule, ctx, targetOperation) {
954
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
955
+ const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
956
+ const needsUsing = targetOperation !== "insert";
957
+ const needsWithCheck = targetOperation === "insert" || targetOperation === "update";
958
+ const results = [];
959
+ if (needsUsing) results.push(clause(usingExpr));
960
+ if (needsWithCheck) results.push(clause(withCheckExpr));
961
+ return kleeneAnd(results);
962
+ }
963
+ function resolveTriState(value, onUnknown) {
964
+ if (value === "unknown") return onUnknown === "allow";
965
+ return value;
966
+ }
967
+ /**
968
+ * Decide whether an operation is permitted for a user on a (possibly null) row,
969
+ * by evaluating the collection's security rules with the shared policy model —
970
+ * the same model compiled to Postgres RLS DDL, so the decision matches database
971
+ * enforcement for every non-raw rule.
972
+ *
973
+ * @param options.onUnknown how to treat rules that cannot be decided
974
+ * client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
975
+ * for optimistic UI gating; enforcement callers should pass `"deny"`.
976
+ */
977
+ function checkOperation(collection, authContext, entity, targetOperation, options) {
978
+ const onUnknown = options?.onUnknown ?? "allow";
979
+ const securityRules = getDataSourceCapabilities(collection.engine).supportsRLS ? collection.securityRules : void 0;
757
980
  if (!securityRules || securityRules.length === 0) return true;
758
- const applicableRules = securityRules.filter((r) => r.operation === targetOperation || r.operation === "all" || r.operations?.includes(targetOperation) || r.operations?.includes("all"));
981
+ const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
759
982
  if (applicableRules.length === 0) return false;
760
- const userRoles = [...authContext.user?.roles ?? [], "public"];
761
- const roleApplicableRules = applicableRules.filter((rule) => {
762
- if (!rule.roles || rule.roles.length === 0) return true;
763
- return rule.roles.some((r) => userRoles.includes(r));
764
- });
765
- if (roleApplicableRules.length === 0) return false;
983
+ const ctx = {
984
+ uid: authContext.user?.uid,
985
+ roles: authContext.user?.roles ?? [],
986
+ entity
987
+ };
766
988
  let grantedByPermissive = false;
767
989
  let deniedByRestrictive = false;
768
- for (const rule of roleApplicableRules) {
990
+ let hasPermissive = false;
991
+ for (const rule of applicableRules) {
769
992
  const mode = rule.mode || "permissive";
770
- const passed = evaluateRule(rule, authContext, entity);
771
- if (mode === "restrictive" && !passed) {
772
- deniedByRestrictive = true;
773
- break;
993
+ const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation), onUnknown);
994
+ if (mode === "restrictive") {
995
+ if (!passed) {
996
+ deniedByRestrictive = true;
997
+ break;
998
+ }
999
+ } else {
1000
+ hasPermissive = true;
1001
+ if (passed) grantedByPermissive = true;
774
1002
  }
775
- if (mode === "permissive" && passed) grantedByPermissive = true;
776
1003
  }
777
1004
  if (deniedByRestrictive) return false;
778
- if (roleApplicableRules.some((r) => (r.mode || "permissive") === "permissive")) return grantedByPermissive;
779
- else return false;
1005
+ return hasPermissive ? grantedByPermissive : false;
780
1006
  }
781
1007
  function canReadCollection(collection, authContext) {
782
1008
  return checkOperation(collection, authContext, null, "select");
@@ -807,7 +1033,7 @@ function getEntityImagePreviewPropertyKey(collection) {
807
1033
  }
808
1034
  for (const key in collection.properties) {
809
1035
  const property = collection.properties[key];
810
- if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.url === "image") return key;
1036
+ if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") return key;
811
1037
  }
812
1038
  for (const key in collection.properties) {
813
1039
  const property = collection.properties[key];
@@ -1041,6 +1267,13 @@ function buildCollection(collection) {
1041
1267
  return collection;
1042
1268
  }
1043
1269
  /**
1270
+ * Implementation — delegates to the correct overload at the type level.
1271
+ * At runtime this is a plain identity function.
1272
+ */
1273
+ function defineCollection(collection) {
1274
+ return collection;
1275
+ }
1276
+ /**
1044
1277
  * Identity function we use to defeat the type system of Typescript and preserve
1045
1278
  * the property keys.
1046
1279
  * @param property
@@ -1105,6 +1338,29 @@ function buildAdditionalFieldDelegate(additionalFieldDelegate) {
1105
1338
  }
1106
1339
  //#endregion
1107
1340
  //#region src/util/storage.ts
1341
+ /**
1342
+ * Resolve the {@link StorageSource} to use for a property, given the key
1343
+ * referenced by `StorageConfig.storageSource`.
1344
+ *
1345
+ * Resolution priority:
1346
+ * 1. No `sourceKey` → the default source (backward compatible).
1347
+ * 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
1348
+ * 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
1349
+ * 4. Fall back to the default source.
1350
+ *
1351
+ * Shared by the upload hook, the markdown editor, and the read-only previews
1352
+ * so the resolution logic lives in one place.
1353
+ *
1354
+ * @group Storage
1355
+ */
1356
+ function resolveStorageSource(params) {
1357
+ const { sourceKey, sources, registry, defaultSource } = params;
1358
+ if (!sourceKey) return defaultSource;
1359
+ if (registry) return registry.getOrDefault(sourceKey);
1360
+ const fromSources = sources?.[sourceKey];
1361
+ if (fromSources) return fromSources;
1362
+ return defaultSource;
1363
+ }
1108
1364
  async function resolveStorageFilenameString({ input, storage, values, entityId, path, property, file, propertyKey }) {
1109
1365
  let result;
1110
1366
  if (typeof input === "function") {
@@ -1427,13 +1683,13 @@ function createDataSourceRegistry(definitions) {
1427
1683
  * editor's capability lookups.
1428
1684
  *
1429
1685
  * Resolution order:
1430
- * 1. The routing **key** is `collection.dataSource`, else the legacy
1431
- * `collection.driver`, else {@link DEFAULT_DATA_SOURCE_KEY}.
1686
+ * 1. The routing **key** is `collection.dataSource`, else
1687
+ * {@link DEFAULT_DATA_SOURCE_KEY}.
1432
1688
  * 2. If a definition is registered for that key, it provides `engine`,
1433
1689
  * `transport`, and `databaseId`.
1434
- * 3. Otherwise values are synthesized for backward compatibility: `engine`
1435
- * from the legacy `driver` (or the key, or `"postgres"`), `transport`
1436
- * defaults to `"server"`, and `databaseId` from the collection.
1690
+ * 3. Otherwise values are synthesized: `engine` from `collection.engine`
1691
+ * (or the key, or `"postgres"`), `transport` defaults to `"server"`,
1692
+ * and `databaseId` from the collection.
1437
1693
  *
1438
1694
  * `capabilities` are always derived from the resolved `engine`, so two
1439
1695
  * data sources sharing an engine share capabilities.
@@ -1442,9 +1698,9 @@ function createDataSourceRegistry(definitions) {
1442
1698
  * @param registry optional registry of declared data sources
1443
1699
  */
1444
1700
  function resolveDataSource(collection, registry) {
1445
- const key = collection?.dataSource ?? collection?.driver ?? DEFAULT_DATA_SOURCE_KEY;
1701
+ const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;
1446
1702
  const def = registry?.[key];
1447
- const engine = def?.engine ?? collection?.driver ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
1703
+ const engine = def?.engine ?? collection?.engine ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
1448
1704
  return {
1449
1705
  key,
1450
1706
  engine,
@@ -1459,9 +1715,28 @@ var CollectionRegistry = class {
1459
1715
  /**
1460
1716
  * Declared data sources, used during normalization to resolve each
1461
1717
  * collection's engine (so `dataSource`-only collections get the right
1462
- * capabilities). Empty by default → behaviour keys off `driver` as before.
1718
+ * capabilities). Empty by default.
1463
1719
  */
1464
1720
  dataSources = {};
1721
+ /**
1722
+ * Global lifecycle callbacks applied to every collection.
1723
+ * Runs on all data paths (REST, WebSocket, `rebase.data`).
1724
+ * Execution order: global → collection → property callbacks.
1725
+ */
1726
+ _globalCallbacks;
1727
+ /**
1728
+ * Set global lifecycle callbacks that apply to every collection.
1729
+ * Typically called once during backend initialization.
1730
+ */
1731
+ setGlobalCallbacks(callbacks) {
1732
+ this._globalCallbacks = callbacks;
1733
+ }
1734
+ /**
1735
+ * Get the currently registered global callbacks, if any.
1736
+ */
1737
+ getGlobalCallbacks() {
1738
+ return this._globalCallbacks;
1739
+ }
1465
1740
  collectionsByTableName = /* @__PURE__ */ new Map();
1466
1741
  collectionsBySlug = /* @__PURE__ */ new Map();
1467
1742
  rootCollections = [];
@@ -1553,13 +1828,14 @@ var CollectionRegistry = class {
1553
1828
  }
1554
1829
  normalizeCollection(collection) {
1555
1830
  const result = { ...collection };
1556
- if (result.dataSource && !result.driver) {
1557
- const engine = resolveDataSource(result, this.dataSources).engine;
1558
- if (engine) result.driver = engine;
1831
+ {
1832
+ const resolved = resolveDataSource(result, this.dataSources);
1833
+ if (!result.dataSource) result.dataSource = resolved.key;
1834
+ if (!result.engine) result.engine = resolved.engine;
1559
1835
  }
1560
1836
  const extractedRelations = this.extractRelationsFromProperties(result.properties);
1561
1837
  const relResult = result;
1562
- const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
1838
+ const manualRelations = getDataSourceCapabilities(result.engine).supportsRelations ? relResult.relations ?? [] : [];
1563
1839
  const mergedRelationsRaw = [...extractedRelations];
1564
1840
  for (const manual of manualRelations) {
1565
1841
  const name = manual.relationName;
@@ -1574,7 +1850,7 @@ var CollectionRegistry = class {
1574
1850
  }
1575
1851
  }
1576
1852
  let mergedRelations = mergedRelationsRaw;
1577
- if (getDataSourceCapabilities(result.driver).supportsRelations) {
1853
+ if (getDataSourceCapabilities(result.engine).supportsRelations) {
1578
1854
  mergedRelations = mergedRelationsRaw.map((r) => {
1579
1855
  try {
1580
1856
  return sanitizeRelation(r, result, (slug) => this.get(slug));
@@ -1586,8 +1862,10 @@ var CollectionRegistry = class {
1586
1862
  }
1587
1863
  result.properties = this.normalizeProperties(result.properties, mergedRelations);
1588
1864
  if (!result.childCollections) {
1589
- if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) result.childCollections = result.subcollections;
1590
- else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
1865
+ const capabilities = getDataSourceCapabilities(result.engine);
1866
+ const declaredSubcollections = getDeclaredSubcollections(result);
1867
+ if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
1868
+ else if (capabilities.supportsRelations && relResult.relations) {
1591
1869
  const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
1592
1870
  if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
1593
1871
  const target = r.target();
@@ -1689,7 +1967,7 @@ var CollectionRegistry = class {
1689
1967
  if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
1690
1968
  for (let i = 2; i < pathSegments.length; i += 2) {
1691
1969
  const relationKey = pathSegments[i];
1692
- if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
1970
+ if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
1693
1971
  const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
1694
1972
  if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
1695
1973
  const target = relation.target();
@@ -1750,7 +2028,7 @@ var CollectionRegistry = class {
1750
2028
  * Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
1751
2029
  * override by defining their own collection with `slug: "users"`.
1752
2030
  */
1753
- var defaultUsersCollection = {
2031
+ var defaultUsersCollection = defineCollection({
1754
2032
  name: "Users",
1755
2033
  singularName: "User",
1756
2034
  slug: "users",
@@ -1798,7 +2076,7 @@ var defaultUsersCollection = {
1798
2076
  name: "Photo URL",
1799
2077
  type: "string",
1800
2078
  columnName: "photo_url",
1801
- url: "image"
2079
+ ui: { url: "image" }
1802
2080
  },
1803
2081
  roles: {
1804
2082
  name: "Roles",
@@ -1893,7 +2171,7 @@ var defaultUsersCollection = {
1893
2171
  "roles",
1894
2172
  "createdAt"
1895
2173
  ]
1896
- };
2174
+ });
1897
2175
  //#endregion
1898
2176
  //#region src/data/query_builder.ts
1899
2177
  function or(...conditions) {
@@ -1945,8 +2223,8 @@ var QueryBuilder = class {
1945
2223
  * @example
1946
2224
  * client.collection('users').orderBy('createdAt', 'desc').find()
1947
2225
  */
1948
- orderBy(column, ascending = "asc") {
1949
- this.params.orderBy = `${column}:${ascending}`;
2226
+ orderBy(column, direction = "asc") {
2227
+ this.params.orderBy = `${column}:${direction}`;
1950
2228
  return this;
1951
2229
  }
1952
2230
  /**
@@ -2001,80 +2279,216 @@ var QueryBuilder = class {
2001
2279
  }
2002
2280
  };
2003
2281
  //#endregion
2004
- //#region src/data/buildRebaseData.ts
2282
+ //#region src/data/filter-dialect.ts
2005
2283
  /**
2006
- * Convert where-clause filter object to the internal DataDriver FilterValues format.
2284
+ * REST wire-format adapter for the unified filter system.
2007
2285
  *
2008
- * Supports multiple value formats:
2009
- * - PostgREST string: { status: "eq.published", age: "gte.18" }
2010
- * - Equality shorthand: { company_profile_id: null, status: "active", age: 18 }
2011
- * - Tuple syntax: { age: [">=", 18], role: ["in", ["admin", "editor"]] }
2286
+ * This module is the ONLY code in the entire codebase that knows about
2287
+ * PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
2288
+ * Everything else speaks `FilterValues` exclusively.
2012
2289
  *
2013
- * Internal: { status: ["==", "published"], age: [">=", 18] }
2290
+ * @module
2014
2291
  */
2015
- function convertWhereToFilter(where) {
2016
- if (!where) return void 0;
2017
- const operatorMap = {
2018
- "eq": "==",
2019
- "neq": "!=",
2020
- "gt": ">",
2021
- "gte": ">=",
2022
- "lt": "<",
2023
- "lte": "<=",
2024
- "in": "in",
2025
- "nin": "not-in",
2026
- "not-in": "not-in",
2027
- "cs": "array-contains",
2028
- "csa": "array-contains-any",
2029
- "==": "==",
2030
- "!=": "!=",
2031
- ">": ">",
2032
- ">=": ">=",
2033
- "<": "<",
2034
- "<=": "<=",
2035
- "array-contains": "array-contains",
2036
- "array-contains-any": "array-contains-any"
2037
- };
2038
- const filter = {};
2039
- for (const [field, rawValue] of Object.entries(where)) {
2040
- if (rawValue === null) {
2041
- filter[field] = ["==", null];
2042
- continue;
2043
- }
2044
- if (typeof rawValue === "boolean") {
2045
- filter[field] = ["==", rawValue];
2046
- continue;
2047
- }
2048
- if (typeof rawValue === "number") {
2049
- filter[field] = ["==", rawValue];
2050
- continue;
2292
+ /**
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
+ * Serialize a JS value to its querystring representation.
2308
+ */
2309
+ function stringifyValue(value) {
2310
+ if (value === null) return "null";
2311
+ if (typeof value === "boolean") return String(value);
2312
+ return String(value);
2313
+ }
2314
+ /**
2315
+ * Serialize a single condition tuple to a PostgREST dot-string.
2316
+ *
2317
+ * @example
2318
+ * serializeTuple(["==", "active"]) // "eq.active"
2319
+ * serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
2320
+ * serializeTuple([">=", 18]) // "gte.18"
2321
+ */
2322
+ 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;
2051
2327
  }
2052
- if (Array.isArray(rawValue)) {
2053
- const mappedConditions = (Array.isArray(rawValue[0]) ? rawValue : [rawValue]).map(([rawOp, val]) => {
2054
- return [operatorMap[rawOp] ?? "==", val];
2055
- });
2056
- filter[field] = Array.isArray(rawValue[0]) ? mappedConditions : mappedConditions[0];
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)}`;
2331
+ const [op, value] = tuple;
2332
+ const restOp = CANONICAL_TO_REST[op];
2333
+ if (Array.isArray(value)) return `${restOp}.(${value.map(stringifyValue).join(",")})`;
2334
+ return `${restOp}.${stringifyValue(value)}`;
2335
+ }
2336
+ /**
2337
+ * Convert `FilterValues` to a PostgREST-style querystring record.
2338
+ *
2339
+ * - Single conditions produce a string value.
2340
+ * - Multiple conditions on the same field produce a string array (repeated params).
2341
+ *
2342
+ * @example
2343
+ * serializeFilter({ status: ["==", "active"] })
2344
+ * // → { status: "eq.active" }
2345
+ *
2346
+ * serializeFilter({ age: [[">=", 18], ["<", 65]] })
2347
+ * // → { age: ["gte.18", "lt.65"] }
2348
+ */
2349
+ function serializeFilter(filter) {
2350
+ const result = {};
2351
+ for (const [field, condition] of Object.entries(filter)) {
2352
+ if (condition === void 0) continue;
2353
+ if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
2354
+ else result[field] = serializeTuple(condition);
2355
+ }
2356
+ return result;
2357
+ }
2358
+ /**
2359
+ * Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
2360
+ *
2361
+ * If the string doesn't match a known operator prefix, falls back to
2362
+ * `["==", originalString]` (treating the whole string as an equality value).
2363
+ */
2364
+ function deserializeSingle(raw) {
2365
+ const dotIndex = raw.indexOf(".");
2366
+ if (dotIndex === -1) return ["==", coerceValue(raw)];
2367
+ const prefix = raw.substring(0, dotIndex);
2368
+ const rest = raw.substring(dotIndex + 1);
2369
+ const canonicalOp = REST_TO_CANONICAL[prefix];
2370
+ 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)];
2373
+ }
2374
+ /**
2375
+ * Convert a PostgREST-style querystring record to `FilterValues`.
2376
+ *
2377
+ * - String values are parsed as single conditions.
2378
+ * - String arrays (repeated query params) become multiple conditions on the same field.
2379
+ *
2380
+ * @example
2381
+ * deserializeFilter({ status: "eq.active" })
2382
+ * // → { status: ["==", "active"] }
2383
+ *
2384
+ * deserializeFilter({ age: ["gte.18", "lt.65"] })
2385
+ * // → { age: [[">=", 18], ["<", 65]] }
2386
+ */
2387
+ function deserializeFilter(query) {
2388
+ const result = {};
2389
+ for (const [field, raw] of Object.entries(query)) {
2390
+ if (raw === void 0) continue;
2391
+ if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && toCanonicalOp(raw[0]) === raw[0]) {
2392
+ result[field] = raw;
2057
2393
  continue;
2058
2394
  }
2059
- if (typeof rawValue === "string") {
2060
- const dotIndex = rawValue.indexOf(".");
2061
- if (dotIndex === -1) {
2062
- filter[field] = ["==", rawValue];
2395
+ if (Array.isArray(raw)) {
2396
+ if (raw.length === 0) continue;
2397
+ if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && toCanonicalOp(raw[0][0]) === raw[0][0]) {
2398
+ result[field] = raw;
2063
2399
  continue;
2064
2400
  }
2065
- const op = rawValue.substring(0, dotIndex);
2066
- let value = rawValue.substring(dotIndex + 1);
2067
- if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) value = value.slice(1, -1).split(",").map((v) => v.trim());
2068
- if (value === "null") value = null;
2069
- else if (value === "true") value = true;
2070
- else if (value === "false") value = false;
2071
- else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") value = Number(value);
2072
- const mappedOp = operatorMap[op];
2073
- if (mappedOp) filter[field] = [mappedOp, value];
2401
+ if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
2402
+ else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
2403
+ else result[field] = ["in", raw];
2404
+ } else if (typeof raw === "string") result[field] = deserializeSingle(raw);
2405
+ else result[field] = ["==", raw];
2406
+ }
2407
+ return result;
2408
+ }
2409
+ /**
2410
+ * Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
2411
+ *
2412
+ * @example
2413
+ * serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
2414
+ * // → "status.eq.active"
2415
+ *
2416
+ * serializeLogicalCondition({ type: "or", conditions: [...] })
2417
+ * // → "or(status.eq.active,status.eq.pending)"
2418
+ */
2419
+ function serializeLogicalCondition(cond) {
2420
+ if ("type" in cond) {
2421
+ const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
2422
+ return `${cond.type}(${inner})`;
2423
+ }
2424
+ const restOp = CANONICAL_TO_REST[cond.operator] || "eq";
2425
+ if (Array.isArray(cond.value)) {
2426
+ const items = cond.value.map(stringifyValue).join(",");
2427
+ return `${cond.column}.${restOp}.(${items})`;
2428
+ }
2429
+ return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
2430
+ }
2431
+ /**
2432
+ * Parse a logical condition wire-format string back into a
2433
+ * `LogicalCondition` or `FilterCondition`.
2434
+ *
2435
+ * @example
2436
+ * deserializeLogicalCondition("status.eq.active")
2437
+ * // → { column: "status", operator: "==", value: "active" }
2438
+ *
2439
+ * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
2440
+ * // → { type: "or", conditions: [...] }
2441
+ */
2442
+ function deserializeLogicalCondition(str) {
2443
+ const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
2444
+ if (logicalMatch) {
2445
+ const type = logicalMatch[1];
2446
+ const innerStr = logicalMatch[2];
2447
+ const conditions = [];
2448
+ let depth = 0;
2449
+ let start = 0;
2450
+ for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
2451
+ else if (innerStr[i] === ")") depth--;
2452
+ else if (innerStr[i] === "," && depth === 0) {
2453
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
2454
+ start = i + 1;
2074
2455
  }
2456
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
2457
+ return {
2458
+ type,
2459
+ conditions
2460
+ };
2075
2461
  }
2076
- return Object.keys(filter).length > 0 ? filter : void 0;
2462
+ const firstDot = str.indexOf(".");
2463
+ if (firstDot === -1) return {
2464
+ column: str,
2465
+ operator: "==",
2466
+ value: true
2467
+ };
2468
+ const column = str.substring(0, firstDot);
2469
+ const rest = str.substring(firstDot + 1);
2470
+ const secondDot = rest.indexOf(".");
2471
+ if (secondDot === -1) return {
2472
+ column,
2473
+ operator: "==",
2474
+ value: coerceValue(rest)
2475
+ };
2476
+ const opStr = rest.substring(0, secondDot);
2477
+ let valueStr = rest.substring(secondDot + 1);
2478
+ const operator = toCanonicalOp(opStr) ?? "==";
2479
+ if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
2480
+ column,
2481
+ operator,
2482
+ value: valueStr.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))
2483
+ };
2484
+ return {
2485
+ column,
2486
+ operator,
2487
+ value: coerceValue(valueStr)
2488
+ };
2077
2489
  }
2490
+ //#endregion
2491
+ //#region src/data/buildRebaseData.ts
2078
2492
  /**
2079
2493
  * Parse an orderBy string like "created_at:desc" into [field, direction].
2080
2494
  */
@@ -2087,11 +2501,12 @@ function createDriverAccessor(driver, slug) {
2087
2501
  const accessor = {
2088
2502
  async find(params) {
2089
2503
  const orderParsed = parseOrderBy(params?.orderBy);
2504
+ const filter = params?.where ? deserializeFilter(params.where) : void 0;
2090
2505
  const entities = await driver.fetchCollection({
2091
2506
  path: slug,
2092
2507
  limit: params?.limit,
2093
2508
  offset: params?.offset,
2094
- filter: convertWhereToFilter(params?.where),
2509
+ filter,
2095
2510
  orderBy: orderParsed?.[0],
2096
2511
  order: orderParsed?.[1],
2097
2512
  searchString: params?.searchString
@@ -2141,9 +2556,10 @@ function createDriverAccessor(driver, slug) {
2141
2556
  return driver.deleteAll(slug);
2142
2557
  } : void 0,
2143
2558
  count: driver.countEntities ? async (params) => {
2559
+ const filter = params?.where ? deserializeFilter(params.where) : void 0;
2144
2560
  return driver.countEntities({
2145
2561
  path: slug,
2146
- filter: convertWhereToFilter(params?.where)
2562
+ filter
2147
2563
  });
2148
2564
  } : void 0,
2149
2565
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
@@ -2154,7 +2570,7 @@ function createDriverAccessor(driver, slug) {
2154
2570
  path: slug,
2155
2571
  limit: params?.limit,
2156
2572
  offset: params?.offset,
2157
- filter: convertWhereToFilter(params?.where),
2573
+ filter: params?.where,
2158
2574
  orderBy: orderParsed?.[0],
2159
2575
  order: orderParsed?.[1],
2160
2576
  searchString: params?.searchString,
@@ -2352,6 +2768,6 @@ async function detectJunctionTables(executeSql) {
2352
2768
  return junctionTables;
2353
2769
  }
2354
2770
  //#endregion
2355
- 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, detectJunctionTables, enumToObjectEntries, evaluateCondition, 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, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, sanitizeData, sanitizeRelation, segmentsToStrippedPath, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues };
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 };
2356
2772
 
2357
2773
  //# sourceMappingURL=index.es.js.map