@byline/client 6.9.0 → 6.10.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.
@@ -9,7 +9,7 @@ import { applyAfterRead, assertActorCanPerform, assertLocaleVisibility, buildSea
9
9
  import { getReadSecurityDomain, resolveReadRequestContext, resolveReadSecurityFilters, } from './read-context.js';
10
10
  import { expectedDocumentId, readSingletonForEdit, } from './read-internals.js';
11
11
  import { shapeDocument, shapePopulatedInPlace } from './response.js';
12
- import { assertSearchFieldScope, assertSearchQueryLength, finalizeSearchHits } from './search.js';
12
+ import { assertSearchFieldScope, assertSearchQueryLength, assertSearchRangeFacets, finalizeSearchHits, } from './search.js';
13
13
  function toScheduleInfo(schedule) {
14
14
  const { executionToken: _executionToken, executionExpiresAt: _executionExpiresAt, ...info } = schedule;
15
15
  return info;
@@ -210,6 +210,7 @@ export class CollectionHandle {
210
210
  });
211
211
  }
212
212
  assertSearchFieldScope(provider, options.fieldScope);
213
+ assertSearchRangeFacets(provider, options.rangeFacets, [this.definition]);
213
214
  const results = await provider.search({
214
215
  query: options.query,
215
216
  fieldScope: options.fieldScope,
@@ -219,6 +220,7 @@ export class CollectionHandle {
219
220
  status: readMode,
220
221
  where: options.where,
221
222
  facets: options.facets,
223
+ rangeFacets: options.rangeFacets?.length ? options.rangeFacets : undefined,
222
224
  limit: options.limit,
223
225
  offset: options.offset,
224
226
  });
@@ -242,7 +244,12 @@ export class CollectionHandle {
242
244
  });
243
245
  return aggregateRestricted
244
246
  ? { hits, total: hits.length }
245
- : { hits, total: results.total, facets: results.facets };
247
+ : {
248
+ hits,
249
+ total: results.total,
250
+ facets: results.facets,
251
+ ...(results.rangeFacets === undefined ? {} : { rangeFacets: results.rangeFacets }),
252
+ };
246
253
  }
247
254
  // -------------------------------------------------------------------------
248
255
  // Search index maintenance
package/dist/search.d.ts CHANGED
@@ -28,11 +28,13 @@
28
28
  * lag, logged at warn level, not a reason to degrade the response.
29
29
  */
30
30
  import type { RequestContext } from '@byline/auth';
31
- import type { LocaleVisibility, ReadContext, ReadMode, SearchHit, SearchProvider } from '@byline/core';
31
+ import type { CollectionDefinition, LocaleVisibility, ReadContext, ReadMode, SearchHit, SearchProvider, SearchRangeFacetRequest } from '@byline/core';
32
32
  import type { BylineClient } from './client.js';
33
33
  import type { ClientSearchResults, HydratedSearchHit, ZoneSearchOptions } from './types.js';
34
34
  /** Validate the optional logical body-field scope before calling a provider. */
35
35
  export declare function assertSearchFieldScope(provider: SearchProvider, fieldScope: string[] | undefined): void;
36
+ /** Validate range aggregates against every collection in the provider's scope. */
37
+ export declare function assertSearchRangeFacets(provider: SearchProvider, rangeFacets: SearchRangeFacetRequest[] | undefined, collections: readonly CollectionDefinition[]): void;
36
38
  export interface FinalizeSearchHitsParams {
37
39
  client: BylineClient<any>;
38
40
  requestContext: RequestContext;
package/dist/search.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import { assertActorCanPerform, createReadContext, ERR_VALIDATION, getCollectionAdminConfig, MAX_SEARCH_QUERY_LENGTH, resolveLocaleVisibility, resolveSearchZones, } from '@byline/core';
8
+ import { assertActorCanPerform, createReadContext, ERR_VALIDATION, getCollectionAdminConfig, MAX_RANGE_FACET_BUCKETS, MAX_SEARCH_QUERY_LENGTH, resolveLocaleVisibility, resolveSearchZones, } from '@byline/core';
9
9
  import { resolveReadRequestContext, resolveReadSecurityFilters } from './read-context.js';
10
10
  /** Validate the optional logical body-field scope before calling a provider. */
11
11
  export function assertSearchFieldScope(provider, fieldScope) {
@@ -21,6 +21,55 @@ export function assertSearchFieldScope(provider, fieldScope) {
21
21
  });
22
22
  }
