@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.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
|
|
@@ -809,22 +809,56 @@
|
|
|
809
809
|
* {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
|
|
810
810
|
* and the admin UI derive from the exact same expression.
|
|
811
811
|
*/
|
|
812
|
-
function policyToPostgres(expr, collection) {
|
|
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) {
|
|
813
823
|
switch (expr.kind) {
|
|
814
824
|
case "true": return "true";
|
|
815
825
|
case "false": return "false";
|
|
816
|
-
case "and": return expr.operands.length === 0 ? "true" : expr.operands.map((o) => `(${
|
|
817
|
-
case "or": return expr.operands.length === 0 ? "false" : expr.operands.map((o) => `(${
|
|
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 ");
|
|
818
828
|
case "not":
|
|
819
829
|
if (expr.operand.kind === "authenticated") return "auth.uid() IS NULL";
|
|
820
|
-
return `NOT (${
|
|
821
|
-
case "compare": return `${operandToSql(expr.left,
|
|
830
|
+
return `NOT (${compile(expr.operand, scope)})`;
|
|
831
|
+
case "compare": return `${operandToSql(expr.left, scope)} ${COMPARE_SQL[expr.op]} ${operandToSql(expr.right, scope)}`;
|
|
822
832
|
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
823
833
|
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
824
834
|
case "authenticated": return "auth.uid() IS NOT NULL";
|
|
835
|
+
case "existsIn": return compileExistsIn(expr, scope);
|
|
825
836
|
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
826
837
|
}
|
|
827
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
|
+
}
|
|
828
862
|
var COMPARE_SQL = {
|
|
829
863
|
eq: "=",
|
|
830
864
|
neq: "!=",
|
|
@@ -833,14 +867,18 @@
|
|
|
833
867
|
gt: ">",
|
|
834
868
|
gte: ">="
|
|
835
869
|
};
|
|
836
|
-
function operandToSql(operand,
|
|
870
|
+
function operandToSql(operand, scope) {
|
|
837
871
|
switch (operand.kind) {
|
|
838
|
-
case "field": return resolveColumnName(operand.name,
|
|
872
|
+
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
873
|
+
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
839
874
|
case "literal": return quoteLiteral(operand.value);
|
|
840
875
|
case "authUid": return "auth.uid()";
|
|
841
876
|
case "authRoles": return "string_to_array(auth.roles(), ',')";
|
|
842
877
|
}
|
|
843
878
|
}
|
|
879
|
+
function schemaOf(collection) {
|
|
880
|
+
return collection?.schema || void 0;
|
|
881
|
+
}
|
|
844
882
|
function resolveColumnName(propName, collection) {
|
|
845
883
|
const prop = collection?.properties?.[propName];
|
|
846
884
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
@@ -883,6 +921,7 @@
|
|
|
883
921
|
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
884
922
|
}
|
|
885
923
|
case "authenticated": return ctx.uid != null;
|
|
924
|
+
case "existsIn": return "unknown";
|
|
886
925
|
case "raw": return "unknown";
|
|
887
926
|
}
|
|
888
927
|
}
|
|
@@ -920,6 +959,7 @@
|
|
|
920
959
|
known: true,
|
|
921
960
|
value: ctx.entity.values[operand.name]
|
|
922
961
|
};
|
|
962
|
+
case "outerField": return { known: false };
|
|
923
963
|
}
|
|
924
964
|
}
|
|
925
965
|
function evaluateCompare(op, left, right, ctx) {
|
|
@@ -1131,7 +1171,7 @@
|
|
|
1131
1171
|
} else {
|
|
1132
1172
|
entityId = remainingPath;
|
|
1133
1173
|
remainingPath = "";
|
|
1134
|
-
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.`);
|
|
1135
1175
|
}
|
|
1136
1176
|
resolvedPathParts.push(entityId);
|
|
1137
1177
|
currentCollections = getSubcollections(foundCollection);
|
|
@@ -1287,9 +1327,12 @@
|
|
|
1287
1327
|
//#endregion
|
|
1288
1328
|
//#region src/util/builders.ts
|
|
1289
1329
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
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
|
+
*
|
|
1293
1336
|
* @group Builder
|
|
1294
1337
|
*/
|
|
1295
1338
|
function buildCollection(collection) {
|
|
@@ -1303,68 +1346,16 @@
|
|
|
1303
1346
|
return collection;
|
|
1304
1347
|
}
|
|
1305
1348
|
/**
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
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
|
+
*
|
|
1309
1354
|
* @group Builder
|
|
1310
1355
|
*/
|
|
1311
1356
|
function buildProperty(property) {
|
|
1312
1357
|
return property;
|
|
1313
1358
|
}
|
|
1314
|
-
/**
|
|
1315
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1316
|
-
* the properties keys.
|
|
1317
|
-
* @param properties
|
|
1318
|
-
* @group Builder
|
|
1319
|
-
*/
|
|
1320
|
-
function buildProperties(properties) {
|
|
1321
|
-
return properties;
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1325
|
-
* the properties keys.
|
|
1326
|
-
* @param propertiesOrBuilder
|
|
1327
|
-
* @group Builder
|
|
1328
|
-
*/
|
|
1329
|
-
function buildPropertiesOrBuilder(propertiesOrBuilder) {
|
|
1330
|
-
return propertiesOrBuilder;
|
|
1331
|
-
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1334
|
-
* the properties keys.
|
|
1335
|
-
* @param enumValues
|
|
1336
|
-
* @group Builder
|
|
1337
|
-
*/
|
|
1338
|
-
function buildEnum(enumValues) {
|
|
1339
|
-
return enumValues;
|
|
1340
|
-
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1343
|
-
* the properties keys.
|
|
1344
|
-
* @param enumValueConfig
|
|
1345
|
-
* @group Builder
|
|
1346
|
-
*/
|
|
1347
|
-
function buildEnumValueConfig(enumValueConfig) {
|
|
1348
|
-
return enumValueConfig;
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Identity function we use to defeat the type system of Typescript and preserve
|
|
1352
|
-
* the properties keys.
|
|
1353
|
-
* @param callbacks
|
|
1354
|
-
* @group Builder
|
|
1355
|
-
*/
|
|
1356
|
-
function buildEntityCallbacks(callbacks) {
|
|
1357
|
-
return callbacks;
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Identity function we use to defeat the type system of Typescript and build
|
|
1361
|
-
* additional field delegates views with all its properties
|
|
1362
|
-
* @param additionalFieldDelegate
|
|
1363
|
-
* @group Builder
|
|
1364
|
-
*/
|
|
1365
|
-
function buildAdditionalFieldDelegate(additionalFieldDelegate) {
|
|
1366
|
-
return additionalFieldDelegate;
|
|
1367
|
-
}
|
|
1368
1359
|
//#endregion
|
|
1369
1360
|
//#region src/util/storage.ts
|
|
1370
1361
|
/**
|
|
@@ -1500,16 +1491,17 @@
|
|
|
1500
1491
|
}
|
|
1501
1492
|
/**
|
|
1502
1493
|
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
1503
|
-
* and wrap them into an
|
|
1494
|
+
* and wrap them into an CollectionCallbacks object recursively.
|
|
1504
1495
|
*/
|
|
1505
1496
|
var buildPropertyCallbacks = (properties) => {
|
|
1506
1497
|
if (!properties) return void 0;
|
|
1507
1498
|
const propertyCallbacks = {};
|
|
1508
1499
|
if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
|
|
1509
|
-
const
|
|
1500
|
+
const row = props.row;
|
|
1501
|
+
const processedValues = await processProperties(properties, row, row, props, "afterRead");
|
|
1510
1502
|
return {
|
|
1511
|
-
...props.
|
|
1512
|
-
|
|
1503
|
+
...props.row,
|
|
1504
|
+
...processedValues
|
|
1513
1505
|
};
|
|
1514
1506
|
};
|
|
1515
1507
|
if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
|
|
@@ -1696,6 +1688,84 @@
|
|
|
1696
1688
|
return result;
|
|
1697
1689
|
}
|
|
1698
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
|
|
1699
1769
|
//#region src/data/resolveDataSource.ts
|
|
1700
1770
|
/**
|
|
1701
1771
|
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
@@ -1774,7 +1844,7 @@
|
|
|
1774
1844
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
1775
1845
|
rawRootCollections = [];
|
|
1776
1846
|
cachedRawCollectionsList = null;
|
|
1777
|
-
|
|
1847
|
+
lastRawInputEntity = null;
|
|
1778
1848
|
constructor(collections, dataSources) {
|
|
1779
1849
|
if (dataSources) this.dataSources = dataSources;
|
|
1780
1850
|
if (collections) this.registerMultiple(collections);
|
|
@@ -1804,12 +1874,12 @@
|
|
|
1804
1874
|
* Returns true if the collections have changed, false otherwise.
|
|
1805
1875
|
*
|
|
1806
1876
|
* Idempotent: compares the raw input (before normalization) against a stored
|
|
1807
|
-
*
|
|
1877
|
+
* entity. Only re-normalizes and re-registers when the raw input actually changed.
|
|
1808
1878
|
* @param collections
|
|
1809
1879
|
*/
|
|
1810
1880
|
registerMultiple(collections) {
|
|
1811
|
-
const
|
|
1812
|
-
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;
|
|
1813
1883
|
this.reset();
|
|
1814
1884
|
collections.forEach((c) => {
|
|
1815
1885
|
if (c.slug) this.collectionsBySlug.set(c.slug, c);
|
|
@@ -1833,7 +1903,7 @@
|
|
|
1833
1903
|
this._registerRecursively(this.normalizeCollection({ ...subCollection }), (0, _rebasepro_utils.deepClone)(subCollection));
|
|
1834
1904
|
});
|
|
1835
1905
|
});
|
|
1836
|
-
this.
|
|
1906
|
+
this.lastRawInputEntity = rawEntity;
|
|
1837
1907
|
return true;
|
|
1838
1908
|
}
|
|
1839
1909
|
register(collection, rawCollection) {
|
|
@@ -2253,7 +2323,7 @@
|
|
|
2253
2323
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
2254
2324
|
*/
|
|
2255
2325
|
orderBy(column, direction = "asc") {
|
|
2256
|
-
this.params.orderBy =
|
|
2326
|
+
this.params.orderBy = [column, direction];
|
|
2257
2327
|
return this;
|
|
2258
2328
|
}
|
|
2259
2329
|
/**
|
|
@@ -2316,32 +2386,69 @@
|
|
|
2316
2386
|
* PostgREST-style dot-syntax strings (`eq.active`, `gt.18`, `in.(a,b)`).
|
|
2317
2387
|
* Everything else speaks `FilterValues` exclusively.
|
|
2318
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
|
+
*
|
|
2319
2396
|
* @module
|
|
2320
2397
|
*/
|
|
2321
2398
|
/**
|
|
2322
|
-
* Coerce a raw querystring value to its natural JS type.
|
|
2323
|
-
* - `"true"` / `"false"` → boolean
|
|
2324
|
-
* - `"null"` → null
|
|
2325
|
-
* - Numeric strings → number
|
|
2326
|
-
* - Everything else → string (unchanged)
|
|
2327
|
-
*/
|
|
2328
|
-
function coerceValue(raw) {
|
|
2329
|
-
if (raw === "true") return true;
|
|
2330
|
-
if (raw === "false") return false;
|
|
2331
|
-
if (raw === "null") return null;
|
|
2332
|
-
if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
|
|
2333
|
-
return raw;
|
|
2334
|
-
}
|
|
2335
|
-
/**
|
|
2336
2399
|
* Serialize a JS value to its querystring representation.
|
|
2400
|
+
* `null` is serialized as the literal string `"null"`.
|
|
2337
2401
|
*/
|
|
2338
2402
|
function stringifyValue(value) {
|
|
2339
2403
|
if (value === null) return "null";
|
|
2340
|
-
if (typeof value === "boolean") return String(value);
|
|
2341
2404
|
return String(value);
|
|
2342
2405
|
}
|
|
2343
2406
|
/**
|
|
2344
|
-
*
|
|
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.
|
|
2345
2452
|
*
|
|
2346
2453
|
* @example
|
|
2347
2454
|
* serializeTuple(["==", "active"]) // "eq.active"
|
|
@@ -2349,22 +2456,20 @@
|
|
|
2349
2456
|
* serializeTuple([">=", 18]) // "gte.18"
|
|
2350
2457
|
*/
|
|
2351
2458
|
function serializeTuple(tuple) {
|
|
2352
|
-
if (
|
|
2353
|
-
if (tuple.includes(".")) {
|
|
2354
|
-
const dotIndex = tuple.indexOf(".");
|
|
2355
|
-
if (_rebasepro_types.REST_TO_CANONICAL[tuple.substring(0, dotIndex)]) return tuple;
|
|
2356
|
-
}
|
|
2357
|
-
return tuple;
|
|
2358
|
-
}
|
|
2359
|
-
if (!Array.isArray(tuple) || tuple.length !== 2 || typeof tuple[0] !== "string" || !_rebasepro_types.CANONICAL_TO_REST[tuple[0]]) return `eq.${stringifyValue(tuple)}`;
|
|
2459
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
|
|
2360
2460
|
const [op, value] = tuple;
|
|
2361
|
-
|
|
2362
|
-
|
|
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(",")})`;
|
|
2363
2465
|
return `${restOp}.${stringifyValue(value)}`;
|
|
2364
2466
|
}
|
|
2365
2467
|
/**
|
|
2366
|
-
* Convert `FilterValues` to a PostgREST-style
|
|
2468
|
+
* Convert `FilterValues` (or `WireFilterValues`) to a PostgREST-style
|
|
2469
|
+
* querystring record.
|
|
2367
2470
|
*
|
|
2471
|
+
* - Canonical `[WhereFilterOp, value]` tuples are serialized strictly.
|
|
2472
|
+
* - Pre-serialized PostgREST strings (e.g. `"eq.published"`) are passed through.
|
|
2368
2473
|
* - Single conditions produce a string value.
|
|
2369
2474
|
* - Multiple conditions on the same field produce a string array (repeated params).
|
|
2370
2475
|
*
|
|
@@ -2374,11 +2479,19 @@
|
|
|
2374
2479
|
*
|
|
2375
2480
|
* serializeFilter({ age: [[">=", 18], ["<", 65]] })
|
|
2376
2481
|
* // → { age: ["gte.18", "lt.65"] }
|
|
2482
|
+
*
|
|
2483
|
+
* // Pre-serialized strings pass through unchanged:
|
|
2484
|
+
* serializeFilter({ status: "eq.published" })
|
|
2485
|
+
* // → { status: "eq.published" }
|
|
2377
2486
|
*/
|
|
2378
2487
|
function serializeFilter(filter) {
|
|
2379
2488
|
const result = {};
|
|
2380
2489
|
for (const [field, condition] of Object.entries(filter)) {
|
|
2381
2490
|
if (condition === void 0) continue;
|
|
2491
|
+
if (typeof condition === "string") {
|
|
2492
|
+
result[field] = condition;
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2382
2495
|
if (Array.isArray(condition) && condition.length > 0 && Array.isArray(condition[0])) result[field] = condition.map(serializeTuple);
|
|
2383
2496
|
else result[field] = serializeTuple(condition);
|
|
2384
2497
|
}
|
|
@@ -2387,18 +2500,24 @@
|
|
|
2387
2500
|
/**
|
|
2388
2501
|
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
2389
2502
|
*
|
|
2390
|
-
*
|
|
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
|
|
2391
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.
|
|
2392
2510
|
*/
|
|
2393
2511
|
function deserializeSingle(raw) {
|
|
2394
2512
|
const dotIndex = raw.indexOf(".");
|
|
2395
|
-
if (dotIndex === -1) return ["==",
|
|
2513
|
+
if (dotIndex === -1) return ["==", raw];
|
|
2396
2514
|
const prefix = raw.substring(0, dotIndex);
|
|
2397
2515
|
const rest = raw.substring(dotIndex + 1);
|
|
2398
|
-
const canonicalOp =
|
|
2516
|
+
const canonicalOp = REST_OP_LOOKUP[prefix];
|
|
2399
2517
|
if (!canonicalOp) return ["==", raw];
|
|
2400
|
-
if (
|
|
2401
|
-
return [canonicalOp,
|
|
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];
|
|
2402
2521
|
}
|
|
2403
2522
|
/**
|
|
2404
2523
|
* Convert a PostgREST-style querystring record to `FilterValues`.
|
|
@@ -2411,7 +2530,7 @@
|
|
|
2411
2530
|
* // → { status: ["==", "active"] }
|
|
2412
2531
|
*
|
|
2413
2532
|
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
2414
|
-
* // → { age: [[">=", 18], ["<", 65]] }
|
|
2533
|
+
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
2415
2534
|
*/
|
|
2416
2535
|
function deserializeFilter(query) {
|
|
2417
2536
|
const result = {};
|
|
@@ -2450,9 +2569,9 @@
|
|
|
2450
2569
|
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
2451
2570
|
return `${cond.type}(${inner})`;
|
|
2452
2571
|
}
|
|
2453
|
-
const restOp =
|
|
2572
|
+
const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
|
|
2454
2573
|
if (Array.isArray(cond.value)) {
|
|
2455
|
-
const items = cond.value.map(stringifyValue).join(",");
|
|
2574
|
+
const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
|
|
2456
2575
|
return `${cond.column}.${restOp}.(${items})`;
|
|
2457
2576
|
}
|
|
2458
2577
|
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
@@ -2500,99 +2619,115 @@
|
|
|
2500
2619
|
if (secondDot === -1) return {
|
|
2501
2620
|
column,
|
|
2502
2621
|
operator: "==",
|
|
2503
|
-
value:
|
|
2622
|
+
value: rest
|
|
2504
2623
|
};
|
|
2505
2624
|
const opStr = rest.substring(0, secondDot);
|
|
2506
|
-
|
|
2625
|
+
const valueStr = rest.substring(secondDot + 1);
|
|
2507
2626
|
const operator = (0, _rebasepro_types.toCanonicalOp)(opStr) ?? "==";
|
|
2508
2627
|
if (valueStr.startsWith("(") && valueStr.endsWith(")")) return {
|
|
2509
2628
|
column,
|
|
2510
2629
|
operator,
|
|
2511
|
-
value: valueStr.slice(1, -1)
|
|
2630
|
+
value: splitListItems(valueStr.slice(1, -1))
|
|
2512
2631
|
};
|
|
2513
2632
|
return {
|
|
2514
2633
|
column,
|
|
2515
2634
|
operator,
|
|
2516
|
-
value:
|
|
2635
|
+
value: valueStr
|
|
2517
2636
|
};
|
|
2518
2637
|
}
|
|
2519
2638
|
//#endregion
|
|
2520
2639
|
//#region src/data/buildRebaseData.ts
|
|
2521
2640
|
/**
|
|
2522
|
-
*
|
|
2641
|
+
* Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
|
|
2642
|
+
* Mirrors the client SDK's rowToEntity conversion.
|
|
2523
2643
|
*/
|
|
2524
|
-
function
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2644
|
+
function rowToEntity(row, slug) {
|
|
2645
|
+
return {
|
|
2646
|
+
id: row.id,
|
|
2647
|
+
path: slug,
|
|
2648
|
+
values: row
|
|
2649
|
+
};
|
|
2528
2650
|
}
|
|
2529
2651
|
function createDriverAccessor(driver, slug) {
|
|
2530
2652
|
const accessor = {
|
|
2531
2653
|
async find(params) {
|
|
2532
|
-
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2533
2654
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2534
|
-
const
|
|
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({
|
|
2535
2666
|
path: slug,
|
|
2536
2667
|
limit: params?.limit,
|
|
2537
2668
|
offset: params?.offset,
|
|
2538
2669
|
filter,
|
|
2539
|
-
orderBy:
|
|
2540
|
-
order:
|
|
2670
|
+
orderBy: params?.orderBy?.[0],
|
|
2671
|
+
order: params?.orderBy?.[1],
|
|
2541
2672
|
searchString: params?.searchString
|
|
2542
2673
|
});
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
+
}
|
|
2545
2683
|
return {
|
|
2546
|
-
data:
|
|
2684
|
+
data: rows.map((row) => rowToEntity(row, slug)),
|
|
2547
2685
|
meta: {
|
|
2548
|
-
total
|
|
2686
|
+
total,
|
|
2549
2687
|
limit,
|
|
2550
2688
|
offset,
|
|
2551
|
-
hasMore
|
|
2689
|
+
hasMore
|
|
2552
2690
|
}
|
|
2553
2691
|
};
|
|
2554
2692
|
},
|
|
2555
2693
|
async findById(id) {
|
|
2556
|
-
|
|
2694
|
+
const row = await driver.fetchOne({
|
|
2557
2695
|
path: slug,
|
|
2558
|
-
|
|
2696
|
+
id
|
|
2559
2697
|
});
|
|
2698
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
2560
2699
|
},
|
|
2561
2700
|
async create(data, id) {
|
|
2562
|
-
return driver.
|
|
2701
|
+
return rowToEntity(await driver.save({
|
|
2563
2702
|
path: slug,
|
|
2564
2703
|
values: data,
|
|
2565
|
-
|
|
2704
|
+
id,
|
|
2566
2705
|
status: "new"
|
|
2567
|
-
});
|
|
2706
|
+
}), slug);
|
|
2568
2707
|
},
|
|
2569
2708
|
async update(id, data) {
|
|
2570
|
-
return driver.
|
|
2709
|
+
return rowToEntity(await driver.save({
|
|
2571
2710
|
path: slug,
|
|
2572
2711
|
values: data,
|
|
2573
|
-
|
|
2712
|
+
id,
|
|
2574
2713
|
status: "existing"
|
|
2575
|
-
});
|
|
2714
|
+
}), slug);
|
|
2576
2715
|
},
|
|
2577
2716
|
async delete(id) {
|
|
2578
|
-
return driver.
|
|
2717
|
+
return driver.delete({ row: {
|
|
2579
2718
|
id,
|
|
2580
2719
|
path: slug,
|
|
2581
2720
|
values: {}
|
|
2582
2721
|
} });
|
|
2583
2722
|
},
|
|
2584
|
-
|
|
2585
|
-
return driver.deleteAll(slug);
|
|
2586
|
-
} : void 0,
|
|
2587
|
-
count: driver.countEntities ? async (params) => {
|
|
2723
|
+
count: driver.count ? async (params) => {
|
|
2588
2724
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
2589
|
-
return driver.
|
|
2725
|
+
return driver.count({
|
|
2590
2726
|
path: slug,
|
|
2591
2727
|
filter
|
|
2592
2728
|
});
|
|
2593
2729
|
} : void 0,
|
|
2594
2730
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
2595
|
-
const orderParsed = parseOrderBy(params?.orderBy);
|
|
2596
2731
|
const limit = params?.limit ?? 20;
|
|
2597
2732
|
const offset = params?.offset ?? 0;
|
|
2598
2733
|
return driver.listenCollection({
|
|
@@ -2600,12 +2735,12 @@
|
|
|
2600
2735
|
limit: params?.limit,
|
|
2601
2736
|
offset: params?.offset,
|
|
2602
2737
|
filter: params?.where,
|
|
2603
|
-
orderBy:
|
|
2604
|
-
order:
|
|
2738
|
+
orderBy: params?.orderBy?.[0],
|
|
2739
|
+
order: params?.orderBy?.[1],
|
|
2605
2740
|
searchString: params?.searchString,
|
|
2606
2741
|
onUpdate: (entities) => {
|
|
2607
2742
|
onUpdate({
|
|
2608
|
-
data: entities,
|
|
2743
|
+
data: entities.map((row) => rowToEntity(row, slug)),
|
|
2609
2744
|
meta: {
|
|
2610
2745
|
total: entities.length,
|
|
2611
2746
|
limit,
|
|
@@ -2617,11 +2752,11 @@
|
|
|
2617
2752
|
onError
|
|
2618
2753
|
});
|
|
2619
2754
|
} : void 0,
|
|
2620
|
-
listenById: driver.
|
|
2621
|
-
return driver.
|
|
2755
|
+
listenById: driver.listenOne ? (id, onUpdate, onError) => {
|
|
2756
|
+
return driver.listenOne({
|
|
2622
2757
|
path: slug,
|
|
2623
|
-
|
|
2624
|
-
onUpdate: (entity) => onUpdate(entity
|
|
2758
|
+
id,
|
|
2759
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
2625
2760
|
onError
|
|
2626
2761
|
});
|
|
2627
2762
|
} : void 0,
|
|
@@ -2658,7 +2793,7 @@
|
|
|
2658
2793
|
* @example
|
|
2659
2794
|
* const data = buildRebaseData(driver);
|
|
2660
2795
|
* await data.products.create({ name: "Camera", price: 299 });
|
|
2661
|
-
* const { data: items } = await data.products.find({ where: { status: "
|
|
2796
|
+
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
2662
2797
|
*/
|
|
2663
2798
|
function buildRebaseData(driver) {
|
|
2664
2799
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2677,6 +2812,230 @@
|
|
|
2677
2812
|
return getAccessor((0, _rebasepro_utils.toSnakeCase)(prop));
|
|
2678
2813
|
} });
|
|
2679
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
|
+
}
|
|
2680
3039
|
//#endregion
|
|
2681
3040
|
//#region src/data/buildRoutedRebaseData.ts
|
|
2682
3041
|
/**
|
|
@@ -2721,6 +3080,57 @@
|
|
|
2721
3080
|
} });
|
|
2722
3081
|
}
|
|
2723
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
|
|
2724
3134
|
//#region src/table-classification.ts
|
|
2725
3135
|
/** Schemas that are always considered Rebase-internal. */
|
|
2726
3136
|
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
@@ -2808,18 +3218,13 @@
|
|
|
2808
3218
|
exports.addInitialSlash = addInitialSlash;
|
|
2809
3219
|
exports.and = and;
|
|
2810
3220
|
exports.applyPropertyConditions = applyPropertyConditions;
|
|
2811
|
-
exports.buildAdditionalFieldDelegate = buildAdditionalFieldDelegate;
|
|
2812
3221
|
exports.buildCollection = buildCollection;
|
|
2813
3222
|
exports.buildConditionContext = buildConditionContext;
|
|
2814
|
-
exports.buildEntityCallbacks = buildEntityCallbacks;
|
|
2815
|
-
exports.buildEnum = buildEnum;
|
|
2816
|
-
exports.buildEnumValueConfig = buildEnumValueConfig;
|
|
2817
|
-
exports.buildProperties = buildProperties;
|
|
2818
|
-
exports.buildPropertiesOrBuilder = buildPropertiesOrBuilder;
|
|
2819
3223
|
exports.buildProperty = buildProperty;
|
|
2820
3224
|
exports.buildPropertyCallbacks = buildPropertyCallbacks;
|
|
2821
3225
|
exports.buildRebaseData = buildRebaseData;
|
|
2822
3226
|
exports.buildRoutedRebaseData = buildRoutedRebaseData;
|
|
3227
|
+
exports.buildSdkData = buildSdkData;
|
|
2823
3228
|
exports.canCreateEntity = canCreateEntity;
|
|
2824
3229
|
exports.canDeleteEntity = canDeleteEntity;
|
|
2825
3230
|
exports.canEditEntity = canEditEntity;
|
|
@@ -2834,6 +3239,7 @@
|
|
|
2834
3239
|
exports.defineCollection = defineCollection;
|
|
2835
3240
|
exports.deserializeFilter = deserializeFilter;
|
|
2836
3241
|
exports.deserializeLogicalCondition = deserializeLogicalCondition;
|
|
3242
|
+
exports.deserializeOrderBy = deserializeOrderBy;
|
|
2837
3243
|
exports.detectJunctionTables = detectJunctionTables;
|
|
2838
3244
|
exports.enumToObjectEntries = enumToObjectEntries;
|
|
2839
3245
|
exports.evaluateCondition = evaluateCondition;
|
|
@@ -2877,6 +3283,7 @@
|
|
|
2877
3283
|
exports.resolveDataSource = resolveDataSource;
|
|
2878
3284
|
exports.resolveDefaultSelectedView = resolveDefaultSelectedView;
|
|
2879
3285
|
exports.resolveEnumValues = resolveEnumValues;
|
|
3286
|
+
exports.resolveFilterOperators = resolveFilterOperators;
|
|
2880
3287
|
exports.resolveProperties = resolveProperties;
|
|
2881
3288
|
exports.resolveProperty = resolveProperty;
|
|
2882
3289
|
exports.resolvePropertyEnum = resolvePropertyEnum;
|
|
@@ -2891,11 +3298,14 @@
|
|
|
2891
3298
|
exports.segmentsToStrippedPath = segmentsToStrippedPath;
|
|
2892
3299
|
exports.serializeFilter = serializeFilter;
|
|
2893
3300
|
exports.serializeLogicalCondition = serializeLogicalCondition;
|
|
3301
|
+
exports.serializeOrderBy = serializeOrderBy;
|
|
2894
3302
|
exports.sortProperties = sortProperties;
|
|
2895
3303
|
exports.stripCollectionPath = stripCollectionPath;
|
|
2896
3304
|
exports.traverseValueProperty = traverseValueProperty;
|
|
2897
3305
|
exports.traverseValuesProperties = traverseValuesProperties;
|
|
2898
3306
|
exports.updateDateAutoValues = updateDateAutoValues;
|
|
3307
|
+
exports.wrapAsEntityData = wrapAsEntityData;
|
|
3308
|
+
exports.wrapAsSdkData = wrapAsSdkData;
|
|
2899
3309
|
});
|
|
2900
3310
|
|
|
2901
3311
|
//# sourceMappingURL=index.umd.js.map
|