@kubun/store-graph 0.12.0 → 0.13.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/lib/api.js +4 -4
- package/lib/query-builder.d.ts +3 -2
- package/lib/query-builder.js +48 -8
- package/package.json +10 -7
package/lib/api.js
CHANGED
|
@@ -451,7 +451,7 @@ export function createGraphStore(db, adapter) {
|
|
|
451
451
|
query = query.where('owner', 'in', effectiveOwners);
|
|
452
452
|
}
|
|
453
453
|
if (filter != null) {
|
|
454
|
-
query = query.where((eb)=>applyDocumentFilter(eb, filter, [], coerce));
|
|
454
|
+
query = query.where((eb)=>applyDocumentFilter(eb, filter, adapter, [], coerce));
|
|
455
455
|
}
|
|
456
456
|
for (const model of rest){
|
|
457
457
|
query = query.unionAll((eb)=>{
|
|
@@ -463,7 +463,7 @@ export function createGraphStore(db, adapter) {
|
|
|
463
463
|
q = q.where('owner', 'in', effectiveOwners);
|
|
464
464
|
}
|
|
465
465
|
if (filter != null) {
|
|
466
|
-
q = q.where((eb)=>applyDocumentFilter(eb, filter, [], coerce));
|
|
466
|
+
q = q.where((eb)=>applyDocumentFilter(eb, filter, adapter, [], coerce));
|
|
467
467
|
}
|
|
468
468
|
return q;
|
|
469
469
|
});
|
|
@@ -554,7 +554,7 @@ export function createGraphStore(db, adapter) {
|
|
|
554
554
|
query = query.where('owner', 'in', effectiveOwners);
|
|
555
555
|
}
|
|
556
556
|
if (filter != null) {
|
|
557
|
-
query = query.where((eb)=>applyDocumentFilter(eb, filter, [], coerce));
|
|
557
|
+
query = query.where((eb)=>applyDocumentFilter(eb, filter, adapter, [], coerce));
|
|
558
558
|
}
|
|
559
559
|
if (readAccess != null) {
|
|
560
560
|
query = query.where(adapter.readAccessPredicate(readAccess, modelID));
|
|
@@ -608,7 +608,7 @@ export function createGraphStore(db, adapter) {
|
|
|
608
608
|
query = query.where('owner', 'in', effectiveOwners);
|
|
609
609
|
}
|
|
610
610
|
if (filter != null) {
|
|
611
|
-
query = query.where((eb)=>applyDocumentFilter(eb, filter, [], coerce));
|
|
611
|
+
query = query.where((eb)=>applyDocumentFilter(eb, filter, adapter, [], coerce));
|
|
612
612
|
}
|
|
613
613
|
if (readAccess != null) {
|
|
614
614
|
query = query.where(adapter.readAccessPredicate(readAccess, modelID));
|
package/lib/query-builder.d.ts
CHANGED
|
@@ -17,8 +17,9 @@ export declare function applyGroupBySelections(eb: DocumentExpressionBuilder, gr
|
|
|
17
17
|
expressions: Array<DocumentExpressionWrapper>;
|
|
18
18
|
};
|
|
19
19
|
export declare function applyPagination(query: DocumentQueryBuilder, args: ConnectionArguments, orderBy?: DocumentOrderBy, coerce?: CoerceFilterValue): [DocumentQueryBuilder, number];
|
|
20
|
-
export declare function applyDocumentFilter(eb: DocumentExpressionBuilder, filter: DocumentFilter, path?: Array<string>, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
|
|
21
|
-
export declare function
|
|
20
|
+
export declare function applyDocumentFilter(eb: DocumentExpressionBuilder, filter: DocumentFilter, adapter: Adapter, path?: Array<string>, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
|
|
21
|
+
export declare function escapeLikePattern(value: string): string;
|
|
22
|
+
export declare function applyValueFilter(eb: DocumentExpressionBuilder, keys: Array<string>, filter: AnyValueFilter, adapter: Adapter, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
|
|
22
23
|
export declare function applyDocumentOrderBy(queryBuilder: DocumentQueryBuilder, adapter: Adapter, orderBy?: DocumentOrderBy, isReverse?: boolean): [DocumentQueryBuilder, Array<Array<string>> | null];
|
|
23
24
|
export declare const KNOWN_FIELDS: {
|
|
24
25
|
readonly _createdAt: "created_at";
|
package/lib/query-builder.js
CHANGED
|
@@ -12,6 +12,16 @@ function getKeyPath(eb, keys) {
|
|
|
12
12
|
}
|
|
13
13
|
return keyPath;
|
|
14
14
|
}
|
|
15
|
+
// Same traversal as getKeyPath, but keeps the raw JSON node (`->`) instead of extracting
|
|
16
|
+
// text (`->>`), so array-membership predicates can hand adapters a JSON array to iterate
|
|
17
|
+
// (json_each / jsonb_array_elements_text / `@>`) rather than a text scalar.
|
|
18
|
+
function getRawKeyPath(eb, keys) {
|
|
19
|
+
let keyPath = eb.ref('data', '->');
|
|
20
|
+
for (const key of keys){
|
|
21
|
+
keyPath = keyPath.key(key);
|
|
22
|
+
}
|
|
23
|
+
return keyPath;
|
|
24
|
+
}
|
|
15
25
|
export function aggregateFieldName(keys) {
|
|
16
26
|
return keys.join('.');
|
|
17
27
|
}
|
|
@@ -220,7 +230,7 @@ function applyBackwardPagination(queryBuilder, limit, before, orderBy, coerce) {
|
|
|
220
230
|
}
|
|
221
231
|
return query.limit(limit);
|
|
222
232
|
}
|
|
223
|
-
export function applyDocumentFilter(eb, filter, path = [], coerce) {
|
|
233
|
+
export function applyDocumentFilter(eb, filter, adapter, path = [], coerce) {
|
|
224
234
|
const entries = Object.entries(filter);
|
|
225
235
|
if (entries.length !== 1) {
|
|
226
236
|
throw new Error('Invalid document filter');
|
|
@@ -228,33 +238,40 @@ export function applyDocumentFilter(eb, filter, path = [], coerce) {
|
|
|
228
238
|
const [type, value] = entries[0];
|
|
229
239
|
switch(type){
|
|
230
240
|
case 'where':
|
|
231
|
-
return applyObjectFilter(eb, value, path, coerce);
|
|
241
|
+
return applyObjectFilter(eb, value, adapter, path, coerce);
|
|
232
242
|
case 'and':
|
|
233
243
|
return eb.and(value.map((filter)=>{
|
|
234
|
-
return applyDocumentFilter(eb, filter, path, coerce);
|
|
244
|
+
return applyDocumentFilter(eb, filter, adapter, path, coerce);
|
|
235
245
|
}));
|
|
236
246
|
case 'or':
|
|
237
247
|
return eb.or(value.map((filter)=>{
|
|
238
|
-
return applyDocumentFilter(eb, filter, path, coerce);
|
|
248
|
+
return applyDocumentFilter(eb, filter, adapter, path, coerce);
|
|
239
249
|
}));
|
|
240
250
|
case 'not':
|
|
241
|
-
return eb.not(applyDocumentFilter(eb, value, path, coerce));
|
|
251
|
+
return eb.not(applyDocumentFilter(eb, value, adapter, path, coerce));
|
|
242
252
|
default:
|
|
243
253
|
throw new Error(`Invalid document filter type: ${type}`);
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
|
-
function applyObjectFilter(eb, filter, path, coerce) {
|
|
256
|
+
function applyObjectFilter(eb, filter, adapter, path, coerce) {
|
|
247
257
|
const criteria = Object.entries(filter).map(([fieldName, valueFilter])=>{
|
|
248
258
|
// TODO: check if value filter or nested object filter should be applied
|
|
249
259
|
// for nested object, check if embedded or related object
|
|
250
260
|
return applyValueFilter(eb, [
|
|
251
261
|
...path,
|
|
252
262
|
fieldName
|
|
253
|
-
], valueFilter, coerce);
|
|
263
|
+
], valueFilter, adapter, coerce);
|
|
254
264
|
});
|
|
255
265
|
return eb.and(criteria);
|
|
256
266
|
}
|
|
257
|
-
|
|
267
|
+
// Escapes a LIKE pattern's special characters (backslash first, then the wildcards) and
|
|
268
|
+
// wraps it for substring match. Order matters: escaping `%`/`_` before the backslash
|
|
269
|
+
// would double-escape them.
|
|
270
|
+
export function escapeLikePattern(value) {
|
|
271
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
272
|
+
return `%${escaped}%`;
|
|
273
|
+
}
|
|
274
|
+
export function applyValueFilter(eb, keys, filter, adapter, coerce) {
|
|
258
275
|
const entries = Object.entries(filter);
|
|
259
276
|
if (entries.length !== 1) {
|
|
260
277
|
throw new Error('Invalid value filter');
|
|
@@ -281,6 +298,29 @@ export function applyValueFilter(eb, keys, filter, coerce) {
|
|
|
281
298
|
return eb(fieldName, '>', c(value));
|
|
282
299
|
case 'greaterThanOrEqualTo':
|
|
283
300
|
return eb(fieldName, '>=', c(value));
|
|
301
|
+
case 'contains':
|
|
302
|
+
return adapter.containsPredicate(fieldName, escapeLikePattern(String(value)));
|
|
303
|
+
case 'includesAll':
|
|
304
|
+
{
|
|
305
|
+
// Dedupe the candidate set so SQLite's COUNT(DISTINCT value) = set-size test matches
|
|
306
|
+
// Postgres @> containment: duplicate inputs must not inflate the required count.
|
|
307
|
+
const items = [
|
|
308
|
+
...new Set(value)
|
|
309
|
+
];
|
|
310
|
+
// Vacuously true (D7): every array includes the empty set. Handled here, not in
|
|
311
|
+
// SQL, so the adapter seam never sees an empty IN-list.
|
|
312
|
+
return items.length === 0 ? sql`1 = 1` : adapter.arrayIncludesAllPredicate(getRawKeyPath(eb, keys), items);
|
|
313
|
+
}
|
|
314
|
+
case 'includesAny':
|
|
315
|
+
{
|
|
316
|
+
const items = [
|
|
317
|
+
...new Set(value)
|
|
318
|
+
];
|
|
319
|
+
// An empty candidate set matches nothing.
|
|
320
|
+
return items.length === 0 ? sql`1 = 0` : adapter.arrayIncludesAnyPredicate(getRawKeyPath(eb, keys), items);
|
|
321
|
+
}
|
|
322
|
+
case 'presence':
|
|
323
|
+
return adapter.arrayPresencePredicate(getRawKeyPath(eb, keys), value);
|
|
284
324
|
default:
|
|
285
325
|
throw new Error(`Invalid value filter type: ${type}`);
|
|
286
326
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,16 +16,19 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@sozai/codec": "^0.4.0",
|
|
19
|
-
"kysely": "^0.29.
|
|
20
|
-
"@kubun/db": "^0.12.
|
|
21
|
-
"@kubun/
|
|
22
|
-
"@kubun/
|
|
19
|
+
"kysely": "^0.29.5",
|
|
20
|
+
"@kubun/db": "^0.12.1",
|
|
21
|
+
"@kubun/protocol": "^0.13.0",
|
|
22
|
+
"@kubun/db-adapter": "^0.13.0",
|
|
23
23
|
"@kubun/id": "^0.12.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@testcontainers/postgresql": "^12.1.0",
|
|
27
|
-
"@kubun/db-better-sqlite": "^0.12.
|
|
28
|
-
"@kubun/db-postgres": "^0.12.
|
|
27
|
+
"@kubun/db-better-sqlite": "^0.12.1",
|
|
28
|
+
"@kubun/db-postgres": "^0.12.1"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
29
32
|
},
|
|
30
33
|
"scripts": {
|
|
31
34
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|