@byline/core 6.9.0 → 6.10.1

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.
@@ -7,8 +7,10 @@
7
7
  */
8
8
  /**
9
9
  * Comparison operators usable on a single field-level predicate value. The
10
- * parser normalises a bare value (`'foo'`) to `{ $eq: 'foo' }`; an operator
11
- * object is taken as-is and the first declared operator wins.
10
+ * parser normalises a bare value (`'foo'`) to `{ $eq: 'foo' }`. Every operator
11
+ * in an operator object applies and the conditions are ANDed, so
12
+ * `{ $gte: 10, $lte: 20 }` is a closed range. An empty object or an unknown
13
+ * operator is a validation error.
12
14
  */
13
15
  export interface FilterOperators {
14
16
  $eq?: string | number | boolean | null;
@@ -167,6 +167,24 @@ export interface SearchMatching {
167
167
  }
168
168
  /** Maximum UTF-16 length accepted for one full-text query. */
169
169
  export declare const MAX_SEARCH_QUERY_LENGTH = 1024;
170
+ /** Maximum `ceil((end - start) / gap)` for one range-facet request. */
171
+ export declare const MAX_RANGE_FACET_BUCKETS = 200;
172
+ /** One numeric range-facet request over an integer `search.filters` field. */
173
+ export interface SearchRangeFacetRequest {
174
+ field: string;
175
+ /** Inclusive lower edge of the first bucket. */
176
+ start: number;
177
+ /** Exclusive upper edge of the last bucket. */
178
+ end: number;
179
+ /** Bucket width; must be finite and greater than zero. */
180
+ gap: number;
181
+ }
182
+ /** One range bucket: `[start, end)` and its match count. */
183
+ export interface SearchRangeFacetBucket {
184
+ start: number;
185
+ end: number;
186
+ count: number;
187
+ }
170
188
  /**
171
189
  * A search request. Either collection-scoped (`collectionPath`) for
172
190
  * homogeneous results, or zone-scoped (`zone`) for heterogeneous
@@ -201,6 +219,8 @@ export interface SearchQuery {
201
219
  where?: QueryPredicate;
202
220
  /** Field names to compute facet buckets for (driver-capability gated). */
203
221
  facets?: string[];
222
+ /** Integer range aggregates; requires `capabilities.rangeFacets: true`. */
223
+ rangeFacets?: SearchRangeFacetRequest[];
204
224
  /** Restrict to a single content locale. */
205
225
  locale?: string;
206
226
  /**
@@ -254,6 +274,12 @@ export interface SearchResults {
254
274
  * driver supports them.
255
275
  */
256
276
  facets?: Record<string, SearchFacetBucket[]>;
277
+ /**
278
+ * Contiguous ascending buckets keyed by field, including zero counts.
279
+ * Cover exactly the requested `[start, end)`, truncating the final bucket.
280
+ * Counts respect the query, where, locale, status, and collection/zone scope.
281
+ */
282
+ rangeFacets?: Record<string, SearchRangeFacetBucket[]>;
257
283
  }
258
284
  /**
259
285
  * What a given driver can actually do. Lets consumers (the admin UI, the
@@ -266,6 +292,14 @@ export interface SearchResults {
266
292
  export interface SearchCapabilities {
267
293
  /** Faceted aggregation over indexed fields. */
268
294
  facets: boolean;
295
+ /** Integer range aggregation. Omission means false; unsupported requests must fail. */
296
+ rangeFacets?: boolean;
297
+ /**
298
+ * Applies `SearchQuery.where`. Omission means false. Advertise-only: the
299
+ * client still forwards `where` when absent. Providers may support a
300
+ * documented subset, but must reject unsupported fields or operators.
301
+ */
302
+ structuredFilters?: boolean;
269
303
  /** Fuzzy / typo-tolerant matching (e.g. `pg_trgm`). */
270
304
  typoTolerance: boolean;
271
305
  /** Vector / semantic / hybrid retrieval. */
@@ -7,3 +7,5 @@
7
7
  */
8
8
  /** Maximum UTF-16 length accepted for one full-text query. */
9
9
  export const MAX_SEARCH_QUERY_LENGTH = 1_024;
