@rebasepro/common 0.6.1 → 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 +30 -2
- package/dist/collections/default-collections.d.ts +255 -2
- package/dist/data/buildRoutedRebaseData.d.ts +53 -0
- package/dist/data/filter-dialect.d.ts +61 -0
- package/dist/data/query_builder.d.ts +4 -4
- package/dist/data/resolveDataSource.d.ts +43 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +777 -178
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +793 -176
- package/dist/index.umd.js.map +1 -1
- package/dist/table-classification.d.ts +47 -0
- 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 +3 -3
- package/src/collections/CollectionRegistry.ts +80 -16
- package/src/collections/default-collections.ts +4 -4
- package/src/data/buildRebaseData.ts +9 -120
- package/src/data/buildRoutedRebaseData.ts +97 -0
- package/src/data/filter-dialect.ts +318 -0
- package/src/data/query_builder.ts +10 -10
- package/src/data/resolveDataSource.ts +79 -0
- package/src/index.ts +4 -1
- package/src/table-classification.ts +109 -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") {
|
|
@@ -1440,8 +1696,76 @@
|
|
|
1440
1696
|
return result;
|
|
1441
1697
|
}
|
|
1442
1698
|
//#endregion
|
|
1699
|
+
//#region src/data/resolveDataSource.ts
|
|
1700
|
+
/**
|
|
1701
|
+
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
1702
|
+
* Later entries win on key collision.
|
|
1703
|
+
*/
|
|
1704
|
+
function createDataSourceRegistry(definitions) {
|
|
1705
|
+
const registry = {};
|
|
1706
|
+
for (const def of definitions ?? []) registry[def.key] = def;
|
|
1707
|
+
return registry;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Resolve the effective data source for a collection — the single source of
|
|
1711
|
+
* truth shared by the frontend router, the backend driver registry, and the
|
|
1712
|
+
* editor's capability lookups.
|
|
1713
|
+
*
|
|
1714
|
+
* Resolution order:
|
|
1715
|
+
* 1. The routing **key** is `collection.dataSource`, else
|
|
1716
|
+
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
1717
|
+
* 2. If a definition is registered for that key, it provides `engine`,
|
|
1718
|
+
* `transport`, and `databaseId`.
|
|
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.
|
|
1722
|
+
*
|
|
1723
|
+
* `capabilities` are always derived from the resolved `engine`, so two
|
|
1724
|
+
* data sources sharing an engine share capabilities.
|
|
1725
|
+
*
|
|
1726
|
+
* @param collection the collection (or any object carrying the routing fields)
|
|
1727
|
+
* @param registry optional registry of declared data sources
|
|
1728
|
+
*/
|
|
1729
|
+
function resolveDataSource(collection, registry) {
|
|
1730
|
+
const key = collection?.dataSource ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
|
|
1731
|
+
const def = registry?.[key];
|
|
1732
|
+
const engine = def?.engine ?? collection?.engine ?? (key !== _rebasepro_types.DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
1733
|
+
return {
|
|
1734
|
+
key,
|
|
1735
|
+
engine,
|
|
1736
|
+
transport: def?.transport ?? "server",
|
|
1737
|
+
databaseId: collection?.databaseId ?? def?.databaseId,
|
|
1738
|
+
capabilities: (0, _rebasepro_types.getDataSourceCapabilities)(engine)
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
//#endregion
|
|
1443
1742
|
//#region src/collections/CollectionRegistry.ts
|
|
1444
1743
|
var CollectionRegistry = class {
|
|
1744
|
+
/**
|
|
1745
|
+
* Declared data sources, used during normalization to resolve each
|
|
1746
|
+
* collection's engine (so `dataSource`-only collections get the right
|
|
1747
|
+
* capabilities). Empty by default.
|
|
1748
|
+
*/
|
|
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
|
+
}
|
|
1445
1769
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
1446
1770
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
1447
1771
|
rootCollections = [];
|
|
@@ -1451,9 +1775,20 @@
|
|
|
1451
1775
|
rawRootCollections = [];
|
|
1452
1776
|
cachedRawCollectionsList = null;
|
|
1453
1777
|
lastRawInputSnapshot = null;
|
|
1454
|
-
constructor(collections) {
|
|
1778
|
+
constructor(collections, dataSources) {
|
|
1779
|
+
if (dataSources) this.dataSources = dataSources;
|
|
1455
1780
|
if (collections) this.registerMultiple(collections);
|
|
1456
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Provide the declared data sources used to resolve each collection's
|
|
1784
|
+
* engine during normalization. Set this before registering collections.
|
|
1785
|
+
* Returns true if the registry changed (callers may re-register).
|
|
1786
|
+
*/
|
|
1787
|
+
setDataSources(dataSources) {
|
|
1788
|
+
if ((0, fast_equals.deepEqual)(this.dataSources, dataSources)) return false;
|
|
1789
|
+
this.dataSources = dataSources ?? {};
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1457
1792
|
reset() {
|
|
1458
1793
|
this.collectionsByTableName.clear();
|
|
1459
1794
|
this.collectionsBySlug.clear();
|
|
@@ -1522,9 +1857,14 @@
|
|
|
1522
1857
|
}
|
|
1523
1858
|
normalizeCollection(collection) {
|
|
1524
1859
|
const result = { ...collection };
|
|
1860
|
+
{
|
|
1861
|
+
const resolved = resolveDataSource(result, this.dataSources);
|
|
1862
|
+
if (!result.dataSource) result.dataSource = resolved.key;
|
|
1863
|
+
if (!result.engine) result.engine = resolved.engine;
|
|
1864
|
+
}
|
|
1525
1865
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
1526
1866
|
const relResult = result;
|
|
1527
|
-
const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.
|
|
1867
|
+
const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations ? relResult.relations ?? [] : [];
|
|
1528
1868
|
const mergedRelationsRaw = [...extractedRelations];
|
|
1529
1869
|
for (const manual of manualRelations) {
|
|
1530
1870
|
const name = manual.relationName;
|
|
@@ -1539,7 +1879,7 @@
|
|
|
1539
1879
|
}
|
|
1540
1880
|
}
|
|
1541
1881
|
let mergedRelations = mergedRelationsRaw;
|
|
1542
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(result.
|
|
1882
|
+
if ((0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations) {
|
|
1543
1883
|
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
1544
1884
|
try {
|
|
1545
1885
|
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
@@ -1551,8 +1891,10 @@
|
|
|
1551
1891
|
}
|
|
1552
1892
|
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
1553
1893
|
if (!result.childCollections) {
|
|
1554
|
-
|
|
1555
|
-
|
|
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) {
|
|
1556
1898
|
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
1557
1899
|
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
1558
1900
|
const target = r.target();
|
|
@@ -1654,7 +1996,7 @@
|
|
|
1654
1996
|
if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
|
|
1655
1997
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
1656
1998
|
const relationKey = pathSegments[i];
|
|
1657
|
-
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}'`);
|
|
1658
2000
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
1659
2001
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
1660
2002
|
const target = relation.target();
|
|
@@ -1715,7 +2057,7 @@
|
|
|
1715
2057
|
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
1716
2058
|
* override by defining their own collection with `slug: "users"`.
|
|
1717
2059
|
*/
|
|
1718
|
-
var defaultUsersCollection = {
|
|
2060
|
+
var defaultUsersCollection = defineCollection({
|
|
1719
2061
|
name: "Users",
|
|
1720
2062
|
singularName: "User",
|
|
1721
2063
|
slug: "users",
|
|
@@ -1763,7 +2105,7 @@
|
|
|
1763
2105
|
name: "Photo URL",
|
|
1764
2106
|
type: "string",
|
|
1765
2107
|
columnName: "photo_url",
|
|
1766
|
-
url: "image"
|
|
2108
|
+
ui: { url: "image" }
|
|
1767
2109
|
},
|
|
1768
2110
|
roles: {
|
|
1769
2111
|
name: "Roles",
|
|
@@ -1858,7 +2200,7 @@
|
|
|
1858
2200
|
"roles",
|
|
1859
2201
|
"createdAt"
|
|
1860
2202
|
]
|
|
1861
|
-
};
|
|
2203
|
+
});
|
|
1862
2204
|
//#endregion
|
|
1863
2205
|
//#region src/data/query_builder.ts
|
|
1864
2206
|
function or(...conditions) {
|
|
@@ -1910,8 +2252,8 @@
|
|
|
1910
2252
|
* @example
|
|
1911
2253
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
1912
2254
|
*/
|
|
1913
|
-
orderBy(column,
|
|
1914
|
-
this.params.orderBy = `${column}:${
|
|
2255
|
+
orderBy(column, direction = "asc") {
|
|
2256
|
+
this.params.orderBy = `${column}:${direction}`;
|
|
1915
2257
|
return this;
|
|
1916
2258
|
}
|
|
1917
2259
|
/**
|
|
@@ -1966,80 +2308,216 @@
|
|
|
1966
2308
|
}
|
|
1967
2309
|
};
|
|
1968
2310
|
//#endregion
|
|
1969
|
-
//#region src/data/
|
|
2311
|
+
//#region src/data/filter-dialect.ts
|
|
1970
2312
|
/**
|
|
1971
|
-
*
|
|
2313
|
+
* REST wire-format adapter for the unified filter system.
|
|
1972
2314
|
*
|
|
1973
|
-
*
|
|
1974
|
-
* -
|
|
1975
|
-
*
|
|
1976
|
-
* - 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.
|
|
1977
2318
|
*
|
|
1978
|
-
*
|
|
2319
|
+
* @module
|
|
1979
2320
|
*/
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
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;
|
|
2016
2356
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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;
|
|
2022
2422
|
continue;
|
|
2023
2423
|
}
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
2026
|
-
if (
|
|
2027
|
-
|
|
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;
|
|
2028
2428
|
continue;
|
|
2029
2429
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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;
|
|
2039
2484
|
}
|
|
2485
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
2486
|
+
return {
|
|
2487
|
+
type,
|
|
2488
|
+
conditions
|
|
2489
|
+
};
|
|
2040
2490
|
}
|
|
2041
|
-
|
|
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
|
+
};
|
|
2042
2518
|
}
|
|
2519
|
+
//#endregion
|
|
2520
|
+
//#region src/data/buildRebaseData.ts
|
|
2043
2521
|
/**
|
|
2044
2522
|
* Parse an orderBy string like "created_at:desc" into [field, direction].
|
|
2045
2523
|
*/
|
|
@@ -2052,11 +2530,12 @@
|
|
|
2052
2530
|
const accessor = {
|
|
2053
2531
|
async find(params) {
|
|
2054
2532
|
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2533
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2055
2534
|
const entities = await driver.fetchCollection({
|
|
2056
2535
|
path: slug,
|
|
2057
2536
|
limit: params?.limit,
|
|
2058
2537
|
offset: params?.offset,
|
|
2059
|
-
filter
|
|
2538
|
+
filter,
|
|
2060
2539
|
orderBy: orderParsed?.[0],
|
|
2061
2540
|
order: orderParsed?.[1],
|
|
2062
2541
|
searchString: params?.searchString
|
|
@@ -2106,9 +2585,10 @@
|
|
|
2106
2585
|
return driver.deleteAll(slug);
|
|
2107
2586
|
} : void 0,
|
|
2108
2587
|
count: driver.countEntities ? async (params) => {
|
|
2588
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2109
2589
|
return driver.countEntities({
|
|
2110
2590
|
path: slug,
|
|
2111
|
-
filter
|
|
2591
|
+
filter
|
|
2112
2592
|
});
|
|
2113
2593
|
} : void 0,
|
|
2114
2594
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
@@ -2119,7 +2599,7 @@
|
|
|
2119
2599
|
path: slug,
|
|
2120
2600
|
limit: params?.limit,
|
|
2121
2601
|
offset: params?.offset,
|
|
2122
|
-
filter:
|
|
2602
|
+
filter: params?.where,
|
|
2123
2603
|
orderBy: orderParsed?.[0],
|
|
2124
2604
|
order: orderParsed?.[1],
|
|
2125
2605
|
searchString: params?.searchString,
|
|
@@ -2198,11 +2678,133 @@
|
|
|
2198
2678
|
} });
|
|
2199
2679
|
}
|
|
2200
2680
|
//#endregion
|
|
2681
|
+
//#region src/data/buildRoutedRebaseData.ts
|
|
2682
|
+
/**
|
|
2683
|
+
* Build a {@link RebaseData} that routes each collection to the right
|
|
2684
|
+
* backend based on its resolved data source.
|
|
2685
|
+
*
|
|
2686
|
+
* `.collection(path)` (and dynamic `data.products`-style access) resolves the
|
|
2687
|
+
* collection's data-source key via `resolveKey` and delegates to the matching
|
|
2688
|
+
* entry in `sources`, falling back to `defaultData` when there is no match.
|
|
2689
|
+
* Because routing keys off the *path being accessed*, a reference widget
|
|
2690
|
+
* inside a Firestore form that points at a Postgres collection is still
|
|
2691
|
+
* served by Postgres — routing follows the target, not the ancestor.
|
|
2692
|
+
*
|
|
2693
|
+
* When `sources` is empty this returns `defaultData` untouched, so the
|
|
2694
|
+
* single-driver setup keeps identical behaviour and identity (important for
|
|
2695
|
+
* effect dependencies that key off the data instance).
|
|
2696
|
+
*
|
|
2697
|
+
* @example
|
|
2698
|
+
* const data = buildRoutedRebaseData({
|
|
2699
|
+
* defaultData: client.data,
|
|
2700
|
+
* sources: { analytics: buildRebaseData(firestoreDriver) },
|
|
2701
|
+
* resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
|
|
2702
|
+
* });
|
|
2703
|
+
* await data.products.find(); // → default (server / Postgres)
|
|
2704
|
+
* await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
|
|
2705
|
+
*/
|
|
2706
|
+
function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
|
|
2707
|
+
if (!sources || Object.keys(sources).length === 0) return defaultData;
|
|
2708
|
+
function resolve(slugOrPath) {
|
|
2709
|
+
const key = resolveKey(slugOrPath);
|
|
2710
|
+
if (key && sources[key]) return sources[key];
|
|
2711
|
+
return defaultData;
|
|
2712
|
+
}
|
|
2713
|
+
function getAccessor(slugOrPath) {
|
|
2714
|
+
return resolve(slugOrPath).collection(slugOrPath);
|
|
2715
|
+
}
|
|
2716
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2717
|
+
if (prop === "collection") return getAccessor;
|
|
2718
|
+
if (typeof prop === "symbol") return void 0;
|
|
2719
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
2720
|
+
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
2721
|
+
} });
|
|
2722
|
+
}
|
|
2723
|
+
//#endregion
|
|
2724
|
+
//#region src/table-classification.ts
|
|
2725
|
+
/** Schemas that are always considered Rebase-internal. */
|
|
2726
|
+
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
2727
|
+
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
2728
|
+
var REBASE_INTERNAL_PREFIXES = [
|
|
2729
|
+
"_rebase_",
|
|
2730
|
+
"_auth_",
|
|
2731
|
+
"drizzle_"
|
|
2732
|
+
];
|
|
2733
|
+
/**
|
|
2734
|
+
* Synchronously classify a table based on naming conventions.
|
|
2735
|
+
*
|
|
2736
|
+
* @param tableName - The unqualified name of the table.
|
|
2737
|
+
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
2738
|
+
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
2739
|
+
* carries a reserved prefix; `"user"` otherwise.
|
|
2740
|
+
*
|
|
2741
|
+
* @remarks
|
|
2742
|
+
* Junction-table detection requires an async database query and is therefore
|
|
2743
|
+
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
2744
|
+
* the set of junction tables, then reclassify as needed.
|
|
2745
|
+
*/
|
|
2746
|
+
function classifyTable(tableName, schemaName) {
|
|
2747
|
+
if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
|
|
2748
|
+
return "user";
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
2752
|
+
*
|
|
2753
|
+
* @param tableName - The unqualified name of the table.
|
|
2754
|
+
* @param schemaName - The schema the table belongs to.
|
|
2755
|
+
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
2756
|
+
*/
|
|
2757
|
+
function isRebaseInternalTable(tableName, schemaName) {
|
|
2758
|
+
return classifyTable(tableName, schemaName) === "rebase-internal";
|
|
2759
|
+
}
|
|
2760
|
+
/** SQL query that detects junction tables in the `public` schema. */
|
|
2761
|
+
var JUNCTION_TABLES_SQL = `
|
|
2762
|
+
SELECT t.table_name
|
|
2763
|
+
FROM information_schema.tables t
|
|
2764
|
+
WHERE t.table_schema = 'public'
|
|
2765
|
+
AND t.table_type = 'BASE TABLE'
|
|
2766
|
+
AND NOT EXISTS (
|
|
2767
|
+
SELECT 1
|
|
2768
|
+
FROM information_schema.columns c
|
|
2769
|
+
WHERE c.table_schema = t.table_schema
|
|
2770
|
+
AND c.table_name = t.table_name
|
|
2771
|
+
AND c.column_name NOT IN (
|
|
2772
|
+
SELECT kcu.column_name
|
|
2773
|
+
FROM information_schema.key_column_usage kcu
|
|
2774
|
+
JOIN information_schema.table_constraints tc
|
|
2775
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
2776
|
+
AND tc.table_schema = kcu.table_schema
|
|
2777
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
2778
|
+
AND kcu.table_schema = t.table_schema
|
|
2779
|
+
AND kcu.table_name = t.table_name
|
|
2780
|
+
)
|
|
2781
|
+
)
|
|
2782
|
+
`;
|
|
2783
|
+
/**
|
|
2784
|
+
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
2785
|
+
*
|
|
2786
|
+
* A junction table is defined as a table where **every** column participates in
|
|
2787
|
+
* at least one foreign-key constraint.
|
|
2788
|
+
*
|
|
2789
|
+
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
2790
|
+
* resulting rows.
|
|
2791
|
+
* @returns A `Set` containing the names of all detected junction tables.
|
|
2792
|
+
*/
|
|
2793
|
+
async function detectJunctionTables(executeSql) {
|
|
2794
|
+
const rows = await executeSql(JUNCTION_TABLES_SQL);
|
|
2795
|
+
const junctionTables = /* @__PURE__ */ new Set();
|
|
2796
|
+
for (const row of rows) if (typeof row.table_name === "string") junctionTables.add(row.table_name);
|
|
2797
|
+
return junctionTables;
|
|
2798
|
+
}
|
|
2799
|
+
//#endregion
|
|
2201
2800
|
exports.COLLECTION_PATH_SEPARATOR = COLLECTION_PATH_SEPARATOR;
|
|
2202
2801
|
exports.CollectionRegistry = CollectionRegistry;
|
|
2203
2802
|
exports.DEFAULT_ONE_OF_TYPE = DEFAULT_ONE_OF_TYPE;
|
|
2204
2803
|
exports.DEFAULT_ONE_OF_VALUE = DEFAULT_ONE_OF_VALUE;
|
|
2804
|
+
exports.JUNCTION_TABLES_SQL = JUNCTION_TABLES_SQL;
|
|
2205
2805
|
exports.QueryBuilder = QueryBuilder;
|
|
2806
|
+
exports.REBASE_INTERNAL_PREFIXES = REBASE_INTERNAL_PREFIXES;
|
|
2807
|
+
exports.REBASE_INTERNAL_SCHEMAS = REBASE_INTERNAL_SCHEMAS;
|
|
2206
2808
|
exports.addInitialSlash = addInitialSlash;
|
|
2207
2809
|
exports.and = and;
|
|
2208
2810
|
exports.applyPropertyConditions = applyPropertyConditions;
|
|
@@ -2217,17 +2819,25 @@
|
|
|
2217
2819
|
exports.buildProperty = buildProperty;
|
|
2218
2820
|
exports.buildPropertyCallbacks = buildPropertyCallbacks;
|
|
2219
2821
|
exports.buildRebaseData = buildRebaseData;
|
|
2822
|
+
exports.buildRoutedRebaseData = buildRoutedRebaseData;
|
|
2220
2823
|
exports.canCreateEntity = canCreateEntity;
|
|
2221
2824
|
exports.canDeleteEntity = canDeleteEntity;
|
|
2222
2825
|
exports.canEditEntity = canEditEntity;
|
|
2223
2826
|
exports.canReadCollection = canReadCollection;
|
|
2224
2827
|
exports.checkOperation = checkOperation;
|
|
2828
|
+
exports.classifyTable = classifyTable;
|
|
2225
2829
|
exports.cond = cond;
|
|
2830
|
+
exports.createDataSourceRegistry = createDataSourceRegistry;
|
|
2226
2831
|
exports.createRelationRef = createRelationRef;
|
|
2227
2832
|
exports.createRelationRefWithData = createRelationRefWithData;
|
|
2228
2833
|
exports.defaultUsersCollection = defaultUsersCollection;
|
|
2834
|
+
exports.defineCollection = defineCollection;
|
|
2835
|
+
exports.deserializeFilter = deserializeFilter;
|
|
2836
|
+
exports.deserializeLogicalCondition = deserializeLogicalCondition;
|
|
2837
|
+
exports.detectJunctionTables = detectJunctionTables;
|
|
2229
2838
|
exports.enumToObjectEntries = enumToObjectEntries;
|
|
2230
2839
|
exports.evaluateCondition = evaluateCondition;
|
|
2840
|
+
exports.evaluatePolicy = evaluatePolicy;
|
|
2231
2841
|
exports.findRelation = findRelation;
|
|
2232
2842
|
exports.fullPathToCollectionSegments = fullPathToCollectionSegments;
|
|
2233
2843
|
exports.getArrayResolvedProperties = getArrayResolvedProperties;
|
|
@@ -2253,8 +2863,10 @@
|
|
|
2253
2863
|
exports.isHidden = isHidden;
|
|
2254
2864
|
exports.isPropertyBuilder = isPropertyBuilder;
|
|
2255
2865
|
exports.isReadOnly = isReadOnly;
|
|
2866
|
+
exports.isRebaseInternalTable = isRebaseInternalTable;
|
|
2256
2867
|
exports.normalizeToEntityRelation = normalizeToEntityRelation;
|
|
2257
2868
|
exports.or = or;
|
|
2869
|
+
exports.policyToPostgres = policyToPostgres;
|
|
2258
2870
|
exports.registerConditionOperations = registerConditionOperations;
|
|
2259
2871
|
exports.removeInitialAndTrailingSlashes = removeInitialAndTrailingSlashes;
|
|
2260
2872
|
exports.removeInitialSlash = removeInitialSlash;
|
|
@@ -2262,6 +2874,7 @@
|
|
|
2262
2874
|
exports.resolveArrayProperties = resolveArrayProperties;
|
|
2263
2875
|
exports.resolveCollectionPathIds = resolveCollectionPathIds;
|
|
2264
2876
|
exports.resolveCollectionRelations = resolveCollectionRelations;
|
|
2877
|
+
exports.resolveDataSource = resolveDataSource;
|
|
2265
2878
|
exports.resolveDefaultSelectedView = resolveDefaultSelectedView;
|
|
2266
2879
|
exports.resolveEnumValues = resolveEnumValues;
|
|
2267
2880
|
exports.resolveProperties = resolveProperties;
|
|
@@ -2271,9 +2884,13 @@
|
|
|
2271
2884
|
exports.resolveRelationProperty = resolveRelationProperty;
|
|
2272
2885
|
exports.resolveStorageFilenameString = resolveStorageFilenameString;
|
|
2273
2886
|
exports.resolveStoragePathString = resolveStoragePathString;
|
|
2887
|
+
exports.resolveStorageSource = resolveStorageSource;
|
|
2274
2888
|
exports.sanitizeData = sanitizeData;
|
|
2275
2889
|
exports.sanitizeRelation = sanitizeRelation;
|
|
2890
|
+
exports.securityRuleToConditions = securityRuleToConditions;
|
|
2276
2891
|
exports.segmentsToStrippedPath = segmentsToStrippedPath;
|
|
2892
|
+
exports.serializeFilter = serializeFilter;
|
|
2893
|
+
exports.serializeLogicalCondition = serializeLogicalCondition;
|
|
2277
2894
|
exports.sortProperties = sortProperties;
|
|
2278
2895
|
exports.stripCollectionPath = stripCollectionPath;
|
|
2279
2896
|
exports.traverseValueProperty = traverseValueProperty;
|