@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.
- package/dist/collection-handle.js +9 -2
- package/dist/search.d.ts +3 -1
- package/dist/search.js +58 -2
- package/dist/types.d.ts +7 -2
- package/package.json +9 -9
|
@@ -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
|
-
: {
|
|
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
|
-
: {
|
|
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.
|
|
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.
|
|
47
|
-
"@byline/core": "6.
|
|
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.
|
|
52
|
-
"@byline/db-postgres": "6.
|
|
53
|
-
"@byline/search-postgres": "6.
|
|
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": "^
|
|
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.
|
|
59
|
+
"tsc-alias": "^1.9.8",
|
|
60
60
|
"typescript": "^7.0.2",
|
|
61
|
-
"vitest": "^5.0.
|
|
61
|
+
"vitest": "^5.0.5"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public",
|