23
23
  }
24
+ /** Validate range aggregates against every collection in the provider's scope. */
25
+ export function assertSearchRangeFacets(provider, rangeFacets, collections) {
26
+ if (rangeFacets == null)
27
+ return;
28
+ if (!Array.isArray(rangeFacets)) {
29
+ throw ERR_VALIDATION({ message: 'Search rangeFacets must be an array.' });
30
+ }
31
+ if (rangeFacets.length === 0)
32
+ return;
33
+ if (provider.capabilities.rangeFacets !== true) {
34
+ throw ERR_VALIDATION({
35
+ message: 'The registered search provider does not support range facets.',
36
+ });
37
+ }
38
+ const fields = new Set();
39
+ for (const request of rangeFacets) {
40
+ if (request == null || typeof request.field !== 'string' || request.field.trim() === '') {
41
+ throw ERR_VALIDATION({ message: 'Search rangeFacets must name a field for each request.' });
42
+ }
43
+ const { field, start, end, gap } = request;
44
+ if (![start, end, gap].every(Number.isFinite)) {
45
+ throw ERR_VALIDATION({
46
+ message: `Search range facet '${field}' requires finite start, end, and gap values.`,
47
+ });
48
+ }
49
+ if (gap <= 0 || end <= start) {
50
+ throw ERR_VALIDATION({
51
+ message: `Search range facet '${field}' requires gap > 0 and end > start.`,
52
+ });
53
+ }
54
+ if (Math.ceil((end - start) / gap) > MAX_RANGE_FACET_BUCKETS) {
55
+ throw ERR_VALIDATION({
56
+ message: `Search range facet '${field}' exceeds the maximum of ${MAX_RANGE_FACET_BUCKETS} buckets.`,
57
+ });
58
+ }
59
+ if (fields.has(field)) {
60
+ throw ERR_VALIDATION({ message: `Search rangeFacets contains duplicate field '${field}'.` });
61
+ }
62
+ fields.add(field);
63
+ for (const collection of collections) {
64
+ if (!collection.search?.filters?.includes(field) ||
65
+ collection.fields.find((definition) => definition.name === field)?.type !== 'integer') {
66
+ throw ERR_VALIDATION({
67
+ message: `Search range facet '${field}' must name an integer field declared in search.filters for collection '${collection.path}'.`,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
24
73
  /**
25
74
  * Authorise (and optionally hydrate) provider hits, per collection:
26
75
  *
@@ -235,6 +284,7 @@ export async function zoneSearch(client, options) {
235
284
  throw firstAbilityError;
236
285
  }
237
286
  assertSearchFieldScope(provider, options.fieldScope);
287
+ assertSearchRangeFacets(provider, options.rangeFacets, members);
238
288
  let aggregateRestricted = readable.size !== members.length;
239
289
  if (!options._bypassBeforeRead) {
240
290
  for (const member of members) {
@@ -255,6 +305,7 @@ export async function zoneSearch(client, options) {
255
305
  status: readMode,
256
306
  where: options.where,
257
307
  facets: options.facets,
308
+ rangeFacets: options.rangeFacets?.length ? options.rangeFacets : undefined,
258
309
  limit: options.limit,
259
310
  offset: options.offset,
260
311
  });
@@ -273,7 +324,12 @@ export async function zoneSearch(client, options) {
273
324
  });
274
325
  return aggregateRestricted
275
326
  ? { hits, total: hits.length }
276
- : { hits, total: results.total, facets: results.facets };
327
+ : {
328
+ hits,
329
+ total: results.total,
330
+ facets: results.facets,
331
+ ...(results.rangeFacets === undefined ? {} : { rangeFacets: results.rangeFacets }),
332
+ };
277
333
  }
278
334
  export function assertSearchQueryLength(query) {
279
335
  if (query.length <= MAX_SEARCH_QUERY_LENGTH)
package/dist/types.d.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { RequestContext } from '@byline/auth';
9
- import type { BylineLogger, CollectionDefinition, DocumentPublishSchedule, IDbAdapter, IStorageProvider, LocaleVisibility, MissingLocalePolicy, PopulateSpec, PredicateValue, QueryPredicate, ReadContext, ReadMode, RichTextPopulateFn, RichTextToTextFn, SearchFacetBucket, SearchHit, SearchMatching, SearchProvider, ServerConfig, SingletonSavePrecondition, SlugifierFn, SortSpec, UnavailableBlock } from '@byline/core';
10
- export type { FilterOperators, PredicateValue, QueryPredicate, SearchFacetBucket, SearchHit, SearchProvider, SearchResults, SortDirection, SortSpec, } from '@byline/core';
9
+ import type { BylineLogger, CollectionDefinition, DocumentPublishSchedule, IDbAdapter, IStorageProvider, LocaleVisibility, MissingLocalePolicy, PopulateSpec, PredicateValue, QueryPredicate, ReadContext, ReadMode, RichTextPopulateFn, RichTextToTextFn, SearchFacetBucket, SearchHit, SearchMatching, SearchProvider, SearchRangeFacetBucket, SearchRangeFacetRequest, ServerConfig, SingletonSavePrecondition, SlugifierFn, SortSpec, UnavailableBlock } from '@byline/core';
10
+ export type { FilterOperators, PredicateValue, QueryPredicate, SearchFacetBucket, SearchHit, SearchProvider, SearchRangeFacetBucket, SearchRangeFacetRequest, SearchResults, SortDirection, SortSpec, } from '@byline/core';
11
11
  export interface BylineClientConfig {
12
12
  /**
13
13
  * Convenience shorthand: pass an already-resolved `ServerConfig` (typically
@@ -204,6 +204,8 @@ export interface CollectionSearchOptions extends BeforeReadControls {
204
204
  where?: QueryPredicate;
205
205
  /** Field names to compute facet buckets for (driver-capability gated). */
206
206
  facets?: string[];
207
+ /** Integer range aggregates; requires provider support and declared search filters. */
208
+ rangeFacets?: SearchRangeFacetRequest[];
207
209
  /** Max hits to return. */
208
210
  limit?: number;
209
211
  /** Offset for pagination. */
@@ -242,6 +244,8 @@ export interface ZoneSearchOptions extends BeforeReadControls {
242
244
  where?: QueryPredicate;
243
245
  /** Field names to compute facet buckets for (driver-capability gated). */
244
246
  facets?: string[];
247
+ /** Integer range aggregates over filter fields shared by every zone member. */
248
+ rangeFacets?: SearchRangeFacetRequest[];
245
249
  /** Max hits to return. */
246
250
  limit?: number;
247
251
  /** Offset for pagination. */
@@ -263,6 +267,7 @@ export interface ClientSearchResults {
263
267
  hits: HydratedSearchHit[];
264
268
  total: number;
265
269
  facets?: Record<string, SearchFacetBucket[]>;
270
+ rangeFacets?: Record<string, SearchRangeFacetBucket[]>;
266
271
  }
267
272
  /** Outcome of a collection `reindex()` — counts for reporting. */
268
273
  export interface ReindexResult {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/client",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "6.9.0",
5
+ "version": "6.10.0",
6
6
  "engines": {
7
7
  "node": ">=22.22.2"
8
8
  },
@@ -43,22 +43,22 @@
43
43
  "dist"
44
44
  ],
45
45
  "dependencies": {
46
- "@byline/auth": "6.9.0",
47
- "@byline/core": "6.9.0",
46
+ "@byline/auth": "6.10.0",
47
+ "@byline/core": "6.10.0",
48
48
  "uuid": "^14.0.3"
49
49
  },
50
50
  "devDependencies": {
51
- "@biomejs/biome": "2.5.15",
52
- "@byline/db-postgres": "6.9.0",
53
- "@byline/search-postgres": "6.9.0",
51
+ "@biomejs/biome": "2.6.2",
52
+ "@byline/db-postgres": "6.10.0",
53
+ "@byline/search-postgres": "6.10.0",
54
54
  "@types/node": "^26.6.5",
55
- "chokidar": "^5.0.0",
55
+ "chokidar": "^6.0.0",
56
56
  "chokidar-cli": "^3.0.0",
57
57
  "dotenv": "^18.0.8",
58
58
  "npm-run-all": "^4.1.5",
59
- "tsc-alias": "^1.9.7",
59
+ "tsc-alias": "^1.9.8",
60
60
  "typescript": "^7.0.2",
61
- "vitest": "^5.0.4"
61
+ "vitest": "^5.0.5"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public",