@rebasepro/common 0.7.0 → 0.9.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/README.md +4 -4
- package/dist/collections/CollectionRegistry.d.ts +30 -15
- package/dist/collections/default-collections.d.ts +255 -2
- package/dist/data/buildRebaseData.d.ts +30 -2
- package/dist/data/buildRoutedRebaseData.d.ts +14 -9
- package/dist/data/filter-dialect.d.ts +75 -0
- package/dist/data/query_builder.d.ts +4 -4
- package/dist/data/resolveDataSource.d.ts +8 -8
- package/dist/data/sort-dialect.d.ts +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +1125 -299
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1138 -303
- package/dist/index.umd.js.map +1 -1
- package/dist/util/builders.d.ts +52 -42
- package/dist/util/callbacks.d.ts +8 -3
- package/dist/util/collections.d.ts +4 -4
- package/dist/util/entities.d.ts +2 -2
- package/dist/util/filter-operator-resolution.d.ts +32 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/navigation_from_path.d.ts +4 -4
- package/dist/util/navigation_utils.d.ts +3 -3
- package/dist/util/parent_references_from_path.d.ts +2 -2
- package/dist/util/permissions.d.ts +30 -6
- 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 +22 -0
- package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
- package/dist/util/policy/sqlToPolicy.d.ts +20 -0
- package/dist/util/references.d.ts +2 -2
- package/dist/util/relations.d.ts +5 -5
- package/dist/util/resolutions.d.ts +2 -2
- package/dist/util/storage.d.ts +26 -1
- package/package.json +13 -13
- package/src/collections/CollectionRegistry.ts +92 -61
- package/src/collections/default-collections.ts +4 -4
- package/src/data/buildRebaseData.ts +336 -172
- package/src/data/buildRoutedRebaseData.ts +22 -16
- package/src/data/filter-dialect.ts +403 -0
- package/src/data/query_builder.ts +19 -10
- package/src/data/resolveDataSource.ts +10 -10
- package/src/data/sort-dialect.ts +56 -0
- package/src/index.ts +2 -0
- package/src/util/builders.ts +87 -84
- package/src/util/callbacks.ts +15 -8
- package/src/util/collections.ts +4 -4
- package/src/util/entities.ts +4 -4
- package/src/util/filter-operator-resolution.ts +81 -0
- package/src/util/index.ts +2 -0
- package/src/util/navigation_from_path.ts +4 -4
- package/src/util/navigation_utils.ts +8 -8
- package/src/util/parent_references_from_path.ts +3 -3
- package/src/util/permissions.test.ts +7 -5
- package/src/util/permissions.ts +90 -163
- package/src/util/policy/evaluatePolicy.ts +152 -0
- package/src/util/policy/index.ts +3 -0
- package/src/util/policy/policyToPostgres.ts +165 -0
- package/src/util/policy/securityRuleToConditions.ts +67 -0
- package/src/util/policy/sqlToPolicy.ts +88 -0
- package/src/util/references.ts +3 -3
- package/src/util/relations.ts +19 -20
- package/src/util/resolutions.ts +11 -11
- package/src/util/storage.ts +34 -1
package/dist/index.umd.js
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
else return null;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Update the automatic values in
|
|
86
|
+
* Update the automatic values in a entity before save
|
|
87
87
|
* @group Driver
|
|
88
88
|
*/
|
|
89
89
|
function updateDateAutoValues({ inputValues, properties, status, timestampNowValue }) {
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
}) ?? {};
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* Add missing required fields, expected in the collection, to the values of
|
|
98
|
+
* Add missing required fields, expected in the collection, to the values of a entity
|
|
99
99
|
* @param values
|
|
100
100
|
* @param properties
|
|
101
101
|
* @group Driver
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
return collection.localChangesBackup;
|
|
266
266
|
}
|
|
267
267
|
/**
|
|
268
|
-
* Returns the primary keys for
|
|
268
|
+
* Returns the primary keys for a entity collection by inspecting the properties
|
|
269
269
|
* and finding any properties with `isId`.
|
|
270
270
|
* Fallbacks to `["id"]` if no properties are marked as `isId: true`.
|
|
271
271
|
* @param collection
|
|
@@ -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,379 @@
|
|
|
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, options) {
|
|
813
|
+
return compile(expr, {
|
|
814
|
+
fieldCollection: collection,
|
|
815
|
+
fieldPrefix: "",
|
|
816
|
+
outerCollection: collection,
|
|
817
|
+
outerPrefix: "",
|
|
818
|
+
resolveCollection: options?.resolveCollection,
|
|
819
|
+
alias: { n: 0 }
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function compile(expr, scope) {
|
|
823
|
+
switch (expr.kind) {
|
|
824
|
+
case "true": return "true";
|
|
825
|
+
case "false": return "false";
|
|
826
|
+
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" AND ");
|
|
827
|
+
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" OR ");
|
|
828
|
+
case "not":
|
|
829
|
+
if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
|
|
830
|
+
return `NOT (${compile(expr.operand, scope)})`;
|
|
831
|
+
case "compare": return `${operandToSql(expr.left, scope)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, scope)}`;
|
|
832
|
+
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
833
|
+
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
834
|
+
case "authenticated": return "auth.uid() IS NOT NULL";
|
|
835
|
+
case "existsIn": return compileExistsIn(expr, scope);
|
|
836
|
+
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Compiles `existsIn` to a correlated `EXISTS (SELECT 1 FROM <join> WHERE ...)`.
|
|
841
|
+
* Inside the subquery, `field` operands bind to the aliased join table and
|
|
842
|
+
* `outerField` operands bind to the (table-qualified) outer RLS row.
|
|
843
|
+
*/
|
|
844
|
+
function compileExistsIn(expr, scope) {
|
|
845
|
+
const join = scope.resolveCollection?.(expr.collection);
|
|
846
|
+
const joinTable = join ? getTableName(join) : (0, _rebasepro_utils.toSnakeCase)(expr.collection);
|
|
847
|
+
const joinSchema = schemaOf(join) ?? schemaOf(scope.outerCollection) ?? "public";
|
|
848
|
+
const alias = `_ex${scope.alias.n++}`;
|
|
849
|
+
const outerTable = scope.outerCollection ? getTableName(scope.outerCollection) : void 0;
|
|
850
|
+
const outerSchema = schemaOf(scope.outerCollection) ?? "public";
|
|
851
|
+
const outerPrefix = outerTable ? `"${outerSchema}"."${outerTable}".` : "";
|
|
852
|
+
const innerScope = {
|
|
853
|
+
fieldCollection: join,
|
|
854
|
+
fieldPrefix: `"${alias}".`,
|
|
855
|
+
outerCollection: scope.outerCollection,
|
|
856
|
+
outerPrefix,
|
|
857
|
+
resolveCollection: scope.resolveCollection,
|
|
858
|
+
alias: scope.alias
|
|
859
|
+
};
|
|
860
|
+
return `EXISTS (SELECT 1 FROM "${joinSchema}"."${joinTable}" "${alias}" WHERE ${compile(expr.where, innerScope)})`;
|
|
861
|
+
}
|
|
862
|
+
var COMPARE_SQL = {
|
|
863
|
+
eq: "=",
|
|
864
|
+
neq: "!=",
|
|
865
|
+
lt: "<",
|
|
866
|
+
lte: "<=",
|
|
867
|
+
gt: ">",
|
|
868
|
+
gte: ">="
|
|
869
|
+
};
|
|
870
|
+
function operandToSql(operand, scope) {
|
|
871
|
+
switch (operand.kind) {
|
|
872
|
+
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
873
|
+
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
874
|
+
case "literal": return quoteLiteral(operand.value);
|
|
875
|
+
case "authUid": return "auth.uid()";
|
|
876
|
+
case "authRoles": return "string_to_array(auth.roles(), ',')";
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function schemaOf(collection) {
|
|
880
|
+
return collection?.schema || void 0;
|
|
881
|
+
}
|
|
882
|
+
function resolveColumnName(propName, collection) {
|
|
883
|
+
const prop = collection?.properties?.[propName];
|
|
884
|
+
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
885
|
+
return (0, _rebasepro_utils.toSnakeCase)(propName);
|
|
886
|
+
}
|
|
887
|
+
function quoteLiteral(value) {
|
|
888
|
+
if (value === null) return "NULL";
|
|
889
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
890
|
+
if (typeof value === "number") return String(value);
|
|
891
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
892
|
+
}
|
|
893
|
+
/** Sorted, single-quoted `ARRAY['a','b']` — matches the generators' output. */
|
|
894
|
+
function rolesArraySql(roles) {
|
|
895
|
+
return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
|
|
896
|
+
}
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/util/policy/evaluatePolicy.ts
|
|
899
|
+
/**
|
|
900
|
+
* Evaluates a {@link PolicyExpression} against a user + row, using three-valued
|
|
901
|
+
* (Kleene) logic so that `"unknown"` sub-results propagate soundly.
|
|
902
|
+
*
|
|
903
|
+
* This is the JavaScript twin of {@link policyToPostgres}: both derive from the
|
|
904
|
+
* same expression, so the admin UI matches database enforcement by construction
|
|
905
|
+
* for every non-raw rule.
|
|
906
|
+
*/
|
|
907
|
+
function evaluatePolicy(expr, ctx) {
|
|
908
|
+
switch (expr.kind) {
|
|
909
|
+
case "true": return true;
|
|
910
|
+
case "false": return false;
|
|
911
|
+
case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
912
|
+
case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
|
|
913
|
+
case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
|
|
914
|
+
case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
|
|
915
|
+
case "rolesOverlap": {
|
|
916
|
+
const userRoles = ctx.roles ?? [];
|
|
917
|
+
return expr.roles.some((r) => r === "public" || userRoles.includes(r));
|
|
716
918
|
}
|
|
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
|
-
}
|
|
919
|
+
case "rolesContain": {
|
|
920
|
+
const userRoles = ctx.roles ?? [];
|
|
921
|
+
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
736
922
|
}
|
|
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;
|
|
923
|
+
case "authenticated": return ctx.uid != null;
|
|
924
|
+
case "existsIn": return "unknown";
|
|
925
|
+
case "raw": return "unknown";
|
|
772
926
|
}
|
|
927
|
+
}
|
|
928
|
+
function kleeneAnd$1(values) {
|
|
929
|
+
if (values.some((v) => v === false)) return false;
|
|
930
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
773
931
|
return true;
|
|
774
932
|
}
|
|
775
|
-
function
|
|
776
|
-
if (
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (
|
|
933
|
+
function kleeneOr(values) {
|
|
934
|
+
if (values.some((v) => v === true)) return true;
|
|
935
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
function kleeneNot(value) {
|
|
939
|
+
if (value === "unknown") return "unknown";
|
|
940
|
+
return !value;
|
|
941
|
+
}
|
|
942
|
+
function resolveOperand(operand, ctx) {
|
|
943
|
+
switch (operand.kind) {
|
|
944
|
+
case "literal": return {
|
|
945
|
+
known: true,
|
|
946
|
+
value: operand.value
|
|
947
|
+
};
|
|
948
|
+
case "authUid": return {
|
|
949
|
+
known: true,
|
|
950
|
+
value: ctx.uid ?? null
|
|
951
|
+
};
|
|
952
|
+
case "authRoles": return {
|
|
953
|
+
known: true,
|
|
954
|
+
value: ctx.roles ?? []
|
|
955
|
+
};
|
|
956
|
+
case "field":
|
|
957
|
+
if (!ctx.entity) return { known: false };
|
|
958
|
+
return {
|
|
959
|
+
known: true,
|
|
960
|
+
value: ctx.entity.values[operand.name]
|
|
961
|
+
};
|
|
962
|
+
case "outerField": return { known: false };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function evaluateCompare(op, left, right, ctx) {
|
|
966
|
+
const l = resolveOperand(left, ctx);
|
|
967
|
+
const r = resolveOperand(right, ctx);
|
|
968
|
+
if (!l.known || !r.known) return "unknown";
|
|
969
|
+
const a = l.value;
|
|
970
|
+
const b = r.value;
|
|
971
|
+
if (a === null || b === null) {
|
|
972
|
+
if (op === "eq") return false;
|
|
973
|
+
if (op === "neq") return true;
|
|
974
|
+
return "unknown";
|
|
975
|
+
}
|
|
976
|
+
if (op === "eq") return a === b;
|
|
977
|
+
if (op === "neq") return a !== b;
|
|
978
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
979
|
+
if (op === "lt") return a < b;
|
|
980
|
+
if (op === "lte") return a <= b;
|
|
981
|
+
if (op === "gt") return a > b;
|
|
982
|
+
if (op === "gte") return a >= b;
|
|
983
|
+
}
|
|
984
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
985
|
+
if (op === "lt") return a < b;
|
|
986
|
+
if (op === "lte") return a <= b;
|
|
987
|
+
if (op === "gt") return a > b;
|
|
988
|
+
if (op === "gte") return a >= b;
|
|
989
|
+
}
|
|
990
|
+
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
991
|
+
if (op === "lt") return a < b;
|
|
992
|
+
if (op === "lte") return a <= b;
|
|
993
|
+
if (op === "gt") return a > b;
|
|
994
|
+
if (op === "gte") return a >= b;
|
|
995
|
+
}
|
|
996
|
+
return "unknown";
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/util/permissions.ts
|
|
1000
|
+
/** Combine clause results with AND under three-valued (Kleene) logic. */
|
|
1001
|
+
function kleeneAnd(values) {
|
|
1002
|
+
if (values.some((v) => v === false)) return false;
|
|
1003
|
+
if (values.some((v) => v === "unknown")) return "unknown";
|
|
782
1004
|
return true;
|
|
783
1005
|
}
|
|
784
|
-
|
|
785
|
-
|
|
1006
|
+
/** The operations a rule covers, mirroring the Postgres generator's resolution. */
|
|
1007
|
+
function ruleOperations(rule) {
|
|
1008
|
+
return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
1009
|
+
}
|
|
1010
|
+
function ruleApplies(rule, targetOperation) {
|
|
1011
|
+
const ops = ruleOperations(rule);
|
|
1012
|
+
return ops.includes(targetOperation) || ops.includes("all");
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Evaluate a single rule for one operation, returning a tri-state.
|
|
1016
|
+
*
|
|
1017
|
+
* A `null` clause (the rule contributes no condition for a required clause)
|
|
1018
|
+
* denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
|
|
1019
|
+
* in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
|
|
1020
|
+
* INSERT/UPDATE; both must pass for UPDATE.
|
|
1021
|
+
*/
|
|
1022
|
+
function evaluateRuleForOperation(rule, ctx, targetOperation) {
|
|
1023
|
+
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
1024
|
+
const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
|
|
1025
|
+
const needsUsing = targetOperation !== "insert";
|
|
1026
|
+
const needsWithCheck = targetOperation === "insert" || targetOperation === "update";
|
|
1027
|
+
const results = [];
|
|
1028
|
+
if (needsUsing) results.push(clause(usingExpr));
|
|
1029
|
+
if (needsWithCheck) results.push(clause(withCheckExpr));
|
|
1030
|
+
return kleeneAnd(results);
|
|
1031
|
+
}
|
|
1032
|
+
function resolveTriState(value, onUnknown) {
|
|
1033
|
+
if (value === "unknown") return onUnknown === "allow";
|
|
1034
|
+
return value;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Decide whether an operation is permitted for a user on a (possibly null) row,
|
|
1038
|
+
* by evaluating the collection's security rules with the shared policy model —
|
|
1039
|
+
* the same model compiled to Postgres RLS DDL, so the decision matches database
|
|
1040
|
+
* enforcement for every non-raw rule.
|
|
1041
|
+
*
|
|
1042
|
+
* @param options.onUnknown how to treat rules that cannot be decided
|
|
1043
|
+
* client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
|
|
1044
|
+
* for optimistic UI gating; enforcement callers should pass `"deny"`.
|
|
1045
|
+
*/
|
|
1046
|
+
function checkOperation(collection, authContext, entity, targetOperation, options) {
|
|
1047
|
+
const onUnknown = options?.onUnknown ?? "allow";
|
|
1048
|
+
const securityRules = (0, _rebasepro_types.getDataSourceCapabilities)(collection.engine).supportsRLS ? collection.securityRules : void 0;
|
|
786
1049
|
if (!securityRules || securityRules.length === 0) return true;
|
|
787
|
-
const applicableRules = securityRules.filter((r) => r
|
|
1050
|
+
const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
|
|
788
1051
|
if (applicableRules.length === 0) return false;
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
if (roleApplicableRules.length === 0) return false;
|
|
1052
|
+
const ctx = {
|
|
1053
|
+
uid: authContext.user?.uid,
|
|
1054
|
+
roles: authContext.user?.roles ?? [],
|
|
1055
|
+
entity
|
|
1056
|
+
};
|
|
795
1057
|
let grantedByPermissive = false;
|
|
796
1058
|
let deniedByRestrictive = false;
|
|
797
|
-
|
|
1059
|
+
let hasPermissive = false;
|
|
1060
|
+
for (const rule of applicableRules) {
|
|
798
1061
|
const mode = rule.mode || "permissive";
|
|
799
|
-
const passed =
|
|
800
|
-
if (mode === "restrictive"
|
|
801
|
-
|
|
802
|
-
|
|
1062
|
+
const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation), onUnknown);
|
|
1063
|
+
if (mode === "restrictive") {
|
|
1064
|
+
if (!passed) {
|
|
1065
|
+
deniedByRestrictive = true;
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
} else {
|
|
1069
|
+
hasPermissive = true;
|
|
1070
|
+
if (passed) grantedByPermissive = true;
|
|
803
1071
|
}
|
|
804
|
-
if (mode === "permissive" && passed) grantedByPermissive = true;
|
|
805
1072
|
}
|
|
806
1073
|
if (deniedByRestrictive) return false;
|
|
807
|
-
|
|
808
|
-
else return false;
|
|
1074
|
+
return hasPermissive ? grantedByPermissive : false;
|
|
809
1075
|
}
|
|
810
1076
|
function canReadCollection(collection, authContext) {
|
|
811
1077
|
return checkOperation(collection, authContext, null, "select");
|
|
@@ -836,7 +1102,7 @@
|
|
|
836
1102
|
}
|
|
837
1103
|
for (const key in collection.properties) {
|
|
838
1104
|
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;
|
|
1105
|
+
if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.ui?.url === "image") return key;
|
|
840
1106
|
}
|
|
841
1107
|
for (const key in collection.properties) {
|
|
842
1108
|
const property = collection.properties[key];
|
|
@@ -905,7 +1171,7 @@
|
|
|
905
1171
|
} else {
|
|
906
1172
|
entityId = remainingPath;
|
|
907
1173
|
remainingPath = "";
|
|
908
|
-
console.warn(`resolveCollectionPathIds: Path seems to end with
|
|
1174
|
+
console.warn(`resolveCollectionPathIds: Path seems to end with a entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
|
|
909
1175
|
}
|
|
910
1176
|
resolvedPathParts.push(entityId);
|
|
911
1177
|
currentCollections = getSubcollections(foundCollection);
|
|
@@ -1061,79 +1327,60 @@
|
|
|
1061
1327
|
//#endregion
|
|
1062
1328
|
//#region src/util/builders.ts
|
|
1063
1329
|
/**
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1330
|
+
* @deprecated Use {@link defineCollection} instead — it infers property
|
|
1331
|
+
* types automatically (autocomplete on `titleProperty`, `sort`,
|
|
1332
|
+
* `propertiesOrder`, callbacks) without manual generics.
|
|
1333
|
+
* `buildCollection` is kept for FireCMS migration compatibility and will
|
|
1334
|
+
* be removed before 1.0.
|
|
1335
|
+
*
|
|
1067
1336
|
* @group Builder
|
|
1068
1337
|
*/
|
|
1069
1338
|
function buildCollection(collection) {
|
|
1070
1339
|
return collection;
|
|
1071
1340
|
}
|
|
1072
1341
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
* @param property
|
|
1076
|
-
* @group Builder
|
|
1077
|
-
*/
|
|
1078
|
-
function buildProperty(property) {
|
|
1079
|
-
return property;
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1083
|
-
* the properties keys.
|
|
1084
|
-
* @param properties
|
|
1085
|
-
* @group Builder
|
|
1086
|
-
*/
|
|
1087
|
-
function buildProperties(properties) {
|
|
1088
|
-
return properties;
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1092
|
-
* the properties keys.
|
|
1093
|
-
* @param propertiesOrBuilder
|
|
1094
|
-
* @group Builder
|
|
1095
|
-
*/
|
|
1096
|
-
function buildPropertiesOrBuilder(propertiesOrBuilder) {
|
|
1097
|
-
return propertiesOrBuilder;
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1101
|
-
* the properties keys.
|
|
1102
|
-
* @param enumValues
|
|
1103
|
-
* @group Builder
|
|
1104
|
-
*/
|
|
1105
|
-
function buildEnum(enumValues) {
|
|
1106
|
-
return enumValues;
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1110
|
-
* the properties keys.
|
|
1111
|
-
* @param enumValueConfig
|
|
1112
|
-
* @group Builder
|
|
1342
|
+
* Implementation — delegates to the correct overload at the type level.
|
|
1343
|
+
* At runtime this is a plain identity function.
|
|
1113
1344
|
*/
|
|
1114
|
-
function
|
|
1115
|
-
return
|
|
1345
|
+
function defineCollection(collection) {
|
|
1346
|
+
return collection;
|
|
1116
1347
|
}
|
|
1117
1348
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1349
|
+
* @deprecated Use plain typed property objects with {@link defineCollection}
|
|
1350
|
+
* instead — `defineCollection` infers property types automatically, making
|
|
1351
|
+
* this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
|
|
1352
|
+
* compatibility and will be removed before 1.0.
|
|
1353
|
+
*
|
|
1121
1354
|
* @group Builder
|
|
1122
1355
|
*/
|
|
1123
|
-
function
|
|
1124
|
-
return
|
|
1356
|
+
function buildProperty(property) {
|
|
1357
|
+
return property;
|
|
1125
1358
|
}
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/util/storage.ts
|
|
1126
1361
|
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1362
|
+
* Resolve the {@link StorageSource} to use for a property, given the key
|
|
1363
|
+
* referenced by `StorageConfig.storageSource`.
|
|
1364
|
+
*
|
|
1365
|
+
* Resolution priority:
|
|
1366
|
+
* 1. No `sourceKey` → the default source (backward compatible).
|
|
1367
|
+
* 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
|
|
1368
|
+
* 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
|
|
1369
|
+
* 4. Fall back to the default source.
|
|
1370
|
+
*
|
|
1371
|
+
* Shared by the upload hook, the markdown editor, and the read-only previews
|
|
1372
|
+
* so the resolution logic lives in one place.
|
|
1373
|
+
*
|
|
1374
|
+
* @group Storage
|
|
1131
1375
|
*/
|
|
1132
|
-
function
|
|
1133
|
-
|
|
1376
|
+
function resolveStorageSource(params) {
|
|
1377
|
+
const { sourceKey, sources, registry, defaultSource } = params;
|
|
1378
|
+
if (!sourceKey) return defaultSource;
|
|
1379
|
+
if (registry) return registry.getOrDefault(sourceKey);
|
|
1380
|
+
const fromSources = sources?.[sourceKey];
|
|
1381
|
+
if (fromSources) return fromSources;
|
|
1382
|
+
return defaultSource;
|
|
1134
1383
|
}
|
|
1135
|
-
//#endregion
|
|
1136
|
-
//#region src/util/storage.ts
|
|
1137
1384
|
async function resolveStorageFilenameString({ input, storage, values, entityId, path, property, file, propertyKey }) {
|
|
1138
1385
|
let result;
|
|
1139
1386
|
if (typeof input === "function") {
|
|
@@ -1244,16 +1491,17 @@
|
|
|
1244
1491
|
}
|
|
1245
1492
|
/**
|
|
1246
1493
|
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
1247
|
-
* and wrap them into an
|
|
1494
|
+
* and wrap them into an CollectionCallbacks object recursively.
|
|
1248
1495
|
*/
|
|
1249
1496
|
var buildPropertyCallbacks = (properties) => {
|
|
1250
1497
|
if (!properties) return void 0;
|
|
1251
1498
|
const propertyCallbacks = {};
|
|
1252
1499
|
if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
|
|
1253
|
-
const
|
|
1500
|
+
const row = props.row;
|
|
1501
|
+
const processedValues = await processProperties(properties, row, row, props, "afterRead");
|
|
1254
1502
|
return {
|
|
1255
|
-
...props.
|
|
1256
|
-
|
|
1503
|
+
...props.row,
|
|
1504
|
+
...processedValues
|
|
1257
1505
|
};
|
|
1258
1506
|
};
|
|
1259
1507
|
if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
|
|
@@ -1440,6 +1688,84 @@
|
|
|
1440
1688
|
return result;
|
|
1441
1689
|
}
|
|
1442
1690
|
//#endregion
|
|
1691
|
+
//#region src/util/filter-operator-resolution.ts
|
|
1692
|
+
/**
|
|
1693
|
+
* Default operators offered per property type, before engine capabilities and
|
|
1694
|
+
* per-property narrowing are applied. These mirror what the built-in filter
|
|
1695
|
+
* fields can render.
|
|
1696
|
+
*/
|
|
1697
|
+
var COMPARISON_OPS = [
|
|
1698
|
+
"==",
|
|
1699
|
+
"!=",
|
|
1700
|
+
">",
|
|
1701
|
+
">=",
|
|
1702
|
+
"<",
|
|
1703
|
+
"<="
|
|
1704
|
+
];
|
|
1705
|
+
var NULL_CHECK_OPS = ["is-null", "is-not-null"];
|
|
1706
|
+
var MEMBERSHIP_OPS = ["in", "not-in"];
|
|
1707
|
+
var PATTERN_OPS = [
|
|
1708
|
+
"like",
|
|
1709
|
+
"ilike",
|
|
1710
|
+
"not-like",
|
|
1711
|
+
"not-ilike"
|
|
1712
|
+
];
|
|
1713
|
+
var DEFAULT_OPS_BY_TYPE = {
|
|
1714
|
+
string: [
|
|
1715
|
+
...COMPARISON_OPS,
|
|
1716
|
+
...MEMBERSHIP_OPS,
|
|
1717
|
+
...PATTERN_OPS,
|
|
1718
|
+
...NULL_CHECK_OPS
|
|
1719
|
+
],
|
|
1720
|
+
number: [
|
|
1721
|
+
...COMPARISON_OPS,
|
|
1722
|
+
...MEMBERSHIP_OPS,
|
|
1723
|
+
...NULL_CHECK_OPS
|
|
1724
|
+
],
|
|
1725
|
+
date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
|
|
1726
|
+
boolean: [
|
|
1727
|
+
"==",
|
|
1728
|
+
"!=",
|
|
1729
|
+
...NULL_CHECK_OPS
|
|
1730
|
+
],
|
|
1731
|
+
reference: [
|
|
1732
|
+
"==",
|
|
1733
|
+
"!=",
|
|
1734
|
+
...MEMBERSHIP_OPS,
|
|
1735
|
+
...NULL_CHECK_OPS
|
|
1736
|
+
],
|
|
1737
|
+
relation: [
|
|
1738
|
+
"==",
|
|
1739
|
+
"!=",
|
|
1740
|
+
...MEMBERSHIP_OPS,
|
|
1741
|
+
...NULL_CHECK_OPS
|
|
1742
|
+
]
|
|
1743
|
+
};
|
|
1744
|
+
/** Operators offered when the property is an *array of* a filterable type. */
|
|
1745
|
+
var ARRAY_OPS = ["array-contains", "array-contains-any"];
|
|
1746
|
+
/**
|
|
1747
|
+
* Resolve which filter operators the UI should offer for a property.
|
|
1748
|
+
*
|
|
1749
|
+
* The result is the **intersection** of three sets:
|
|
1750
|
+
* 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
|
|
1751
|
+
* (e.g. Firestore cannot run the LIKE family);
|
|
1752
|
+
* 2. what makes sense for the property type (e.g. no `>` on booleans);
|
|
1753
|
+
* 3. the developer's optional narrowing — `property.ui.filterOperators`.
|
|
1754
|
+
*
|
|
1755
|
+
* Returns an empty array when the property is not filterable (either by
|
|
1756
|
+
* type, or because the developer disabled it with `filterOperators: []`).
|
|
1757
|
+
*
|
|
1758
|
+
* @group Models
|
|
1759
|
+
*/
|
|
1760
|
+
function resolveFilterOperators({ property, isArray, engine }) {
|
|
1761
|
+
const typeDefaults = isArray ? ARRAY_OPS : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
|
|
1762
|
+
if (typeDefaults.length === 0) return [];
|
|
1763
|
+
const engineOps = new Set((0, _rebasepro_types.getDataSourceCapabilities)(engine).filterOperators ?? _rebasepro_types.ALL_WHERE_FILTER_OPS);
|
|
1764
|
+
const narrowing = property.ui?.filterOperators;
|
|
1765
|
+
const narrowingSet = narrowing !== void 0 ? new Set(narrowing) : void 0;
|
|
1766
|
+
return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
|
|
1767
|
+
}
|
|
1768
|
+
//#endregion
|
|
1443
1769
|
//#region src/data/resolveDataSource.ts
|
|
1444
1770
|
/**
|
|
1445
1771
|
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
@@ -1456,13 +1782,13 @@
|
|
|
1456
1782
|
* editor's capability lookups.
|
|
1457
1783
|
*
|
|
1458
1784
|
* Resolution order:
|
|
1459
|
-
* 1. The routing **key** is `collection.dataSource`, else
|
|
1460
|
-
*
|
|
1785
|
+
* 1. The routing **key** is `collection.dataSource`, else
|
|
1786
|
+
* {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
1461
1787
|
* 2. If a definition is registered for that key, it provides `engine`,
|
|
1462
1788
|
* `transport`, and `databaseId`.
|
|
1463
|
-
* 3. Otherwise values are synthesized
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1789
|
+
* 3. Otherwise values are synthesized: `engine` from `collection.engine`
|
|
1790
|
+
* (or the key, or `"postgres"`), `transport` defaults to `"server"`,
|
|
1791
|
+
* and `databaseId` from the collection.
|
|
1466
1792
|
*
|
|
1467
1793
|
* `capabilities` are always derived from the resolved `engine`, so two
|
|
1468
1794
|
* data sources sharing an engine share capabilities.
|
|
@@ -1471,9 +1797,9 @@
|
|
|
1471
1797
|
* @param registry optional registry of declared data sources
|
|
1472
1798
|
*/
|
|
1473
1799
|
function resolveDataSource(collection, registry) {
|
|
1474
|
-
const key = collection?.dataSource ??
|
|
1800
|
+
const key = collection?.dataSource ?? _rebasepro_types.DEFAULT_DATA_SOURCE_KEY;
|
|
1475
1801
|
const def = registry?.[key];
|
|
1476
|
-
const engine = def?.engine ?? collection?.
|
|
1802
|
+
const engine = def?.engine ?? collection?.engine ?? (key !== _rebasepro_types.DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
1477
1803
|
return {
|
|
1478
1804
|
key,
|
|
1479
1805
|
engine,
|
|
@@ -1488,9 +1814,28 @@
|
|
|
1488
1814
|
/**
|
|
1489
1815
|
* Declared data sources, used during normalization to resolve each
|
|
1490
1816
|
* collection's engine (so `dataSource`-only collections get the right
|
|
1491
|
-
* capabilities). Empty by default
|
|
1817
|
+
* capabilities). Empty by default.
|
|
1492
1818
|
*/
|
|
1493
1819
|
dataSources = {};
|
|
1820
|
+
/**
|
|
1821
|
+
* Global lifecycle callbacks applied to every collection.
|
|
1822
|
+
* Runs on all data paths (REST, WebSocket, `rebase.data`).
|
|
1823
|
+
* Execution order: global → collection → property callbacks.
|
|
1824
|
+
*/
|
|
1825
|
+
_globalCallbacks;
|
|
1826
|
+
/**
|
|
1827
|
+
* Set global lifecycle callbacks that apply to every collection.
|
|
1828
|
+
* Typically called once during backend initialization.
|
|
1829
|
+
*/
|
|
1830
|
+
setGlobalCallbacks(callbacks) {
|
|
1831
|
+
this._globalCallbacks = callbacks;
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Get the currently registered global callbacks, if any.
|
|
1835
|
+
*/
|
|
1836
|
+
getGlobalCallbacks() {
|
|
1837
|
+
return this._globalCallbacks;
|
|
1838
|
+
}
|
|
1494
1839
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
1495
1840
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
1496
1841
|
rootCollections = [];
|
|
@@ -1499,7 +1844,7 @@
|
|
|
1499
1844
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
1500
1845
|
rawRootCollections = [];
|
|
1501
1846
|
cachedRawCollectionsList = null;
|
|
1502
|
-
|
|
1847
|
+
lastRawInputEntity = null;
|
|
1503
1848
|
constructor(collections, dataSources) {
|
|
1504
1849
|
if (dataSources) this.dataSources = dataSources;
|
|
1505
1850
|
if (collections) this.registerMultiple(collections);
|
|
@@ -1529,12 +1874,12 @@
|
|
|
1529
1874
|
* Returns true if the collections have changed, false otherwise.
|
|
1530
1875
|
*
|
|
1531
1876
|
* Idempotent: compares the raw input (before normalization) against a stored
|
|
1532
|
-
*
|
|
1877
|
+
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
1533
1878
|
* @param collections
|
|
1534
1879
|
*/
|
|
1535
1880
|
registerMultiple(collections) {
|
|
1536
|
-
const
|
|
1537
|
-
if (this.
|
|
1881
|
+
const rawEntity = collections.map((c) => (0, _rebasepro_utils.removeFunctions)(c));
|
|
1882
|
+
if (this.lastRawInputEntity && (0, fast_equals.deepEqual)(this.lastRawInputEntity, rawEntity)) return false;
|
|
1538
1883
|
this.reset();
|
|
1539
1884
|
collections.forEach((c) => {
|
|
1540
1885
|
if (c.slug) this.collectionsBySlug.set(c.slug, c);
|
|
@@ -1558,7 +1903,7 @@
|
|
|
1558
1903
|
this._registerRecursively(this.normalizeCollection({ ...subCollection }), (0, _rebasepro_utils.deepClone)(subCollection));
|
|
1559
1904
|
});
|
|
1560
1905
|
});
|
|
1561
|
-
this.
|
|
1906
|
+
this.lastRawInputEntity = rawEntity;
|
|
1562
1907
|
return true;
|
|
1563
1908
|
}
|
|
1564
1909
|
register(collection, rawCollection) {
|
|
@@ -1582,13 +1927,14 @@
|
|
|
1582
1927
|
}
|
|
1583
1928
|
normalizeCollection(collection) {
|
|
1584
1929
|
const result = { ...collection };
|
|
1585
|
-
|
|
1586
|
-
const
|
|
1587
|
-
if (
|
|
1930
|
+
{
|
|
1931
|
+
const resolved = resolveDataSource(result, this.dataSources);
|
|
1932
|
+
if (!result.dataSource) result.dataSource = resolved.key;
|
|
1933
|
+
if (!result.engine) result.engine = resolved.engine;
|
|
1588
1934
|
}
|
|
1589
1935
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
1590
1936
|
const relResult = result;
|
|
1591
|
-
const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.
|
|
1937
|
+
const manualRelations = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations ? relResult.relations ?? [] : [];
|
|
1592
1938
|
const mergedRelationsRaw = [...extractedRelations];
|
|
1593
1939
|
for (const manual of manualRelations) {
|
|
1594
1940
|
const name = manual.relationName;
|
|
@@ -1603,7 +1949,7 @@
|
|
|
1603
1949
|
}
|
|
1604
1950
|
}
|
|
1605
1951
|
let mergedRelations = mergedRelationsRaw;
|
|
1606
|
-
if ((0, _rebasepro_types.getDataSourceCapabilities)(result.
|
|
1952
|
+
if ((0, _rebasepro_types.getDataSourceCapabilities)(result.engine).supportsRelations) {
|
|
1607
1953
|
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
1608
1954
|
try {
|
|
1609
1955
|
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
@@ -1615,8 +1961,10 @@
|
|
|
1615
1961
|
}
|
|
1616
1962
|
result.properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
1617
1963
|
if (!result.childCollections) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1964
|
+
const capabilities = (0, _rebasepro_types.getDataSourceCapabilities)(result.engine);
|
|
1965
|
+
const declaredSubcollections = (0, _rebasepro_types.getDeclaredSubcollections)(result);
|
|
1966
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) result.childCollections = declaredSubcollections;
|
|
1967
|
+
else if (capabilities.supportsRelations && relResult.relations) {
|
|
1620
1968
|
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
1621
1969
|
if (manyRelations.length > 0) result.childCollections = () => manyRelations.map((r) => {
|
|
1622
1970
|
const target = r.target();
|
|
@@ -1718,7 +2066,7 @@
|
|
|
1718
2066
|
if (!currentCollection) throw new Error(`Root collection not found: ${rootCollectionPath}`);
|
|
1719
2067
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
1720
2068
|
const relationKey = pathSegments[i];
|
|
1721
|
-
if (!(0, _rebasepro_types.getDataSourceCapabilities)(currentCollection.
|
|
2069
|
+
if (!(0, _rebasepro_types.getDataSourceCapabilities)(currentCollection.engine).supportsRelations) throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
1722
2070
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
1723
2071
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
1724
2072
|
const target = relation.target();
|
|
@@ -1779,7 +2127,7 @@
|
|
|
1779
2127
|
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
1780
2128
|
* override by defining their own collection with `slug: "users"`.
|
|
1781
2129
|
*/
|
|
1782
|
-
var defaultUsersCollection = {
|
|
2130
|
+
var defaultUsersCollection = defineCollection({
|
|
1783
2131
|
name: "Users",
|
|
1784
2132
|
singularName: "User",
|
|
1785
2133
|
slug: "users",
|
|
@@ -1827,7 +2175,7 @@
|
|
|
1827
2175
|
name: "Photo URL",
|
|
1828
2176
|
type: "string",
|
|
1829
2177
|
columnName: "photo_url",
|
|
1830
|
-
url: "image"
|
|
2178
|
+
ui: { url: "image" }
|
|
1831
2179
|
},
|
|
1832
2180
|
roles: {
|
|
1833
2181
|
name: "Roles",
|
|
@@ -1922,7 +2270,7 @@
|
|
|
1922
2270
|
"roles",
|
|
1923
2271
|
"createdAt"
|
|
1924
2272
|
]
|
|
1925
|
-
};
|
|
2273
|
+
});
|
|
1926
2274
|
//#endregion
|
|
1927
2275
|
//#region src/data/query_builder.ts
|
|
1928
2276
|
function or(...conditions) {
|
|
@@ -1974,8 +2322,8 @@
|
|
|
1974
2322
|
* @example
|
|
1975
2323
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
1976
2324
|
*/
|
|
1977
|
-
orderBy(column,
|
|
1978
|
-
this.params.orderBy =
|
|
2325
|
+
orderBy(column, direction = "asc") {
|
|
2326
|
+
this.params.orderBy = [column, direction];
|
|
1979
2327
|
return this;
|
|
1980
2328
|
}
|
|
1981
2329
|
/**
|
|
@@ -2030,166 +2378,369 @@
|
|
|
2030
2378
|
}
|
|
2031
2379
|
};
|
|
2032
2380
|
//#endregion
|
|
2033
|
-
//#region src/data/
|
|
2381
|
+
//#region src/data/filter-dialect.ts
|
|
2034
2382
|
/**
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
* -
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2383
|
+
* REST wire-format adapter for the unified filter system.
|
|
2384
|
+
*
|
|
2385
|
+
* This module is the ONLY code in the entire codebase that knows about
|
|
2386
|
+
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
2387
|
+
* Everything else speaks `FilterValues` exclusively.
|
|
2388
|
+
*
|
|
2389
|
+
* Wire-format values are always strings — the wire format carries no type
|
|
2390
|
+
* metadata, so type coercion is the responsibility of the server-side data
|
|
2391
|
+
* driver which has access to the collection schema.
|
|
2392
|
+
*
|
|
2393
|
+
* Commas inside list values are backslash-escaped (`\,`), and literal
|
|
2394
|
+
* backslashes are escaped as `\\`.
|
|
2395
|
+
*
|
|
2396
|
+
* @module
|
|
2397
|
+
*/
|
|
2398
|
+
/**
|
|
2399
|
+
* Serialize a JS value to its querystring representation.
|
|
2400
|
+
* `null` is serialized as the literal string `"null"`.
|
|
2401
|
+
*/
|
|
2402
|
+
function stringifyValue(value) {
|
|
2403
|
+
if (value === null) return "null";
|
|
2404
|
+
return String(value);
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Escape a single list item for the wire format.
|
|
2408
|
+
* `\` → `\\`, `,` → `\,`
|
|
2409
|
+
*/
|
|
2410
|
+
function escapeListItem(value) {
|
|
2411
|
+
return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Unescape a single list item from the wire format.
|
|
2415
|
+
* `\\` → `\`, `\,` → `,`
|
|
2416
|
+
*/
|
|
2417
|
+
function unescapeListItem(value) {
|
|
2418
|
+
let result = "";
|
|
2419
|
+
for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
|
|
2420
|
+
result += value[i + 1];
|
|
2421
|
+
i++;
|
|
2422
|
+
} else result += value[i];
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Split a parenthesized list string on unescaped commas.
|
|
2427
|
+
* Input is the content between `(` and `)`.
|
|
2428
|
+
*
|
|
2429
|
+
* @example
|
|
2430
|
+
* splitListItems("admin,editor") // ["admin", "editor"]
|
|
2431
|
+
* splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
|
|
2432
|
+
*/
|
|
2433
|
+
function splitListItems(inner) {
|
|
2434
|
+
const items = [];
|
|
2435
|
+
let current = "";
|
|
2436
|
+
for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
|
|
2437
|
+
current += inner[i] + inner[i + 1];
|
|
2438
|
+
i++;
|
|
2439
|
+
} else if (inner[i] === ",") {
|
|
2440
|
+
items.push(unescapeListItem(current));
|
|
2441
|
+
current = "";
|
|
2442
|
+
} else current += inner[i];
|
|
2443
|
+
items.push(unescapeListItem(current));
|
|
2444
|
+
return items;
|
|
2445
|
+
}
|
|
2446
|
+
var REST_OP_LOOKUP = _rebasepro_types.REST_TO_CANONICAL;
|
|
2447
|
+
var CANONICAL_OP_LOOKUP = _rebasepro_types.CANONICAL_TO_REST;
|
|
2448
|
+
/**
|
|
2449
|
+
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
2450
|
+
*
|
|
2451
|
+
* Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
|
|
2452
|
+
*
|
|
2453
|
+
* @example
|
|
2454
|
+
* serializeTuple(["==", "active"]) // "eq.active"
|
|
2455
|
+
* serializeTuple(["in", ["admin","editor"]]) // "in.(admin,editor)"
|
|
2456
|
+
* serializeTuple([">=", 18]) // "gte.18"
|
|
2457
|
+
*/
|
|
2458
|
+
function serializeTuple(tuple) {
|
|
2459
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
|
|
2460
|
+
const [op, value] = tuple;
|
|
2461
|
+
if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
|
|
2462
|
+
const restOp = CANONICAL_OP_LOOKUP[op];
|
|
2463
|
+
if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(_rebasepro_types.CANONICAL_TO_REST).join(", ")}`);
|
|
2464
|
+
if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
|
|
2465
|
+
return `${restOp}.${stringifyValue(value)}`;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
2469
|
+
* querystring record.
|
|
2470
|
+
*
|
|
2471
|
+
* - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
|
|
2472
|
+
* - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
|
|
2473
|
+
* - Single conditions produce a string value.
|
|
2474
|
+
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
2475
|
+
*
|
|
2476
|
+
* @example
|
|
2477
|
+
* serializeFilter({ status: ["==", "active"] })
|
|
2478
|
+
* // → { status: "eq.active" }
|
|
2479
|
+
*
|
|
2480
|
+
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
2481
|
+
* // → { age: ["gte.18", "lt.65"] }
|
|
2482
|
+
*
|
|
2483
|
+
* // Pre-serialized strings pass through unchanged:
|
|
2484
|
+
* serializeFilter({ status: "eq.published" })
|
|
2485
|
+
* // → { status: "eq.published" }
|
|
2486
|
+
*/
|
|
2487
|
+
function serializeFilter(filter) {
|
|
2488
|
+
const result = {};
|
|
2489
|
+
for (const [field, condition] of Object.entries(filter)) {
|
|
2490
|
+
if (condition === void 0) continue;
|
|
2491
|
+
if (typeof condition === "string") {
|
|
2492
|
+
result[field] = condition;
|
|
2079
2493
|
continue;
|
|
2080
2494
|
}
|
|
2081
|
-
if (Array.isArray(
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2495
|
+
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
|
|
2496
|
+
else result[field] = serializeTuple(condition);
|
|
2497
|
+
}
|
|
2498
|
+
return result;
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
2502
|
+
*
|
|
2503
|
+
* All values are returned as strings — the wire format carries no type
|
|
2504
|
+
* metadata, so coercion is the data driver's responsibility.
|
|
2505
|
+
*
|
|
2506
|
+
* If the string doesn't match a known operator prefix, it falls back to
|
|
2507
|
+
* `["==", originalString]` (treating the whole string as an equality value).
|
|
2508
|
+
* This intentional defense handles values like `"user@host.com"` or
|
|
2509
|
+
* `"1.2.3"` that happen to contain dots.
|
|
2510
|
+
*/
|
|
2511
|
+
function deserializeSingle(raw) {
|
|
2512
|
+
const dotIndex = raw.indexOf(".");
|
|
2513
|
+
if (dotIndex === -1) return ["==", raw];
|
|
2514
|
+
const prefix = raw.substring(0, dotIndex);
|
|
2515
|
+
const rest = raw.substring(dotIndex + 1);
|
|
2516
|
+
const canonicalOp = REST_OP_LOOKUP[prefix];
|
|
2517
|
+
if (!canonicalOp) return ["==", raw];
|
|
2518
|
+
if (_rebasepro_types.NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
|
|
2519
|
+
if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
|
|
2520
|
+
return [canonicalOp, rest];
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
2524
|
+
*
|
|
2525
|
+
* - String values are parsed as single conditions.
|
|
2526
|
+
* - String arrays (repeated query params) become multiple conditions on the same field.
|
|
2527
|
+
*
|
|
2528
|
+
* @example
|
|
2529
|
+
* deserializeFilter({ status: "eq.active" })
|
|
2530
|
+
* // → { status: ["==", "active"] }
|
|
2531
|
+
*
|
|
2532
|
+
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
2533
|
+
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
2534
|
+
*/
|
|
2535
|
+
function deserializeFilter(query) {
|
|
2536
|
+
const result = {};
|
|
2537
|
+
for (const [field, raw] of Object.entries(query)) {
|
|
2538
|
+
if (raw === void 0) continue;
|
|
2539
|
+
if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0]) === raw[0]) {
|
|
2540
|
+
result[field] = raw;
|
|
2086
2541
|
continue;
|
|
2087
2542
|
}
|
|
2088
|
-
if (
|
|
2089
|
-
|
|
2090
|
-
if (
|
|
2091
|
-
|
|
2543
|
+
if (Array.isArray(raw)) {
|
|
2544
|
+
if (raw.length === 0) continue;
|
|
2545
|
+
if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && (0, _rebasepro_types.toCanonicalOp)(raw[0][0]) === raw[0][0]) {
|
|
2546
|
+
result[field] = raw;
|
|
2092
2547
|
continue;
|
|
2093
2548
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2549
|
+
if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
|
|
2550
|
+
else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
|
|
2551
|
+
else result[field] = ["in", raw];
|
|
2552
|
+
} else if (typeof raw === "string") result[field] = deserializeSingle(raw);
|
|
2553
|
+
else result[field] = ["==", raw];
|
|
2554
|
+
}
|
|
2555
|
+
return result;
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Serialize a `LogicalCondition` or `FilterCondition` to its wire-format string.
|
|
2559
|
+
*
|
|
2560
|
+
* @example
|
|
2561
|
+
* serializeLogicalCondition({ column: "status", operator: "==", value: "active" })
|
|
2562
|
+
* // → "status.eq.active"
|
|
2563
|
+
*
|
|
2564
|
+
* serializeLogicalCondition({ type: "or", conditions: [...] })
|
|
2565
|
+
* // → "or(status.eq.active,status.eq.pending)"
|
|
2566
|
+
*/
|
|
2567
|
+
function serializeLogicalCondition(cond) {
|
|
2568
|
+
if ("type" in cond) {
|
|
2569
|
+
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
2570
|
+
return `${cond.type}(${inner})`;
|
|
2571
|
+
}
|
|
2572
|
+
const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
|
|
2573
|
+
if (Array.isArray(cond.value)) {
|
|
2574
|
+
const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
|
|
2575
|
+
return `${cond.column}.${restOp}.(${items})`;
|
|
2576
|
+
}
|
|
2577
|
+
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Parse a logical condition wire-format string back into a
|
|
2581
|
+
* `LogicalCondition` or `FilterCondition`.
|
|
2582
|
+
*
|
|
2583
|
+
* @example
|
|
2584
|
+
* deserializeLogicalCondition("status.eq.active")
|
|
2585
|
+
* // → { column: "status", operator: "==", value: "active" }
|
|
2586
|
+
*
|
|
2587
|
+
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
2588
|
+
* // → { type: "or", conditions: [...] }
|
|
2589
|
+
*/
|
|
2590
|
+
function deserializeLogicalCondition(str) {
|
|
2591
|
+
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
2592
|
+
if (logicalMatch) {
|
|
2593
|
+
const type = logicalMatch[1];
|
|
2594
|
+
const innerStr = logicalMatch[2];
|
|
2595
|
+
const conditions = [];
|
|
2596
|
+
let depth = 0;
|
|
2597
|
+
let start = 0;
|
|
2598
|
+
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
2599
|
+
else if (innerStr[i] === ")") depth--;
|
|
2600
|
+
else if (innerStr[i] === "," && depth === 0) {
|
|
2601
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
2602
|
+
start = i + 1;
|
|
2103
2603
|
}
|
|
2604
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
2605
|
+
return {
|
|
2606
|
+
type,
|
|
2607
|
+
conditions
|
|
2608
|
+
};
|
|
2104
2609
|
}
|
|
2105
|
-
|
|
2610
|
+
const firstDot = str.indexOf(".");
|
|
2611
|
+
if (firstDot === -1) return {
|
|
2612
|
+
column: str,
|
|
2613
|
+
operator: "==",
|
|
2614
|
+
value: true
|
|
2615
|
+
};
|
|
2616
|
+
const column = str.substring(0, firstDot);
|
|
2617
|
+
const rest = str.substring(firstDot + 1);
|
|
2618
|
+
const secondDot = rest.indexOf(".");
|
|
2619
|
+
if (secondDot === -1) return {
|
|
2620
|
+
column,
|
|
2621
|
+
operator: "==",
|
|
2622
|
+
value: rest
|
|
2623
|
+
};
|
|
2624
|
+
const opStr = rest.substring(0, secondDot);
|
|
2625
|
+
const valueStr = rest.substring(secondDot + 1);
|
|
2626
|
+
const operator = (0, _rebasepro_types.toCanonicalOp)(opStr) ?? "==";
|
|
2627
|
+
if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
|
|
2628
|
+
column,
|
|
2629
|
+
operator,
|
|
2630
|
+
value: splitListItems(valueStr.slice(1, -1))
|
|
2631
|
+
};
|
|
2632
|
+
return {
|
|
2633
|
+
column,
|
|
2634
|
+
operator,
|
|
2635
|
+
value: valueStr
|
|
2636
|
+
};
|
|
2106
2637
|
}
|
|
2638
|
+
//#endregion
|
|
2639
|
+
//#region src/data/buildRebaseData.ts
|
|
2107
2640
|
/**
|
|
2108
|
-
*
|
|
2641
|
+
* Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
|
|
2642
|
+
* Mirrors the client SDK's rowToEntity conversion.
|
|
2109
2643
|
*/
|
|
2110
|
-
function
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2644
|
+
function rowToEntity(row, slug) {
|
|
2645
|
+
return {
|
|
2646
|
+
id: row.id,
|
|
2647
|
+
path: slug,
|
|
2648
|
+
values: row
|
|
2649
|
+
};
|
|
2114
2650
|
}
|
|
2115
2651
|
function createDriverAccessor(driver, slug) {
|
|
2116
2652
|
const accessor = {
|
|
2117
2653
|
async find(params) {
|
|
2118
|
-
const
|
|
2119
|
-
const
|
|
2654
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2655
|
+
const limit = params?.limit ?? 20;
|
|
2656
|
+
const offset = params?.offset ?? 0;
|
|
2657
|
+
const fetchService = driver.restFetchService;
|
|
2658
|
+
const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
|
|
2659
|
+
filter,
|
|
2660
|
+
limit: params?.limit,
|
|
2661
|
+
offset: params?.offset,
|
|
2662
|
+
orderBy: params?.orderBy?.[0],
|
|
2663
|
+
order: params?.orderBy?.[1],
|
|
2664
|
+
searchString: params?.searchString
|
|
2665
|
+
}, params.include) : await driver.fetchCollection({
|
|
2120
2666
|
path: slug,
|
|
2121
2667
|
limit: params?.limit,
|
|
2122
2668
|
offset: params?.offset,
|
|
2123
|
-
filter
|
|
2124
|
-
orderBy:
|
|
2125
|
-
order:
|
|
2669
|
+
filter,
|
|
2670
|
+
orderBy: params?.orderBy?.[0],
|
|
2671
|
+
order: params?.orderBy?.[1],
|
|
2126
2672
|
searchString: params?.searchString
|
|
2127
2673
|
});
|
|
2128
|
-
|
|
2129
|
-
|
|
2674
|
+
let total = rows.length + offset;
|
|
2675
|
+
let hasMore = rows.length >= limit;
|
|
2676
|
+
if (driver.count) {
|
|
2677
|
+
total = await driver.count({
|
|
2678
|
+
path: slug,
|
|
2679
|
+
filter
|
|
2680
|
+
});
|
|
2681
|
+
hasMore = offset + rows.length < total;
|
|
2682
|
+
}
|
|
2130
2683
|
return {
|
|
2131
|
-
data:
|
|
2684
|
+
data: rows.map((row) => rowToEntity(row, slug)),
|
|
2132
2685
|
meta: {
|
|
2133
|
-
total
|
|
2686
|
+
total,
|
|
2134
2687
|
limit,
|
|
2135
2688
|
offset,
|
|
2136
|
-
hasMore
|
|
2689
|
+
hasMore
|
|
2137
2690
|
}
|
|
2138
2691
|
};
|
|
2139
2692
|
},
|
|
2140
2693
|
async findById(id) {
|
|
2141
|
-
|
|
2694
|
+
const row = await driver.fetchOne({
|
|
2142
2695
|
path: slug,
|
|
2143
|
-
|
|
2696
|
+
id
|
|
2144
2697
|
});
|
|
2698
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
2145
2699
|
},
|
|
2146
2700
|
async create(data, id) {
|
|
2147
|
-
return driver.
|
|
2701
|
+
return rowToEntity(await driver.save({
|
|
2148
2702
|
path: slug,
|
|
2149
2703
|
values: data,
|
|
2150
|
-
|
|
2704
|
+
id,
|
|
2151
2705
|
status: "new"
|
|
2152
|
-
});
|
|
2706
|
+
}), slug);
|
|
2153
2707
|
},
|
|
2154
2708
|
async update(id, data) {
|
|
2155
|
-
return driver.
|
|
2709
|
+
return rowToEntity(await driver.save({
|
|
2156
2710
|
path: slug,
|
|
2157
2711
|
values: data,
|
|
2158
|
-
|
|
2712
|
+
id,
|
|
2159
2713
|
status: "existing"
|
|
2160
|
-
});
|
|
2714
|
+
}), slug);
|
|
2161
2715
|
},
|
|
2162
2716
|
async delete(id) {
|
|
2163
|
-
return driver.
|
|
2717
|
+
return driver.delete({ row: {
|
|
2164
2718
|
id,
|
|
2165
2719
|
path: slug,
|
|
2166
2720
|
values: {}
|
|
2167
2721
|
} });
|
|
2168
2722
|
},
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
count: driver.countEntities ? async (params) => {
|
|
2173
|
-
return driver.countEntities({
|
|
2723
|
+
count: driver.count ? async (params) => {
|
|
2724
|
+
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2725
|
+
return driver.count({
|
|
2174
2726
|
path: slug,
|
|
2175
|
-
filter
|
|
2727
|
+
filter
|
|
2176
2728
|
});
|
|
2177
2729
|
} : void 0,
|
|
2178
2730
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
2179
|
-
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2180
2731
|
const limit = params?.limit ?? 20;
|
|
2181
2732
|
const offset = params?.offset ?? 0;
|
|
2182
2733
|
return driver.listenCollection({
|
|
2183
2734
|
path: slug,
|
|
2184
2735
|
limit: params?.limit,
|
|
2185
2736
|
offset: params?.offset,
|
|
2186
|
-
filter:
|
|
2187
|
-
orderBy:
|
|
2188
|
-
order:
|
|
2737
|
+
filter: params?.where,
|
|
2738
|
+
orderBy: params?.orderBy?.[0],
|
|
2739
|
+
order: params?.orderBy?.[1],
|
|
2189
2740
|
searchString: params?.searchString,
|
|
2190
2741
|
onUpdate: (entities) => {
|
|
2191
2742
|
onUpdate({
|
|
2192
|
-
data: entities,
|
|
2743
|
+
data: entities.map((row) => rowToEntity(row, slug)),
|
|
2193
2744
|
meta: {
|
|
2194
2745
|
total: entities.length,
|
|
2195
2746
|
limit,
|
|
@@ -2201,11 +2752,11 @@
|
|
|
2201
2752
|
onError
|
|
2202
2753
|
});
|
|
2203
2754
|
} : void 0,
|
|
2204
|
-
listenById: driver.
|
|
2205
|
-
return driver.
|
|
2755
|
+
listenById: driver.listenOne ? (id, onUpdate, onError) => {
|
|
2756
|
+
return driver.listenOne({
|
|
2206
2757
|
path: slug,
|
|
2207
|
-
|
|
2208
|
-
onUpdate: (entity) => onUpdate(entity
|
|
2758
|
+
id,
|
|
2759
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
2209
2760
|
onError
|
|
2210
2761
|
});
|
|
2211
2762
|
} : void 0,
|
|
@@ -2242,7 +2793,7 @@
|
|
|
2242
2793
|
* @example
|
|
2243
2794
|
* const data = buildRebaseData(driver);
|
|
2244
2795
|
* await data.products.create({ name: "Camera", price: 299 });
|
|
2245
|
-
* const { data: items } = await data.products.find({ where: { status: "
|
|
2796
|
+
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
2246
2797
|
*/
|
|
2247
2798
|
function buildRebaseData(driver) {
|
|
2248
2799
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2261,6 +2812,230 @@
|
|
|
2261
2812
|
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
2262
2813
|
} });
|
|
2263
2814
|
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
|
|
2817
|
+
* (id included) under `.values`, so this is just that payload.
|
|
2818
|
+
*/
|
|
2819
|
+
function entityToRow(entity) {
|
|
2820
|
+
return entity.values;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
|
|
2824
|
+
* but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
|
|
2825
|
+
* `FindResponse<M>`.
|
|
2826
|
+
*/
|
|
2827
|
+
var SdkQueryBuilder = class {
|
|
2828
|
+
client;
|
|
2829
|
+
params = { where: {} };
|
|
2830
|
+
constructor(client) {
|
|
2831
|
+
this.client = client;
|
|
2832
|
+
}
|
|
2833
|
+
where(columnOrCondition, operator, value) {
|
|
2834
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
2835
|
+
this.params.logical = columnOrCondition;
|
|
2836
|
+
return this;
|
|
2837
|
+
}
|
|
2838
|
+
if (!this.params.where) this.params.where = {};
|
|
2839
|
+
const column = columnOrCondition;
|
|
2840
|
+
const condition = [operator, value];
|
|
2841
|
+
const existing = this.params.where[column];
|
|
2842
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
2843
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
2844
|
+
else {
|
|
2845
|
+
let firstCondition;
|
|
2846
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
2847
|
+
else firstCondition = ["==", existing];
|
|
2848
|
+
this.params.where[column] = [firstCondition, condition];
|
|
2849
|
+
}
|
|
2850
|
+
return this;
|
|
2851
|
+
}
|
|
2852
|
+
orderBy(column, direction = "asc") {
|
|
2853
|
+
this.params.orderBy = [column, direction];
|
|
2854
|
+
return this;
|
|
2855
|
+
}
|
|
2856
|
+
limit(count) {
|
|
2857
|
+
this.params.limit = count;
|
|
2858
|
+
return this;
|
|
2859
|
+
}
|
|
2860
|
+
offset(count) {
|
|
2861
|
+
this.params.offset = count;
|
|
2862
|
+
return this;
|
|
2863
|
+
}
|
|
2864
|
+
search(searchString) {
|
|
2865
|
+
this.params.searchString = searchString;
|
|
2866
|
+
return this;
|
|
2867
|
+
}
|
|
2868
|
+
include(...relations) {
|
|
2869
|
+
this.params.include = relations;
|
|
2870
|
+
return this;
|
|
2871
|
+
}
|
|
2872
|
+
async find() {
|
|
2873
|
+
return this.client.find(this.params);
|
|
2874
|
+
}
|
|
2875
|
+
async count() {
|
|
2876
|
+
return this.client.count ? this.client.count(this.params) : 0;
|
|
2877
|
+
}
|
|
2878
|
+
listen(onUpdate, onError) {
|
|
2879
|
+
if (!this.client.listen) throw new Error("Listen is only available when the driver supports realtime.");
|
|
2880
|
+
return this.client.listen(this.params, onUpdate, onError);
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
/**
|
|
2884
|
+
* Wrap a Entity-shaped {@link CollectionAccessor} into a flat
|
|
2885
|
+
* {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
|
|
2886
|
+
* so the backend SDK is byte-for-byte the same shape as the frontend client.
|
|
2887
|
+
*/
|
|
2888
|
+
function toSdkCollectionClient(snap) {
|
|
2889
|
+
const client = {
|
|
2890
|
+
async find(params) {
|
|
2891
|
+
const res = await snap.find(params);
|
|
2892
|
+
return {
|
|
2893
|
+
data: res.data.map(entityToRow),
|
|
2894
|
+
meta: res.meta
|
|
2895
|
+
};
|
|
2896
|
+
},
|
|
2897
|
+
async findById(id) {
|
|
2898
|
+
const s = await snap.findById(id);
|
|
2899
|
+
return s ? entityToRow(s) : void 0;
|
|
2900
|
+
},
|
|
2901
|
+
async create(data, id) {
|
|
2902
|
+
return entityToRow(await snap.create(data, id));
|
|
2903
|
+
},
|
|
2904
|
+
async update(id, data) {
|
|
2905
|
+
return entityToRow(await snap.update(id, data));
|
|
2906
|
+
},
|
|
2907
|
+
delete(id) {
|
|
2908
|
+
return snap.delete(id);
|
|
2909
|
+
},
|
|
2910
|
+
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
2911
|
+
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
2912
|
+
data: res.data.map(entityToRow),
|
|
2913
|
+
meta: res.meta
|
|
2914
|
+
}), onError) : void 0,
|
|
2915
|
+
listenById: snap.listenById ? (id, onUpdate, onError) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError) : void 0,
|
|
2916
|
+
where(columnOrCondition, operator, value) {
|
|
2917
|
+
const builder = new SdkQueryBuilder(client);
|
|
2918
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2919
|
+
return builder.where(columnOrCondition, operator, value);
|
|
2920
|
+
},
|
|
2921
|
+
orderBy: (column, direction) => new SdkQueryBuilder(client).orderBy(column, direction),
|
|
2922
|
+
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
2923
|
+
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
2924
|
+
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
2925
|
+
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
2926
|
+
};
|
|
2927
|
+
return client;
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
2931
|
+
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
2932
|
+
* `{ id, path, values }` view-model the admin CMS renders.
|
|
2933
|
+
*/
|
|
2934
|
+
function toEntityAccessor(sdk, slug) {
|
|
2935
|
+
const accessor = {
|
|
2936
|
+
async find(params) {
|
|
2937
|
+
const res = await sdk.find(params);
|
|
2938
|
+
return {
|
|
2939
|
+
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2940
|
+
meta: res.meta
|
|
2941
|
+
};
|
|
2942
|
+
},
|
|
2943
|
+
async findById(id) {
|
|
2944
|
+
const row = await sdk.findById(id);
|
|
2945
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
2946
|
+
},
|
|
2947
|
+
async create(data, id) {
|
|
2948
|
+
return rowToEntity(await sdk.create(data, id), slug);
|
|
2949
|
+
},
|
|
2950
|
+
async update(id, data) {
|
|
2951
|
+
const row = await sdk.update(id, data);
|
|
2952
|
+
if (!row) throw new Error(`Update returned no data for id ${id}`);
|
|
2953
|
+
return rowToEntity(row, slug);
|
|
2954
|
+
},
|
|
2955
|
+
delete(id) {
|
|
2956
|
+
return sdk.delete(id);
|
|
2957
|
+
},
|
|
2958
|
+
count: sdk.count ? (params) => sdk.count(params) : void 0,
|
|
2959
|
+
listen: sdk.listen ? (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
|
|
2960
|
+
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2961
|
+
meta: res.meta
|
|
2962
|
+
}), onError) : void 0,
|
|
2963
|
+
listenById: sdk.listenById ? (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug) : void 0), onError) : void 0,
|
|
2964
|
+
where(columnOrCondition, operator, value) {
|
|
2965
|
+
const builder = new QueryBuilder(accessor);
|
|
2966
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2967
|
+
return builder.where(columnOrCondition, operator, value);
|
|
2968
|
+
},
|
|
2969
|
+
orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
|
|
2970
|
+
limit: (count) => new QueryBuilder(accessor).limit(count),
|
|
2971
|
+
offset: (count) => new QueryBuilder(accessor).offset(count),
|
|
2972
|
+
search: (searchString) => new QueryBuilder(accessor).search(searchString),
|
|
2973
|
+
include: (...relations) => new QueryBuilder(accessor).include(...relations)
|
|
2974
|
+
};
|
|
2975
|
+
return accessor;
|
|
2976
|
+
}
|
|
2977
|
+
/**
|
|
2978
|
+
* Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
|
|
2979
|
+
*
|
|
2980
|
+
* This is the **CMS boundary**: the SDK client (`client.data`) returns flat
|
|
2981
|
+
* rows, but the admin renders the `Entity` view-model (`entity.values.*`).
|
|
2982
|
+
* `core/Rebase.tsx` wraps `client.data` through this before handing it to the
|
|
2983
|
+
* CMS `RebaseDataContext` — without it the admin renders rows with only their
|
|
2984
|
+
* `id`.
|
|
2985
|
+
*/
|
|
2986
|
+
function wrapAsEntityData(sdkData) {
|
|
2987
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2988
|
+
function getAccessor(slug) {
|
|
2989
|
+
let accessor = cache.get(slug);
|
|
2990
|
+
if (!accessor) {
|
|
2991
|
+
accessor = toEntityAccessor(sdkData.collection(slug), slug);
|
|
2992
|
+
cache.set(slug, accessor);
|
|
2993
|
+
}
|
|
2994
|
+
return accessor;
|
|
2995
|
+
}
|
|
2996
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2997
|
+
if (prop === "collection") return getAccessor;
|
|
2998
|
+
if (typeof prop === "symbol") return void 0;
|
|
2999
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3000
|
+
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
3001
|
+
} });
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
|
|
3005
|
+
*
|
|
3006
|
+
* Every collection accessor is adapted to return flat rows. Use this to derive
|
|
3007
|
+
* the flat SDK data layer (`context.data`) from an existing Entity data layer
|
|
3008
|
+
* — e.g. the admin routes its Entity data via `useData()` and exposes the
|
|
3009
|
+
* same routing as flat `context.data` for callbacks by wrapping it here.
|
|
3010
|
+
*/
|
|
3011
|
+
function wrapAsSdkData(entityData) {
|
|
3012
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3013
|
+
function getAccessor(slug) {
|
|
3014
|
+
let accessor = cache.get(slug);
|
|
3015
|
+
if (!accessor) {
|
|
3016
|
+
accessor = toSdkCollectionClient(entityData.collection(slug));
|
|
3017
|
+
cache.set(slug, accessor);
|
|
3018
|
+
}
|
|
3019
|
+
return accessor;
|
|
3020
|
+
}
|
|
3021
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
3022
|
+
if (prop === "collection") return getAccessor;
|
|
3023
|
+
if (typeof prop === "symbol") return void 0;
|
|
3024
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
3025
|
+
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
3026
|
+
} });
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Build a flat {@link RebaseSdkData} from a `DataDriver`.
|
|
3030
|
+
*
|
|
3031
|
+
* This is the developer-facing SDK data layer used by backend framework
|
|
3032
|
+
* callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
|
|
3033
|
+
* identical in shape to the frontend SDK client — so the API is symmetric
|
|
3034
|
+
* across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
|
|
3035
|
+
*/
|
|
3036
|
+
function buildSdkData(driver) {
|
|
3037
|
+
return wrapAsSdkData(buildRebaseData(driver));
|
|
3038
|
+
}
|
|
2264
3039
|
//#endregion
|
|
2265
3040
|
//#region src/data/buildRoutedRebaseData.ts
|
|
2266
3041
|
/**
|
|
@@ -2305,6 +3080,57 @@
|
|
|
2305
3080
|
} });
|
|
2306
3081
|
}
|
|
2307
3082
|
//#endregion
|
|
3083
|
+
//#region src/data/sort-dialect.ts
|
|
3084
|
+
/**
|
|
3085
|
+
* Sort-order wire codec.
|
|
3086
|
+
*
|
|
3087
|
+
* This is the ONLY module that knows about the colon-delimited wire format
|
|
3088
|
+
* (`"field:direction"`) used in HTTP query parameters.
|
|
3089
|
+
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
3090
|
+
*
|
|
3091
|
+
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
3092
|
+
*
|
|
3093
|
+
* @module
|
|
3094
|
+
*/
|
|
3095
|
+
/**
|
|
3096
|
+
* Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
|
|
3097
|
+
*
|
|
3098
|
+
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
3099
|
+
* (from an untyped JS caller), it is returned unchanged.
|
|
3100
|
+
* This is undocumented tolerance, not public API — don't rely on it.
|
|
3101
|
+
*
|
|
3102
|
+
* @param orderBy - A canonical `[field, direction]` tuple, or at runtime
|
|
3103
|
+
* possibly a pre-serialized string (undocumented tolerance).
|
|
3104
|
+
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
3105
|
+
*
|
|
3106
|
+
* @remarks
|
|
3107
|
+
* Field names containing `:` are representable in the tuple form but
|
|
3108
|
+
* **not** on the wire — this is an inherent limitation of the colon-delimited
|
|
3109
|
+
* encoding and is not resolved here.
|
|
3110
|
+
*/
|
|
3111
|
+
function serializeOrderBy(orderBy) {
|
|
3112
|
+
if (!orderBy) return void 0;
|
|
3113
|
+
if (typeof orderBy === "string") return orderBy;
|
|
3114
|
+
return `${orderBy[0]}:${orderBy[1]}`;
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
|
|
3118
|
+
*
|
|
3119
|
+
* Lenient parsing (matches existing server behaviour):
|
|
3120
|
+
* - Bare field name (no colon): `"name"` → `["name", "asc"]`
|
|
3121
|
+
* - Unknown direction: `"name:foo"` → `["name", "asc"]`
|
|
3122
|
+
* - Empty / falsy input: → `undefined`
|
|
3123
|
+
*
|
|
3124
|
+
* @param raw - The wire-format string from an HTTP query parameter.
|
|
3125
|
+
* @returns The canonical tuple, or `undefined` if the input is empty/falsy.
|
|
3126
|
+
*/
|
|
3127
|
+
function deserializeOrderBy(raw) {
|
|
3128
|
+
if (!raw) return void 0;
|
|
3129
|
+
const idx = raw.indexOf(":");
|
|
3130
|
+
if (idx === -1) return [raw, "asc"];
|
|
3131
|
+
return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
|
|
3132
|
+
}
|
|
3133
|
+
//#endregion
|
|
2308
3134
|
//#region src/table-classification.ts
|
|
2309
3135
|
/** Schemas that are always considered Rebase-internal. */
|
|
2310
3136
|
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
@@ -2392,18 +3218,13 @@
|
|
|
2392
3218
|
exports.addInitialSlash = addInitialSlash;
|
|
2393
3219
|
exports.and = and;
|
|
2394
3220
|
exports.applyPropertyConditions = applyPropertyConditions;
|
|
2395
|
-
exports.buildAdditionalFieldDelegate = buildAdditionalFieldDelegate;
|
|
2396
3221
|
exports.buildCollection = buildCollection;
|
|
2397
3222
|
exports.buildConditionContext = buildConditionContext;
|
|
2398
|
-
exports.buildEntityCallbacks = buildEntityCallbacks;
|
|
2399
|
-
exports.buildEnum = buildEnum;
|
|
2400
|
-
exports.buildEnumValueConfig = buildEnumValueConfig;
|
|
2401
|
-
exports.buildProperties = buildProperties;
|
|
2402
|
-
exports.buildPropertiesOrBuilder = buildPropertiesOrBuilder;
|
|
2403
3223
|
exports.buildProperty = buildProperty;
|
|
2404
3224
|
exports.buildPropertyCallbacks = buildPropertyCallbacks;
|
|
2405
3225
|
exports.buildRebaseData = buildRebaseData;
|
|
2406
3226
|
exports.buildRoutedRebaseData = buildRoutedRebaseData;
|
|
3227
|
+
exports.buildSdkData = buildSdkData;
|
|
2407
3228
|
exports.canCreateEntity = canCreateEntity;
|
|
2408
3229
|
exports.canDeleteEntity = canDeleteEntity;
|
|
2409
3230
|
exports.canEditEntity = canEditEntity;
|
|
@@ -2415,9 +3236,14 @@
|
|
|
2415
3236
|
exports.createRelationRef = createRelationRef;
|
|
2416
3237
|
exports.createRelationRefWithData = createRelationRefWithData;
|
|
2417
3238
|
exports.defaultUsersCollection = defaultUsersCollection;
|
|
3239
|
+
exports.defineCollection = defineCollection;
|
|
3240
|
+
exports.deserializeFilter = deserializeFilter;
|
|
3241
|
+
exports.deserializeLogicalCondition = deserializeLogicalCondition;
|
|
3242
|
+
exports.deserializeOrderBy = deserializeOrderBy;
|
|
2418
3243
|
exports.detectJunctionTables = detectJunctionTables;
|
|
2419
3244
|
exports.enumToObjectEntries = enumToObjectEntries;
|
|
2420
3245
|
exports.evaluateCondition = evaluateCondition;
|
|
3246
|
+
exports.evaluatePolicy = evaluatePolicy;
|
|
2421
3247
|
exports.findRelation = findRelation;
|
|
2422
3248
|
exports.fullPathToCollectionSegments = fullPathToCollectionSegments;
|
|
2423
3249
|
exports.getArrayResolvedProperties = getArrayResolvedProperties;
|
|
@@ -2446,6 +3272,7 @@
|
|
|
2446
3272
|
exports.isRebaseInternalTable = isRebaseInternalTable;
|
|
2447
3273
|
exports.normalizeToEntityRelation = normalizeToEntityRelation;
|
|
2448
3274
|
exports.or = or;
|
|
3275
|
+
exports.policyToPostgres = policyToPostgres;
|
|
2449
3276
|
exports.registerConditionOperations = registerConditionOperations;
|
|
2450
3277
|
exports.removeInitialAndTrailingSlashes = removeInitialAndTrailingSlashes;
|
|
2451
3278
|
exports.removeInitialSlash = removeInitialSlash;
|
|
@@ -2456,6 +3283,7 @@
|
|
|
2456
3283
|
exports.resolveDataSource = resolveDataSource;
|
|
2457
3284
|
exports.resolveDefaultSelectedView = resolveDefaultSelectedView;
|
|
2458
3285
|
exports.resolveEnumValues = resolveEnumValues;
|
|
3286
|
+
exports.resolveFilterOperators = resolveFilterOperators;
|
|
2459
3287
|
exports.resolveProperties = resolveProperties;
|
|
2460
3288
|
exports.resolveProperty = resolveProperty;
|
|
2461
3289
|
exports.resolvePropertyEnum = resolvePropertyEnum;
|
|
@@ -2463,14 +3291,21 @@
|
|
|
2463
3291
|
exports.resolveRelationProperty = resolveRelationProperty;
|
|
2464
3292
|
exports.resolveStorageFilenameString = resolveStorageFilenameString;
|
|
2465
3293
|
exports.resolveStoragePathString = resolveStoragePathString;
|
|
3294
|
+
exports.resolveStorageSource = resolveStorageSource;
|
|
2466
3295
|
exports.sanitizeData = sanitizeData;
|
|
2467
3296
|
exports.sanitizeRelation = sanitizeRelation;
|
|
3297
|
+
exports.securityRuleToConditions = securityRuleToConditions;
|
|
2468
3298
|
exports.segmentsToStrippedPath = segmentsToStrippedPath;
|
|
3299
|
+
exports.serializeFilter = serializeFilter;
|
|
3300
|
+
exports.serializeLogicalCondition = serializeLogicalCondition;
|
|
3301
|
+
exports.serializeOrderBy = serializeOrderBy;
|
|
2469
3302
|
exports.sortProperties = sortProperties;
|
|
2470
3303
|
exports.stripCollectionPath = stripCollectionPath;
|
|
2471
3304
|
exports.traverseValueProperty = traverseValueProperty;
|
|
2472
3305
|
exports.traverseValuesProperties = traverseValuesProperties;
|
|
2473
3306
|
exports.updateDateAutoValues = updateDateAutoValues;
|
|
3307
|
+
exports.wrapAsEntityData = wrapAsEntityData;
|
|
3308
|
+
exports.wrapAsSdkData = wrapAsSdkData;
|
|
2474
3309
|
});
|
|
2475
3310
|
|
|
2476
3311
|
//# sourceMappingURL=index.umd.js.map
|