@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.
- package/dist/collections/CollectionRegistry.d.ts +17 -2
- package/dist/collections/default-collections.d.ts +255 -2
- package/dist/data/filter-dialect.d.ts +61 -0
- package/dist/data/query_builder.d.ts +4 -4
- package/dist/data/resolveDataSource.d.ts +7 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +604 -188
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +611 -186
- package/dist/index.umd.js.map +1 -1
- package/dist/util/builders.d.ts +48 -1
- package/dist/util/callbacks.d.ts +6 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/permissions.d.ts +26 -2
- package/dist/util/policy/evaluatePolicy.d.ts +31 -0
- package/dist/util/policy/index.d.ts +3 -0
- package/dist/util/policy/policyToPostgres.d.ts +10 -0
- package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
- package/dist/util/policy/sqlToPolicy.d.ts +20 -0
- package/dist/util/storage.d.ts +26 -1
- package/package.json +13 -13
- package/src/collections/CollectionRegistry.ts +59 -28
- package/src/collections/default-collections.ts +4 -4
- package/src/data/buildRebaseData.ts +9 -120
- package/src/data/filter-dialect.ts +318 -0
- package/src/data/query_builder.ts +10 -10
- package/src/data/resolveDataSource.ts +9 -9
- package/src/index.ts +1 -0
- package/src/util/builders.ts +78 -1
- package/src/util/callbacks.ts +8 -1
- package/src/util/index.ts +1 -0
- package/src/util/permissions.test.ts +5 -3
- package/src/util/permissions.ts +85 -158
- package/src/util/policy/evaluatePolicy.ts +146 -0
- package/src/util/policy/index.ts +3 -0
- package/src/util/policy/policyToPostgres.ts +85 -0
- package/src/util/policy/securityRuleToConditions.ts +67 -0
- package/src/util/policy/sqlToPolicy.ts +88 -0
- package/src/util/references.ts +1 -1
- package/src/util/relations.ts +8 -9
- package/src/util/resolutions.ts +6 -6
- 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.
|
|
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.
|
|
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.
|
|
391
|
-
const relCollection = collection;
|
|
390
|
+
if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
|
|
392
391
|
const relations = {};
|
|
393
392
|
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
394
|
-
if (
|
|
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.
|
|
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
|
-
|
|
647
|
-
if (getDataSourceCapabilities(collection.
|
|
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/
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
709
|
-
return
|
|
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
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
749
|
-
|
|
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
|
-
|
|
752
|
-
|
|
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
|
-
|
|
756
|
-
|
|
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
|
|
981
|
+
const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
|
|
759
982
|
if (applicableRules.length === 0) return false;
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
990
|
+
let hasPermissive = false;
|
|
991
|
+
for (const rule of applicableRules) {
|
|
769
992
|
const mode = rule.mode || "permissive";
|
|
770
|
-
const passed =
|
|
771
|
-
if (mode === "restrictive"
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
|
1431
|
-
*
|
|
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
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
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 ??
|
|
1701
|
+
const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;
|
|
1446
1702
|
const def = registry?.[key];
|
|
1447
|
-
const engine = def?.engine ?? collection?.
|
|
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
|
|
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
|
-
|
|
1557
|
-
const
|
|
1558
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1590
|
-
|
|
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.
|
|
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,
|
|
1949
|
-
this.params.orderBy = `${column}:${
|
|
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/
|
|
2282
|
+
//#region src/data/filter-dialect.ts
|
|
2005
2283
|
/**
|
|
2006
|
-
*
|
|
2284
|
+
* REST wire-format adapter for the unified filter system.
|
|
2007
2285
|
*
|
|
2008
|
-
*
|
|
2009
|
-
* -
|
|
2010
|
-
*
|
|
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
|
-
*
|
|
2290
|
+
* @module
|
|
2014
2291
|
*/
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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 (
|
|
2060
|
-
|
|
2061
|
-
if (
|
|
2062
|
-
|
|
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
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|