10
+ /** Maximum `ceil((end - start) / gap)` for one range-facet request. */
11
+ export const MAX_RANGE_FACET_BUCKETS = 200;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { expect, it } from 'vitest';
9
+ import { MAX_RANGE_FACET_BUCKETS } from '../index.js';
10
+ it('exports the range-facet contract and bucket cap from the public core entry point', () => {
11
+ const request = { field: 'year', start: 2000, end: 2003, gap: 2 };
12
+ const buckets = [
13
+ { start: 2000, end: 2002, count: 0 },
14
+ { start: 2002, end: 2003, count: 1 },
15
+ ];
16
+ const query = { query: '', rangeFacets: [request] };
17
+ const result = { hits: [], total: 1, rangeFacets: { year: buckets } };
18
+ expect(MAX_RANGE_FACET_BUCKETS).toBe(200);
19
+ expect(query.rangeFacets).toEqual([request]);
20
+ expect(result.rangeFacets?.year).toEqual(buckets);
21
+ });
@@ -134,9 +134,13 @@ async function validateSecurityPredicate(predicate, definition, ctx, allowEmpty
134
134
  }
135
135
  function validateDocumentColumnPredicate(value, collectionPath, column) {
136
136
  validateSecurityOperator(value, collectionPath, column);
137
- const parsed = normaliseToOperator(value);
138
- if (parsed == null || !DOCUMENT_COLUMN_OPERATORS[column].has(parsed.operator)) {
139
- throw invalidSecurityPredicate(collectionPath, `operator '${parsed?.operator ?? 'unknown'}' is not supported for '${column}'`);
137
+ for (const parsed of normaliseToOperators(value, collectionPath, column)) {
138
+ validateDocumentColumnOperator(parsed, collectionPath, column);
139
+ }
140
+ }
141
+ function validateDocumentColumnOperator(parsed, collectionPath, column) {
142
+ if (!DOCUMENT_COLUMN_OPERATORS[column].has(parsed.operator)) {
143
+ throw invalidSecurityPredicate(collectionPath, `operator '${parsed.operator}' is not supported for '${column}'`);
140
144
  }
141
145
  const values = Array.isArray(parsed.value) ? parsed.value : [parsed.value];
142
146
  if ((parsed.operator === '$in' || parsed.operator === '$nin') && !Array.isArray(parsed.value)) {
@@ -163,18 +167,20 @@ function validateSecurityOperator(value, collectionPath, fieldName) {
163
167
  throw invalidSecurityPredicate(collectionPath, `invalid predicate for '${fieldName}'`);
164
168
  }
165
169
  const entries = Object.entries(value);
166
- if (entries.length !== 1 || !FILTER_OPERATORS.has(entries[0]?.[0] ?? '')) {
167
- throw invalidSecurityPredicate(collectionPath, `predicate for '${fieldName}' must contain exactly one supported operator`);
168
- }
169
- const [operator, operand] = entries[0] ?? [];
170
- if ((operator === '$in' || operator === '$nin') && !Array.isArray(operand)) {
171
- throw invalidSecurityPredicate(collectionPath, `${operator} for '${fieldName}' requires an array`);
172
- }
173
- if (operator === '$contains' && typeof operand !== 'string') {
174
- throw invalidSecurityPredicate(collectionPath, `$contains for '${fieldName}' requires a string`);
170
+ if (entries.length === 0 || entries.some(([operator]) => !FILTER_OPERATORS.has(operator))) {
171
+ throw invalidSecurityPredicate(collectionPath, `predicate for '${fieldName}' must contain only supported operators`);
175
172
  }
176
- if (operand === undefined) {
177
- throw invalidSecurityPredicate(collectionPath, `operator for '${fieldName}' requires a value`);
173
+ // Every operator in the object applies (ANDed), so every one is checked.
174
+ for (const [operator, operand] of entries) {
175
+ if ((operator === '$in' || operator === '$nin') && !Array.isArray(operand)) {
176
+ throw invalidSecurityPredicate(collectionPath, `${operator} for '${fieldName}' requires an array`);
177
+ }
178
+ if (operator === '$contains' && typeof operand !== 'string') {
179
+ throw invalidSecurityPredicate(collectionPath, `$contains for '${fieldName}' requires a string`);
180
+ }
181
+ if (operand === undefined) {
182
+ throw invalidSecurityPredicate(collectionPath, `operator for '${fieldName}' requires a value`);
183
+ }
178
184
  }
179
185
  }
180
186
  function invalidSecurityPredicate(collectionPath, reason) {
@@ -241,8 +247,7 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
241
247
  // belong at the top level of the where clause.
242
248
  if (key === '$and' || key === '$or') {
243
249
  if (!Array.isArray(rawValue)) {
244
- ctx?.logger?.debug({ key, collection: definition.path }, 'parse-where: dropping combinator — value is not an array of predicates');
245
- continue;
250
+ throw invalidWhere(definition.path, `${key} must be an array of predicates`);
246
251
  }
247
252
  const childFilters = [];
248
253
  for (const child of rawValue) {
@@ -311,9 +316,10 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
311
316
  // text search has no sensible OR-composing form, and a per-target
312
317
  // EXISTS over `store_text` is intentionally deferred.
313
318
  if (key === 'status') {
314
- if (isNested || inCombinator) {
315
- const parsed = normaliseToOperator(rawValue);
316
- if (parsed) {
319
+ // A bare string at the top level keeps the scalar `status` form. An
320
+ // operator object takes the `DocumentColumnFilter` form at any scope.
321
+ if (isNested || inCombinator || typeof rawValue !== 'string') {
322
+ for (const parsed of normaliseToOperators(rawValue, definition.path, key)) {
317
323
  result.filters.push({
318
324
  kind: 'docColumn',
319
325
  column: 'status',
@@ -323,9 +329,7 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
323
329
  }
324
330
  continue;
325
331
  }
326
- if (typeof rawValue === 'string') {
327
- result.status = rawValue;
328
- }
332
+ result.status = rawValue;
329
333
  continue;
330
334
  }
331
335
  if (key === 'query') {
@@ -343,9 +347,12 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
343
347
  continue;
344
348
  }
345
349
  if (key === 'path') {
346
- if (isNested || inCombinator) {
347
- const parsed = normaliseToOperator(rawValue);
348
- if (parsed) {
350
+ const operators = normaliseToOperators(rawValue, definition.path, key);
351
+ // `pathFilter` holds one operator. Several operators on a top-level
352
+ // `path` take the `DocumentColumnFilter` form instead, which the
353
+ // adapter resolves against the top-level scope just as well.
354
+ if (isNested || inCombinator || operators.length > 1) {
355
+ for (const parsed of operators) {
349
356
  result.filters.push({
350
357
  kind: 'docColumn',
351
358
  column: 'path',
@@ -355,7 +362,7 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
355
362
  }
356
363
  continue;
357
364
  }
358
- const parsed = normaliseToOperator(rawValue);
365
+ const [parsed] = operators;
359
366
  if (parsed) {
360
367
  result.pathFilter = {
361
368
  operator: parsed.operator,
@@ -373,8 +380,7 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
373
380
  // don't `String(...)` it the way status/path do, because the headline
374
381
  // use case is `$in` / `$nin` carrying an array of document ids.
375
382
  if (key === 'id') {
376
- const parsed = normaliseToOperator(rawValue);
377
- if (parsed) {
383
+ for (const parsed of normaliseToOperators(rawValue, definition.path, key)) {
378
384
  result.filters.push({
379
385
  kind: 'docColumn',
380
386
  column: 'id',
@@ -453,17 +459,16 @@ async function parseWhereInternal(where, definition, ctx, { isNested, inCombinat
453
459
  const storeInfo = fieldTypeToStore[field.type];
454
460
  if (!storeInfo)
455
461
  continue; // Structure fields can't be filtered directly
456
- const parsed = normaliseToOperator(rawValue);
457
- if (!parsed)
458
- continue;
459
- result.filters.push({
460
- kind: 'field',
461
- fieldName: key,
462
- storeType: storeInfo.storeType,
463
- valueColumn: storeInfo.valueColumn,
464
- operator: parsed.operator,
465
- value: parsed.value,
466
- });
462
+ for (const parsed of normaliseToOperators(rawValue, definition.path, key)) {
463
+ result.filters.push({
464
+ kind: 'field',
465
+ fieldName: key,
466
+ storeType: storeInfo.storeType,
467
+ valueColumn: storeInfo.valueColumn,
468
+ operator: parsed.operator,
469
+ value: parsed.value,
470
+ });
471
+ }
467
472
  }
468
473
  return result;
469
474
  }
@@ -555,7 +560,7 @@ function isPlainSubWhere(raw) {
555
560
  return false;
556
561
  const keys = Object.keys(raw);
557
562
  // An empty object is not a meaningful sub-where; treat as non-match
558
- // (ordinary field-filter path will then reject it via normaliseToOperator).
563
+ // (ordinary field-filter path will then reject it via normaliseToOperators).
559
564
  if (keys.length === 0)
560
565
  return false;
561
566
  for (const k of keys) {
@@ -565,28 +570,46 @@ function isPlainSubWhere(raw) {
565
570
  return true;
566
571
  }
567
572
  /**
568
- * Normalise a WhereValue (bare value or operator object) into a single
569
- * operator + value pair.
573
+ * Normalise a WhereValue (bare value or operator object) into a list of
574
+ * operator + value pairs. Every operator in an operator object applies, so
575
+ * `{ $gte: 10, $lte: 20 }` yields two pairs that the caller ANDs together.
576
+ * Operators whose value is `undefined` are skipped, matching how the parser
577
+ * skips `undefined` keys. An object with no operators left, or with an
578
+ * unknown operator, is a validation error rather than a dropped filter.
570
579
  */
571
- function normaliseToOperator(raw) {
580
+ function normaliseToOperators(raw, collectionPath, key) {
572
581
  // Bare value → $eq
573
582
  if (raw === null ||
574
583
  typeof raw === 'string' ||
575
584
  typeof raw === 'number' ||
576
585
  typeof raw === 'boolean') {
577
- return { operator: '$eq', value: raw };
586
+ return [{ operator: '$eq', value: raw }];
578
587
  }
579
- // Operator object
580
- const ops = raw;
581
- const operatorEntries = Object.entries(ops);
582
- if (operatorEntries.length === 0)
583
- return undefined;
584
- // Use the first operator found
585
- const [op, val] = operatorEntries[0];
586
- return {
588
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
589
+ throw invalidWhere(collectionPath, `invalid value for '${key}'`);
590
+ }
591
+ // Operator object. Names are checked before `undefined` operands are
592
+ // skipped, so an unknown operator throws even when its value is missing.
593
+ const entries = Object.entries(raw);
594
+ for (const [op] of entries) {
595
+ if (!FILTER_OPERATORS.has(op)) {
596
+ throw invalidWhere(collectionPath, `unknown operator '${op}' for '${key}'`);
597
+ }
598
+ }
599
+ const operatorEntries = entries.filter(([, val]) => val !== undefined);
600
+ if (operatorEntries.length === 0) {
601
+ throw invalidWhere(collectionPath, `operator object for '${key}' is empty`);
602
+ }
603
+ return operatorEntries.map(([op, val]) => ({
587
604
  operator: op,
588
605
  value: val,
589
- };
606
+ }));
607
+ }
608
+ function invalidWhere(collectionPath, reason) {
609
+ return ERR_VALIDATION({
610
+ message: `invalid where clause for '${collectionPath}': ${reason}`,
611
+ details: { collectionPath, reason },
612
+ });
590
613
  }
591
614
  function stringifyDocumentColumnValue(value) {
592
615
  if (value === null)
@@ -522,12 +522,10 @@ describe('parseWhere — combinators', () => {
522
522
  // landmine if compiled to SQL.
523
523
  expect(result.filters).toEqual([]);
524
524
  });
525
- it('drops a non-array combinator value (defensive)', async () => {
526
- const result = await parseWhere({
527
- // biome-ignore lint/suspicious/noExplicitAny: deliberately malformed input
528
- $or: { title: 'Hello' },
529
- }, testCollection);
530
- expect(result.filters).toEqual([]);
525
+ it.each(['$and', '$or'])('rejects a non-array %s value', async (key) => {
526
+ await expect(parseWhere(
527
+ // biome-ignore lint/suspicious/noExplicitAny: deliberately malformed input
528
+ { [key]: { title: 'Hello' } }, testCollection)).rejects.toThrow(`${key} must be an array of predicates`);
531
529
  });
532
530
  it('nests $or inside $and correctly', async () => {
533
531
  const result = await parseWhere({
@@ -867,3 +865,132 @@ describe('parseWhere — relation quantifiers', () => {
867
865
  expect(result.filters).toHaveLength(0);
868
866
  });
869
867
  });
868
+ // ---------------------------------------------------------------------------
869
+ // Multi-operator objects
870
+ // ---------------------------------------------------------------------------
871
+ describe('parseWhere — multi-operator objects', () => {
872
+ const views = (operator, value) => ({
873
+ kind: 'field',
874
+ fieldName: 'views',
875
+ storeType: 'numeric',
876
+ valueColumn: 'value_integer',
877
+ operator,
878
+ value,
879
+ });
880
+ it('emits one filter per operator in a closed range', async () => {
881
+ const result = await parseWhere({ views: { $gte: 10, $lte: 20 } }, testCollection);
882
+ expect(result.filters).toEqual([views('$gte', 10), views('$lte', 20)]);
883
+ });
884
+ it('keeps every operator of the downstream date-range shape', async () => {
885
+ const result = await parseWhere({ views: { $gt: 0, $gte: 20200000, $lte: 20201231 } }, testCollection);
886
+ expect(result.filters).toEqual([
887
+ views('$gt', 0),
888
+ views('$gte', 20200000),
889
+ views('$lte', 20201231),
890
+ ]);
891
+ });
892
+ it('combines $eq with a range operator', async () => {
893
+ const result = await parseWhere({ views: { $eq: 42, $lt: 100 } }, testCollection);
894
+ expect(result.filters).toEqual([views('$eq', 42), views('$lt', 100)]);
895
+ });
896
+ it('skips operators whose value is undefined', async () => {
897
+ const result = await parseWhere({ views: { $gte: undefined, $lte: 20 } }, testCollection);
898
+ expect(result.filters).toEqual([views('$lte', 20)]);
899
+ });
900
+ it('keeps the $and workaround shape working', async () => {
901
+ const result = await parseWhere({ $and: [{ views: { $gt: 0 } }, { views: { $gte: 10 } }, { views: { $lte: 20 } }] }, testCollection);
902
+ expect(result.filters).toEqual([views('$gt', 0), views('$gte', 10), views('$lte', 20)]);
903
+ });
904
+ it('emits every operator on a document column', async () => {
905
+ const result = await parseWhere({ $or: [{ status: { $ne: 'archived', $in: ['draft', 'published'] } }] }, testCollection);
906
+ expect(result.filters).toEqual([
907
+ {
908
+ kind: 'or',
909
+ children: [
910
+ {
911
+ kind: 'and',
912
+ children: [
913
+ { kind: 'docColumn', column: 'status', operator: '$ne', value: 'archived' },
914
+ {
915
+ kind: 'docColumn',
916
+ column: 'status',
917
+ operator: '$in',
918
+ value: ['draft', 'published'],
919
+ },
920
+ ],
921
+ },
922
+ ],
923
+ },
924
+ ]);
925
+ });
926
+ it('compiles a top-level status operator object instead of dropping it', async () => {
927
+ const scalar = await parseWhere({ status: 'draft' }, testCollection);
928
+ expect(scalar.status).toBe('draft');
929
+ expect(scalar.filters).toEqual([]);
930
+ const result = await parseWhere({ status: { $in: ['draft', 'published'] } }, testCollection);
931
+ expect(result.status).toBeUndefined();
932
+ expect(result.filters).toEqual([
933
+ { kind: 'docColumn', column: 'status', operator: '$in', value: ['draft', 'published'] },
934
+ ]);
935
+ });
936
+ it('moves a multi-operator top-level path to document-column filters', async () => {
937
+ const single = await parseWhere({ path: { $contains: 'docs' } }, testCollection);
938
+ expect(single.pathFilter).toEqual({ operator: '$contains', value: 'docs' });
939
+ const multi = await parseWhere({ path: { $contains: 'docs', $ne: 'docs/old' } }, testCollection);
940
+ expect(multi.pathFilter).toBeUndefined();
941
+ expect(multi.filters).toEqual([
942
+ { kind: 'docColumn', column: 'path', operator: '$contains', value: 'docs' },
943
+ { kind: 'docColumn', column: 'path', operator: '$ne', value: 'docs/old' },
944
+ ]);
945
+ });
946
+ it('emits every operator inside a relation sub-where', async () => {
947
+ const result = await parseWhere({ category: { slug: { $contains: 'new', $ne: 'news-old' } } }, testCollection, ctx);
948
+ expect(result.filters[0]).toMatchObject({
949
+ kind: 'relation',
950
+ fieldName: 'category',
951
+ nested: [
952
+ expect.objectContaining({ fieldName: 'slug', operator: '$contains', value: 'new' }),
953
+ expect.objectContaining({ fieldName: 'slug', operator: '$ne', value: 'news-old' }),
954
+ ],
955
+ });
956
+ });
957
+ it('emits every operator inside a quantified relation filter', async () => {
958
+ const result = await parseWhere({ tags: { $every: { slug: { $contains: 'a', $ne: 'b' } } } }, testCollection, ctx);
959
+ expect(result.filters[0]).toMatchObject({
960
+ kind: 'relation',
961
+ quantifier: 'every',
962
+ nested: [
963
+ expect.objectContaining({ operator: '$contains', value: 'a' }),
964
+ expect.objectContaining({ operator: '$ne', value: 'b' }),
965
+ ],
966
+ });
967
+ });
968
+ it.each([
969
+ [{ views: {} }, "operator object for 'views' is empty"],
970
+ [{ views: { $gte: undefined } }, "operator object for 'views' is empty"],
971
+ [{ views: { $gte: 10, $bogus: 1 } }, "unknown operator '$bogus' for 'views'"],
972
+ [{ views: { $gte: 10, $bogus: undefined } }, "unknown operator '$bogus' for 'views'"],
973
+ [{ status: { $wat: 'x' } }, "unknown operator '$wat' for 'status'"],
974
+ [{ id: {} }, "operator object for 'id' is empty"],
975
+ [{ views: [1, 2] }, "invalid value for 'views'"],
976
+ [{ category: {} }, "operator object for 'category' is empty"],
977
+ ])('rejects %o', async (where, message) => {
978
+ await expect(parseWhere(where, testCollection, ctx)).rejects.toThrow(message);
979
+ });
980
+ it('accepts multi-operator objects in strict security predicates', async () => {
981
+ const filters = await parsePredicateFilters({ views: { $gte: 10, $lte: 20 }, status: { $ne: 'archived', $in: ['published'] } }, testCollection, ctx, { strict: true });
982
+ expect(filters).toEqual([
983
+ views('$gte', 10),
984
+ views('$lte', 20),
985
+ { kind: 'docColumn', column: 'status', operator: '$ne', value: 'archived' },
986
+ { kind: 'docColumn', column: 'status', operator: '$in', value: ['published'] },
987
+ ]);
988
+ });
989
+ it.each([
990
+ [{ views: { $gte: 10, $wat: 1 } }, 'supported operators'],
991
+ [{ status: { $eq: 'published', $contains: 'pub' } }, "'$contains' is not supported"],
992
+ [{ status: { $ne: 'archived', $in: 'published' } }, 'requires an array'],
993
+ ])('validates every operator of strict predicate %o', async (predicate, message) => {
994
+ await expect(parsePredicateFilters(predicate, testCollection, ctx, { strict: true })).rejects.toThrow(message);
995
+ });
996
+ });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/core",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "6.9.0",
5
+ "version": "6.10.1",
6
6
  "engines": {
7
7
  "node": ">=22.22.2"
8
8
  },
@@ -82,7 +82,7 @@
82
82
  "dist"
83
83
  ],
84
84
  "dependencies": {
85
- "@byline/auth": "6.9.0",
85
+ "@byline/auth": "6.10.1",
86
86
  "dotenv": "^18.0.8",
87
87
  "pino": "^10.4.0",
88
88
  "sharp": "^0.35.5",
@@ -90,15 +90,15 @@
90
90
  "zod": "^4.6.7"
91
91
  },
92
92
  "devDependencies": {
93
- "@biomejs/biome": "2.5.15",
93
+ "@biomejs/biome": "2.6.2",
94
94
  "@types/node": "^26.6.5",
95
- "chokidar": "^5.0.0",
95
+ "chokidar": "^6.0.0",
96
96
  "chokidar-cli": "^3.0.0",
97
97
  "npm-run-all": "^4.1.5",
98
- "tsc-alias": "^1.9.7",
98
+ "tsc-alias": "^1.9.8",
99
99
  "tsx": "^4.23.15",
100
100
  "typescript": "^7.0.2",
101
- "vitest": "^5.0.4"
101
+ "vitest": "^5.0.5"
102
102
  },
103
103
  "publishConfig": {
104
104
  "access": "public",