@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.umd.js CHANGED
@@ -365,7 +365,7 @@
365
365
  if (!newRelation.foreignKeyOnTarget) {
366
366
  let foundForeignKey = false;
367
367
  try {
368
- const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
368
+ const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
369
369
  for (const targetRel of targetRelations) if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) try {
370
370
  if (targetRel.target().slug === sourceCollection.slug) {
371
371
  newRelation.foreignKeyOnTarget = targetRel.localKey;
@@ -381,7 +381,7 @@
381
381
  } else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
382
382
  let isManyToManyInverse = false;
383
383
  if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) try {
384
- const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
384
+ const targetRelations = (0, _rebasepro_types.getDataSourceCapabilities)(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
385
385
  for (const targetRel of targetRelations) if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
386
386
  isManyToManyInverse = true;
387
387
  break;
@@ -416,11 +416,10 @@
416
416
  function resolveCollectionRelations(collection) {
417
417
  const cached = _resolvedRelationsCache.get(collection);
418
418
  if (cached) return cached;
419
- if (!(0, _rebasepro_types.getDataSourceCapabilities)(collection.driver).supportsRelations) return {};
420
- const relCollection = collection;
419
+ if (!(0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) return {};
421
420
  const relations = {};
422
421
  const registeredRelationNames = /* @__PURE__ */ new Set();
423
- if (relCollection.relations) relCollection.relations.forEach((relation) => {
422
+ if (collection.relations) collection.relations.forEach((relation) => {
424
423
  try {
425
424
  const normalizedRelation = sanitizeRelation(relation, collection);
426
425
  const relationKey = normalizedRelation.relationName;
@@ -467,7 +466,7 @@
467
466
  console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
468
467
  }
469
468
  function getTableName(collection) {
470
- if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.driver).supportsRelations) return collection.table ?? (0, _rebasepro_utils.toSnakeCase)(collection.slug) ?? (0, _rebasepro_utils.toSnakeCase)(collection.name);
469
+ if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) return collection.table ?? (0, _rebasepro_utils.toSnakeCase)(collection.slug) ?? (0, _rebasepro_utils.toSnakeCase)(collection.name);
471
470
  return (0, _rebasepro_utils.toSnakeCase)(collection.slug) ?? (0, _rebasepro_utils.toSnakeCase)(collection.name);
472
471
  }
473
472
  function getTableVarName(tableName) {
@@ -672,8 +671,9 @@
672
671
  }
673
672
  function getSubcollections(collection) {
674
673
  if (collection.childCollections) return collection.childCollections() ?? [];
675
- if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.driver).supportsSubcollections && collection.subcollections) return collection.subcollections() ?? [];
676
- if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.driver).supportsRelations) {
674
+ const declaredSubcollections = (0, _rebasepro_types.getDeclaredSubcollections)(collection);
675
+ if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsSubcollections && declaredSubcollections) return declaredSubcollections() ?? [];
676
+ if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRelations) {
677
677
  const resolvedRelations = resolveCollectionRelations(collection);
678
678
  return Object.values(resolvedRelations).filter((r) => r.cardinality === "many").map((r) => {
679
679
  const target = r.target();
@@ -699,113 +699,339 @@
699
699
  return [];
700
700
  }
701
701
  //#endregion
702
- //#region src/util/permissions.ts
703
- function evaluateAST(sqlString, auth, entity) {
704
- if (!entity) return true;
705
- let cleanedSQL = sqlString.trim();
706
- while (cleanedSQL.startsWith("(") && cleanedSQL.endsWith(")")) {
707
- let openCount = 0;
708
- let isEnclosing = true;
709
- for (let i = 0; i < cleanedSQL.length - 1; i++) {
710
- if (cleanedSQL[i] === "(") openCount++;
711
- else if (cleanedSQL[i] === ")") openCount--;
712
- if (openCount === 0) {
713
- isEnclosing = false;
714
- break;
715
- }
702
+ //#region src/util/policy/sqlToPolicy.ts
703
+ /**
704
+ * A tiny, regex-based SQL "parser" for security rules.
705
+ *
706
+ * This is NOT a full SQL parser. It is designed to handle the subset of SQL
707
+ * commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
708
+ * optimistic client-side UI decision.
709
+ *
710
+ * It handles:
711
+ * - `field = 'literal'`
712
+ * - `field != 'literal'`
713
+ * - `field = current_setting('app.user_id')`
714
+ * - `A AND B`
715
+ * - `true`
716
+ * - `IN (...)` (as optimistic true)
717
+ *
718
+ * For anything it doesn't understand, it returns a `raw` expression, which
719
+ * the evaluator treats as "unknown" (and usually optimistic true).
720
+ */
721
+ function sqlToPolicy(sql) {
722
+ const trimmed = sql.trim();
723
+ if (trimmed.toLowerCase() === "true") return _rebasepro_types.policy.true();
724
+ if (trimmed.toLowerCase() === "false") return _rebasepro_types.policy.false();
725
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
726
+ if (overlapMatch) {
727
+ const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
728
+ return _rebasepro_types.policy.rolesOverlap(roles);
729
+ }
730
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
731
+ if (containMatch) {
732
+ const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
733
+ return _rebasepro_types.policy.rolesContain(roles);
734
+ }
735
+ if (trimmed.toUpperCase().includes(" OR ")) {
736
+ const parts = trimmed.split(/ OR /i);
737
+ return _rebasepro_types.policy.or(...parts.map(sqlToPolicy));
738
+ }
739
+ if (trimmed.toUpperCase().includes(" AND ")) {
740
+ const parts = trimmed.split(/ AND /i);
741
+ return _rebasepro_types.policy.and(...parts.map(sqlToPolicy));
742
+ }
743
+ const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
744
+ if (match) {
745
+ const [, leftStr, op, rightStr] = match;
746
+ const left = parseOperand(leftStr.trim());
747
+ const right = parseOperand(rightStr.trim());
748
+ if (left && right) return _rebasepro_types.policy.compare(left, op === "=" ? "eq" : "neq", right);
749
+ }
750
+ return _rebasepro_types.policy.raw(sql);
751
+ }
752
+ function parseOperand(str) {
753
+ if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return _rebasepro_types.policy.authUid();
754
+ const stringMatch = str.match(/^'(.+)'$/);
755
+ if (stringMatch) return _rebasepro_types.policy.literal(stringMatch[1]);
756
+ if (/^\w+$/.test(str)) return _rebasepro_types.policy.field(str);
757
+ return null;
758
+ }
759
+ //#endregion
760
+ //#region src/util/policy/securityRuleToConditions.ts
761
+ /**
762
+ * Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
763
+ * structured `condition`/`check`, and raw `using`/`withCheck` — into a single
764
+ * normalized {@link PolicyExpression} pair.
765
+ *
766
+ * **This is the linchpin against drift:** both the Postgres DDL generators and
767
+ * the client-side evaluator consume this one function, so there is exactly one
768
+ * definition of what a rule means. In particular, application `roles` are folded
769
+ * into the expression here (AND'd with the base condition, matching how Postgres
770
+ * generates the clause) rather than being handled separately by each consumer.
771
+ */
772
+ function securityRuleToConditions(rule) {
773
+ return {
774
+ usingExpr: withRoles(baseUsing(rule), rule),
775
+ withCheckExpr: withRoles(baseWithCheck(rule), rule)
776
+ };
777
+ }
778
+ function baseUsing(rule) {
779
+ if (rule.condition) return rule.condition;
780
+ if (rule.using != null) return sqlToPolicy(rule.using);
781
+ if (rule.access === "public") return _rebasepro_types.policy.true();
782
+ if (rule.ownerField) return _rebasepro_types.policy.compare(_rebasepro_types.policy.field(rule.ownerField), "eq", _rebasepro_types.policy.authUid());
783
+ return null;
784
+ }
785
+ function baseWithCheck(rule) {
786
+ if (rule.check) return rule.check;
787
+ if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
788
+ return baseUsing(rule);
789
+ }
790
+ /**
791
+ * AND the base condition with an application-role check, or produce a roles-only
792
+ * condition when there is no base. Mirrors the Postgres generator so that a
793
+ * role-scoped restrictive rule denies exactly the same set of users on both
794
+ * sides.
795
+ */
796
+ function withRoles(base, rule) {
797
+ if (!rule.roles || rule.roles.length === 0) return base;
798
+ const rolesExpr = _rebasepro_types.policy.rolesOverlap(rule.roles);
799
+ if (rule.mode === "restrictive") return base ? _rebasepro_types.policy.or(_rebasepro_types.policy.not(rolesExpr), base) : _rebasepro_types.policy.not(rolesExpr);
800
+ return base ? _rebasepro_types.policy.and(base, rolesExpr) : rolesExpr;
801
+ }
802
+ //#endregion
803
+ //#region src/util/policy/policyToPostgres.ts
804
+ /**
805
+ * Compiles a {@link PolicyExpression} to a PostgreSQL boolean SQL string,
806
+ * suitable for a `USING (...)` / `WITH CHECK (...)` clause.
807
+ *
808
+ * This is one of the two consumers of the shared policy model (the other being
809
+ * {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
810
+ * and the admin UI derive from the exact same expression.
811
+ */
812
+ function policyToPostgres(expr, collection) {
813
+ switch (expr.kind) {
814
+ case "true": return "true";
815
+ case "false": return "false";
816
+ case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" AND ");
817
+ case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" OR ");
818
+ case "not":
819
+ if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
820
+ return `NOT (${policyToPostgres(expr.operand, collection)})`;
821
+ case "compare": return `${operandToSql(expr.left, collection)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, collection)}`;
822
+ case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
823
+ case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
824
+ case "authenticated": return "auth.uid() IS NOT NULL";
825
+ case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
826
+ }
827
+ }
828
+ var COMPARE_SQL = {
829
+ eq: "=",
830
+ neq: "!=",
831
+ lt: "<",
832
+ lte: "<=",
833
+ gt: ">",
834
+ gte: ">="
835
+ };
836
+ function operandToSql(operand, collection) {
837
+ switch (operand.kind) {
838
+ case "field": return resolveColumnName(operand.name, collection);
839
+ case "literal": return quoteLiteral(operand.value);
840
+ case "authUid": return "auth.uid()";
841
+ case "authRoles": return "string_to_array(auth.roles(), ',')";
842
+ }
843
+ }
844
+ function resolveColumnName(propName, collection) {
845
+ const prop = collection?.properties?.[propName];
846
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
847
+ return (0, _rebasepro_utils.toSnakeCase)(propName);
848
+ }
849
+ function quoteLiteral(value) {
850
+ if (value === null) return "NULL";
851
+ if (typeof value === "boolean") return value ? "true" : "false";
852
+ if (typeof value === "number") return String(value);
853
+ return `'${value.replace(/'/g, "''")}'`;
854
+ }
855
+ /** Sorted, single-quoted `ARRAY['a','b']` — matches the generators' output. */
856
+ function rolesArraySql(roles) {
857
+ return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
858
+ }
859
+ //#endregion
860
+ //#region src/util/policy/evaluatePolicy.ts
861
+ /**
862
+ * Evaluates a {@link PolicyExpression} against a user + row, using three-valued
863
+ * (Kleene) logic so that `"unknown"` sub-results propagate soundly.
864
+ *
865
+ * This is the JavaScript twin of {@link policyToPostgres}: both derive from the
866
+ * same expression, so the admin UI matches database enforcement by construction
867
+ * for every non-raw rule.
868
+ */
869
+ function evaluatePolicy(expr, ctx) {
870
+ switch (expr.kind) {
871
+ case "true": return true;
872
+ case "false": return false;
873
+ case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
874
+ case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
875
+ case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
876
+ case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
877
+ case "rolesOverlap": {
878
+ const userRoles = ctx.roles ?? [];
879
+ return expr.roles.some((r) => r === "public" || userRoles.includes(r));
716
880
  }
717
- if (isEnclosing) cleanedSQL = cleanedSQL.substring(1, cleanedSQL.length - 1).trim();
718
- else break;
719
- }
720
- const splitByTopLevel = (str, delimiter) => {
721
- const parts = [];
722
- let current = "";
723
- let openCount = 0;
724
- let i = 0;
725
- while (i < str.length) {
726
- if (str[i] === "(") openCount++;
727
- else if (str[i] === ")") openCount--;
728
- if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
729
- parts.push(current);
730
- current = "";
731
- i += delimiter.length;
732
- } else {
733
- current += str[i];
734
- i++;
735
- }
881
+ case "rolesContain": {
882
+ const userRoles = ctx.roles ?? [];
883
+ return expr.roles.every((r) => r === "public" || userRoles.includes(r));
736
884
  }
737
- parts.push(current);
738
- return parts;
739
- };
740
- const orParts = splitByTopLevel(cleanedSQL, " OR ");
741
- if (orParts.length > 1) return orParts.some((part) => evaluateAST(part, auth, entity));
742
- const andParts = splitByTopLevel(cleanedSQL, " AND ");
743
- if (andParts.length > 1) return andParts.every((part) => evaluateAST(part, auth, entity));
744
- const upperSQL = cleanedSQL.toUpperCase();
745
- if (upperSQL.includes(" IN ") || upperSQL.includes(" EXISTS ")) return true;
746
- const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
747
- if (roleIntersectMatch && roleIntersectMatch[1]) {
748
- const requiredRoles = roleIntersectMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
749
- const userRoles = auth.user?.roles || [];
750
- return requiredRoles.some((r) => userRoles.includes(r));
751
- }
752
- const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
753
- if (roleContainMatch && roleContainMatch[1]) {
754
- const requiredRoles = roleContainMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
755
- const userRoles = auth.user?.roles || [];
756
- return requiredRoles.every((r) => userRoles.includes(r));
757
- }
758
- const pattern1 = /* @__PURE__ */ new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
759
- const pattern2 = /* @__PURE__ */ new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
760
- const match1 = cleanedSQL.match(pattern1);
761
- if (match1 && match1[1]) return entity.values[match1[1]] === auth.user?.uid;
762
- const match2 = cleanedSQL.match(pattern2);
763
- if (match2 && match2[1]) return entity.values[match2[1]] === auth.user?.uid;
764
- const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
765
- if (simpleEqualityMatch) {
766
- const field = simpleEqualityMatch[1];
767
- const operator = simpleEqualityMatch[2];
768
- const value = simpleEqualityMatch[3];
769
- const entityValue = entity.values[field];
770
- if (operator === "=") return entityValue === value;
771
- if (operator === "!=") return entityValue !== value;
885
+ case "authenticated": return ctx.uid != null;
886
+ case "raw": return "unknown";
772
887
  }
888
+ }
889
+ function kleeneAnd$1(values) {
890
+ if (values.some((v) => v === false)) return false;
891
+ if (values.some((v) => v === "unknown")) return "unknown";
773
892
  return true;
774
893
  }
775
- function evaluateRule(rule, auth, entity) {
776
- if (rule.access === "public") return true;
777
- if (rule.ownerField) {
778
- if (!entity) {} else if (entity.values[rule.ownerField] !== auth.user?.uid) return false;
894
+ function kleeneOr(values) {
895
+ if (values.some((v) => v === true)) return true;
896
+ if (values.some((v) => v === "unknown")) return "unknown";
897
+ return false;
898
+ }
899
+ function kleeneNot(value) {
900
+ if (value === "unknown") return "unknown";
901
+ return !value;
902
+ }
903
+ function resolveOperand(operand, ctx) {
904
+ switch (operand.kind) {
905
+ case "literal": return {
906
+ known: true,
907
+ value: operand.value
908
+ };
909
+ case "authUid": return {
910
+ known: true,
911
+ value: ctx.uid ?? null
912
+ };
913
+ case "authRoles": return {
914
+ known: true,
915
+ value: ctx.roles ?? []
916
+ };
917
+ case "field":
918
+ if (!ctx.entity) return { known: false };
919
+ return {
920
+ known: true,
921
+ value: ctx.entity.values[operand.name]
922
+ };
779
923
  }
780
- if (rule.using && !evaluateAST(rule.using, auth, entity)) return false;
781
- if (rule.withCheck && !evaluateAST(rule.withCheck, auth, entity)) return false;
924
+ }
925
+ function evaluateCompare(op, left, right, ctx) {
926
+ const l = resolveOperand(left, ctx);
927
+ const r = resolveOperand(right, ctx);
928
+ if (!l.known || !r.known) return "unknown";
929
+ const a = l.value;
930
+ const b = r.value;
931
+ if (a === null || b === null) {
932
+ if (op === "eq") return false;
933
+ if (op === "neq") return true;
934
+ return "unknown";
935
+ }
936
+ if (op === "eq") return a === b;
937
+ if (op === "neq") return a !== b;
938
+ if (typeof a === "string" && typeof b === "string") {
939
+ if (op === "lt") return a < b;
940
+ if (op === "lte") return a <= b;
941
+ if (op === "gt") return a > b;
942
+ if (op === "gte") return a >= b;
943
+ }
944
+ if (typeof a === "number" && typeof b === "number") {
945
+ if (op === "lt") return a < b;
946
+ if (op === "lte") return a <= b;
947
+ if (op === "gt") return a > b;
948
+ if (op === "gte") return a >= b;
949
+ }
950
+ if (typeof a === "bigint" && typeof b === "bigint") {
951
+ if (op === "lt") return a < b;
952
+ if (op === "lte") return a <= b;
953
+ if (op === "gt") return a > b;
954
+ if (op === "gte") return a >= b;
955
+ }
956
+ return "unknown";
957
+ }
958
+ //#endregion
959
+ //#region src/util/permissions.ts
960
+ /** Combine clause results with AND under three-valued (Kleene) logic. */
961
+ function kleeneAnd(values) {
962
+ if (values.some((v) => v === false)) return false;
963
+ if (values.some((v) => v === "unknown")) return "unknown";
782
964
  return true;
783
965
  }
784
- function checkOperation(collection, authContext, entity, targetOperation) {
785
- const securityRules = (0, _rebasepro_types.getDataSourceCapabilities)(collection.driver).supportsRLS ? collection.securityRules : void 0;
966
+ /** The operations a rule covers, mirroring the Postgres generator's resolution. */
967
+ function ruleOperations(rule) {
968
+ return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
969
+ }
970
+ function ruleApplies(rule, targetOperation) {
971
+ const ops = ruleOperations(rule);
972
+ return ops.includes(targetOperation) || ops.includes("all");
973
+ }
974
+ /**
975
+ * Evaluate a single rule for one operation, returning a tri-state.
976
+ *
977
+ * A `null` clause (the rule contributes no condition for a required clause)
978
+ * denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
979
+ * in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
980
+ * INSERT/UPDATE; both must pass for UPDATE.
981
+ */
982
+ function evaluateRuleForOperation(rule, ctx, targetOperation) {
983
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
984
+ const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
985
+ const needsUsing = targetOperation !== "insert";
986
+ const needsWithCheck = targetOperation === "insert" || targetOperation === "update";
987
+ const results = [];
988
+ if (needsUsing) results.push(clause(usingExpr));
989
+ if (needsWithCheck) results.push(clause(withCheckExpr));
990
+ return kleeneAnd(results);
991
+ }
992
+ function resolveTriState(value, onUnknown) {
993
+ if (value === "unknown") return onUnknown === "allow";
994
+ return value;
995
+ }
996
+ /**
997
+ * Decide whether an operation is permitted for a user on a (possibly null) row,
998
+ * by evaluating the collection's security rules with the shared policy model —
999
+ * the same model compiled to Postgres RLS DDL, so the decision matches database
1000
+ * enforcement for every non-raw rule.
1001
+ *
1002
+ * @param options.onUnknown how to treat rules that cannot be decided
1003
+ * client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
1004
+ * for optimistic UI gating; enforcement callers should pass `"deny"`.
1005
+ */
1006
+ function checkOperation(collection, authContext, entity, targetOperation, options) {
1007
+ const onUnknown = options?.onUnknown ?? "allow";
1008
+ const securityRules = (0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRLS ? collection.securityRules : void 0;
786
1009
  if (!securityRules || securityRules.length === 0) return true;
787
- const applicableRules = securityRules.filter((r) => r.operation === targetOperation || r.operation === "all" || r.operations?.includes(targetOperation) || r.operations?.includes("all"));
1010
+ const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
788
1011
  if (applicableRules.length === 0) return false;
789
- const userRoles = [...authContext.user?.roles ?? [], "public"];
790
- const roleApplicableRules = applicableRules.filter((rule) => {
791
- if (!rule.roles || rule.roles.length === 0) return true;
792
- return rule.roles.some((r) => userRoles.includes(r));
793
- });
794
- if (roleApplicableRules.length === 0) return false;
1012
+ const ctx = {
1013
+ uid: authContext.user?.uid,
1014
+ roles: authContext.user?.roles ?? [],
1015
+ entity
1016
+ };
795
1017
  let grantedByPermissive = false;
796
1018
  let deniedByRestrictive = false;
797
- for (const rule of roleApplicableRules) {
1019
+ let hasPermissive = false;
1020
+ for (const rule of applicableRules) {
798
1021
  const mode = rule.mode || "permissive";
799
- const passed = evaluateRule(rule, authContext, entity);
800
- if (mode === "restrictive" && !passed) {
801
- deniedByRestrictive = true;
802
- break;
1022
+ const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation), onUnknown);
1023
+ if (mode === "restrictive") {
1024
+ if (!passed) {
1025
+ deniedByRestrictive = true;
1026
+ break;
1027
+ }
1028
+ } else {
1029
+ hasPermissive = true;
1030
+ if (passed) grantedByPermissive = true;
803
1031
  }
804
- if (mode === "permissive" && passed) grantedByPermissive = true;
805
1032
  }
806
1033
  if (deniedByRestrictive) return false;
807
- if (roleApplicableRules.some((r) => (r.mode || "permissive") === "permissive")) return grantedByPermissive;
808
- else return false;
1034
+ return hasPermissive ? grantedByPermissive : false;
809
1035
  }
810
1036
  function canReadCollection(collection, authContext) {
811
1037
  return checkOperation(collection, authContext, null, "select");
@@ -836,7 +1062,7 @@
836
1062
  }
837
1063
  for (const key in collection.properties) {
838
1064
  const property = collection.properties[key];
839
- if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.url === "image") return key;
1065
+ if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") return key;
840
1066
  }
841
1067
  for (const key in collection.properties) {
842
1068
  const property = collection.properties[key];
@@ -1070,6 +1296,13 @@
1070
1296
  return collection;
1071
1297
  }
1072
1298
  /**
1299
+ * Implementation — delegates to the correct overload at the type level.
1300
+ * At runtime this is a plain identity function.
1301
+ */
1302
+ function defineCollection(collection) {
1303
+ return collection;
1304
+ }
1305
+ /**
1073
1306
  * Identity function we use to defeat the type system of Typescript and preserve
1074
1307
  * the property keys.
1075
1308
  * @param property
@@ -1134,6 +1367,29 @@
1134
1367
  }
1135
1368
  //#endregion
1136
1369
  //#region src/util/storage.ts
1370
+ /**
1371
+ * Resolve the {@link StorageSource} to use for a property, given the key
1372
+ * referenced by `StorageConfig.storageSource`.
1373
+ *
1374
+ * Resolution priority:
1375
+ * 1. No `sourceKey` → the default source (backward compatible).
1376
+ * 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
1377
+ * 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
1378
+ * 4. Fall back to the default source.
1379
+ *
1380
+ * Shared by the upload hook, the markdown editor, and the read-only previews
1381
+ * so the resolution logic lives in one place.
1382
+ *
1383
+ * @group Storage
1384
+ */
1385
+ function resolveStorageSource(params) {
1386
+ const { sourceKey, sources, registry, defaultSource } = params;
1387
+ if (!sourceKey) return defaultSource;
1388
+ if (registry) return registry.getOrDefault(sourceKey);
1389
+ const fromSources = sources?.[sourceKey];
1390
+ if (fromSources) return fromSources;
1391
+ return defaultSource;
1392
+ }
1137
1393
  async function resolveStorageFilenameString({ input, storage, values, entityId, path, property, file, propertyKey }) {
1138
1394
  let result;
1139
1395
  if (typeof input === "function") {
@@ -1456,13 +1712,13 @@
1456
1712
  * editor's capability lookups.
1457
1713
  *
1458
1714
  * Resolution order:
1459
- * 1. The routing **key** is `collection.dataSource`, else the legacy
1460
- * `collection.driver`, else {@link DEFAULT_DATA_SOURCE_KEY}.
1715
+ * 1. The routing **key** is `collection.dataSource`, else
1716
+ * {@link DEFAULT_DATA_SOURCE_KEY}.
1461
1717
  * 2. If a definition is registered for that key, it provides `engine`,
1462
1718
  * `transport`, and `databaseId`.
1463
- * 3. Otherwise values are synthesized for backward compatibility: `engine`
1464
- * from the legacy `driver` (or the key, or `"postgres"`), `transport`
1465
- * defaults to `"server"`, and `databaseId` from the collection.
1719
+ * 3. Otherwise values are synthesized: `engine` from `collection.engine`
1720
+ * (or the key, or `"postgres"`), `transport` defaults to `"server"`,
1721
+ * and `databaseId` from the collection.
1466
1722
  *
1467
1723
  * `capabilities` are always derived from the resolved `engine`, so two
1468
1724
  * data sources sharing an engine share capabilities.
@@ -1471,9 +1727,9 @@
1471
1727
  * @param registry optional registry of declared data sources
1472
1728
  */
1473
1729
  function resolveDataSource(collection, registry) {
1474
- const key = collection?.dataSource ?? collection?.driver ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
1730
+ const key = collection?.dataSource ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
1475
1731
  const def = registry?.[key];
1476
- const engine = def?.engine ?? collection?.driver ?? (key !== _rebasepro_types.DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
1732
+ const engine = def?.engine ?? collection?.engine ?? (key !== _rebasepro_types.DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
1477
1733
  return {
1478
1734
  key,
1479
1735
  engine,
@@ -1488,9 +1744,28 @@
1488
1744
  /**
1489
1745
  * Declared data sources, used during normalization to resolve each
1490
1746
  * collection's engine (so `dataSource`-only collections get the right
1491
- * capabilities). Empty by default → behaviour keys off `driver` as before.
1747
+ * capabilities). Empty by default.
1492
1748
  */
1493
1749
  dataSources = {};
1750
+ /**
1751
+ * Global lifecycle callbacks applied to every collection.
1752
+ * Runs on all data paths (REST, WebSocket, `rebase.data`).
1753
+ * Execution order: global → collection → property callbacks.
1754
+ */
1755
+ _globalCallbacks;
1756
+ /**
1757
+ * Set global lifecycle callbacks that apply to every collection.
1758
+ * Typically called once during backend initialization.
1759
+ */
1760
+ setGlobalCallbacks(callbacks) {
1761
+ this._globalCallbacks = callbacks;
1762
+ }
1763
+ /**
1764
+ * Get the currently registered global callbacks, if any.
1765
+ */
1766
+ getGlobalCallbacks() {
1767
+ return this._globalCallbacks;
1768
+ }
1494
1769
  collectionsByTableName = /* @__PURE__ */ new Map();
1495
1770
  collectionsBySlug = /* @__PURE__ */ new Map();
1496
1771
  rootCollections = [];
@@ -1582,13 +1857,14 @@
1582
1857
  }
1583
1858
  normalizeCollection(collection) {
1584
1859
  const result = { ...collection };
1585
- if (result.dataSource && !result.driver) {
1586
- const engine = resolveDataSource(result, this.dataSources).engine;
1587
- if (engine) result.driver = engine;
1860
+ {
1861
+ const resolved = resolveDataSource(result, this.dataSources);
1862
+ if (!result.dataSource) result.dataSource = resolved.key;
1863
+ if (!result.engine) result.engine = resolved.engine;
1588
1864
  }
1589
1865
  const extractedRelations = this.extractRelationsFromProperties(result.properties);
1590
1866
  const relResult = result;
1591
- const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.driver).supportsRelations ? relResult.relations ?? [] : [];
1867
+ const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations ? relResult.relations ?? [] : [];
1592
1868
  const mergedRelationsRaw = [...extractedRelations];
1593
1869
  for (const manual of manualRelations) {
1594
1870
  const name = manual.relationName;
@@ -1603,7 +1879,7 @@
1603
1879
  }
1604
1880
  }
1605
1881
  let mergedRelations = mergedRelationsRaw;
1606
- if ((0, _rebasepro_types.getDataSourceCapabilities)(result.driver).supportsRelations) {
1882
+ if ((0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations) {
1607
1883
  mergedRelations = mergedRelationsRaw.map((r) => {
1608
1884
  try {
1609
1885
  return sanitizeRelation(r, result, (slug) => this.get(slug));
@@ -1615,8 +1891,10 @@
1615
1891
  }
1616
1892
  result.properties = this.normalizeProperties(result.properties, mergedRelations);
1617
1893
  if (!result.childCollections) {
1618
- if ((0, _rebasepro_types.getDataSourceCapabilities)(result.driver).supportsSubcollections && result.subcollections) result.childCollections = result.subcollections;
1619
- else if ((0, _rebasepro_types.getDataSourceCapabilities)(result.driver).supportsRelations && relResult.relations) {
1894
+ const capabilities = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine);
1895
+ const declaredSubcollections = (0, _rebasepro_types.getDeclaredSubcollections)(result);
1896
+ if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
1897
+ else if (capabilities.supportsRelations && relResult.relations) {
1620
1898
  const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
1621
1899
  if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
1622
1900
  const target = r.target();
@@ -1718,7 +1996,7 @@
1718
1996
  if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
1719
1997
  for (let i = 2; i < pathSegments.length; i += 2) {
1720
1998
  const relationKey = pathSegments[i];
1721
- if (!(0, _rebasepro_types.getDataSourceCapabilities)(currentCollection.driver).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
1999
+ if (!(0, _rebasepro_types.getDataSourceCapabilities)(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
1722
2000
  const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
1723
2001
  if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
1724
2002
  const target = relation.target();
@@ -1779,7 +2057,7 @@
1779
2057
  * Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
1780
2058
  * override by defining their own collection with `slug: "users"`.
1781
2059
  */
1782
- var defaultUsersCollection = {
2060
+ var defaultUsersCollection = defineCollection({
1783
2061
  name: "Users",
1784
2062
  singularName: "User",
1785
2063
  slug: "users",
@@ -1827,7 +2105,7 @@
1827
2105
  name: "Photo URL",
1828
2106
  type: "string",
1829
2107
  columnName: "photo_url",
1830
- url: "image"
2108
+ ui: { url: "image" }
1831
2109
  },
1832
2110
  roles: {
1833
2111
  name: "Roles",
@@ -1922,7 +2200,7 @@
1922
2200
  "roles",
1923
2201
  "createdAt"
1924
2202
  ]
1925
- };
2203
+ });
1926
2204
  //#endregion
1927
2205
  //#region src/data/query_builder.ts
1928
2206
  function or(...conditions) {
@@ -1974,8 +2252,8 @@
1974
2252
  * @example
1975
2253
  * client.collection('users').orderBy('createdAt', 'desc').find()
1976
2254
  */
1977
- orderBy(column, ascending = "asc") {
1978
- this.params.orderBy = `${column}:${ascending}`;
2255
+ orderBy(column, direction = "asc") {
2256
+ this.params.orderBy = `${column}:${direction}`;
1979
2257
  return this;
1980
2258
  }
1981
2259
  /**
@@ -2030,80 +2308,216 @@
2030
2308
  }
2031
2309
  };
2032
2310
  //#endregion
2033
- //#region src/data/buildRebaseData.ts
2311
+ //#region src/data/filter-dialect.ts
2034
2312
  /**
2035
- * Convert where-clause filter object to the internal DataDriver FilterValues format.
2313
+ * REST wire-format adapter for the unified filter system.
2036
2314
  *
2037
- * Supports multiple value formats:
2038
- * - PostgREST string: { status: "eq.published", age: "gte.18" }
2039
- * - Equality shorthand: { company_profile_id: null, status: "active", age: 18 }
2040
- * - Tuple syntax: { age: [">=", 18], role: ["in", ["admin", "editor"]] }
2315
+ * This module is the ONLY code in the entire codebase that knows about
2316
+ * PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
2317
+ * Everything else speaks `FilterValues` exclusively.
2041
2318
  *
2042
- * Internal: { status: ["==", "published"], age: [">=", 18] }
2319
+ * @module
2043
2320
  */
2044
- function convertWhereToFilter(where) {
2045
- if (!where) return void 0;
2046
- const operatorMap = {
2047
- "eq": "==",
2048
- "neq": "!=",
2049
- "gt": ">",
2050
- "gte": ">=",
2051
- "lt": "<",
2052
- "lte": "<=",
2053
- "in": "in",
2054
- "nin": "not-in",
2055
- "not-in": "not-in",
2056
- "cs": "array-contains",
2057
- "csa": "array-contains-any",
2058
- "==": "==",
2059
- "!=": "!=",
2060
- ">": ">",
2061
- ">=": ">=",
2062
- "<": "<",
2063
- "<=": "<=",
2064
- "array-contains": "array-contains",
2065
- "array-contains-any": "array-contains-any"
2066
- };
2067
- const filter = {};
2068
- for (const [field, rawValue] of Object.entries(where)) {
2069
- if (rawValue === null) {
2070
- filter[field] = ["==", null];
2071
- continue;
2072
- }
2073
- if (typeof rawValue === "boolean") {
2074
- filter[field] = ["==", rawValue];
2075
- continue;
2076
- }
2077
- if (typeof rawValue === "number") {
2078
- filter[field] = ["==", rawValue];
2079
- continue;
2321
+ /**
2322
+ * Coerce a raw querystring value to its natural JS type.
2323
+ * - `"true"` / `"false"` → boolean
2324
+ * - `"null"` → null
2325
+ * - Numeric strings → number
2326
+ * - Everything else → string (unchanged)
2327
+ */
2328
+ function coerceValue(raw) {
2329
+ if (raw === "true") return true;
2330
+ if (raw === "false") return false;
2331
+ if (raw === "null") return null;
2332
+ if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
2333
+ return raw;
2334
+ }
2335
+ /**
2336
+ * Serialize a JS value to its querystring representation.
2337
+ */
2338
+ function stringifyValue(value) {
2339
+ if (value === null) return "null";
2340
+ if (typeof value === "boolean") return String(value);
2341
+ return String(value);
2342
+ }
2343
+ /**
2344
+ * Serialize a single condition tuple to a PostgREST dot-string.
2345
+ *
2346
+ * @example
2347
+ * serializeTuple(["==", "active"]) // "eq.active"
2348
+ * serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
2349
+ * serializeTuple([">=", 18]) // "gte.18"
2350
+ */
2351
+ function serializeTuple(tuple) {
2352
+ if (typeof tuple === "string") {
2353
+ if (tuple.includes(".")) {
2354
+ const dotIndex = tuple.indexOf(".");
2355
+ if (_rebasepro_types.REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
2080
2356
  }
2081
- if (Array.isArray(rawValue)) {
2082
- const mappedConditions = (Array.isArray(rawValue[0]) ? rawValue : [rawValue]).map(([rawOp, val]) => {
2083
- return [operatorMap[rawOp] ?? "==", val];
2084
- });
2085
- filter[field] = Array.isArray(rawValue[0]) ? mappedConditions : mappedConditions[0];
2357
+ return tuple;
2358
+ }
2359
+ if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !_rebasepro_types.CANONICAL_TO_REST[tuple[0]]) return `eq.${stringifyValue(tuple)}`;
2360
+ const [op, value] = tuple;
2361
+ const restOp = _rebasepro_types.CANONICAL_TO_REST[op];
2362
+ if (Array.isArray(value)) return `${restOp}.(${value.map(stringifyValue).join(",")})`;
2363
+ return `${restOp}.${stringifyValue(value)}`;
2364
+ }
2365
+ /**
2366
+ * Convert `FilterValues` to a PostgREST-style querystring record.
2367
+ *
2368
+ * - Single conditions produce a string value.
2369
+ * - Multiple conditions on the same field produce a string array (repeated params).
2370
+ *
2371
+ * @example
2372
+ * serializeFilter({ status: ["==", "active"] })
2373
+ * // → { status: "eq.active" }
2374
+ *
2375
+ * serializeFilter({ age: [[">=", 18], ["<", 65]] })
2376
+ * // → { age: ["gte.18", "lt.65"] }
2377
+ */
2378
+ function serializeFilter(filter) {
2379
+ const result = {};
2380
+ for (const [field, condition] of Object.entries(filter)) {
2381
+ if (condition === void 0) continue;
2382
+ if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
2383
+ else result[field] = serializeTuple(condition);
2384
+ }
2385
+ return result;
2386
+ }
2387
+ /**
2388
+ * Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
2389
+ *
2390
+ * If the string doesn't match a known operator prefix, falls back to
2391
+ * `["==", originalString]` (treating the whole string as an equality value).
2392
+ */
2393
+ function deserializeSingle(raw) {
2394
+ const dotIndex = raw.indexOf(".");
2395
+ if (dotIndex === -1) return ["==", coerceValue(raw)];
2396
+ const prefix = raw.substring(0, dotIndex);
2397
+ const rest = raw.substring(dotIndex + 1);
2398
+ const canonicalOp = _rebasepro_types.REST_TO_CANONICAL[prefix];
2399
+ if (!canonicalOp) return ["==", raw];
2400
+ if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, rest.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))];
2401
+ return [canonicalOp, coerceValue(rest)];
2402
+ }
2403
+ /**
2404
+ * Convert a PostgREST-style querystring record to `FilterValues`.
2405
+ *
2406
+ * - String values are parsed as single conditions.
2407
+ * - String arrays (repeated query params) become multiple conditions on the same field.
2408
+ *
2409
+ * @example
2410
+ * deserializeFilter({ status: "eq.active" })
2411
+ * // → { status: ["==", "active"] }
2412
+ *
2413
+ * deserializeFilter({ age: ["gte.18", "lt.65"] })
2414
+ * // → { age: [[">=", 18], ["<", 65]] }
2415
+ */
2416
+ function deserializeFilter(query) {
2417
+ const result = {};
2418
+ for (const [field, raw] of Object.entries(query)) {
2419
+ if (raw === void 0) continue;
2420
+ if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0]) === raw[0]) {
2421
+ result[field] = raw;
2086
2422
  continue;
2087
2423
  }
2088
- if (typeof rawValue === "string") {
2089
- const dotIndex = rawValue.indexOf(".");
2090
- if (dotIndex === -1) {
2091
- filter[field] = ["==", rawValue];
2424
+ if (Array.isArray(raw)) {
2425
+ if (raw.length === 0) continue;
2426
+ if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0][0]) === raw[0][0]) {
2427
+ result[field] = raw;
2092
2428
  continue;
2093
2429
  }
2094
- const op = rawValue.substring(0, dotIndex);
2095
- let value = rawValue.substring(dotIndex + 1);
2096
- if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) value = value.slice(1, -1).split(",").map((v) => v.trim());
2097
- if (value === "null") value = null;
2098
- else if (value === "true") value = true;
2099
- else if (value === "false") value = false;
2100
- else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") value = Number(value);
2101
- const mappedOp = operatorMap[op];
2102
- if (mappedOp) filter[field] = [mappedOp, value];
2430
+ if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
2431
+ else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
2432
+ else result[field] = ["in", raw];
2433
+ } else if (typeof raw === "string") result[field] = deserializeSingle(raw);
2434
+ else result[field] = ["==", raw];
2435
+ }
2436
+ return result;
2437
+ }
2438
+ /**
2439
+ * Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
2440
+ *
2441
+ * @example
2442
+ * serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
2443
+ * // → "status.eq.active"
2444
+ *
2445
+ * serializeLogicalCondition({ type: "or", conditions: [...] })
2446
+ * // → "or(status.eq.active,status.eq.pending)"
2447
+ */
2448
+ function serializeLogicalCondition(cond) {
2449
+ if ("type" in cond) {
2450
+ const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
2451
+ return `${cond.type}(${inner})`;
2452
+ }
2453
+ const restOp = _rebasepro_types.CANONICAL_TO_REST[cond.operator] || "eq";
2454
+ if (Array.isArray(cond.value)) {
2455
+ const items = cond.value.map(stringifyValue).join(",");
2456
+ return `${cond.column}.${restOp}.(${items})`;
2457
+ }
2458
+ return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
2459
+ }
2460
+ /**
2461
+ * Parse a logical condition wire-format string back into a
2462
+ * `LogicalCondition` or `FilterCondition`.
2463
+ *
2464
+ * @example
2465
+ * deserializeLogicalCondition("status.eq.active")
2466
+ * // → { column: "status", operator: "==", value: "active" }
2467
+ *
2468
+ * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
2469
+ * // → { type: "or", conditions: [...] }
2470
+ */
2471
+ function deserializeLogicalCondition(str) {
2472
+ const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
2473
+ if (logicalMatch) {
2474
+ const type = logicalMatch[1];
2475
+ const innerStr = logicalMatch[2];
2476
+ const conditions = [];
2477
+ let depth = 0;
2478
+ let start = 0;
2479
+ for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
2480
+ else if (innerStr[i] === ")") depth--;
2481
+ else if (innerStr[i] === "," && depth === 0) {
2482
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
2483
+ start = i + 1;
2103
2484
  }
2485
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
2486
+ return {
2487
+ type,
2488
+ conditions
2489
+ };
2104
2490
  }
2105
- return Object.keys(filter).length > 0 ? filter : void 0;
2491
+ const firstDot = str.indexOf(".");
2492
+ if (firstDot === -1) return {
2493
+ column: str,
2494
+ operator: "==",
2495
+ value: true
2496
+ };
2497
+ const column = str.substring(0, firstDot);
2498
+ const rest = str.substring(firstDot + 1);
2499
+ const secondDot = rest.indexOf(".");
2500
+ if (secondDot === -1) return {
2501
+ column,
2502
+ operator: "==",
2503
+ value: coerceValue(rest)
2504
+ };
2505
+ const opStr = rest.substring(0, secondDot);
2506
+ let valueStr = rest.substring(secondDot + 1);
2507
+ const operator = (0, _rebasepro_types.toCanonicalOp)(opStr) ?? "==";
2508
+ if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
2509
+ column,
2510
+ operator,
2511
+ value: valueStr.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))
2512
+ };
2513
+ return {
2514
+ column,
2515
+ operator,
2516
+ value: coerceValue(valueStr)
2517
+ };
2106
2518
  }
2519
+ //#endregion
2520
+ //#region src/data/buildRebaseData.ts
2107
2521
  /**
2108
2522
  * Parse an orderBy string like "created_at:desc" into [field, direction].
2109
2523
  */
@@ -2116,11 +2530,12 @@
2116
2530
  const accessor = {
2117
2531
  async find(params) {
2118
2532
  const orderParsed = parseOrderBy(params?.orderBy);
2533
+ const filter = params?.where ? deserializeFilter(params.where) : void 0;
2119
2534
  const entities = await driver.fetchCollection({
2120
2535
  path: slug,
2121
2536
  limit: params?.limit,
2122
2537
  offset: params?.offset,
2123
- filter: convertWhereToFilter(params?.where),
2538
+ filter,
2124
2539
  orderBy: orderParsed?.[0],
2125
2540
  order: orderParsed?.[1],
2126
2541
  searchString: params?.searchString
@@ -2170,9 +2585,10 @@
2170
2585
  return driver.deleteAll(slug);
2171
2586
  } : void 0,
2172
2587
  count: driver.countEntities ? async (params) => {
2588
+ const filter = params?.where ? deserializeFilter(params.where) : void 0;
2173
2589
  return driver.countEntities({
2174
2590
  path: slug,
2175
- filter: convertWhereToFilter(params?.where)
2591
+ filter
2176
2592
  });
2177
2593
  } : void 0,
2178
2594
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
@@ -2183,7 +2599,7 @@
2183
2599
  path: slug,
2184
2600
  limit: params?.limit,
2185
2601
  offset: params?.offset,
2186
- filter: convertWhereToFilter(params?.where),
2602
+ filter: params?.where,
2187
2603
  orderBy: orderParsed?.[0],
2188
2604
  order: orderParsed?.[1],
2189
2605
  searchString: params?.searchString,
@@ -2415,9 +2831,13 @@
2415
2831
  exports.createRelationRef = createRelationRef;
2416
2832
  exports.createRelationRefWithData = createRelationRefWithData;
2417
2833
  exports.defaultUsersCollection = defaultUsersCollection;
2834
+ exports.defineCollection = defineCollection;
2835
+ exports.deserializeFilter = deserializeFilter;
2836
+ exports.deserializeLogicalCondition = deserializeLogicalCondition;
2418
2837
  exports.detectJunctionTables = detectJunctionTables;
2419
2838
  exports.enumToObjectEntries = enumToObjectEntries;
2420
2839
  exports.evaluateCondition = evaluateCondition;
2840
+ exports.evaluatePolicy = evaluatePolicy;
2421
2841
  exports.findRelation = findRelation;
2422
2842
  exports.fullPathToCollectionSegments = fullPathToCollectionSegments;
2423
2843
  exports.getArrayResolvedProperties = getArrayResolvedProperties;
@@ -2446,6 +2866,7 @@
2446
2866
  exports.isRebaseInternalTable = isRebaseInternalTable;
2447
2867
  exports.normalizeToEntityRelation = normalizeToEntityRelation;
2448
2868
  exports.or = or;
2869
+ exports.policyToPostgres = policyToPostgres;
2449
2870
  exports.registerConditionOperations = registerConditionOperations;
2450
2871
  exports.removeInitialAndTrailingSlashes = removeInitialAndTrailingSlashes;
2451
2872
  exports.removeInitialSlash = removeInitialSlash;
@@ -2463,9 +2884,13 @@
2463
2884
  exports.resolveRelationProperty = resolveRelationProperty;
2464
2885
  exports.resolveStorageFilenameString = resolveStorageFilenameString;
2465
2886
  exports.resolveStoragePathString = resolveStoragePathString;
2887
+ exports.resolveStorageSource = resolveStorageSource;
2466
2888
  exports.sanitizeData = sanitizeData;
2467
2889
  exports.sanitizeRelation = sanitizeRelation;
2890
+ exports.securityRuleToConditions = securityRuleToConditions;
2468
2891
  exports.segmentsToStrippedPath = segmentsToStrippedPath;
2892
+ exports.serializeFilter = serializeFilter;
2893
+ exports.serializeLogicalCondition = serializeLogicalCondition;
2469
2894
  exports.sortProperties = sortProperties;
2470
2895
  exports.stripCollectionPath = stripCollectionPath;
2471
2896
  exports.traverseValueProperty = traverseValueProperty;