@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.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityReference, EntityRelation, getDataSourceCapabilities } from "@rebasepro/types";
|
|
1
|
+
import { CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, policy, toCanonicalOp } from "@rebasepro/types";
|
|
2
2
|
import { deepClone, generateForeignKeyName, getIn, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
import jsonLogic from "json-logic-js";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
@@ -336,7 +336,7 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
|
336
336
|
if (!newRelation.foreignKeyOnTarget) {
|
|
337
337
|
let foundForeignKey = false;
|
|
338
338
|
try {
|
|
339
|
-
const targetRelations = getDataSourceCapabilities(targetCollection.
|
|
339
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
|
|
340
340
|
for (const targetRel of targetRelations) if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) try {
|
|
341
341
|
if (targetRel.target().slug === sourceCollection.slug) {
|
|
342
342
|
newRelation.foreignKeyOnTarget = targetRel.localKey;
|
|
@@ -352,7 +352,7 @@ function sanitizeRelation(relation, sourceCollection, resolveCollection) {
|
|
|
352
352
|
} else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
|
|
353
353
|
let isManyToManyInverse = false;
|
|
354
354
|
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) try {
|
|
355
|
-
const targetRelations = getDataSourceCapabilities(targetCollection.
|
|
355
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.engine).supportsRelations ? targetCollection.relations || [] : [];
|
|
356
356
|
for (const targetRel of targetRelations) if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
|
|
357
357
|
isManyToManyInverse = true;
|
|
358
358
|
break;
|
|
@@ -387,11 +387,10 @@ var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
|
387
387
|
function resolveCollectionRelations(collection) {
|
|
388
388
|
const cached = _resolvedRelationsCache.get(collection);
|
|
389
389
|
if (cached) return cached;
|
|
390
|
-
if (!getDataSourceCapabilities(collection.
|
|
391
|
-
const relCollection = collection;
|
|
390
|
+
if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
|
|
392
391
|
const relations = {};
|
|
393
392
|
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
394
|
-
if (
|
|
393
|
+
if (collection.relations) collection.relations.forEach((relation) => {
|
|
395
394
|
try {
|
|
396
395
|
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
397
396
|
const relationKey = normalizedRelation.relationName;
|
|
@@ -438,7 +437,7 @@ function resolvePropertyRelation({ propertyKey, property, sourceCollection }) {
|
|
|
438
437
|
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
439
438
|
}
|
|
440
439
|
function getTableName(collection) {
|
|
441
|
-
if (getDataSourceCapabilities(collection.
|
|
440
|
+
if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
442
441
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
443
442
|
}
|
|
444
443
|
function getTableVarName(tableName) {
|
|
@@ -643,8 +642,9 @@ function resolveEnumValues(input) {
|
|
|
643
642
|
}
|
|
644
643
|
function getSubcollections(collection) {
|
|
645
644
|
if (collection.childCollections) return collection.childCollections() ?? [];
|
|
646
|
-
|
|
647
|
-
if (getDataSourceCapabilities(collection.
|
|
645
|
+
const declaredSubcollections = getDeclaredSubcollections(collection);
|
|
646
|
+
if (getDataSourceCapabilities(collection.engine).supportsSubcollections && declaredSubcollections) return declaredSubcollections() ?? [];
|
|
647
|
+
if (getDataSourceCapabilities(collection.engine).supportsRelations) {
|
|
648
648
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
649
649
|
return Object.values(resolvedRelations).filter((r) => r.cardinality === "many").map((r) => {
|
|
650
650
|
const target = r.target();
|
|
@@ -670,113 +670,339 @@ function getSubcollections(collection) {
|
|
|
670
670
|
return [];
|
|
671
671
|
}
|
|
672
672
|
//#endregion
|
|
673
|
-
//#region src/util/
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
673
|
+
//#region src/util/policy/sqlToPolicy.ts
|
|
674
|
+
/**
|
|
675
|
+
* A tiny, regex-based SQL "parser" for security rules.
|
|
676
|
+
*
|
|
677
|
+
* This is NOT a full SQL parser. It is designed to handle the subset of SQL
|
|
678
|
+
* commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
|
|
679
|
+
* optimistic client-side UI decision.
|
|
680
|
+
*
|
|
681
|
+
* It handles:
|
|
682
|
+
* - `field = 'literal'`
|
|
683
|
+
* - `field != 'literal'`
|
|
684
|
+
* - `field = current_setting('app.user_id')`
|
|
685
|
+
* - `A AND B`
|
|
686
|
+
* - `true`
|
|
687
|
+
* - `IN (...)` (as optimistic true)
|
|
688
|
+
*
|
|
689
|
+
* For anything it doesn't understand, it returns a `raw` expression, which
|
|
690
|
+
* the evaluator treats as "unknown" (and usually optimistic true).
|
|
691
|
+
*/
|
|
692
|
+
function sqlToPolicy(sql) {
|
|
693
|
+
const trimmed = sql.trim();
|
|
694
|
+
if (trimmed.toLowerCase() === "true") return policy.true();
|
|
695
|
+
if (trimmed.toLowerCase() === "false") return policy.false();
|
|
696
|
+
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
697
|
+
if (overlapMatch) {
|
|
698
|
+
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
699
|
+
return policy.rolesOverlap(roles);
|
|
700
|
+
}
|
|
701
|
+
const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
702
|
+
if (containMatch) {
|
|
703
|
+
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
704
|
+
return policy.rolesContain(roles);
|
|
705
|
+
}
|
|
706
|
+
if (trimmed.toUpperCase().includes(" OR ")) {
|
|
707
|
+
const parts = trimmed.split(/ OR /i);
|
|
708
|
+
return policy.or(...parts.map(sqlToPolicy));
|
|
709
|
+
}
|
|
710
|
+
if (trimmed.toUpperCase().includes(" AND ")) {
|
|
711
|
+
const parts = trimmed.split(/ AND /i);
|
|
712
|
+
return policy.and(...parts.map(sqlToPolicy));
|
|
713
|
+
}
|
|
714
|
+
const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
|
|
715
|
+
if (match) {
|
|
716
|
+
const [, leftStr, op, rightStr] = match;
|
|
717
|
+
const left = parseOperand(leftStr.trim());
|
|
718
|
+
const right = parseOperand(rightStr.trim());
|
|
719
|
+
if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
720
|
+
}
|
|
721
|
+
return policy.raw(sql);
|
|
722
|
+
}
|
|
723
|
+
function parseOperand(str) {
|
|
724
|
+
if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
|
|
725
|
+
const stringMatch = str.match(/^'(.+)'$/);
|
|
726
|
+
if (stringMatch) return policy.literal(stringMatch[1]);
|
|
727
|
+
if (/^\w+$/.test(str)) return policy.field(str);
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/util/policy/securityRuleToConditions.ts
|
|
732
|
+
/**
|
|
733
|
+
* Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
|
|
734
|
+
* structured `condition`/`check`, and raw `using`/`withCheck` — into a single
|
|
735
|
+
* normalized {@link PolicyExpression} pair.
|
|
736
|
+
*
|
|
737
|
+
* **This is the linchpin against drift:** both the Postgres DDL generators and
|
|
738
|
+
* the client-side evaluator consume this one function, so there is exactly one
|
|
739
|
+
* definition of what a rule means. In particular, application `roles` are folded
|
|
740
|
+
* into the expression here (AND'd with the base condition, matching how Postgres
|
|
741
|
+
* generates the clause) rather than being handled separately by each consumer.
|
|
742
|
+
*/
|
|
743
|
+
function securityRuleToConditions(rule) {
|
|
744
|
+
return {
|
|
745
|
+
usingExpr: withRoles(baseUsing(rule), rule),
|
|
746
|
+
withCheckExpr: withRoles(baseWithCheck(rule), rule)
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function baseUsing(rule) {
|
|
750
|
+
if (rule.condition) return rule.condition;
|
|
751
|
+
if (rule.using != null) return sqlToPolicy(rule.using);
|
|
752
|
+
if (rule.access === "public") return policy.true();
|
|
753
|
+
if (rule.ownerField) return policy.compare(policy.field(rule.ownerField), "eq", policy.authUid());
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
function baseWithCheck(rule) {
|
|
757
|
+
if (rule.check) return rule.check;
|
|
758
|
+
if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
|
|
759
|
+
return baseUsing(rule);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* AND the base condition with an application-role check, or produce a roles-only
|
|
763
|
+
* condition when there is no base. Mirrors the Postgres generator so that a
|
|
764
|
+
* role-scoped restrictive rule denies exactly the same set of users on both
|
|
765
|
+
* sides.
|
|
766
|
+
*/
|
|
767
|
+
function withRoles(base, rule) {
|
|
768
|
+
if (!rule.roles || rule.roles.length === 0) return base;
|
|
769
|
+
const rolesExpr = policy.rolesOverlap(rule.roles);
|
|
770
|
+
if (rule.mode === "restrictive") return base ? policy.or(policy.not(rolesExpr), base) : policy.not(rolesExpr);
|
|
771
|
+
return base ? policy.and(base, rolesExpr) : rolesExpr;
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/util/policy/policyToPostgres.ts
|
|
775
|
+
/**
|
|
776
|
+
* Compiles a {@link PolicyExpression} to a PostgreSQL boolean SQL string,
|
|
777
|
+
* suitable for a `USING (...)` / `WITH CHECK (...)` clause.
|
|
778
|
+
*
|
|
779
|
+
* This is one of the two consumers of the shared policy model (the other being
|
|
780
|
+
* {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
|
|
781
|
+
* and the admin UI derive from the exact same expression.
|
|
782
|
+
*/
|
|
783
|
+
function policyToPostgres(expr, collection) {
|
|
784
|
+
switch (expr.kind) {
|
|
785
|
+
case "true": return "true";
|
|
786
|
+
case "false": return "false";
|
|
787
|
+
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" AND ");
|
|
788
|
+
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${policyToPostgres(o, collection)})`).join(" OR ");
|
|
789
|
+
case "not":
|
|
790
|
+
if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
|
|
791
|
+
return `NOT (${policyToPostgres(expr.operand, collection)})`;
|
|
792
|
+
case "compare": return `${operandToSql(expr.left, collection)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, collection)}`;
|
|
793
|
+
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
794
|
+
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
795
|
+
case "authenticated": return "auth.uid() IS NOT NULL";
|
|
796
|
+
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
var COMPARE_SQL = {
|
|
800
|
+
eq: "=",
|
|
801
|
+
neq: "!=",
|
|
802
|
+
lt: "<",
|
|
803
|
+
lte: "<=",
|
|
804
|
+
gt: ">",
|
|
805
|
+
gte: ">="
|
|
806
|
+
};
|
|
807
|
+
function operandToSql(operand, collection) {
|
|
808
|
+
switch (operand.kind) {
|
|
809
|
+
case "field": return resolveColumnName(operand.name, collection);
|
|
810
|
+
case "literal": return quoteLiteral(operand.value);
|
|
811
|
+
case "authUid": return "auth.uid()";
|
|
812
|
+
case "authRoles": return "string_to_array(auth.roles(), ',')";
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function resolveColumnName(propName, collection) {
|
|
816
|
+
const prop = collection?.properties?.[propName];
|
|
817
|
+
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
818
|
+
return toSnakeCase(propName);
|
|
819
|
+
}
|
|
820
|
+
function quoteLiteral(value) {
|
|
821
|
+
if (value === null) return "NULL";
|
|
822
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
823
|
+
if (typeof value === "number") return String(value);
|
|
824
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
825
|
+
}
|
|
826
|
+
/** Sorted, single-quoted `ARRAY['a','b']` — matches the generators' output. */
|
|
827
|
+
function rolesArraySql(roles) {
|
|
828
|
+
return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
|
|
829
|
+
}
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/util/policy/evaluatePolicy.ts
|
|
832
|
+
/**
|
|
833
|
+
* Evaluates a {@link PolicyExpression} against a user + row, using three-valued
|
|
834
|
+
* (Kleene) logic so that `"unknown"` sub-results propagate soundly.
|
|
835
|
+
*
|
|
836
|
+
* This is the JavaScript twin of {@link policyToPostgres}: both derive from the
|
|
837
|
+
* same expression, so the admin UI matches database enforcement by construction
|
|
838
|
+
* for every non-raw rule.
|
|
839
|
+
*/
|
|
840
|
+
function evaluatePolicy(expr, ctx) {
|
|
841
|
+
switch (expr.kind) {
|
|
842
|
+
case "true": return true;
|
|
843
|
+
case "false": return false;
|
|
844
|
+
case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
845
|
+
case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
846
|
+
case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
|
|
847
|
+
case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
|
|
848
|
+
case "rolesOverlap": {
|
|
849
|
+
const userRoles = ctx.roles ?? [];
|
|
850
|
+
return expr.roles.some((r) => r === "public" || userRoles.includes(r));
|
|
687
851
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const splitByTopLevel = (str, delimiter) => {
|
|
692
|
-
const parts = [];
|
|
693
|
-
let current = "";
|
|
694
|
-
let openCount = 0;
|
|
695
|
-
let i = 0;
|
|
696
|
-
while (i < str.length) {
|
|
697
|
-
if (str[i] === "(") openCount++;
|
|
698
|
-
else if (str[i] === ")") openCount--;
|
|
699
|
-
if (openCount === 0 && str.substring(i).toUpperCase().startsWith(delimiter)) {
|
|
700
|
-
parts.push(current);
|
|
701
|
-
current = "";
|
|
702
|
-
i += delimiter.length;
|
|
703
|
-
} else {
|
|
704
|
-
current += str[i];
|
|
705
|
-
i++;
|
|
706
|
-
}
|
|
852
|
+
case "rolesContain": {
|
|
853
|
+
const userRoles = ctx.roles ?? [];
|
|
854
|
+
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
707
855
|
}
|
|
708
|
-
|
|
709
|
-
return
|
|
710
|
-
};
|
|
711
|
-
const orParts = splitByTopLevel(cleanedSQL, " OR ");
|
|
712
|
-
if (orParts.length > 1) return orParts.some((part) => evaluateAST(part, auth, entity));
|
|
713
|
-
const andParts = splitByTopLevel(cleanedSQL, " AND ");
|
|
714
|
-
if (andParts.length > 1) return andParts.every((part) => evaluateAST(part, auth, entity));
|
|
715
|
-
const upperSQL = cleanedSQL.toUpperCase();
|
|
716
|
-
if (upperSQL.includes(" IN ") || upperSQL.includes(" EXISTS ")) return true;
|
|
717
|
-
const roleIntersectMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\[(.*?)\]/i);
|
|
718
|
-
if (roleIntersectMatch && roleIntersectMatch[1]) {
|
|
719
|
-
const requiredRoles = roleIntersectMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
|
|
720
|
-
const userRoles = auth.user?.roles || [];
|
|
721
|
-
return requiredRoles.some((r) => userRoles.includes(r));
|
|
722
|
-
}
|
|
723
|
-
const roleContainMatch = cleanedSQL.match(/string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\[(.*?)\]/i);
|
|
724
|
-
if (roleContainMatch && roleContainMatch[1]) {
|
|
725
|
-
const requiredRoles = roleContainMatch[1].split(",").map((r) => r.trim().replace(/'/g, ""));
|
|
726
|
-
const userRoles = auth.user?.roles || [];
|
|
727
|
-
return requiredRoles.every((r) => userRoles.includes(r));
|
|
728
|
-
}
|
|
729
|
-
const pattern1 = /* @__PURE__ */ new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
|
|
730
|
-
const pattern2 = /* @__PURE__ */ new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
|
|
731
|
-
const match1 = cleanedSQL.match(pattern1);
|
|
732
|
-
if (match1 && match1[1]) return entity.values[match1[1]] === auth.user?.uid;
|
|
733
|
-
const match2 = cleanedSQL.match(pattern2);
|
|
734
|
-
if (match2 && match2[1]) return entity.values[match2[1]] === auth.user?.uid;
|
|
735
|
-
const simpleEqualityMatch = cleanedSQL.match(/^\{?([\w_]+)\}?\s*(=|!=)\s*'([^']+)'$/i);
|
|
736
|
-
if (simpleEqualityMatch) {
|
|
737
|
-
const field = simpleEqualityMatch[1];
|
|
738
|
-
const operator = simpleEqualityMatch[2];
|
|
739
|
-
const value = simpleEqualityMatch[3];
|
|
740
|
-
const entityValue = entity.values[field];
|
|
741
|
-
if (operator === "=") return entityValue === value;
|
|
742
|
-
if (operator === "!=") return entityValue !== value;
|
|
856
|
+
case "authenticated": return ctx.uid != null;
|
|
857
|
+
case "raw": return "unknown";
|
|
743
858
|
}
|
|
859
|
+
}
|
|
860
|
+
function kleeneAnd$1(values) {
|
|
861
|
+
if (values.some((v) => v === false)) return false;
|
|
862
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
744
863
|
return true;
|
|
745
864
|
}
|
|
746
|
-
function
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
749
|
-
|
|
865
|
+
function kleeneOr(values) {
|
|
866
|
+
if (values.some((v) => v === true)) return true;
|
|
867
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
function kleeneNot(value) {
|
|
871
|
+
if (value === "unknown") return "unknown";
|
|
872
|
+
return !value;
|
|
873
|
+
}
|
|
874
|
+
function resolveOperand(operand, ctx) {
|
|
875
|
+
switch (operand.kind) {
|
|
876
|
+
case "literal": return {
|
|
877
|
+
known: true,
|
|
878
|
+
value: operand.value
|
|
879
|
+
};
|
|
880
|
+
case "authUid": return {
|
|
881
|
+
known: true,
|
|
882
|
+
value: ctx.uid ?? null
|
|
883
|
+
};
|
|
884
|
+
case "authRoles": return {
|
|
885
|
+
known: true,
|
|
886
|
+
value: ctx.roles ?? []
|
|
887
|
+
};
|
|
888
|
+
case "field":
|
|
889
|
+
if (!ctx.entity) return { known: false };
|
|
890
|
+
return {
|
|
891
|
+
known: true,
|
|
892
|
+
value: ctx.entity.values[operand.name]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function evaluateCompare(op, left, right, ctx) {
|
|
897
|
+
const l = resolveOperand(left, ctx);
|
|
898
|
+
const r = resolveOperand(right, ctx);
|
|
899
|
+
if (!l.known || !r.known) return "unknown";
|
|
900
|
+
const a = l.value;
|
|
901
|
+
const b = r.value;
|
|
902
|
+
if (a === null || b === null) {
|
|
903
|
+
if (op === "eq") return false;
|
|
904
|
+
if (op === "neq") return true;
|
|
905
|
+
return "unknown";
|
|
906
|
+
}
|
|
907
|
+
if (op === "eq") return a === b;
|
|
908
|
+
if (op === "neq") return a !== b;
|
|
909
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
910
|
+
if (op === "lt") return a < b;
|
|
911
|
+
if (op === "lte") return a <= b;
|
|
912
|
+
if (op === "gt") return a > b;
|
|
913
|
+
if (op === "gte") return a >= b;
|
|
750
914
|
}
|
|
751
|
-
if (
|
|
752
|
-
|
|
915
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
916
|
+
if (op === "lt") return a < b;
|
|
917
|
+
if (op === "lte") return a <= b;
|
|
918
|
+
if (op === "gt") return a > b;
|
|
919
|
+
if (op === "gte") return a >= b;
|
|
920
|
+
}
|
|
921
|
+
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
922
|
+
if (op === "lt") return a < b;
|
|
923
|
+
if (op === "lte") return a <= b;
|
|
924
|
+
if (op === "gt") return a > b;
|
|
925
|
+
if (op === "gte") return a >= b;
|
|
926
|
+
}
|
|
927
|
+
return "unknown";
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/util/permissions.ts
|
|
931
|
+
/** Combine clause results with AND under three-valued (Kleene) logic. */
|
|
932
|
+
function kleeneAnd(values) {
|
|
933
|
+
if (values.some((v) => v === false)) return false;
|
|
934
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
753
935
|
return true;
|
|
754
936
|
}
|
|
755
|
-
|
|
756
|
-
|
|
937
|
+
/** The operations a rule covers, mirroring the Postgres generator's resolution. */
|
|
938
|
+
function ruleOperations(rule) {
|
|
939
|
+
return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
940
|
+
}
|
|
941
|
+
function ruleApplies(rule, targetOperation) {
|
|
942
|
+
const ops = ruleOperations(rule);
|
|
943
|
+
return ops.includes(targetOperation) || ops.includes("all");
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Evaluate a single rule for one operation, returning a tri-state.
|
|
947
|
+
*
|
|
948
|
+
* A `null` clause (the rule contributes no condition for a required clause)
|
|
949
|
+
* denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
|
|
950
|
+
* in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
|
|
951
|
+
* INSERT/UPDATE; both must pass for UPDATE.
|
|
952
|
+
*/
|
|
953
|
+
function evaluateRuleForOperation(rule, ctx, targetOperation) {
|
|
954
|
+
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
955
|
+
const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
|
|
956
|
+
const needsUsing = targetOperation !== "insert";
|
|
957
|
+
const needsWithCheck = targetOperation === "insert" || targetOperation === "update";
|
|
958
|
+
const results = [];
|
|
959
|
+
if (needsUsing) results.push(clause(usingExpr));
|
|
960
|
+
if (needsWithCheck) results.push(clause(withCheckExpr));
|
|
961
|
+
return kleeneAnd(results);
|
|
962
|
+
}
|
|
963
|
+
function resolveTriState(value, onUnknown) {
|
|
964
|
+
if (value === "unknown") return onUnknown === "allow";
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Decide whether an operation is permitted for a user on a (possibly null) row,
|
|
969
|
+
* by evaluating the collection's security rules with the shared policy model —
|
|
970
|
+
* the same model compiled to Postgres RLS DDL, so the decision matches database
|
|
971
|
+
* enforcement for every non-raw rule.
|
|
972
|
+
*
|
|
973
|
+
* @param options.onUnknown how to treat rules that cannot be decided
|
|
974
|
+
* client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
|
|
975
|
+
* for optimistic UI gating; enforcement callers should pass `"deny"`.
|
|
976
|
+
*/
|
|
977
|
+
function checkOperation(collection, authContext, entity, targetOperation, options) {
|
|
978
|
+
const onUnknown = options?.onUnknown ?? "allow";
|
|
979
|
+
const securityRules = getDataSourceCapabilities(collection.engine).supportsRLS ? collection.securityRules : void 0;
|
|
757
980
|
if (!securityRules || securityRules.length === 0) return true;
|
|
758
|
-
const applicableRules = securityRules.filter((r) => r
|
|
981
|
+
const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
|
|
759
982
|
if (applicableRules.length === 0) return false;
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
if (roleApplicableRules.length === 0) return false;
|
|
983
|
+
const ctx = {
|
|
984
|
+
uid: authContext.user?.uid,
|
|
985
|
+
roles: authContext.user?.roles ?? [],
|
|
986
|
+
entity
|
|
987
|
+
};
|
|
766
988
|
let grantedByPermissive = false;
|
|
767
989
|
let deniedByRestrictive = false;
|
|
768
|
-
|
|
990
|
+
let hasPermissive = false;
|
|
991
|
+
for (const rule of applicableRules) {
|
|
769
992
|
const mode = rule.mode || "permissive";
|
|
770
|
-
const passed =
|
|
771
|
-
if (mode === "restrictive"
|
|
772
|
-
|
|
773
|
-
|
|
993
|
+
const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation), onUnknown);
|
|
994
|
+
if (mode === "restrictive") {
|
|
995
|
+
if (!passed) {
|
|
996
|
+
deniedByRestrictive = true;
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
hasPermissive = true;
|
|
1001
|
+
if (passed) grantedByPermissive = true;
|
|
774
1002
|
}
|
|
775
|
-
if (mode === "permissive" && passed) grantedByPermissive = true;
|
|
776
1003
|
}
|
|
777
1004
|
if (deniedByRestrictive) return false;
|
|
778
|
-
|
|
779
|
-
else return false;
|
|
1005
|
+
return hasPermissive ? grantedByPermissive : false;
|
|
780
1006
|
}
|
|
781
1007
|
function canReadCollection(collection, authContext) {
|
|
782
1008
|
return checkOperation(collection, authContext, null, "select");
|
|
@@ -807,7 +1033,7 @@ function getEntityImagePreviewPropertyKey(collection) {
|
|
|
807
1033
|
}
|
|
808
1034
|
for (const key in collection.properties) {
|
|
809
1035
|
const property = collection.properties[key];
|
|
810
|
-
if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.url === "image") return key;
|
|
1036
|
+
if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") return key;
|
|
811
1037
|
}
|
|
812
1038
|
for (const key in collection.properties) {
|
|
813
1039
|
const property = collection.properties[key];
|
|
@@ -1041,6 +1267,13 @@ function buildCollection(collection) {
|
|
|
1041
1267
|
return collection;
|
|
1042
1268
|
}
|
|
1043
1269
|
/**
|
|
1270
|
+
* Implementation — delegates to the correct overload at the type level.
|
|
1271
|
+
* At runtime this is a plain identity function.
|
|
1272
|
+
*/
|
|
1273
|
+
function defineCollection(collection) {
|
|
1274
|
+
return collection;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1044
1277
|
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1045
1278
|
* the property keys.
|
|
1046
1279
|
* @param property
|
|
@@ -1105,6 +1338,29 @@ function buildAdditionalFieldDelegate(additionalFieldDelegate) {
|
|
|
1105
1338
|
}
|
|
1106
1339
|
//#endregion
|
|
1107
1340
|
//#region src/util/storage.ts
|
|
1341
|
+
/**
|
|
1342
|
+
* Resolve the {@link StorageSource} to use for a property, given the key
|
|
1343
|
+
* referenced by `StorageConfig.storageSource`.
|
|
1344
|
+
*
|
|
1345
|
+
* Resolution priority:
|
|
1346
|
+
* 1. No `sourceKey` → the default source (backward compatible).
|
|
1347
|
+
* 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
|
|
1348
|
+
* 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
|
|
1349
|
+
* 4. Fall back to the default source.
|
|
1350
|
+
*
|
|
1351
|
+
* Shared by the upload hook, the markdown editor, and the read-only previews
|
|
1352
|
+
* so the resolution logic lives in one place.
|
|
1353
|
+
*
|
|
1354
|
+
* @group Storage
|
|
1355
|
+
*/
|
|
1356
|
+
function resolveStorageSource(params) {
|
|
1357
|
+
const { sourceKey, sources, registry, defaultSource } = params;
|
|
1358
|
+
if (!sourceKey) return defaultSource;
|
|
1359
|
+
if (registry) return registry.getOrDefault(sourceKey);
|
|
1360
|
+
const fromSources = sources?.[sourceKey];
|
|
1361
|
+
if (fromSources) return fromSources;
|
|
1362
|
+
return defaultSource;
|
|
1363
|
+
}
|
|
1108
1364
|
async function resolveStorageFilenameString({ input, storage, values, entityId, path, property, file, propertyKey }) {
|
|
1109
1365
|
let result;
|
|
1110
1366
|
if (typeof input === "function") {
|
|
@@ -1411,8 +1667,76 @@ function applyEnumConditions(enumValues, conditions, context) {
|
|
|
1411
1667
|
return result;
|
|
1412
1668
|
}
|
|
1413
1669
|
//#endregion
|
|
1670
|
+
//#region src/data/resolveDataSource.ts
|
|
1671
|
+
/**
|
|
1672
|
+
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
1673
|
+
* Later entries win on key collision.
|
|
1674
|
+
*/
|
|
1675
|
+
function createDataSourceRegistry(definitions) {
|
|
1676
|
+
const registry = {};
|
|
1677
|
+
for (const def of definitions ?? []) registry[def.key] = def;
|
|
1678
|
+
return registry;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Resolve the effective data source for a collection — the single source of
|
|
1682
|
+
* truth shared by the frontend router, the backend driver registry, and the
|
|
1683
|
+
* editor's capability lookups.
|
|
1684
|
+
*
|
|
1685
|
+
* Resolution order:
|
|
1686
|
+
* 1. The routing **key** is `collection.dataSource`, else
|
|
1687
|
+
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
1688
|
+
* 2. If a definition is registered for that key, it provides `engine`,
|
|
1689
|
+
* `transport`, and `databaseId`.
|
|
1690
|
+
* 3. Otherwise values are synthesized: `engine` from `collection.engine`
|
|
1691
|
+
* (or the key, or `"postgres"`), `transport` defaults to `"server"`,
|
|
1692
|
+
* and `databaseId` from the collection.
|
|
1693
|
+
*
|
|
1694
|
+
* `capabilities` are always derived from the resolved `engine`, so two
|
|
1695
|
+
* data sources sharing an engine share capabilities.
|
|
1696
|
+
*
|
|
1697
|
+
* @param collection the collection (or any object carrying the routing fields)
|
|
1698
|
+
* @param registry optional registry of declared data sources
|
|
1699
|
+
*/
|
|
1700
|
+
function resolveDataSource(collection, registry) {
|
|
1701
|
+
const key = collection?.dataSource ?? DEFAULT_DATA_SOURCE_KEY;
|
|
1702
|
+
const def = registry?.[key];
|
|
1703
|
+
const engine = def?.engine ?? collection?.engine ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
1704
|
+
return {
|
|
1705
|
+
key,
|
|
1706
|
+
engine,
|
|
1707
|
+
transport: def?.transport ?? "server",
|
|
1708
|
+
databaseId: collection?.databaseId ?? def?.databaseId,
|
|
1709
|
+
capabilities: getDataSourceCapabilities(engine)
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
//#endregion
|
|
1414
1713
|
//#region src/collections/CollectionRegistry.ts
|
|
1415
1714
|
var CollectionRegistry = class {
|
|
1715
|
+
/**
|
|
1716
|
+
* Declared data sources, used during normalization to resolve each
|
|
1717
|
+
* collection's engine (so `dataSource`-only collections get the right
|
|
1718
|
+
* capabilities). Empty by default.
|
|
1719
|
+
*/
|
|
1720
|
+
dataSources = {};
|
|
1721
|
+
/**
|
|
1722
|
+
* Global lifecycle callbacks applied to every collection.
|
|
1723
|
+
* Runs on all data paths (REST, WebSocket, `rebase.data`).
|
|
1724
|
+
* Execution order: global → collection → property callbacks.
|
|
1725
|
+
*/
|
|
1726
|
+
_globalCallbacks;
|
|
1727
|
+
/**
|
|
1728
|
+
* Set global lifecycle callbacks that apply to every collection.
|
|
1729
|
+
* Typically called once during backend initialization.
|
|
1730
|
+
*/
|
|
1731
|
+
setGlobalCallbacks(callbacks) {
|
|
1732
|
+
this._globalCallbacks = callbacks;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Get the currently registered global callbacks, if any.
|
|
1736
|
+
*/
|
|
1737
|
+
getGlobalCallbacks() {
|
|
1738
|
+
return this._globalCallbacks;
|
|
1739
|
+
}
|
|
1416
1740
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
1417
1741
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
1418
1742
|
rootCollections = [];
|
|
@@ -1422,9 +1746,20 @@ var CollectionRegistry = class {
|
|
|
1422
1746
|
rawRootCollections = [];
|
|
1423
1747
|
cachedRawCollectionsList = null;
|
|
1424
1748
|
lastRawInputSnapshot = null;
|
|
1425
|
-
constructor(collections) {
|
|
1749
|
+
constructor(collections, dataSources) {
|
|
1750
|
+
if (dataSources) this.dataSources = dataSources;
|
|
1426
1751
|
if (collections) this.registerMultiple(collections);
|
|
1427
1752
|
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Provide the declared data sources used to resolve each collection's
|
|
1755
|
+
* engine during normalization. Set this before registering collections.
|
|
1756
|
+
* Returns true if the registry changed (callers may re-register).
|
|
1757
|
+
*/
|
|
1758
|
+
setDataSources(dataSources) {
|
|
1759
|
+
if (deepEqual(this.dataSources, dataSources)) return false;
|
|
1760
|
+
this.dataSources = dataSources ?? {};
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1428
1763
|
reset() {
|
|
1429
1764
|
this.collectionsByTableName.clear();
|
|
1430
1765
|
this.collectionsBySlug.clear();
|
|
@@ -1493,9 +1828,14 @@ var CollectionRegistry = class {
|
|
|
1493
1828
|
}
|
|
1494
1829
|
normalizeCollection(collection) {
|
|
1495
1830
|
const result = { ...collection };
|
|
1831
|
+
{
|
|
1832
|
+
const resolved = resolveDataSource(result, this.dataSources);
|
|
1833
|
+
if (!result.dataSource) result.dataSource = resolved.key;
|
|
1834
|
+
if (!result.engine) result.engine = resolved.engine;
|
|
1835
|
+
}
|
|
1496
1836
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
1497
1837
|
const relResult = result;
|
|
1498
|
-
const manualRelations = getDataSourceCapabilities(result.
|
|
1838
|
+
const manualRelations = getDataSourceCapabilities(result.engine).supportsRelations ? relResult.relations ?? [] : [];
|
|
1499
1839
|
const mergedRelationsRaw = [...extractedRelations];
|
|
1500
1840
|
for (const manual of manualRelations) {
|
|
1501
1841
|
const name = manual.relationName;
|
|
@@ -1510,7 +1850,7 @@ var CollectionRegistry = class {
|
|
|
1510
1850
|
}
|
|
1511
1851
|
}
|
|
1512
1852
|
let mergedRelations = mergedRelationsRaw;
|
|
1513
|
-
if (getDataSourceCapabilities(result.
|
|
1853
|
+
if (getDataSourceCapabilities(result.engine).supportsRelations) {
|
|
1514
1854
|
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
1515
1855
|
try {
|
|
1516
1856
|
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
@@ -1522,8 +1862,10 @@ var CollectionRegistry = class {
|
|
|
1522
1862
|
}
|
|
1523
1863
|
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
1524
1864
|
if (!result.childCollections) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1865
|
+
const capabilities = getDataSourceCapabilities(result.engine);
|
|
1866
|
+
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
1867
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
1868
|
+
else if (capabilities.supportsRelations && relResult.relations) {
|
|
1527
1869
|
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
1528
1870
|
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
1529
1871
|
const target = r.target();
|
|
@@ -1625,7 +1967,7 @@ var CollectionRegistry = class {
|
|
|
1625
1967
|
if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
|
|
1626
1968
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
1627
1969
|
const relationKey = pathSegments[i];
|
|
1628
|
-
if (!getDataSourceCapabilities(currentCollection.
|
|
1970
|
+
if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
1629
1971
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
1630
1972
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
1631
1973
|
const target = relation.target();
|
|
@@ -1686,7 +2028,7 @@ var CollectionRegistry = class {
|
|
|
1686
2028
|
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
1687
2029
|
* override by defining their own collection with `slug: "users"`.
|
|
1688
2030
|
*/
|
|
1689
|
-
var defaultUsersCollection = {
|
|
2031
|
+
var defaultUsersCollection = defineCollection({
|
|
1690
2032
|
name: "Users",
|
|
1691
2033
|
singularName: "User",
|
|
1692
2034
|
slug: "users",
|
|
@@ -1734,7 +2076,7 @@ var defaultUsersCollection = {
|
|
|
1734
2076
|
name: "Photo URL",
|
|
1735
2077
|
type: "string",
|
|
1736
2078
|
columnName: "photo_url",
|
|
1737
|
-
url: "image"
|
|
2079
|
+
ui: { url: "image" }
|
|
1738
2080
|
},
|
|
1739
2081
|
roles: {
|
|
1740
2082
|
name: "Roles",
|
|
@@ -1829,7 +2171,7 @@ var defaultUsersCollection = {
|
|
|
1829
2171
|
"roles",
|
|
1830
2172
|
"createdAt"
|
|
1831
2173
|
]
|
|
1832
|
-
};
|
|
2174
|
+
});
|
|
1833
2175
|
//#endregion
|
|
1834
2176
|
//#region src/data/query_builder.ts
|
|
1835
2177
|
function or(...conditions) {
|
|
@@ -1881,8 +2223,8 @@ var QueryBuilder = class {
|
|
|
1881
2223
|
* @example
|
|
1882
2224
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
1883
2225
|
*/
|
|
1884
|
-
orderBy(column,
|
|
1885
|
-
this.params.orderBy = `${column}:${
|
|
2226
|
+
orderBy(column, direction = "asc") {
|
|
2227
|
+
this.params.orderBy = `${column}:${direction}`;
|
|
1886
2228
|
return this;
|
|
1887
2229
|
}
|
|
1888
2230
|
/**
|
|
@@ -1937,80 +2279,216 @@ var QueryBuilder = class {
|
|
|
1937
2279
|
}
|
|
1938
2280
|
};
|
|
1939
2281
|
//#endregion
|
|
1940
|
-
//#region src/data/
|
|
2282
|
+
//#region src/data/filter-dialect.ts
|
|
1941
2283
|
/**
|
|
1942
|
-
*
|
|
2284
|
+
* REST wire-format adapter for the unified filter system.
|
|
1943
2285
|
*
|
|
1944
|
-
*
|
|
1945
|
-
* -
|
|
1946
|
-
*
|
|
1947
|
-
* - Tuple syntax: { age: [">=", 18], role: ["in", ["admin", "editor"]] }
|
|
2286
|
+
* This module is the ONLY code in the entire codebase that knows about
|
|
2287
|
+
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
2288
|
+
* Everything else speaks `FilterValues` exclusively.
|
|
1948
2289
|
*
|
|
1949
|
-
*
|
|
2290
|
+
* @module
|
|
1950
2291
|
*/
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
continue;
|
|
2292
|
+
/**
|
|
2293
|
+
* Coerce a raw querystring value to its natural JS type.
|
|
2294
|
+
* - `"true"` / `"false"` → boolean
|
|
2295
|
+
* - `"null"` → null
|
|
2296
|
+
* - Numeric strings → number
|
|
2297
|
+
* - Everything else → string (unchanged)
|
|
2298
|
+
*/
|
|
2299
|
+
function coerceValue(raw) {
|
|
2300
|
+
if (raw === "true") return true;
|
|
2301
|
+
if (raw === "false") return false;
|
|
2302
|
+
if (raw === "null") return null;
|
|
2303
|
+
if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
|
|
2304
|
+
return raw;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Serialize a JS value to its querystring representation.
|
|
2308
|
+
*/
|
|
2309
|
+
function stringifyValue(value) {
|
|
2310
|
+
if (value === null) return "null";
|
|
2311
|
+
if (typeof value === "boolean") return String(value);
|
|
2312
|
+
return String(value);
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Serialize a single condition tuple to a PostgREST dot-string.
|
|
2316
|
+
*
|
|
2317
|
+
* @example
|
|
2318
|
+
* serializeTuple(["==", "active"]) // "eq.active"
|
|
2319
|
+
* serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
|
|
2320
|
+
* serializeTuple([">=", 18]) // "gte.18"
|
|
2321
|
+
*/
|
|
2322
|
+
function serializeTuple(tuple) {
|
|
2323
|
+
if (typeof tuple === "string") {
|
|
2324
|
+
if (tuple.includes(".")) {
|
|
2325
|
+
const dotIndex = tuple.indexOf(".");
|
|
2326
|
+
if (REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
|
|
1987
2327
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2328
|
+
return tuple;
|
|
2329
|
+
}
|
|
2330
|
+
if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !CANONICAL_TO_REST[tuple[0]]) return `eq.${stringifyValue(tuple)}`;
|
|
2331
|
+
const [op, value] = tuple;
|
|
2332
|
+
const restOp = CANONICAL_TO_REST[op];
|
|
2333
|
+
if (Array.isArray(value)) return `${restOp}.(${value.map(stringifyValue).join(",")})`;
|
|
2334
|
+
return `${restOp}.${stringifyValue(value)}`;
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Convert `FilterValues` to a PostgREST-style querystring record.
|
|
2338
|
+
*
|
|
2339
|
+
* - Single conditions produce a string value.
|
|
2340
|
+
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
2341
|
+
*
|
|
2342
|
+
* @example
|
|
2343
|
+
* serializeFilter({ status: ["==", "active"] })
|
|
2344
|
+
* // → { status: "eq.active" }
|
|
2345
|
+
*
|
|
2346
|
+
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
2347
|
+
* // → { age: ["gte.18", "lt.65"] }
|
|
2348
|
+
*/
|
|
2349
|
+
function serializeFilter(filter) {
|
|
2350
|
+
const result = {};
|
|
2351
|
+
for (const [field, condition] of Object.entries(filter)) {
|
|
2352
|
+
if (condition === void 0) continue;
|
|
2353
|
+
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
|
|
2354
|
+
else result[field] = serializeTuple(condition);
|
|
2355
|
+
}
|
|
2356
|
+
return result;
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
2360
|
+
*
|
|
2361
|
+
* If the string doesn't match a known operator prefix, falls back to
|
|
2362
|
+
* `["==", originalString]` (treating the whole string as an equality value).
|
|
2363
|
+
*/
|
|
2364
|
+
function deserializeSingle(raw) {
|
|
2365
|
+
const dotIndex = raw.indexOf(".");
|
|
2366
|
+
if (dotIndex === -1) return ["==", coerceValue(raw)];
|
|
2367
|
+
const prefix = raw.substring(0, dotIndex);
|
|
2368
|
+
const rest = raw.substring(dotIndex + 1);
|
|
2369
|
+
const canonicalOp = REST_TO_CANONICAL[prefix];
|
|
2370
|
+
if (!canonicalOp) return ["==", raw];
|
|
2371
|
+
if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, rest.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))];
|
|
2372
|
+
return [canonicalOp, coerceValue(rest)];
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
2376
|
+
*
|
|
2377
|
+
* - String values are parsed as single conditions.
|
|
2378
|
+
* - String arrays (repeated query params) become multiple conditions on the same field.
|
|
2379
|
+
*
|
|
2380
|
+
* @example
|
|
2381
|
+
* deserializeFilter({ status: "eq.active" })
|
|
2382
|
+
* // → { status: ["==", "active"] }
|
|
2383
|
+
*
|
|
2384
|
+
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
2385
|
+
* // → { age: [[">=", 18], ["<", 65]] }
|
|
2386
|
+
*/
|
|
2387
|
+
function deserializeFilter(query) {
|
|
2388
|
+
const result = {};
|
|
2389
|
+
for (const [field, raw] of Object.entries(query)) {
|
|
2390
|
+
if (raw === void 0) continue;
|
|
2391
|
+
if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && toCanonicalOp(raw[0]) === raw[0]) {
|
|
2392
|
+
result[field] = raw;
|
|
1993
2393
|
continue;
|
|
1994
2394
|
}
|
|
1995
|
-
if (
|
|
1996
|
-
|
|
1997
|
-
if (
|
|
1998
|
-
|
|
2395
|
+
if (Array.isArray(raw)) {
|
|
2396
|
+
if (raw.length === 0) continue;
|
|
2397
|
+
if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && toCanonicalOp(raw[0][0]) === raw[0][0]) {
|
|
2398
|
+
result[field] = raw;
|
|
1999
2399
|
continue;
|
|
2000
2400
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2401
|
+
if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
|
|
2402
|
+
else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
|
|
2403
|
+
else result[field] = ["in", raw];
|
|
2404
|
+
} else if (typeof raw === "string") result[field] = deserializeSingle(raw);
|
|
2405
|
+
else result[field] = ["==", raw];
|
|
2406
|
+
}
|
|
2407
|
+
return result;
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
|
|
2411
|
+
*
|
|
2412
|
+
* @example
|
|
2413
|
+
* serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
|
|
2414
|
+
* // → "status.eq.active"
|
|
2415
|
+
*
|
|
2416
|
+
* serializeLogicalCondition({ type: "or", conditions: [...] })
|
|
2417
|
+
* // → "or(status.eq.active,status.eq.pending)"
|
|
2418
|
+
*/
|
|
2419
|
+
function serializeLogicalCondition(cond) {
|
|
2420
|
+
if ("type" in cond) {
|
|
2421
|
+
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
2422
|
+
return `${cond.type}(${inner})`;
|
|
2423
|
+
}
|
|
2424
|
+
const restOp = CANONICAL_TO_REST[cond.operator] || "eq";
|
|
2425
|
+
if (Array.isArray(cond.value)) {
|
|
2426
|
+
const items = cond.value.map(stringifyValue).join(",");
|
|
2427
|
+
return `${cond.column}.${restOp}.(${items})`;
|
|
2428
|
+
}
|
|
2429
|
+
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Parse a logical condition wire-format string back into a
|
|
2433
|
+
* `LogicalCondition` or `FilterCondition`.
|
|
2434
|
+
*
|
|
2435
|
+
* @example
|
|
2436
|
+
* deserializeLogicalCondition("status.eq.active")
|
|
2437
|
+
* // → { column: "status", operator: "==", value: "active" }
|
|
2438
|
+
*
|
|
2439
|
+
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
2440
|
+
* // → { type: "or", conditions: [...] }
|
|
2441
|
+
*/
|
|
2442
|
+
function deserializeLogicalCondition(str) {
|
|
2443
|
+
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
2444
|
+
if (logicalMatch) {
|
|
2445
|
+
const type = logicalMatch[1];
|
|
2446
|
+
const innerStr = logicalMatch[2];
|
|
2447
|
+
const conditions = [];
|
|
2448
|
+
let depth = 0;
|
|
2449
|
+
let start = 0;
|
|
2450
|
+
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
2451
|
+
else if (innerStr[i] === ")") depth--;
|
|
2452
|
+
else if (innerStr[i] === "," && depth === 0) {
|
|
2453
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
2454
|
+
start = i + 1;
|
|
2010
2455
|
}
|
|
2456
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
2457
|
+
return {
|
|
2458
|
+
type,
|
|
2459
|
+
conditions
|
|
2460
|
+
};
|
|
2011
2461
|
}
|
|
2012
|
-
|
|
2462
|
+
const firstDot = str.indexOf(".");
|
|
2463
|
+
if (firstDot === -1) return {
|
|
2464
|
+
column: str,
|
|
2465
|
+
operator: "==",
|
|
2466
|
+
value: true
|
|
2467
|
+
};
|
|
2468
|
+
const column = str.substring(0, firstDot);
|
|
2469
|
+
const rest = str.substring(firstDot + 1);
|
|
2470
|
+
const secondDot = rest.indexOf(".");
|
|
2471
|
+
if (secondDot === -1) return {
|
|
2472
|
+
column,
|
|
2473
|
+
operator: "==",
|
|
2474
|
+
value: coerceValue(rest)
|
|
2475
|
+
};
|
|
2476
|
+
const opStr = rest.substring(0, secondDot);
|
|
2477
|
+
let valueStr = rest.substring(secondDot + 1);
|
|
2478
|
+
const operator = toCanonicalOp(opStr) ?? "==";
|
|
2479
|
+
if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
|
|
2480
|
+
column,
|
|
2481
|
+
operator,
|
|
2482
|
+
value: valueStr.slice(1, -1).split(",").map((s) => coerceValue(s.trim()))
|
|
2483
|
+
};
|
|
2484
|
+
return {
|
|
2485
|
+
column,
|
|
2486
|
+
operator,
|
|
2487
|
+
value: coerceValue(valueStr)
|
|
2488
|
+
};
|
|
2013
2489
|
}
|
|
2490
|
+
//#endregion
|
|
2491
|
+
//#region src/data/buildRebaseData.ts
|
|
2014
2492
|
/**
|
|
2015
2493
|
* Parse an orderBy string like "created_at:desc" into [field, direction].
|
|
2016
2494
|
*/
|
|
@@ -2023,11 +2501,12 @@ function createDriverAccessor(driver, slug) {
|
|
|
2023
2501
|
const accessor = {
|
|
2024
2502
|
async find(params) {
|
|
2025
2503
|
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2504
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2026
2505
|
const entities = await driver.fetchCollection({
|
|
2027
2506
|
path: slug,
|
|
2028
2507
|
limit: params?.limit,
|
|
2029
2508
|
offset: params?.offset,
|
|
2030
|
-
filter
|
|
2509
|
+
filter,
|
|
2031
2510
|
orderBy: orderParsed?.[0],
|
|
2032
2511
|
order: orderParsed?.[1],
|
|
2033
2512
|
searchString: params?.searchString
|
|
@@ -2077,9 +2556,10 @@ function createDriverAccessor(driver, slug) {
|
|
|
2077
2556
|
return driver.deleteAll(slug);
|
|
2078
2557
|
} : void 0,
|
|
2079
2558
|
count: driver.countEntities ? async (params) => {
|
|
2559
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2080
2560
|
return driver.countEntities({
|
|
2081
2561
|
path: slug,
|
|
2082
|
-
filter
|
|
2562
|
+
filter
|
|
2083
2563
|
});
|
|
2084
2564
|
} : void 0,
|
|
2085
2565
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
@@ -2090,7 +2570,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2090
2570
|
path: slug,
|
|
2091
2571
|
limit: params?.limit,
|
|
2092
2572
|
offset: params?.offset,
|
|
2093
|
-
filter:
|
|
2573
|
+
filter: params?.where,
|
|
2094
2574
|
orderBy: orderParsed?.[0],
|
|
2095
2575
|
order: orderParsed?.[1],
|
|
2096
2576
|
searchString: params?.searchString,
|
|
@@ -2169,6 +2649,125 @@ function buildRebaseData(driver) {
|
|
|
2169
2649
|
} });
|
|
2170
2650
|
}
|
|
2171
2651
|
//#endregion
|
|
2172
|
-
|
|
2652
|
+
//#region src/data/buildRoutedRebaseData.ts
|
|
2653
|
+
/**
|
|
2654
|
+
* Build a {@link RebaseData} that routes each collection to the right
|
|
2655
|
+
* backend based on its resolved data source.
|
|
2656
|
+
*
|
|
2657
|
+
* `.collection(path)` (and dynamic `data.products`-style access) resolves the
|
|
2658
|
+
* collection's data-source key via `resolveKey` and delegates to the matching
|
|
2659
|
+
* entry in `sources`, falling back to `defaultData` when there is no match.
|
|
2660
|
+
* Because routing keys off the *path being accessed*, a reference widget
|
|
2661
|
+
* inside a Firestore form that points at a Postgres collection is still
|
|
2662
|
+
* served by Postgres — routing follows the target, not the ancestor.
|
|
2663
|
+
*
|
|
2664
|
+
* When `sources` is empty this returns `defaultData` untouched, so the
|
|
2665
|
+
* single-driver setup keeps identical behaviour and identity (important for
|
|
2666
|
+
* effect dependencies that key off the data instance).
|
|
2667
|
+
*
|
|
2668
|
+
* @example
|
|
2669
|
+
* const data = buildRoutedRebaseData({
|
|
2670
|
+
* defaultData: client.data,
|
|
2671
|
+
* sources: { analytics: buildRebaseData(firestoreDriver) },
|
|
2672
|
+
* resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
|
|
2673
|
+
* });
|
|
2674
|
+
* await data.products.find(); // → default (server / Postgres)
|
|
2675
|
+
* await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
|
|
2676
|
+
*/
|
|
2677
|
+
function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
|
|
2678
|
+
if (!sources || Object.keys(sources).length === 0) return defaultData;
|
|
2679
|
+
function resolve(slugOrPath) {
|
|
2680
|
+
const key = resolveKey(slugOrPath);
|
|
2681
|
+
if (key && sources[key]) return sources[key];
|
|
2682
|
+
return defaultData;
|
|
2683
|
+
}
|
|
2684
|
+
function getAccessor(slugOrPath) {
|
|
2685
|
+
return resolve(slugOrPath).collection(slugOrPath);
|
|
2686
|
+
}
|
|
2687
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2688
|
+
if (prop === "collection") return getAccessor;
|
|
2689
|
+
if (typeof prop === "symbol") return void 0;
|
|
2690
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
2691
|
+
return getAccessor(toSnakeCase(prop));
|
|
2692
|
+
} });
|
|
2693
|
+
}
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/table-classification.ts
|
|
2696
|
+
/** Schemas that are always considered Rebase-internal. */
|
|
2697
|
+
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
2698
|
+
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
2699
|
+
var REBASE_INTERNAL_PREFIXES = [
|
|
2700
|
+
"_rebase_",
|
|
2701
|
+
"_auth_",
|
|
2702
|
+
"drizzle_"
|
|
2703
|
+
];
|
|
2704
|
+
/**
|
|
2705
|
+
* Synchronously classify a table based on naming conventions.
|
|
2706
|
+
*
|
|
2707
|
+
* @param tableName - The unqualified name of the table.
|
|
2708
|
+
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
2709
|
+
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
2710
|
+
* carries a reserved prefix; `"user"` otherwise.
|
|
2711
|
+
*
|
|
2712
|
+
* @remarks
|
|
2713
|
+
* Junction-table detection requires an async database query and is therefore
|
|
2714
|
+
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
2715
|
+
* the set of junction tables, then reclassify as needed.
|
|
2716
|
+
*/
|
|
2717
|
+
function classifyTable(tableName, schemaName) {
|
|
2718
|
+
if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
|
|
2719
|
+
return "user";
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
2723
|
+
*
|
|
2724
|
+
* @param tableName - The unqualified name of the table.
|
|
2725
|
+
* @param schemaName - The schema the table belongs to.
|
|
2726
|
+
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
2727
|
+
*/
|
|
2728
|
+
function isRebaseInternalTable(tableName, schemaName) {
|
|
2729
|
+
return classifyTable(tableName, schemaName) === "rebase-internal";
|
|
2730
|
+
}
|
|
2731
|
+
/** SQL query that detects junction tables in the `public` schema. */
|
|
2732
|
+
var JUNCTION_TABLES_SQL = `
|
|
2733
|
+
SELECT t.table_name
|
|
2734
|
+
FROM information_schema.tables t
|
|
2735
|
+
WHERE t.table_schema = 'public'
|
|
2736
|
+
AND t.table_type = 'BASE TABLE'
|
|
2737
|
+
AND NOT EXISTS (
|
|
2738
|
+
SELECT 1
|
|
2739
|
+
FROM information_schema.columns c
|
|
2740
|
+
WHERE c.table_schema = t.table_schema
|
|
2741
|
+
AND c.table_name = t.table_name
|
|
2742
|
+
AND c.column_name NOT IN (
|
|
2743
|
+
SELECT kcu.column_name
|
|
2744
|
+
FROM information_schema.key_column_usage kcu
|
|
2745
|
+
JOIN information_schema.table_constraints tc
|
|
2746
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
2747
|
+
AND tc.table_schema = kcu.table_schema
|
|
2748
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
2749
|
+
AND kcu.table_schema = t.table_schema
|
|
2750
|
+
AND kcu.table_name = t.table_name
|
|
2751
|
+
)
|
|
2752
|
+
)
|
|
2753
|
+
`;
|
|
2754
|
+
/**
|
|
2755
|
+
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
2756
|
+
*
|
|
2757
|
+
* A junction table is defined as a table where **every** column participates in
|
|
2758
|
+
* at least one foreign-key constraint.
|
|
2759
|
+
*
|
|
2760
|
+
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
2761
|
+
* resulting rows.
|
|
2762
|
+
* @returns A `Set` containing the names of all detected junction tables.
|
|
2763
|
+
*/
|
|
2764
|
+
async function detectJunctionTables(executeSql) {
|
|
2765
|
+
const rows = await executeSql(JUNCTION_TABLES_SQL);
|
|
2766
|
+
const junctionTables = /* @__PURE__ */ new Set();
|
|
2767
|
+
for (const row of rows) if (typeof row.table_name === "string") junctionTables.add(row.table_name);
|
|
2768
|
+
return junctionTables;
|
|
2769
|
+
}
|
|
2770
|
+
//#endregion
|
|
2771
|
+
export { COLLECTION_PATH_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildAdditionalFieldDelegate, buildCollection, buildConditionContext, buildEntityCallbacks, buildEnum, buildEnumValueConfig, buildProperties, buildPropertiesOrBuilder, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, detectJunctionTables, enumToObjectEntries, evaluateCondition, evaluatePolicy, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getEnumVarName, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, policyToPostgres, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues };
|
|
2173
2772
|
|
|
2174
2773
|
//# sourceMappingURL=index.es.js.map
|