@rebasepro/common 0.8.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 +16 -16
- package/dist/collections/default-collections.d.ts +1 -1
- package/dist/data/buildRebaseData.d.ts +30 -2
- package/dist/data/buildRoutedRebaseData.d.ts +14 -9
- package/dist/data/filter-dialect.d.ts +18 -4
- package/dist/data/query_builder.d.ts +1 -1
- package/dist/data/resolveDataSource.d.ts +1 -1
- package/dist/data/sort-dialect.d.ts +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +569 -159
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +573 -163
- package/dist/index.umd.js.map +1 -1
- package/dist/util/builders.d.ts +19 -56
- package/dist/util/callbacks.d.ts +3 -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 +1 -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 +6 -6
- package/dist/util/policy/policyToPostgres.d.ts +14 -2
- 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/package.json +3 -3
- package/src/collections/CollectionRegistry.ts +36 -36
- package/src/data/buildRebaseData.ts +332 -57
- package/src/data/buildRoutedRebaseData.ts +22 -16
- package/src/data/filter-dialect.ts +145 -60
- package/src/data/query_builder.ts +11 -2
- package/src/data/resolveDataSource.ts +1 -1
- package/src/data/sort-dialect.ts +56 -0
- package/src/index.ts +1 -0
- package/src/util/builders.ts +25 -99
- package/src/util/callbacks.ts +8 -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 +1 -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 +2 -2
- package/src/util/permissions.ts +7 -7
- package/src/util/policy/evaluatePolicy.ts +6 -0
- package/src/util/policy/policyToPostgres.ts +90 -10
- package/src/util/references.ts +2 -2
- package/src/util/relations.ts +12 -12
- package/src/util/resolutions.ts +5 -5
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, policy, toCanonicalOp } from "@rebasepro/types";
|
|
1
|
+
import { ALL_WHERE_FILTER_OPS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, 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";
|
|
@@ -54,7 +54,7 @@ function getDefaultValueFortype(type) {
|
|
|
54
54
|
else return null;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* Update the automatic values in
|
|
57
|
+
* Update the automatic values in a entity before save
|
|
58
58
|
* @group Driver
|
|
59
59
|
*/
|
|
60
60
|
function updateDateAutoValues({ inputValues, properties, status, timestampNowValue }) {
|
|
@@ -66,7 +66,7 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
|
|
|
66
66
|
}) ?? {};
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Add missing required fields, expected in the collection, to the values of
|
|
69
|
+
* Add missing required fields, expected in the collection, to the values of a entity
|
|
70
70
|
* @param values
|
|
71
71
|
* @param properties
|
|
72
72
|
* @group Driver
|
|
@@ -236,7 +236,7 @@ function getLocalChangesBackup(collection) {
|
|
|
236
236
|
return collection.localChangesBackup;
|
|
237
237
|
}
|
|
238
238
|
/**
|
|
239
|
-
* Returns the primary keys for
|
|
239
|
+
* Returns the primary keys for a entity collection by inspecting the properties
|
|
240
240
|
* and finding any properties with `isId`.
|
|
241
241
|
* Fallbacks to `["id"]` if no properties are marked as `isId: true`.
|
|
242
242
|
* @param collection
|
|
@@ -780,22 +780,56 @@ function withRoles(base, rule) {
|
|
|
780
780
|
* {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
|
|
781
781
|
* and the admin UI derive from the exact same expression.
|
|
782
782
|
*/
|
|
783
|
-
function policyToPostgres(expr, collection) {
|
|
783
|
+
function policyToPostgres(expr, collection, options) {
|
|
784
|
+
return compile(expr, {
|
|
785
|
+
fieldCollection: collection,
|
|
786
|
+
fieldPrefix: "",
|
|
787
|
+
outerCollection: collection,
|
|
788
|
+
outerPrefix: "",
|
|
789
|
+
resolveCollection: options?.resolveCollection,
|
|
790
|
+
alias: { n: 0 }
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
function compile(expr, scope) {
|
|
784
794
|
switch (expr.kind) {
|
|
785
795
|
case "true": return "true";
|
|
786
796
|
case "false": return "false";
|
|
787
|
-
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${
|
|
788
|
-
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${
|
|
797
|
+
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" AND ");
|
|
798
|
+
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${compile(o, scope)})`).join(" OR ");
|
|
789
799
|
case "not":
|
|
790
800
|
if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
|
|
791
|
-
return `NOT (${
|
|
792
|
-
case "compare": return `${operandToSql(expr.left,
|
|
801
|
+
return `NOT (${compile(expr.operand, scope)})`;
|
|
802
|
+
case "compare": return `${operandToSql(expr.left, scope)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, scope)}`;
|
|
793
803
|
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
794
804
|
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
795
805
|
case "authenticated": return "auth.uid() IS NOT NULL";
|
|
806
|
+
case "existsIn": return compileExistsIn(expr, scope);
|
|
796
807
|
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
797
808
|
}
|
|
798
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Compiles `existsIn` to a correlated `EXISTS (SELECT 1 FROM <join> WHERE ...)`.
|
|
812
|
+
* Inside the subquery, `field` operands bind to the aliased join table and
|
|
813
|
+
* `outerField` operands bind to the (table-qualified) outer RLS row.
|
|
814
|
+
*/
|
|
815
|
+
function compileExistsIn(expr, scope) {
|
|
816
|
+
const join = scope.resolveCollection?.(expr.collection);
|
|
817
|
+
const joinTable = join ? getTableName(join) : toSnakeCase(expr.collection);
|
|
818
|
+
const joinSchema = schemaOf(join) ?? schemaOf(scope.outerCollection) ?? "public";
|
|
819
|
+
const alias = `_ex${scope.alias.n++}`;
|
|
820
|
+
const outerTable = scope.outerCollection ? getTableName(scope.outerCollection) : void 0;
|
|
821
|
+
const outerSchema = schemaOf(scope.outerCollection) ?? "public";
|
|
822
|
+
const outerPrefix = outerTable ? `"${outerSchema}"."${outerTable}".` : "";
|
|
823
|
+
const innerScope = {
|
|
824
|
+
fieldCollection: join,
|
|
825
|
+
fieldPrefix: `"${alias}".`,
|
|
826
|
+
outerCollection: scope.outerCollection,
|
|
827
|
+
outerPrefix,
|
|
828
|
+
resolveCollection: scope.resolveCollection,
|
|
829
|
+
alias: scope.alias
|
|
830
|
+
};
|
|
831
|
+
return `EXISTS (SELECT 1 FROM "${joinSchema}"."${joinTable}" "${alias}" WHERE ${compile(expr.where, innerScope)})`;
|
|
832
|
+
}
|
|
799
833
|
var COMPARE_SQL = {
|
|
800
834
|
eq: "=",
|
|
801
835
|
neq: "!=",
|
|
@@ -804,14 +838,18 @@ var COMPARE_SQL = {
|
|
|
804
838
|
gt: ">",
|
|
805
839
|
gte: ">="
|
|
806
840
|
};
|
|
807
|
-
function operandToSql(operand,
|
|
841
|
+
function operandToSql(operand, scope) {
|
|
808
842
|
switch (operand.kind) {
|
|
809
|
-
case "field": return resolveColumnName(operand.name,
|
|
843
|
+
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
844
|
+
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
810
845
|
case "literal": return quoteLiteral(operand.value);
|
|
811
846
|
case "authUid": return "auth.uid()";
|
|
812
847
|
case "authRoles": return "string_to_array(auth.roles(), ',')";
|
|
813
848
|
}
|
|
814
849
|
}
|
|
850
|
+
function schemaOf(collection) {
|
|
851
|
+
return collection?.schema || void 0;
|
|
852
|
+
}
|
|
815
853
|
function resolveColumnName(propName, collection) {
|
|
816
854
|
const prop = collection?.properties?.[propName];
|
|
817
855
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
@@ -854,6 +892,7 @@ function evaluatePolicy(expr, ctx) {
|
|
|
854
892
|
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
855
893
|
}
|
|
856
894
|
case "authenticated": return ctx.uid != null;
|
|
895
|
+
case "existsIn": return "unknown";
|
|
857
896
|
case "raw": return "unknown";
|
|
858
897
|
}
|
|
859
898
|
}
|
|
@@ -891,6 +930,7 @@ function resolveOperand(operand, ctx) {
|
|
|
891
930
|
known: true,
|
|
892
931
|
value: ctx.entity.values[operand.name]
|
|
893
932
|
};
|
|
933
|
+
case "outerField": return { known: false };
|
|
894
934
|
}
|
|
895
935
|
}
|
|
896
936
|
function evaluateCompare(op, left, right, ctx) {
|
|
@@ -1102,7 +1142,7 @@ function resolveCollectionPathIds(path, allCollections) {
|
|
|
1102
1142
|
} else {
|
|
1103
1143
|
entityId = remainingPath;
|
|
1104
1144
|
remainingPath = "";
|
|
1105
|
-
console.warn(`resolveCollectionPathIds: Path seems to end with
|
|
1145
|
+
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.`);
|
|
1106
1146
|
}
|
|
1107
1147
|
resolvedPathParts.push(entityId);
|
|
1108
1148
|
currentCollections = getSubcollections(foundCollection);
|
|
@@ -1258,9 +1298,12 @@ function getParentReferencesFromPath(props) {
|
|
|
1258
1298
|
//#endregion
|
|
1259
1299
|
//#region src/util/builders.ts
|
|
1260
1300
|
/**
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1301
|
+
* @deprecated Use {@link defineCollection} instead — it infers property
|
|
1302
|
+
* types automatically (autocomplete on `titleProperty`, `sort`,
|
|
1303
|
+
* `propertiesOrder`, callbacks) without manual generics.
|
|
1304
|
+
* `buildCollection` is kept for FireCMS migration compatibility and will
|
|
1305
|
+
* be removed before 1.0.
|
|
1306
|
+
*
|
|
1264
1307
|
* @group Builder
|
|
1265
1308
|
*/
|
|
1266
1309
|
function buildCollection(collection) {
|
|
@@ -1274,68 +1317,16 @@ function defineCollection(collection) {
|
|
|
1274
1317
|
return collection;
|
|
1275
1318
|
}
|
|
1276
1319
|
/**
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1320
|
+
* @deprecated Use plain typed property objects with {@link defineCollection}
|
|
1321
|
+
* instead — `defineCollection` infers property types automatically, making
|
|
1322
|
+
* this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
|
|
1323
|
+
* compatibility and will be removed before 1.0.
|
|
1324
|
+
*
|
|
1280
1325
|
* @group Builder
|
|
1281
1326
|
*/
|
|
1282
1327
|
function buildProperty(property) {
|
|
1283
1328
|
return property;
|
|
1284
1329
|
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1287
|
-
* the properties keys.
|
|
1288
|
-
* @param properties
|
|
1289
|
-
* @group Builder
|
|
1290
|
-
*/
|
|
1291
|
-
function buildProperties(properties) {
|
|
1292
|
-
return properties;
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1296
|
-
* the properties keys.
|
|
1297
|
-
* @param propertiesOrBuilder
|
|
1298
|
-
* @group Builder
|
|
1299
|
-
*/
|
|
1300
|
-
function buildPropertiesOrBuilder(propertiesOrBuilder) {
|
|
1301
|
-
return propertiesOrBuilder;
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1305
|
-
* the properties keys.
|
|
1306
|
-
* @param enumValues
|
|
1307
|
-
* @group Builder
|
|
1308
|
-
*/
|
|
1309
|
-
function buildEnum(enumValues) {
|
|
1310
|
-
return enumValues;
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1314
|
-
* the properties keys.
|
|
1315
|
-
* @param enumValueConfig
|
|
1316
|
-
* @group Builder
|
|
1317
|
-
*/
|
|
1318
|
-
function buildEnumValueConfig(enumValueConfig) {
|
|
1319
|
-
return enumValueConfig;
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1323
|
-
* the properties keys.
|
|
1324
|
-
* @param callbacks
|
|
1325
|
-
* @group Builder
|
|
1326
|
-
*/
|
|
1327
|
-
function buildEntityCallbacks(callbacks) {
|
|
1328
|
-
return callbacks;
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Identity function we use to defeat the type system of Typescript and build
|
|
1332
|
-
* additional field delegates views with all its properties
|
|
1333
|
-
* @param additionalFieldDelegate
|
|
1334
|
-
* @group Builder
|
|
1335
|
-
*/
|
|
1336
|
-
function buildAdditionalFieldDelegate(additionalFieldDelegate) {
|
|
1337
|
-
return additionalFieldDelegate;
|
|
1338
|
-
}
|
|
1339
1330
|
//#endregion
|
|
1340
1331
|
//#region src/util/storage.ts
|
|
1341
1332
|
/**
|
|
@@ -1471,16 +1462,17 @@ async function processProperties(properties, values, previousValues, propsContex
|
|
|
1471
1462
|
}
|
|
1472
1463
|
/**
|
|
1473
1464
|
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
1474
|
-
* and wrap them into an
|
|
1465
|
+
* and wrap them into an CollectionCallbacks object recursively.
|
|
1475
1466
|
*/
|
|
1476
1467
|
var buildPropertyCallbacks = (properties) => {
|
|
1477
1468
|
if (!properties) return void 0;
|
|
1478
1469
|
const propertyCallbacks = {};
|
|
1479
1470
|
if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
|
|
1480
|
-
const
|
|
1471
|
+
const row = props.row;
|
|
1472
|
+
const processedValues = await processProperties(properties, row, row, props, "afterRead");
|
|
1481
1473
|
return {
|
|
1482
|
-
...props.
|
|
1483
|
-
|
|
1474
|
+
...props.row,
|
|
1475
|
+
...processedValues
|
|
1484
1476
|
};
|
|
1485
1477
|
};
|
|
1486
1478
|
if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
|
|
@@ -1667,6 +1659,84 @@ function applyEnumConditions(enumValues, conditions, context) {
|
|
|
1667
1659
|
return result;
|
|
1668
1660
|
}
|
|
1669
1661
|
//#endregion
|
|
1662
|
+
//#region src/util/filter-operator-resolution.ts
|
|
1663
|
+
/**
|
|
1664
|
+
* Default operators offered per property type, before engine capabilities and
|
|
1665
|
+
* per-property narrowing are applied. These mirror what the built-in filter
|
|
1666
|
+
* fields can render.
|
|
1667
|
+
*/
|
|
1668
|
+
var COMPARISON_OPS = [
|
|
1669
|
+
"==",
|
|
1670
|
+
"!=",
|
|
1671
|
+
">",
|
|
1672
|
+
">=",
|
|
1673
|
+
"<",
|
|
1674
|
+
"<="
|
|
1675
|
+
];
|
|
1676
|
+
var NULL_CHECK_OPS = ["is-null", "is-not-null"];
|
|
1677
|
+
var MEMBERSHIP_OPS = ["in", "not-in"];
|
|
1678
|
+
var PATTERN_OPS = [
|
|
1679
|
+
"like",
|
|
1680
|
+
"ilike",
|
|
1681
|
+
"not-like",
|
|
1682
|
+
"not-ilike"
|
|
1683
|
+
];
|
|
1684
|
+
var DEFAULT_OPS_BY_TYPE = {
|
|
1685
|
+
string: [
|
|
1686
|
+
...COMPARISON_OPS,
|
|
1687
|
+
...MEMBERSHIP_OPS,
|
|
1688
|
+
...PATTERN_OPS,
|
|
1689
|
+
...NULL_CHECK_OPS
|
|
1690
|
+
],
|
|
1691
|
+
number: [
|
|
1692
|
+
...COMPARISON_OPS,
|
|
1693
|
+
...MEMBERSHIP_OPS,
|
|
1694
|
+
...NULL_CHECK_OPS
|
|
1695
|
+
],
|
|
1696
|
+
date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
|
|
1697
|
+
boolean: [
|
|
1698
|
+
"==",
|
|
1699
|
+
"!=",
|
|
1700
|
+
...NULL_CHECK_OPS
|
|
1701
|
+
],
|
|
1702
|
+
reference: [
|
|
1703
|
+
"==",
|
|
1704
|
+
"!=",
|
|
1705
|
+
...MEMBERSHIP_OPS,
|
|
1706
|
+
...NULL_CHECK_OPS
|
|
1707
|
+
],
|
|
1708
|
+
relation: [
|
|
1709
|
+
"==",
|
|
1710
|
+
"!=",
|
|
1711
|
+
...MEMBERSHIP_OPS,
|
|
1712
|
+
...NULL_CHECK_OPS
|
|
1713
|
+
]
|
|
1714
|
+
};
|
|
1715
|
+
/** Operators offered when the property is an *array of* a filterable type. */
|
|
1716
|
+
var ARRAY_OPS = ["array-contains", "array-contains-any"];
|
|
1717
|
+
/**
|
|
1718
|
+
* Resolve which filter operators the UI should offer for a property.
|
|
1719
|
+
*
|
|
1720
|
+
* The result is the **intersection** of three sets:
|
|
1721
|
+
* 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
|
|
1722
|
+
* (e.g. Firestore cannot run the LIKE family);
|
|
1723
|
+
* 2. what makes sense for the property type (e.g. no `>` on booleans);
|
|
1724
|
+
* 3. the developer's optional narrowing — `property.ui.filterOperators`.
|
|
1725
|
+
*
|
|
1726
|
+
* Returns an empty array when the property is not filterable (either by
|
|
1727
|
+
* type, or because the developer disabled it with `filterOperators: []`).
|
|
1728
|
+
*
|
|
1729
|
+
* @group Models
|
|
1730
|
+
*/
|
|
1731
|
+
function resolveFilterOperators({ property, isArray, engine }) {
|
|
1732
|
+
const typeDefaults = isArray ? ARRAY_OPS : DEFAULT_OPS_BY_TYPE[property.type] ?? [];
|
|
1733
|
+
if (typeDefaults.length === 0) return [];
|
|
1734
|
+
const engineOps = new Set(getDataSourceCapabilities(engine).filterOperators ?? ALL_WHERE_FILTER_OPS);
|
|
1735
|
+
const narrowing = property.ui?.filterOperators;
|
|
1736
|
+
const narrowingSet = narrowing !== void 0 ? new Set(narrowing) : void 0;
|
|
1737
|
+
return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
|
|
1738
|
+
}
|
|
1739
|
+
//#endregion
|
|
1670
1740
|
//#region src/data/resolveDataSource.ts
|
|
1671
1741
|
/**
|
|
1672
1742
|
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
@@ -1745,7 +1815,7 @@ var CollectionRegistry = class {
|
|
|
1745
1815
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
1746
1816
|
rawRootCollections = [];
|
|
1747
1817
|
cachedRawCollectionsList = null;
|
|
1748
|
-
|
|
1818
|
+
lastRawInputEntity = null;
|
|
1749
1819
|
constructor(collections, dataSources) {
|
|
1750
1820
|
if (dataSources) this.dataSources = dataSources;
|
|
1751
1821
|
if (collections) this.registerMultiple(collections);
|
|
@@ -1775,12 +1845,12 @@ var CollectionRegistry = class {
|
|
|
1775
1845
|
* Returns true if the collections have changed, false otherwise.
|
|
1776
1846
|
*
|
|
1777
1847
|
* Idempotent: compares the raw input (before normalization) against a stored
|
|
1778
|
-
*
|
|
1848
|
+
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
1779
1849
|
* @param collections
|
|
1780
1850
|
*/
|
|
1781
1851
|
registerMultiple(collections) {
|
|
1782
|
-
const
|
|
1783
|
-
if (this.
|
|
1852
|
+
const rawEntity = collections.map((c) => removeFunctions(c));
|
|
1853
|
+
if (this.lastRawInputEntity && deepEqual(this.lastRawInputEntity, rawEntity)) return false;
|
|
1784
1854
|
this.reset();
|
|
1785
1855
|
collections.forEach((c) => {
|
|
1786
1856
|
if (c.slug) this.collectionsBySlug.set(c.slug, c);
|
|
@@ -1804,7 +1874,7 @@ var CollectionRegistry = class {
|
|
|
1804
1874
|
this._registerRecursively(this.normalizeCollection({ ...subCollection }), deepClone(subCollection));
|
|
1805
1875
|
});
|
|
1806
1876
|
});
|
|
1807
|
-
this.
|
|
1877
|
+
this.lastRawInputEntity = rawEntity;
|
|
1808
1878
|
return true;
|
|
1809
1879
|
}
|
|
1810
1880
|
register(collection, rawCollection) {
|
|
@@ -2224,7 +2294,7 @@ var QueryBuilder = class {
|
|
|
2224
2294
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
2225
2295
|
*/
|
|
2226
2296
|
orderBy(column, direction = "asc") {
|
|
2227
|
-
this.params.orderBy =
|
|
2297
|
+
this.params.orderBy = [column, direction];
|
|
2228
2298
|
return this;
|
|
2229
2299
|
}
|
|
2230
2300
|
/**
|
|
@@ -2287,32 +2357,69 @@ var QueryBuilder = class {
|
|
|
2287
2357
|
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
2288
2358
|
* Everything else speaks `FilterValues` exclusively.
|
|
2289
2359
|
*
|
|
2360
|
+
* Wire-format values are always strings — the wire format carries no type
|
|
2361
|
+
* metadata, so type coercion is the responsibility of the server-side data
|
|
2362
|
+
* driver which has access to the collection schema.
|
|
2363
|
+
*
|
|
2364
|
+
* Commas inside list values are backslash-escaped (`\,`), and literal
|
|
2365
|
+
* backslashes are escaped as `\\`.
|
|
2366
|
+
*
|
|
2290
2367
|
* @module
|
|
2291
2368
|
*/
|
|
2292
2369
|
/**
|
|
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
2370
|
* Serialize a JS value to its querystring representation.
|
|
2371
|
+
* `null` is serialized as the literal string `"null"`.
|
|
2308
2372
|
*/
|
|
2309
2373
|
function stringifyValue(value) {
|
|
2310
2374
|
if (value === null) return "null";
|
|
2311
|
-
if (typeof value === "boolean") return String(value);
|
|
2312
2375
|
return String(value);
|
|
2313
2376
|
}
|
|
2314
2377
|
/**
|
|
2315
|
-
*
|
|
2378
|
+
* Escape a single list item for the wire format.
|
|
2379
|
+
* `\` → `\\`, `,` → `\,`
|
|
2380
|
+
*/
|
|
2381
|
+
function escapeListItem(value) {
|
|
2382
|
+
return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Unescape a single list item from the wire format.
|
|
2386
|
+
* `\\` → `\`, `\,` → `,`
|
|
2387
|
+
*/
|
|
2388
|
+
function unescapeListItem(value) {
|
|
2389
|
+
let result = "";
|
|
2390
|
+
for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
|
|
2391
|
+
result += value[i + 1];
|
|
2392
|
+
i++;
|
|
2393
|
+
} else result += value[i];
|
|
2394
|
+
return result;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Split a parenthesized list string on unescaped commas.
|
|
2398
|
+
* Input is the content between `(` and `)`.
|
|
2399
|
+
*
|
|
2400
|
+
* @example
|
|
2401
|
+
* splitListItems("admin,editor") // ["admin", "editor"]
|
|
2402
|
+
* splitListItems("hello\\, world,foo") // ["hello, world", "foo"]
|
|
2403
|
+
*/
|
|
2404
|
+
function splitListItems(inner) {
|
|
2405
|
+
const items = [];
|
|
2406
|
+
let current = "";
|
|
2407
|
+
for (let i = 0; i < inner.length; i++) if (inner[i] === "\\" && i + 1 < inner.length) {
|
|
2408
|
+
current += inner[i] + inner[i + 1];
|
|
2409
|
+
i++;
|
|
2410
|
+
} else if (inner[i] === ",") {
|
|
2411
|
+
items.push(unescapeListItem(current));
|
|
2412
|
+
current = "";
|
|
2413
|
+
} else current += inner[i];
|
|
2414
|
+
items.push(unescapeListItem(current));
|
|
2415
|
+
return items;
|
|
2416
|
+
}
|
|
2417
|
+
var REST_OP_LOOKUP = REST_TO_CANONICAL;
|
|
2418
|
+
var CANONICAL_OP_LOOKUP = CANONICAL_TO_REST;
|
|
2419
|
+
/**
|
|
2420
|
+
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
2421
|
+
*
|
|
2422
|
+
* Throws `TypeError` if the input is not a valid `[WhereFilterOp, unknown]` tuple.
|
|
2316
2423
|
*
|
|
2317
2424
|
* @example
|
|
2318
2425
|
* serializeTuple(["==", "active"]) // "eq.active"
|
|
@@ -2320,22 +2427,20 @@ function stringifyValue(value) {
|
|
|
2320
2427
|
* serializeTuple([">=", 18]) // "gte.18"
|
|
2321
2428
|
*/
|
|
2322
2429
|
function serializeTuple(tuple) {
|
|
2323
|
-
if (
|
|
2324
|
-
if (tuple.includes(".")) {
|
|
2325
|
-
const dotIndex = tuple.indexOf(".");
|
|
2326
|
-
if (REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
|
|
2327
|
-
}
|
|
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)}`;
|
|
2430
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
|
|
2331
2431
|
const [op, value] = tuple;
|
|
2332
|
-
|
|
2333
|
-
|
|
2432
|
+
if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
|
|
2433
|
+
const restOp = CANONICAL_OP_LOOKUP[op];
|
|
2434
|
+
if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`);
|
|
2435
|
+
if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
|
|
2334
2436
|
return `${restOp}.${stringifyValue(value)}`;
|
|
2335
2437
|
}
|
|
2336
2438
|
/**
|
|
2337
|
-
* Convert `FilterValues` to a PostgREST-style
|
|
2439
|
+
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
2440
|
+
* querystring record.
|
|
2338
2441
|
*
|
|
2442
|
+
* - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
|
|
2443
|
+
* - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
|
|
2339
2444
|
* - Single conditions produce a string value.
|
|
2340
2445
|
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
2341
2446
|
*
|
|
@@ -2345,11 +2450,19 @@ function serializeTuple(tuple) {
|
|
|
2345
2450
|
*
|
|
2346
2451
|
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
2347
2452
|
* // → { age: ["gte.18", "lt.65"] }
|
|
2453
|
+
*
|
|
2454
|
+
* // Pre-serialized strings pass through unchanged:
|
|
2455
|
+
* serializeFilter({ status: "eq.published" })
|
|
2456
|
+
* // → { status: "eq.published" }
|
|
2348
2457
|
*/
|
|
2349
2458
|
function serializeFilter(filter) {
|
|
2350
2459
|
const result = {};
|
|
2351
2460
|
for (const [field, condition] of Object.entries(filter)) {
|
|
2352
2461
|
if (condition === void 0) continue;
|
|
2462
|
+
if (typeof condition === "string") {
|
|
2463
|
+
result[field] = condition;
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2353
2466
|
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
|
|
2354
2467
|
else result[field] = serializeTuple(condition);
|
|
2355
2468
|
}
|
|
@@ -2358,18 +2471,24 @@ function serializeFilter(filter) {
|
|
|
2358
2471
|
/**
|
|
2359
2472
|
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
2360
2473
|
*
|
|
2361
|
-
*
|
|
2474
|
+
* All values are returned as strings — the wire format carries no type
|
|
2475
|
+
* metadata, so coercion is the data driver's responsibility.
|
|
2476
|
+
*
|
|
2477
|
+
* If the string doesn't match a known operator prefix, it falls back to
|
|
2362
2478
|
* `["==", originalString]` (treating the whole string as an equality value).
|
|
2479
|
+
* This intentional defense handles values like `"user@host.com"` or
|
|
2480
|
+
* `"1.2.3"` that happen to contain dots.
|
|
2363
2481
|
*/
|
|
2364
2482
|
function deserializeSingle(raw) {
|
|
2365
2483
|
const dotIndex = raw.indexOf(".");
|
|
2366
|
-
if (dotIndex === -1) return ["==",
|
|
2484
|
+
if (dotIndex === -1) return ["==", raw];
|
|
2367
2485
|
const prefix = raw.substring(0, dotIndex);
|
|
2368
2486
|
const rest = raw.substring(dotIndex + 1);
|
|
2369
|
-
const canonicalOp =
|
|
2487
|
+
const canonicalOp = REST_OP_LOOKUP[prefix];
|
|
2370
2488
|
if (!canonicalOp) return ["==", raw];
|
|
2371
|
-
if (
|
|
2372
|
-
return [canonicalOp,
|
|
2489
|
+
if (NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
|
|
2490
|
+
if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
|
|
2491
|
+
return [canonicalOp, rest];
|
|
2373
2492
|
}
|
|
2374
2493
|
/**
|
|
2375
2494
|
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
@@ -2382,7 +2501,7 @@ function deserializeSingle(raw) {
|
|
|
2382
2501
|
* // → { status: ["==", "active"] }
|
|
2383
2502
|
*
|
|
2384
2503
|
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
2385
|
-
* // → { age: [[">=", 18], ["<", 65]] }
|
|
2504
|
+
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
2386
2505
|
*/
|
|
2387
2506
|
function deserializeFilter(query) {
|
|
2388
2507
|
const result = {};
|
|
@@ -2421,9 +2540,9 @@ function serializeLogicalCondition(cond) {
|
|
|
2421
2540
|
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
2422
2541
|
return `${cond.type}(${inner})`;
|
|
2423
2542
|
}
|
|
2424
|
-
const restOp =
|
|
2543
|
+
const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
|
|
2425
2544
|
if (Array.isArray(cond.value)) {
|
|
2426
|
-
const items = cond.value.map(stringifyValue).join(",");
|
|
2545
|
+
const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
|
|
2427
2546
|
return `${cond.column}.${restOp}.(${items})`;
|
|
2428
2547
|
}
|
|
2429
2548
|
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
@@ -2471,99 +2590,115 @@ function deserializeLogicalCondition(str) {
|
|
|
2471
2590
|
if (secondDot === -1) return {
|
|
2472
2591
|
column,
|
|
2473
2592
|
operator: "==",
|
|
2474
|
-
value:
|
|
2593
|
+
value: rest
|
|
2475
2594
|
};
|
|
2476
2595
|
const opStr = rest.substring(0, secondDot);
|
|
2477
|
-
|
|
2596
|
+
const valueStr = rest.substring(secondDot + 1);
|
|
2478
2597
|
const operator = toCanonicalOp(opStr) ?? "==";
|
|
2479
2598
|
if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
|
|
2480
2599
|
column,
|
|
2481
2600
|
operator,
|
|
2482
|
-
value: valueStr.slice(1, -1)
|
|
2601
|
+
value: splitListItems(valueStr.slice(1, -1))
|
|
2483
2602
|
};
|
|
2484
2603
|
return {
|
|
2485
2604
|
column,
|
|
2486
2605
|
operator,
|
|
2487
|
-
value:
|
|
2606
|
+
value: valueStr
|
|
2488
2607
|
};
|
|
2489
2608
|
}
|
|
2490
2609
|
//#endregion
|
|
2491
2610
|
//#region src/data/buildRebaseData.ts
|
|
2492
2611
|
/**
|
|
2493
|
-
*
|
|
2612
|
+
* Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
|
|
2613
|
+
* Mirrors the client SDK's rowToEntity conversion.
|
|
2494
2614
|
*/
|
|
2495
|
-
function
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2615
|
+
function rowToEntity(row, slug) {
|
|
2616
|
+
return {
|
|
2617
|
+
id: row.id,
|
|
2618
|
+
path: slug,
|
|
2619
|
+
values: row
|
|
2620
|
+
};
|
|
2499
2621
|
}
|
|
2500
2622
|
function createDriverAccessor(driver, slug) {
|
|
2501
2623
|
const accessor = {
|
|
2502
2624
|
async find(params) {
|
|
2503
|
-
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2504
2625
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2505
|
-
const
|
|
2626
|
+
const limit = params?.limit ?? 20;
|
|
2627
|
+
const offset = params?.offset ?? 0;
|
|
2628
|
+
const fetchService = driver.restFetchService;
|
|
2629
|
+
const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
|
|
2630
|
+
filter,
|
|
2631
|
+
limit: params?.limit,
|
|
2632
|
+
offset: params?.offset,
|
|
2633
|
+
orderBy: params?.orderBy?.[0],
|
|
2634
|
+
order: params?.orderBy?.[1],
|
|
2635
|
+
searchString: params?.searchString
|
|
2636
|
+
}, params.include) : await driver.fetchCollection({
|
|
2506
2637
|
path: slug,
|
|
2507
2638
|
limit: params?.limit,
|
|
2508
2639
|
offset: params?.offset,
|
|
2509
2640
|
filter,
|
|
2510
|
-
orderBy:
|
|
2511
|
-
order:
|
|
2641
|
+
orderBy: params?.orderBy?.[0],
|
|
2642
|
+
order: params?.orderBy?.[1],
|
|
2512
2643
|
searchString: params?.searchString
|
|
2513
2644
|
});
|
|
2514
|
-
|
|
2515
|
-
|
|
2645
|
+
let total = rows.length + offset;
|
|
2646
|
+
let hasMore = rows.length >= limit;
|
|
2647
|
+
if (driver.count) {
|
|
2648
|
+
total = await driver.count({
|
|
2649
|
+
path: slug,
|
|
2650
|
+
filter
|
|
2651
|
+
});
|
|
2652
|
+
hasMore = offset + rows.length < total;
|
|
2653
|
+
}
|
|
2516
2654
|
return {
|
|
2517
|
-
data:
|
|
2655
|
+
data: rows.map((row) => rowToEntity(row, slug)),
|
|
2518
2656
|
meta: {
|
|
2519
|
-
total
|
|
2657
|
+
total,
|
|
2520
2658
|
limit,
|
|
2521
2659
|
offset,
|
|
2522
|
-
hasMore
|
|
2660
|
+
hasMore
|
|
2523
2661
|
}
|
|
2524
2662
|
};
|
|
2525
2663
|
},
|
|
2526
2664
|
async findById(id) {
|
|
2527
|
-
|
|
2665
|
+
const row = await driver.fetchOne({
|
|
2528
2666
|
path: slug,
|
|
2529
|
-
|
|
2667
|
+
id
|
|
2530
2668
|
});
|
|
2669
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
2531
2670
|
},
|
|
2532
2671
|
async create(data, id) {
|
|
2533
|
-
return driver.
|
|
2672
|
+
return rowToEntity(await driver.save({
|
|
2534
2673
|
path: slug,
|
|
2535
2674
|
values: data,
|
|
2536
|
-
|
|
2675
|
+
id,
|
|
2537
2676
|
status: "new"
|
|
2538
|
-
});
|
|
2677
|
+
}), slug);
|
|
2539
2678
|
},
|
|
2540
2679
|
async update(id, data) {
|
|
2541
|
-
return driver.
|
|
2680
|
+
return rowToEntity(await driver.save({
|
|
2542
2681
|
path: slug,
|
|
2543
2682
|
values: data,
|
|
2544
|
-
|
|
2683
|
+
id,
|
|
2545
2684
|
status: "existing"
|
|
2546
|
-
});
|
|
2685
|
+
}), slug);
|
|
2547
2686
|
},
|
|
2548
2687
|
async delete(id) {
|
|
2549
|
-
return driver.
|
|
2688
|
+
return driver.delete({ row: {
|
|
2550
2689
|
id,
|
|
2551
2690
|
path: slug,
|
|
2552
2691
|
values: {}
|
|
2553
2692
|
} });
|
|
2554
2693
|
},
|
|
2555
|
-
|
|
2556
|
-
return driver.deleteAll(slug);
|
|
2557
|
-
} : void 0,
|
|
2558
|
-
count: driver.countEntities ? async (params) => {
|
|
2694
|
+
count: driver.count ? async (params) => {
|
|
2559
2695
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2560
|
-
return driver.
|
|
2696
|
+
return driver.count({
|
|
2561
2697
|
path: slug,
|
|
2562
2698
|
filter
|
|
2563
2699
|
});
|
|
2564
2700
|
} : void 0,
|
|
2565
2701
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
2566
|
-
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2567
2702
|
const limit = params?.limit ?? 20;
|
|
2568
2703
|
const offset = params?.offset ?? 0;
|
|
2569
2704
|
return driver.listenCollection({
|
|
@@ -2571,12 +2706,12 @@ function createDriverAccessor(driver, slug) {
|
|
|
2571
2706
|
limit: params?.limit,
|
|
2572
2707
|
offset: params?.offset,
|
|
2573
2708
|
filter: params?.where,
|
|
2574
|
-
orderBy:
|
|
2575
|
-
order:
|
|
2709
|
+
orderBy: params?.orderBy?.[0],
|
|
2710
|
+
order: params?.orderBy?.[1],
|
|
2576
2711
|
searchString: params?.searchString,
|
|
2577
2712
|
onUpdate: (entities) => {
|
|
2578
2713
|
onUpdate({
|
|
2579
|
-
data: entities,
|
|
2714
|
+
data: entities.map((row) => rowToEntity(row, slug)),
|
|
2580
2715
|
meta: {
|
|
2581
2716
|
total: entities.length,
|
|
2582
2717
|
limit,
|
|
@@ -2588,11 +2723,11 @@ function createDriverAccessor(driver, slug) {
|
|
|
2588
2723
|
onError
|
|
2589
2724
|
});
|
|
2590
2725
|
} : void 0,
|
|
2591
|
-
listenById: driver.
|
|
2592
|
-
return driver.
|
|
2726
|
+
listenById: driver.listenOne ? (id, onUpdate, onError) => {
|
|
2727
|
+
return driver.listenOne({
|
|
2593
2728
|
path: slug,
|
|
2594
|
-
|
|
2595
|
-
onUpdate: (entity) => onUpdate(entity
|
|
2729
|
+
id,
|
|
2730
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
2596
2731
|
onError
|
|
2597
2732
|
});
|
|
2598
2733
|
} : void 0,
|
|
@@ -2629,7 +2764,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
2629
2764
|
* @example
|
|
2630
2765
|
* const data = buildRebaseData(driver);
|
|
2631
2766
|
* await data.products.create({ name: "Camera", price: 299 });
|
|
2632
|
-
* const { data: items } = await data.products.find({ where: { status: "
|
|
2767
|
+
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
2633
2768
|
*/
|
|
2634
2769
|
function buildRebaseData(driver) {
|
|
2635
2770
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2648,6 +2783,230 @@ function buildRebaseData(driver) {
|
|
|
2648
2783
|
return getAccessor(toSnakeCase(prop));
|
|
2649
2784
|
} });
|
|
2650
2785
|
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
|
|
2788
|
+
* (id included) under `.values`, so this is just that payload.
|
|
2789
|
+
*/
|
|
2790
|
+
function entityToRow(entity) {
|
|
2791
|
+
return entity.values;
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Fluent query builder for the flat SDK data layer. Mirrors {@link QueryBuilder}
|
|
2795
|
+
* but resolves to `FindResult<M>` (flat rows) instead of Entity-wrapped
|
|
2796
|
+
* `FindResponse<M>`.
|
|
2797
|
+
*/
|
|
2798
|
+
var SdkQueryBuilder = class {
|
|
2799
|
+
client;
|
|
2800
|
+
params = { where: {} };
|
|
2801
|
+
constructor(client) {
|
|
2802
|
+
this.client = client;
|
|
2803
|
+
}
|
|
2804
|
+
where(columnOrCondition, operator, value) {
|
|
2805
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
2806
|
+
this.params.logical = columnOrCondition;
|
|
2807
|
+
return this;
|
|
2808
|
+
}
|
|
2809
|
+
if (!this.params.where) this.params.where = {};
|
|
2810
|
+
const column = columnOrCondition;
|
|
2811
|
+
const condition = [operator, value];
|
|
2812
|
+
const existing = this.params.where[column];
|
|
2813
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
2814
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
2815
|
+
else {
|
|
2816
|
+
let firstCondition;
|
|
2817
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
2818
|
+
else firstCondition = ["==", existing];
|
|
2819
|
+
this.params.where[column] = [firstCondition, condition];
|
|
2820
|
+
}
|
|
2821
|
+
return this;
|
|
2822
|
+
}
|
|
2823
|
+
orderBy(column, direction = "asc") {
|
|
2824
|
+
this.params.orderBy = [column, direction];
|
|
2825
|
+
return this;
|
|
2826
|
+
}
|
|
2827
|
+
limit(count) {
|
|
2828
|
+
this.params.limit = count;
|
|
2829
|
+
return this;
|
|
2830
|
+
}
|
|
2831
|
+
offset(count) {
|
|
2832
|
+
this.params.offset = count;
|
|
2833
|
+
return this;
|
|
2834
|
+
}
|
|
2835
|
+
search(searchString) {
|
|
2836
|
+
this.params.searchString = searchString;
|
|
2837
|
+
return this;
|
|
2838
|
+
}
|
|
2839
|
+
include(...relations) {
|
|
2840
|
+
this.params.include = relations;
|
|
2841
|
+
return this;
|
|
2842
|
+
}
|
|
2843
|
+
async find() {
|
|
2844
|
+
return this.client.find(this.params);
|
|
2845
|
+
}
|
|
2846
|
+
async count() {
|
|
2847
|
+
return this.client.count ? this.client.count(this.params) : 0;
|
|
2848
|
+
}
|
|
2849
|
+
listen(onUpdate, onError) {
|
|
2850
|
+
if (!this.client.listen) throw new Error("Listen is only available when the driver supports realtime.");
|
|
2851
|
+
return this.client.listen(this.params, onUpdate, onError);
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
/**
|
|
2855
|
+
* Wrap a Entity-shaped {@link CollectionAccessor} into a flat
|
|
2856
|
+
* {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
|
|
2857
|
+
* so the backend SDK is byte-for-byte the same shape as the frontend client.
|
|
2858
|
+
*/
|
|
2859
|
+
function toSdkCollectionClient(snap) {
|
|
2860
|
+
const client = {
|
|
2861
|
+
async find(params) {
|
|
2862
|
+
const res = await snap.find(params);
|
|
2863
|
+
return {
|
|
2864
|
+
data: res.data.map(entityToRow),
|
|
2865
|
+
meta: res.meta
|
|
2866
|
+
};
|
|
2867
|
+
},
|
|
2868
|
+
async findById(id) {
|
|
2869
|
+
const s = await snap.findById(id);
|
|
2870
|
+
return s ? entityToRow(s) : void 0;
|
|
2871
|
+
},
|
|
2872
|
+
async create(data, id) {
|
|
2873
|
+
return entityToRow(await snap.create(data, id));
|
|
2874
|
+
},
|
|
2875
|
+
async update(id, data) {
|
|
2876
|
+
return entityToRow(await snap.update(id, data));
|
|
2877
|
+
},
|
|
2878
|
+
delete(id) {
|
|
2879
|
+
return snap.delete(id);
|
|
2880
|
+
},
|
|
2881
|
+
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
2882
|
+
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
2883
|
+
data: res.data.map(entityToRow),
|
|
2884
|
+
meta: res.meta
|
|
2885
|
+
}), onError) : void 0,
|
|
2886
|
+
listenById: snap.listenById ? (id, onUpdate, onError) => snap.listenById(id, (s) => onUpdate(s ? entityToRow(s) : void 0), onError) : void 0,
|
|
2887
|
+
where(columnOrCondition, operator, value) {
|
|
2888
|
+
const builder = new SdkQueryBuilder(client);
|
|
2889
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2890
|
+
return builder.where(columnOrCondition, operator, value);
|
|
2891
|
+
},
|
|
2892
|
+
orderBy: (column, direction) => new SdkQueryBuilder(client).orderBy(column, direction),
|
|
2893
|
+
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
2894
|
+
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
2895
|
+
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
2896
|
+
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
2897
|
+
};
|
|
2898
|
+
return client;
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
2902
|
+
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
2903
|
+
* `{ id, path, values }` view-model the admin CMS renders.
|
|
2904
|
+
*/
|
|
2905
|
+
function toEntityAccessor(sdk, slug) {
|
|
2906
|
+
const accessor = {
|
|
2907
|
+
async find(params) {
|
|
2908
|
+
const res = await sdk.find(params);
|
|
2909
|
+
return {
|
|
2910
|
+
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2911
|
+
meta: res.meta
|
|
2912
|
+
};
|
|
2913
|
+
},
|
|
2914
|
+
async findById(id) {
|
|
2915
|
+
const row = await sdk.findById(id);
|
|
2916
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
2917
|
+
},
|
|
2918
|
+
async create(data, id) {
|
|
2919
|
+
return rowToEntity(await sdk.create(data, id), slug);
|
|
2920
|
+
},
|
|
2921
|
+
async update(id, data) {
|
|
2922
|
+
const row = await sdk.update(id, data);
|
|
2923
|
+
if (!row) throw new Error(`Update returned no data for id ${id}`);
|
|
2924
|
+
return rowToEntity(row, slug);
|
|
2925
|
+
},
|
|
2926
|
+
delete(id) {
|
|
2927
|
+
return sdk.delete(id);
|
|
2928
|
+
},
|
|
2929
|
+
count: sdk.count ? (params) => sdk.count(params) : void 0,
|
|
2930
|
+
listen: sdk.listen ? (params, onUpdate, onError) => sdk.listen(params, (res) => onUpdate({
|
|
2931
|
+
data: res.data.map((row) => rowToEntity(row, slug)),
|
|
2932
|
+
meta: res.meta
|
|
2933
|
+
}), onError) : void 0,
|
|
2934
|
+
listenById: sdk.listenById ? (id, onUpdate, onError) => sdk.listenById(id, (row) => onUpdate(row ? rowToEntity(row, slug) : void 0), onError) : void 0,
|
|
2935
|
+
where(columnOrCondition, operator, value) {
|
|
2936
|
+
const builder = new QueryBuilder(accessor);
|
|
2937
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
2938
|
+
return builder.where(columnOrCondition, operator, value);
|
|
2939
|
+
},
|
|
2940
|
+
orderBy: (column, direction) => new QueryBuilder(accessor).orderBy(column, direction),
|
|
2941
|
+
limit: (count) => new QueryBuilder(accessor).limit(count),
|
|
2942
|
+
offset: (count) => new QueryBuilder(accessor).offset(count),
|
|
2943
|
+
search: (searchString) => new QueryBuilder(accessor).search(searchString),
|
|
2944
|
+
include: (...relations) => new QueryBuilder(accessor).include(...relations)
|
|
2945
|
+
};
|
|
2946
|
+
return accessor;
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
|
|
2950
|
+
*
|
|
2951
|
+
* This is the **CMS boundary**: the SDK client (`client.data`) returns flat
|
|
2952
|
+
* rows, but the admin renders the `Entity` view-model (`entity.values.*`).
|
|
2953
|
+
* `core/Rebase.tsx` wraps `client.data` through this before handing it to the
|
|
2954
|
+
* CMS `RebaseDataContext` — without it the admin renders rows with only their
|
|
2955
|
+
* `id`.
|
|
2956
|
+
*/
|
|
2957
|
+
function wrapAsEntityData(sdkData) {
|
|
2958
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2959
|
+
function getAccessor(slug) {
|
|
2960
|
+
let accessor = cache.get(slug);
|
|
2961
|
+
if (!accessor) {
|
|
2962
|
+
accessor = toEntityAccessor(sdkData.collection(slug), slug);
|
|
2963
|
+
cache.set(slug, accessor);
|
|
2964
|
+
}
|
|
2965
|
+
return accessor;
|
|
2966
|
+
}
|
|
2967
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2968
|
+
if (prop === "collection") return getAccessor;
|
|
2969
|
+
if (typeof prop === "symbol") return void 0;
|
|
2970
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
2971
|
+
return getAccessor(toSnakeCase(prop));
|
|
2972
|
+
} });
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Wrap a Entity-shaped {@link RebaseData} into a flat {@link RebaseSdkData}.
|
|
2976
|
+
*
|
|
2977
|
+
* Every collection accessor is adapted to return flat rows. Use this to derive
|
|
2978
|
+
* the flat SDK data layer (`context.data`) from an existing Entity data layer
|
|
2979
|
+
* — e.g. the admin routes its Entity data via `useData()` and exposes the
|
|
2980
|
+
* same routing as flat `context.data` for callbacks by wrapping it here.
|
|
2981
|
+
*/
|
|
2982
|
+
function wrapAsSdkData(entityData) {
|
|
2983
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2984
|
+
function getAccessor(slug) {
|
|
2985
|
+
let accessor = cache.get(slug);
|
|
2986
|
+
if (!accessor) {
|
|
2987
|
+
accessor = toSdkCollectionClient(entityData.collection(slug));
|
|
2988
|
+
cache.set(slug, accessor);
|
|
2989
|
+
}
|
|
2990
|
+
return accessor;
|
|
2991
|
+
}
|
|
2992
|
+
return new Proxy({ collection: getAccessor }, { get(_target, prop) {
|
|
2993
|
+
if (prop === "collection") return getAccessor;
|
|
2994
|
+
if (typeof prop === "symbol") return void 0;
|
|
2995
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
2996
|
+
return getAccessor(toSnakeCase(prop));
|
|
2997
|
+
} });
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Build a flat {@link RebaseSdkData} from a `DataDriver`.
|
|
3001
|
+
*
|
|
3002
|
+
* This is the developer-facing SDK data layer used by backend framework
|
|
3003
|
+
* callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
|
|
3004
|
+
* identical in shape to the frontend SDK client — so the API is symmetric
|
|
3005
|
+
* across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
|
|
3006
|
+
*/
|
|
3007
|
+
function buildSdkData(driver) {
|
|
3008
|
+
return wrapAsSdkData(buildRebaseData(driver));
|
|
3009
|
+
}
|
|
2651
3010
|
//#endregion
|
|
2652
3011
|
//#region src/data/buildRoutedRebaseData.ts
|
|
2653
3012
|
/**
|
|
@@ -2692,6 +3051,57 @@ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
|
|
|
2692
3051
|
} });
|
|
2693
3052
|
}
|
|
2694
3053
|
//#endregion
|
|
3054
|
+
//#region src/data/sort-dialect.ts
|
|
3055
|
+
/**
|
|
3056
|
+
* Sort-order wire codec.
|
|
3057
|
+
*
|
|
3058
|
+
* This is the ONLY module that knows about the colon-delimited wire format
|
|
3059
|
+
* (`"field:direction"`) used in HTTP query parameters.
|
|
3060
|
+
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
3061
|
+
*
|
|
3062
|
+
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
3063
|
+
*
|
|
3064
|
+
* @module
|
|
3065
|
+
*/
|
|
3066
|
+
/**
|
|
3067
|
+
* Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
|
|
3068
|
+
*
|
|
3069
|
+
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
3070
|
+
* (from an untyped JS caller), it is returned unchanged.
|
|
3071
|
+
* This is undocumented tolerance, not public API — don't rely on it.
|
|
3072
|
+
*
|
|
3073
|
+
* @param orderBy - A canonical `[field, direction]` tuple, or at runtime
|
|
3074
|
+
* possibly a pre-serialized string (undocumented tolerance).
|
|
3075
|
+
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
3076
|
+
*
|
|
3077
|
+
* @remarks
|
|
3078
|
+
* Field names containing `:` are representable in the tuple form but
|
|
3079
|
+
* **not** on the wire — this is an inherent limitation of the colon-delimited
|
|
3080
|
+
* encoding and is not resolved here.
|
|
3081
|
+
*/
|
|
3082
|
+
function serializeOrderBy(orderBy) {
|
|
3083
|
+
if (!orderBy) return void 0;
|
|
3084
|
+
if (typeof orderBy === "string") return orderBy;
|
|
3085
|
+
return `${orderBy[0]}:${orderBy[1]}`;
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
|
|
3089
|
+
*
|
|
3090
|
+
* Lenient parsing (matches existing server behaviour):
|
|
3091
|
+
* - Bare field name (no colon): `"name"` → `["name", "asc"]`
|
|
3092
|
+
* - Unknown direction: `"name:foo"` → `["name", "asc"]`
|
|
3093
|
+
* - Empty / falsy input: → `undefined`
|
|
3094
|
+
*
|
|
3095
|
+
* @param raw - The wire-format string from an HTTP query parameter.
|
|
3096
|
+
* @returns The canonical tuple, or `undefined` if the input is empty/falsy.
|
|
3097
|
+
*/
|
|
3098
|
+
function deserializeOrderBy(raw) {
|
|
3099
|
+
if (!raw) return void 0;
|
|
3100
|
+
const idx = raw.indexOf(":");
|
|
3101
|
+
if (idx === -1) return [raw, "asc"];
|
|
3102
|
+
return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
|
|
3103
|
+
}
|
|
3104
|
+
//#endregion
|
|
2695
3105
|
//#region src/table-classification.ts
|
|
2696
3106
|
/** Schemas that are always considered Rebase-internal. */
|
|
2697
3107
|
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
@@ -2768,6 +3178,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
2768
3178
|
return junctionTables;
|
|
2769
3179
|
}
|
|
2770
3180
|
//#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,
|
|
3181
|
+
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, buildCollection, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, 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, resolveFilterOperators, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
2772
3182
|
|
|
2773
3183
|
//# sourceMappingURL=index.es.js.map
|