@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 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));
@@ -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 applyValueFilter(eb: DocumentExpressionBuilder, keys: Array<string>, filter: AnyValueFilter, coerce?: CoerceFilterValue): DocumentExpressionWrapper;
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";
@@ -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
- export function applyValueFilter(eb, keys, filter, coerce) {
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.12.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.4",
20
- "@kubun/db": "^0.12.0",
21
- "@kubun/db-adapter": "^0.12.0",
22
- "@kubun/protocol": "^0.12.0",
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.0",
28
- "@kubun/db-postgres": "^0.12.0"
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",