@rebasepro/common 0.13.0 → 0.13.1-canary.g394d868
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/filter-dialect.d.ts +15 -1
- package/dist/data/paginate.d.ts +20 -0
- package/dist/data/resolveDataSource.d.ts +36 -0
- package/dist/index.es.js +231 -21
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +2 -2
- package/dist/util/conditions.d.ts +7 -3
- package/dist/util/entities.d.ts +8 -1
- package/dist/util/relations.d.ts +18 -1
- package/dist/util/resolutions.d.ts +31 -0
- package/package.json +3 -3
- package/src/data/buildRebaseData.ts +90 -15
- package/src/data/filter-dialect.ts +28 -3
- package/src/data/paginate.ts +30 -0
- package/src/data/resolveDataSource.ts +56 -0
- package/src/util/builders.ts +3 -3
- package/src/util/conditions.ts +8 -3
- package/src/util/entities.ts +15 -1
- package/src/util/relations.ts +31 -0
- package/src/util/resolutions.ts +77 -5
|
@@ -72,4 +72,18 @@ export declare function serializeLogicalCondition(cond: LogicalCondition | Filte
|
|
|
72
72
|
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
73
73
|
* // → { type: "or", conditions: [...] }
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* How deeply `or(...)`/`and(...)` groups may nest.
|
|
77
|
+
*
|
|
78
|
+
* This parser recurses once per level, on a value that arrives in a query
|
|
79
|
+
* string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
|
|
80
|
+
* stack size exceeded`, which a caller sees as a 500 about the call stack
|
|
81
|
+
* rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
|
|
82
|
+
* below that in practice, but "the HTTP layer happens to stop it" is not a
|
|
83
|
+
* bound this parser should rely on.
|
|
84
|
+
*
|
|
85
|
+
* Thirty-two is far past anything a real filter expresses; the deepest in this
|
|
86
|
+
* repository's own tests is three.
|
|
87
|
+
*/
|
|
88
|
+
export declare const MAX_LOGICAL_NESTING_DEPTH = 32;
|
|
89
|
+
export declare function deserializeLogicalCondition(str: string, nesting?: number): LogicalCondition | FilterCondition;
|
package/dist/data/paginate.d.ts
CHANGED
|
@@ -45,6 +45,26 @@ export declare class RebasePaginationError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
/** The one thing a transport has to provide to be paginated. */
|
|
47
47
|
export type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> = (params: FindParams<M>) => Promise<FindResult<M>>;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
50
|
+
*
|
|
51
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
52
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
53
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
54
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
55
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
56
|
+
* rows are the mildest of those outcomes.
|
|
57
|
+
*
|
|
58
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
59
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
60
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
61
|
+
* look like it is paging by offset.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveFindWindow(params?: Pick<FindParams, "limit" | "offset" | "page">): {
|
|
64
|
+
limit: number;
|
|
65
|
+
offset: number;
|
|
66
|
+
driverOffset: number | undefined;
|
|
67
|
+
};
|
|
48
68
|
/**
|
|
49
69
|
* Walk every row a query matches, yielding one row at a time and fetching the
|
|
50
70
|
* next page only when the consumer asks for it.
|
|
@@ -41,3 +41,39 @@ export declare function createDataSourceRegistry(definitions?: DataSourceDefinit
|
|
|
41
41
|
* @param registry optional registry of declared data sources
|
|
42
42
|
*/
|
|
43
43
|
export declare function resolveDataSource(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): ResolvedDataSource;
|
|
44
|
+
/**
|
|
45
|
+
* Does a SQL toolchain own this collection's storage?
|
|
46
|
+
*
|
|
47
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
48
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
49
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
50
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
51
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
52
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
53
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
54
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
55
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
56
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
57
|
+
*
|
|
58
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
59
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
60
|
+
* gets the same treatment as the built-in ones.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
63
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
64
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
65
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
66
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
67
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
68
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isRelationalCollection(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
73
|
+
* {@link isRelationalCollection}.
|
|
74
|
+
*
|
|
75
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
76
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
77
|
+
* keeps the input order.
|
|
78
|
+
*/
|
|
79
|
+
export declare function relationalCollections<C extends DataSourceResolvable>(collections: readonly C[], registry?: DataSourceRegistry): C[];
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
1
|
+
import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, DEFAULT_LIST_LIMIT, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
2
2
|
import { deepClone, generateForeignKeyName, getIn, getPolicyNamesForRules, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, prettifyIdentifier, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
import jsonLogic from "json-logic-js";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
@@ -89,10 +89,21 @@ function getRelationFrom(entity) {
|
|
|
89
89
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
90
90
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
91
91
|
*
|
|
92
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
93
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
94
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
95
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
96
|
+
* reported half of its own data as a type error. The declared target is the
|
|
97
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
98
|
+
*
|
|
92
99
|
* Returns null if the value cannot be coerced.
|
|
93
100
|
*/
|
|
94
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
101
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
95
102
|
if (value instanceof EntityRelation) return value;
|
|
103
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
104
|
+
if (value === "") return null;
|
|
105
|
+
return new EntityRelation(value, targetPath);
|
|
106
|
+
}
|
|
96
107
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
97
108
|
const obj = value;
|
|
98
109
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -608,6 +619,33 @@ function resolveCollectionRelations(collection) {
|
|
|
608
619
|
_resolvedRelationsCache.set(collection, relations);
|
|
609
620
|
return relations;
|
|
610
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* The path of the collection a relation property points at, derived from the
|
|
624
|
+
* property alone.
|
|
625
|
+
*
|
|
626
|
+
* A preview holds a property and a value and no collection, so it cannot call
|
|
627
|
+
* `resolveRelationProperty`. It does not need to: both forms that carry a
|
|
628
|
+
* target — the stamped `resolvedRelation` and the inline `relation` — name it
|
|
629
|
+
* directly. Only the third form, a relation declared by name in the
|
|
630
|
+
* collection's `relations` array, is out of reach, and that one has no target
|
|
631
|
+
* to read without the collection anyway.
|
|
632
|
+
*
|
|
633
|
+
* This is what lets a preview render a relation column that arrived as a bare
|
|
634
|
+
* foreign key: the id says *which* row, the declared target says *which
|
|
635
|
+
* collection*, and `RelationPreview` fetches the rest. Without it a scalar id
|
|
636
|
+
* is indistinguishable from a value of the wrong type.
|
|
637
|
+
*/
|
|
638
|
+
function getRelationTargetPath(property) {
|
|
639
|
+
const stamped = property.resolvedRelation?.targetSlug;
|
|
640
|
+
if (stamped) return stamped;
|
|
641
|
+
const target = property.relation?.target;
|
|
642
|
+
if (typeof target !== "function") return void 0;
|
|
643
|
+
try {
|
|
644
|
+
return target()?.slug;
|
|
645
|
+
} catch (_e) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
611
649
|
function getTableName(collection) {
|
|
612
650
|
if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
613
651
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -882,6 +920,59 @@ function getEntityChildViews(collection) {
|
|
|
882
920
|
return views;
|
|
883
921
|
}
|
|
884
922
|
/**
|
|
923
|
+
* Each of `collection`'s tabs paired with the property that declared it, when a
|
|
924
|
+
* property declared it: child view key → property key.
|
|
925
|
+
*
|
|
926
|
+
* A many-relation can only be declared as a property — that is the documented
|
|
927
|
+
* and only mechanism — and {@link getEntityChildViews} promotes it to a tab. So
|
|
928
|
+
* one declaration reaches the panel twice, and neither surface knew about the
|
|
929
|
+
* other. The form rendered a relation picker beside the tab, and the collection
|
|
930
|
+
* table rendered *two* columns under one heading: the relation's own column,
|
|
931
|
+
* showing the child rows, and a jump-to-tab button carrying the same name.
|
|
932
|
+
*
|
|
933
|
+
* The pairing is what lets each surface decide which half is redundant, and it
|
|
934
|
+
* has to be a pairing rather than two sets because the two keys differ whenever
|
|
935
|
+
* a relation is named. The match is on the resolved `relationName` — the
|
|
936
|
+
* identity `getEntityChildViews` itself dedupes on — so a relation declared in
|
|
937
|
+
* `relations` and pointed at by a differently-named property is recognised too.
|
|
938
|
+
*
|
|
939
|
+
* A relation with no property of its own is absent here, which is the point: it
|
|
940
|
+
* has exactly one surface already, and nothing to weigh it against.
|
|
941
|
+
*
|
|
942
|
+
* Only top-level properties: a relation nested inside a `map` gets no tab.
|
|
943
|
+
*/
|
|
944
|
+
function getChildViewDeclaringProperties(collection) {
|
|
945
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
946
|
+
const relationProperties = Object.entries(collection.properties ?? {}).filter(([, property]) => property?.type === "relation");
|
|
947
|
+
if (relationProperties.length === 0) return pairs;
|
|
948
|
+
const relationViews = getEntityChildViews(collection).filter((view) => view.source.kind === "relation");
|
|
949
|
+
if (relationViews.length === 0) return pairs;
|
|
950
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
951
|
+
const identityOf = (relationKey) => resolvedRelations[relationKey]?.relationName ?? relationKey;
|
|
952
|
+
const declaringPropertyByIdentity = /* @__PURE__ */ new Map();
|
|
953
|
+
for (const [propertyKey, property] of relationProperties) {
|
|
954
|
+
const relation = property.resolvedRelation ?? resolvedRelations[propertyKey];
|
|
955
|
+
if (relation?.cardinality !== "many") continue;
|
|
956
|
+
const identity = relation.relationName ?? propertyKey;
|
|
957
|
+
if (!declaringPropertyByIdentity.has(identity)) declaringPropertyByIdentity.set(identity, propertyKey);
|
|
958
|
+
}
|
|
959
|
+
for (const view of relationViews) {
|
|
960
|
+
const propertyKey = declaringPropertyByIdentity.get(identityOf(view.source.relationKey));
|
|
961
|
+
if (propertyKey) pairs.set(view.key, propertyKey);
|
|
962
|
+
}
|
|
963
|
+
return pairs;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* The property keys of `collection` whose relation is already one of its tabs.
|
|
967
|
+
*
|
|
968
|
+
* What a form asks: the tab is the treatment for a list of child rows, so the
|
|
969
|
+
* picker beside it is the redundant half. See
|
|
970
|
+
* {@link getChildViewDeclaringProperties}.
|
|
971
|
+
*/
|
|
972
|
+
function getChildViewRelationPropertyKeys(collection) {
|
|
973
|
+
return new Set(getChildViewDeclaringProperties(collection).values());
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
885
976
|
* The child views of `collection` as bare collections.
|
|
886
977
|
*
|
|
887
978
|
* The flattened view of {@link getEntityChildViews}, for navigation code that
|
|
@@ -1996,9 +2087,14 @@ function registerConditionOperations() {
|
|
|
1996
2087
|
operationsRegistered = true;
|
|
1997
2088
|
}
|
|
1998
2089
|
/**
|
|
1999
|
-
* Evaluate a
|
|
2090
|
+
* Evaluate a condition against the given context.
|
|
2091
|
+
*
|
|
2092
|
+
* A condition may be stated as a literal instead of a rule — `hidden: true`
|
|
2093
|
+
* rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
|
|
2094
|
+
* answer, so it is returned rather than handed to the evaluator.
|
|
2000
2095
|
*/
|
|
2001
2096
|
function evaluateCondition(rule, context) {
|
|
2097
|
+
if (typeof rule === "boolean") return rule;
|
|
2002
2098
|
registerConditionOperations();
|
|
2003
2099
|
return jsonLogic.apply(rule, context);
|
|
2004
2100
|
}
|
|
@@ -2359,6 +2455,46 @@ function resolveDataSource(collection, registry) {
|
|
|
2359
2455
|
capabilities: getDataSourceCapabilities(engine)
|
|
2360
2456
|
};
|
|
2361
2457
|
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Does a SQL toolchain own this collection's storage?
|
|
2460
|
+
*
|
|
2461
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
2462
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
2463
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
2464
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
2465
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
2466
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
2467
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
2468
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
2469
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
2470
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
2471
|
+
*
|
|
2472
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
2473
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
2474
|
+
* gets the same treatment as the built-in ones.
|
|
2475
|
+
*
|
|
2476
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
2477
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
2478
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
2479
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
2480
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
2481
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
2482
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
2483
|
+
*/
|
|
2484
|
+
function isRelationalCollection(collection, registry) {
|
|
2485
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
2489
|
+
* {@link isRelationalCollection}.
|
|
2490
|
+
*
|
|
2491
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
2492
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
2493
|
+
* keeps the input order.
|
|
2494
|
+
*/
|
|
2495
|
+
function relationalCollections(collections, registry) {
|
|
2496
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
2497
|
+
}
|
|
2362
2498
|
//#endregion
|
|
2363
2499
|
//#region src/collections/CollectionRegistry.ts
|
|
2364
2500
|
var CollectionRegistry = class {
|
|
@@ -2861,6 +2997,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
2861
2997
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
2862
2998
|
}
|
|
2863
2999
|
};
|
|
3000
|
+
/**
|
|
3001
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3002
|
+
*
|
|
3003
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3004
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3005
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3006
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3007
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3008
|
+
* rows are the mildest of those outcomes.
|
|
3009
|
+
*
|
|
3010
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3011
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3012
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3013
|
+
* look like it is paging by offset.
|
|
3014
|
+
*/
|
|
3015
|
+
function resolveFindWindow(params) {
|
|
3016
|
+
const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
|
|
3017
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3018
|
+
return {
|
|
3019
|
+
limit,
|
|
3020
|
+
offset,
|
|
3021
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
2864
3024
|
function normalizePageSize(raw) {
|
|
2865
3025
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
2866
3026
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3194,7 +3354,22 @@ function serializeLogicalCondition(cond) {
|
|
|
3194
3354
|
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
3195
3355
|
* // → { type: "or", conditions: [...] }
|
|
3196
3356
|
*/
|
|
3197
|
-
|
|
3357
|
+
/**
|
|
3358
|
+
* How deeply `or(...)`/`and(...)` groups may nest.
|
|
3359
|
+
*
|
|
3360
|
+
* This parser recurses once per level, on a value that arrives in a query
|
|
3361
|
+
* string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
|
|
3362
|
+
* stack size exceeded`, which a caller sees as a 500 about the call stack
|
|
3363
|
+
* rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
|
|
3364
|
+
* below that in practice, but "the HTTP layer happens to stop it" is not a
|
|
3365
|
+
* bound this parser should rely on.
|
|
3366
|
+
*
|
|
3367
|
+
* Thirty-two is far past anything a real filter expresses; the deepest in this
|
|
3368
|
+
* repository's own tests is three.
|
|
3369
|
+
*/
|
|
3370
|
+
var MAX_LOGICAL_NESTING_DEPTH = 32;
|
|
3371
|
+
function deserializeLogicalCondition(str, nesting = 0) {
|
|
3372
|
+
if (nesting > 32) throw new Error(`Filter groups nest more than 32 levels deep. Flatten the condition — \`or(a,or(b,c))\` is \`or(a,b,c)\`.`);
|
|
3198
3373
|
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
3199
3374
|
if (logicalMatch) {
|
|
3200
3375
|
const type = logicalMatch[1];
|
|
@@ -3205,10 +3380,10 @@ function deserializeLogicalCondition(str) {
|
|
|
3205
3380
|
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
3206
3381
|
else if (innerStr[i] === ")") depth--;
|
|
3207
3382
|
else if (innerStr[i] === "," && depth === 0) {
|
|
3208
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
3383
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i), nesting + 1));
|
|
3209
3384
|
start = i + 1;
|
|
3210
3385
|
}
|
|
3211
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
3386
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start), nesting + 1));
|
|
3212
3387
|
return {
|
|
3213
3388
|
type,
|
|
3214
3389
|
conditions
|
|
@@ -3322,21 +3497,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3322
3497
|
const accessor = {
|
|
3323
3498
|
async find(params) {
|
|
3324
3499
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3325
|
-
const limit = params
|
|
3326
|
-
const offset = params?.offset ?? 0;
|
|
3500
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3327
3501
|
const fetchService = driver.restFetchService;
|
|
3328
3502
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3329
3503
|
filter,
|
|
3330
|
-
|
|
3331
|
-
|
|
3504
|
+
logical: params?.logical,
|
|
3505
|
+
limit,
|
|
3506
|
+
offset: driverOffset,
|
|
3332
3507
|
orderBy: params?.orderBy?.[0],
|
|
3333
3508
|
order: params?.orderBy?.[1],
|
|
3334
3509
|
searchString: params?.searchString
|
|
3335
3510
|
}, params?.include) : await driver.fetchCollection({
|
|
3336
3511
|
path: slug,
|
|
3337
|
-
limit
|
|
3338
|
-
offset:
|
|
3512
|
+
limit,
|
|
3513
|
+
offset: driverOffset,
|
|
3339
3514
|
filter,
|
|
3515
|
+
logical: params?.logical,
|
|
3340
3516
|
orderBy: params?.orderBy?.[0],
|
|
3341
3517
|
order: params?.orderBy?.[1],
|
|
3342
3518
|
searchString: params?.searchString
|
|
@@ -3346,7 +3522,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3346
3522
|
if (driver.count) {
|
|
3347
3523
|
total = await driver.count({
|
|
3348
3524
|
path: slug,
|
|
3349
|
-
filter
|
|
3525
|
+
filter,
|
|
3526
|
+
logical: params?.logical,
|
|
3527
|
+
searchString: params?.searchString
|
|
3350
3528
|
});
|
|
3351
3529
|
hasMore = offset + rows.length < total;
|
|
3352
3530
|
}
|
|
@@ -3398,22 +3576,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3398
3576
|
values: {}
|
|
3399
3577
|
} });
|
|
3400
3578
|
},
|
|
3579
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
3580
|
+
return (await driver.updateMany({
|
|
3581
|
+
path: slug,
|
|
3582
|
+
updates: updates.map((u) => ({
|
|
3583
|
+
id: u.id,
|
|
3584
|
+
values: u.data
|
|
3585
|
+
}))
|
|
3586
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
3587
|
+
} : void 0,
|
|
3588
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
3589
|
+
await driver.deleteMany({
|
|
3590
|
+
path: slug,
|
|
3591
|
+
ids
|
|
3592
|
+
});
|
|
3593
|
+
} : void 0,
|
|
3401
3594
|
count: driver.count ? async (params) => {
|
|
3402
3595
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3403
3596
|
return driver.count({
|
|
3404
3597
|
path: slug,
|
|
3405
|
-
filter
|
|
3598
|
+
filter,
|
|
3599
|
+
logical: params?.logical,
|
|
3600
|
+
searchString: params?.searchString
|
|
3406
3601
|
});
|
|
3407
3602
|
} : void 0,
|
|
3408
3603
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
3409
|
-
const limit = params
|
|
3410
|
-
const offset = params?.offset ?? 0;
|
|
3604
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3411
3605
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
3412
3606
|
return driver.listenCollection({
|
|
3413
3607
|
path: slug,
|
|
3414
|
-
limit
|
|
3415
|
-
offset:
|
|
3608
|
+
limit,
|
|
3609
|
+
offset: driverOffset,
|
|
3416
3610
|
filter: params?.where,
|
|
3611
|
+
logical: params?.logical,
|
|
3417
3612
|
orderBy: params?.orderBy?.[0],
|
|
3418
3613
|
order: params?.orderBy?.[1],
|
|
3419
3614
|
searchString: params?.searchString,
|
|
@@ -3421,7 +3616,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3421
3616
|
onUpdate({
|
|
3422
3617
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
3423
3618
|
meta: {
|
|
3424
|
-
total: entities.length,
|
|
3619
|
+
total: offset + entities.length,
|
|
3425
3620
|
limit,
|
|
3426
3621
|
offset,
|
|
3427
3622
|
hasMore: entities.length >= limit
|
|
@@ -3598,9 +3793,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3598
3793
|
async update(id, data) {
|
|
3599
3794
|
return entityToRow(await snap.update(id, data));
|
|
3600
3795
|
},
|
|
3796
|
+
async updateMany(updates) {
|
|
3797
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
3798
|
+
if (updates.length === 0) return [];
|
|
3799
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
3800
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
3801
|
+
id: u.id,
|
|
3802
|
+
data: u.data
|
|
3803
|
+
})))).map(entityToRow);
|
|
3804
|
+
},
|
|
3601
3805
|
delete(id) {
|
|
3602
3806
|
return snap.delete(id);
|
|
3603
3807
|
},
|
|
3808
|
+
async deleteMany(ids) {
|
|
3809
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
3810
|
+
if (ids.length === 0) return;
|
|
3811
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
3812
|
+
await snap.deleteMany(ids);
|
|
3813
|
+
},
|
|
3604
3814
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
3605
3815
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
3606
3816
|
data: res.data.map(entityToRow),
|
|
@@ -3623,7 +3833,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3623
3833
|
/**
|
|
3624
3834
|
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
3625
3835
|
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
3626
|
-
* `{ id, path, values }` view-model the admin
|
|
3836
|
+
* `{ id, path, values }` view-model the admin panel renders.
|
|
3627
3837
|
*/
|
|
3628
3838
|
function toEntityAccessor(sdk, slug, getPks = () => []) {
|
|
3629
3839
|
const accessor = {
|
|
@@ -3906,6 +4116,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
3906
4116
|
return junctionTables;
|
|
3907
4117
|
}
|
|
3908
4118
|
//#endregion
|
|
3909
|
-
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
4119
|
+
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationalCollection, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, relationalCollections, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
3910
4120
|
|
|
3911
4121
|
//# sourceMappingURL=index.es.js.map
|