@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.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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
676
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(collection.
|
|
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/
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
718
|
-
|
|
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
|
-
|
|
738
|
-
return
|
|
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
|
|
776
|
-
if (
|
|
777
|
-
if (
|
|
778
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
785
|
-
|
|
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
|
|
1010
|
+
const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
|
|
788
1011
|
if (applicableRules.length === 0) return false;
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
1019
|
+
let hasPermissive = false;
|
|
1020
|
+
for (const rule of applicableRules) {
|
|
798
1021
|
const mode = rule.mode || "permissive";
|
|
799
|
-
const passed =
|
|
800
|
-
if (mode === "restrictive"
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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
|
|
1460
|
-
*
|
|
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
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
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 ??
|
|
1730
|
+
const key = collection?.dataSource ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
|
|
1475
1731
|
const def = registry?.[key];
|
|
1476
|
-
const engine = def?.engine ?? collection?.
|
|
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
|
|
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
|
-
|
|
1586
|
-
const
|
|
1587
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1619
|
-
|
|
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.
|
|
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,
|
|
1978
|
-
this.params.orderBy = `${column}:${
|
|
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/
|
|
2311
|
+
//#region src/data/filter-dialect.ts
|
|
2034
2312
|
/**
|
|
2035
|
-
*
|
|
2313
|
+
* REST wire-format adapter for the unified filter system.
|
|
2036
2314
|
*
|
|
2037
|
-
*
|
|
2038
|
-
* -
|
|
2039
|
-
*
|
|
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
|
-
*
|
|
2319
|
+
* @module
|
|
2043
2320
|
*/
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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 (
|
|
2089
|
-
|
|
2090
|
-
if (
|
|
2091
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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;
|