@byline/core 3.13.3 → 3.15.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/@types/admin-types.d.ts +45 -7
- package/dist/@types/collection-types.d.ts +32 -4
- package/dist/@types/field-types.d.ts +43 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/@types/search-types.d.ts +275 -0
- package/dist/@types/search-types.js +8 -0
- package/dist/@types/site-config.d.ts +31 -1
- package/dist/auth/assert-actor-can-perform.test.node.js +9 -1
- package/dist/auth/register-collection-abilities.d.ts +8 -5
- package/dist/auth/register-collection-abilities.js +14 -4
- package/dist/auth/register-collection-abilities.test.node.js +11 -7
- package/dist/config/config.d.ts +12 -1
- package/dist/config/config.js +11 -0
- package/dist/config/resolve-item-view-columns.test.node.d.ts +8 -0
- package/dist/config/resolve-item-view-columns.test.node.js +30 -0
- package/dist/core.js +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/schemas/zod/builder.js +16 -5
- package/dist/services/build-search-document.d.ts +46 -0
- package/dist/services/build-search-document.js +220 -0
- package/dist/services/build-search-document.test.node.d.ts +8 -0
- package/dist/services/build-search-document.test.node.js +164 -0
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.js +3 -1
- package/dist/services/populate.d.ts +6 -0
- package/dist/services/populate.js +19 -1
- package/dist/services/relation-projection.d.ts +4 -15
- package/dist/services/relation-projection.js +19 -6
- package/dist/services/validate-search-config.d.ts +28 -0
- package/dist/services/validate-search-config.js +32 -0
- package/dist/storage/collection-fingerprint.test.node.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Contract tests for the document-grain search assembler. Verifies the
|
|
10
|
+
* role-based projection (body / facets / filters / zones), the schema-derived
|
|
11
|
+
* type enrichment, per-field boost, and that nothing outside the config is
|
|
12
|
+
* pulled (no leakage of unindexed content).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import { buildSearchDocument } from './build-search-document.js';
|
|
16
|
+
// A controlled-vocabulary facet collection: a `counter` id + a `name` term.
|
|
17
|
+
const topics = {
|
|
18
|
+
path: 'topics',
|
|
19
|
+
labels: { singular: 'Topic', plural: 'Topics' },
|
|
20
|
+
useAsTitle: 'name',
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: 'name', label: 'Name', type: 'text', localized: true },
|
|
23
|
+
{ name: 'facetId', label: 'Facet Id', type: 'counter', group: 'publication-facets' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
const publications = {
|
|
27
|
+
path: 'publications',
|
|
28
|
+
labels: { singular: 'Publication', plural: 'Publications' },
|
|
29
|
+
useAsTitle: 'title',
|
|
30
|
+
fields: [
|
|
31
|
+
{ name: 'title', label: 'Title', type: 'text', localized: true },
|
|
32
|
+
{ name: 'abstract', label: 'Abstract', type: 'richText', localized: true },
|
|
33
|
+
{ name: 'editorialNotes', label: 'Editorial Notes', type: 'textArea' },
|
|
34
|
+
{ name: 'publicationDate', label: 'Date', type: 'datetime' },
|
|
35
|
+
{ name: 'citationCount', label: 'Citations', type: 'integer' },
|
|
36
|
+
{
|
|
37
|
+
name: 'topics',
|
|
38
|
+
label: 'Topics',
|
|
39
|
+
type: 'relation',
|
|
40
|
+
targetCollection: 'topics',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
search: {
|
|
44
|
+
body: ['title', { field: 'abstract', boost: 2 }],
|
|
45
|
+
facets: ['topics'],
|
|
46
|
+
filters: ['publicationDate', 'citationCount'],
|
|
47
|
+
zones: ['site', 'publications'],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const resolveTargetDefinition = (path) => (path === 'topics' ? topics : null);
|
|
51
|
+
// Stub `toText` seam — returns a string property so we don't depend on Lexical.
|
|
52
|
+
const richTextToText = ({ value }) => typeof value === 'object' && value != null ? String(value.text ?? '') : '';
|
|
53
|
+
function source() {
|
|
54
|
+
return {
|
|
55
|
+
documentId: 'doc-1',
|
|
56
|
+
locale: 'en',
|
|
57
|
+
status: 'published',
|
|
58
|
+
path: 'forest-restoration',
|
|
59
|
+
updatedAt: '2026-06-01T00:00:00.000Z',
|
|
60
|
+
fields: {
|
|
61
|
+
title: 'Forest Restoration',
|
|
62
|
+
abstract: { text: 'Methods for restoring degraded forest.' },
|
|
63
|
+
editorialNotes: 'Internal: chase the author for figures.',
|
|
64
|
+
publicationDate: '2026-05-01T00:00:00.000Z',
|
|
65
|
+
citationCount: 42,
|
|
66
|
+
topics: [
|
|
67
|
+
{
|
|
68
|
+
_resolved: true,
|
|
69
|
+
document: { documentId: 't-1', fields: { name: 'Ecology', facetId: 1 } },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
_resolved: true,
|
|
73
|
+
document: { documentId: 't-2', fields: { name: 'Biodiversity', facetId: 2 } },
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function fieldByName(fields, name) {
|
|
80
|
+
return fields.find((f) => f.name === name);
|
|
81
|
+
}
|
|
82
|
+
describe('buildSearchDocument', () => {
|
|
83
|
+
const doc = buildSearchDocument(source(), publications, {
|
|
84
|
+
richTextToText,
|
|
85
|
+
resolveTargetDefinition,
|
|
86
|
+
});
|
|
87
|
+
it('sets identity, status, locale, path, and updatedAt', () => {
|
|
88
|
+
expect(doc.collectionPath).toBe('publications');
|
|
89
|
+
expect(doc.documentId).toBe('doc-1');
|
|
90
|
+
expect(doc.locale).toBe('en');
|
|
91
|
+
expect(doc.status).toBe('published');
|
|
92
|
+
expect(doc.title).toBe('Forest Restoration');
|
|
93
|
+
expect(doc.path).toBe('forest-restoration');
|
|
94
|
+
expect(doc.updatedAt).toBe('2026-06-01T00:00:00.000Z');
|
|
95
|
+
});
|
|
96
|
+
it('resolves zones from config', () => {
|
|
97
|
+
expect(doc.zones).toEqual(['site', 'publications']);
|
|
98
|
+
});
|
|
99
|
+
it('projects body text fields as type:text role:body', () => {
|
|
100
|
+
const title = fieldByName(doc.fields, 'title');
|
|
101
|
+
expect(title).toMatchObject({
|
|
102
|
+
name: 'title',
|
|
103
|
+
type: 'text',
|
|
104
|
+
role: 'body',
|
|
105
|
+
value: 'Forest Restoration',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it('extracts richText body via the toText seam and carries boost', () => {
|
|
109
|
+
const abstract = fieldByName(doc.fields, 'abstract');
|
|
110
|
+
expect(abstract).toMatchObject({
|
|
111
|
+
name: 'abstract',
|
|
112
|
+
type: 'text',
|
|
113
|
+
role: 'body',
|
|
114
|
+
value: 'Methods for restoring degraded forest.',
|
|
115
|
+
boost: 2,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('resolves facets to {id, term} from target counter + useAsTitle', () => {
|
|
119
|
+
const topicsField = fieldByName(doc.fields, 'topics');
|
|
120
|
+
expect(topicsField).toMatchObject({ name: 'topics', type: 'facet', role: 'facet' });
|
|
121
|
+
expect(topicsField?.value).toEqual([
|
|
122
|
+
{ id: 1, term: 'Ecology' },
|
|
123
|
+
{ id: 2, term: 'Biodiversity' },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
it('projects filters with schema-derived types', () => {
|
|
127
|
+
expect(fieldByName(doc.fields, 'publicationDate')).toMatchObject({
|
|
128
|
+
type: 'datetime',
|
|
129
|
+
role: 'filter',
|
|
130
|
+
value: '2026-05-01T00:00:00.000Z',
|
|
131
|
+
});
|
|
132
|
+
expect(fieldByName(doc.fields, 'citationCount')).toMatchObject({
|
|
133
|
+
type: 'integer',
|
|
134
|
+
role: 'filter',
|
|
135
|
+
value: 42,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it('does not pull fields outside the search config (no leakage)', () => {
|
|
139
|
+
expect(fieldByName(doc.fields, 'editorialNotes')).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
it('defaults zones to the collection path when unset', () => {
|
|
142
|
+
const noZones = { ...publications, search: { body: ['title'] } };
|
|
143
|
+
const out = buildSearchDocument(source(), noZones);
|
|
144
|
+
expect(out.zones).toEqual(['publications']);
|
|
145
|
+
});
|
|
146
|
+
it('skips richText body fields when no toText seam is registered', () => {
|
|
147
|
+
const out = buildSearchDocument(source(), publications, { resolveTargetDefinition });
|
|
148
|
+
expect(fieldByName(out.fields, 'abstract')).toBeUndefined();
|
|
149
|
+
// Plain text body field still projects.
|
|
150
|
+
expect(fieldByName(out.fields, 'title')).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
it('resolves localized values to the requested locale', () => {
|
|
153
|
+
const localized = {
|
|
154
|
+
...source(),
|
|
155
|
+
fields: { ...source().fields, title: { en: 'Forest Restoration', fr: 'Restauration' } },
|
|
156
|
+
};
|
|
157
|
+
const out = buildSearchDocument(localized, publications, {
|
|
158
|
+
richTextToText,
|
|
159
|
+
resolveTargetDefinition,
|
|
160
|
+
locale: 'fr',
|
|
161
|
+
});
|
|
162
|
+
expect(out.title).toBe('Restauration');
|
|
163
|
+
});
|
|
164
|
+
});
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_PATCH_FAILED, ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION, } from '../lib/errors.js';
|
|
2
2
|
export { normaliseDateFields } from '../utils/normalise-dates.js';
|
|
3
3
|
export { type AssignCounterValuesInput, assignCounterValues } from './assign-counter-values.js';
|
|
4
|
+
export { type BuildSearchDocumentOptions, buildSearchDocument, type SearchSourceDocument, } from './build-search-document.js';
|
|
4
5
|
export { type CollectionRecord, type EnsureCollectionsInput, ensureCollections, } from './collection-bootstrap.js';
|
|
5
6
|
export { type DiscoverCounterGroupsInput, discoverCounterGroups, } from './discover-counter-groups.js';
|
|
6
7
|
export * from './document-lifecycle/index.js';
|
|
@@ -8,8 +9,9 @@ export * from './document-read.js';
|
|
|
8
9
|
export * from './document-to-markdown.js';
|
|
9
10
|
export * from './field-upload.js';
|
|
10
11
|
export { type InterfaceI18nConfig, type TranslationDriftWarning, type ValidateTranslationsResult, validateTranslations, } from './i18n-validator.js';
|
|
11
|
-
export { type CycleRelationValue, createReadContext, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateOptions, type PopulateSpec, populateDocuments, type ReadContext, type UnresolvedRelationValue, } from './populate.js';
|
|
12
|
+
export { type CycleRelationValue, createReadContext, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateOptions, type PopulateSpec, populateDocuments, type ReadContext, resolveIdentityField, type UnresolvedRelationValue, } from './populate.js';
|
|
12
13
|
export { buildRelationSummaryPopulateMap, type RelationTargetResolver, resolveRelationProjection, } from './relation-projection.js';
|
|
13
14
|
export { type EmbedRichTextFieldsOptions, embedRichTextFields, resolveEmbedOnSave, } from './richtext-embed.js';
|
|
14
15
|
export { collectRichTextLeaves, type PopulateRichTextFieldsOptions, populateRichTextFields, type RichTextAdapterPresence, type RichTextLeaf, resolvePopulateOnRead, validateRichTextFieldFlags, } from './richtext-populate.js';
|
|
16
|
+
export { type SearchProviderPresence, validateSearchConfig, } from './validate-search-config.js';
|
|
15
17
|
export { type FieldLeaf, walkFieldTree } from './walk-field-tree.js';
|
package/dist/services/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_PATCH_FAILED, ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION, } from '../lib/errors.js';
|
|
3
3
|
export { normaliseDateFields } from '../utils/normalise-dates.js';
|
|
4
4
|
export { assignCounterValues } from './assign-counter-values.js';
|
|
5
|
+
export { buildSearchDocument, } from './build-search-document.js';
|
|
5
6
|
export { ensureCollections, } from './collection-bootstrap.js';
|
|
6
7
|
export { discoverCounterGroups, } from './discover-counter-groups.js';
|
|
7
8
|
export * from './document-lifecycle/index.js';
|
|
@@ -9,8 +10,9 @@ export * from './document-read.js';
|
|
|
9
10
|
export * from './document-to-markdown.js';
|
|
10
11
|
export * from './field-upload.js';
|
|
11
12
|
export { validateTranslations, } from './i18n-validator.js';
|
|
12
|
-
export { createReadContext, populateDocuments, } from './populate.js';
|
|
13
|
+
export { createReadContext, populateDocuments, resolveIdentityField, } from './populate.js';
|
|
13
14
|
export { buildRelationSummaryPopulateMap, resolveRelationProjection, } from './relation-projection.js';
|
|
14
15
|
export { embedRichTextFields, resolveEmbedOnSave, } from './richtext-embed.js';
|
|
15
16
|
export { collectRichTextLeaves, populateRichTextFields, resolvePopulateOnRead, validateRichTextFieldFlags, } from './richtext-populate.js';
|
|
17
|
+
export { validateSearchConfig, } from './validate-search-config.js';
|
|
16
18
|
export { walkFieldTree } from './walk-field-tree.js';
|
|
@@ -276,6 +276,12 @@ declare function matchesPopulate(fieldName: string, populate: PopulateSpec): Pop
|
|
|
276
276
|
* not need to appear in the `fields` list.
|
|
277
277
|
*/
|
|
278
278
|
declare function buildBatchSelect(leaves: RelationLeafRef[], targetDef: CollectionDefinition | undefined): string[] | undefined;
|
|
279
|
+
/**
|
|
280
|
+
* The field that represents a target document's identity for populate's
|
|
281
|
+
* default projection. Prefers `useAsTitle` (server-safe schema-level
|
|
282
|
+
* config), falling back to the first declared text field.
|
|
283
|
+
*/
|
|
284
|
+
export declare function resolveIdentityField(def: CollectionDefinition): string | undefined;
|
|
279
285
|
/**
|
|
280
286
|
* Merge the per-leaf sub-populate specs for all leaves pointing at a
|
|
281
287
|
* single (now-populated) target document into a single PopulateSpec for
|
|
@@ -317,6 +317,24 @@ function collectRelationLeaves(fields, fieldDefs, populate, acc) {
|
|
|
317
317
|
const sub = matchesPopulate(leaf.field.name, populate);
|
|
318
318
|
if (sub === undefined)
|
|
319
319
|
continue;
|
|
320
|
+
// hasMany: the value is an ordered array of relation envelopes. Expand it
|
|
321
|
+
// into one leaf ref per element, whose `parent` is the array itself and
|
|
322
|
+
// `key` is the index — so the envelope-assignment pass writes each
|
|
323
|
+
// populated result back into its array slot, in place.
|
|
324
|
+
if (leaf.field.hasMany) {
|
|
325
|
+
if (!Array.isArray(leaf.value))
|
|
326
|
+
continue;
|
|
327
|
+
const arr = leaf.value;
|
|
328
|
+
for (let i = 0; i < arr.length; i++) {
|
|
329
|
+
const item = arr[i];
|
|
330
|
+
if (!isRelatedDocumentValue(item))
|
|
331
|
+
continue;
|
|
332
|
+
if ('_resolved' in item)
|
|
333
|
+
continue;
|
|
334
|
+
acc.push({ parent: arr, key: String(i), field: leaf.field, value: item, sub });
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
320
338
|
if (!isRelatedDocumentValue(leaf.value))
|
|
321
339
|
continue;
|
|
322
340
|
// Skip leaves that have already been replaced (e.g. via shared-ref
|
|
@@ -387,7 +405,7 @@ function buildBatchSelect(leaves, targetDef) {
|
|
|
387
405
|
* default projection. Prefers `useAsTitle` (server-safe schema-level
|
|
388
406
|
* config), falling back to the first declared text field.
|
|
389
407
|
*/
|
|
390
|
-
function resolveIdentityField(def) {
|
|
408
|
+
export function resolveIdentityField(def) {
|
|
391
409
|
if (def.useAsTitle)
|
|
392
410
|
return def.useAsTitle;
|
|
393
411
|
const firstText = def.fields.find((f) => f.type === 'text');
|
|
@@ -5,17 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Relation projection helpers.
|
|
10
|
-
*
|
|
11
|
-
* Translate a source collection's relation fields into a `PopulateMap`
|
|
12
|
-
* projection that fetches just enough data from each target to render
|
|
13
|
-
* a relation-summary tile on the admin edit view (picker columns,
|
|
14
|
-
* `useAsTitle`, plus an optional source-side `displayField` override).
|
|
15
|
-
*
|
|
16
|
-
* Consumed by the admin webapp's edit-route server fn so relation tiles
|
|
17
|
-
* arrive pre-hydrated on first paint — no client-side fetch per relation.
|
|
18
|
-
*/
|
|
19
8
|
import type { CollectionAdminConfig, CollectionDefinition, FieldSet, RelationField } from '../@types/index.js';
|
|
20
9
|
import type { PopulateMap } from './populate.js';
|
|
21
10
|
/**
|
|
@@ -25,10 +14,10 @@ import type { PopulateMap } from './populate.js';
|
|
|
25
14
|
* - `sourceField.displayField` (explicit per-relation override)
|
|
26
15
|
* - target's `useAsTitle`
|
|
27
16
|
* - target's first declared text field (safety fallback)
|
|
28
|
-
* - every `
|
|
29
|
-
* to a real schema field (
|
|
30
|
-
* like `status` or `updated_at` which ride on the document row
|
|
31
|
-
*
|
|
17
|
+
* - every item-view column `fieldName` on the target admin config that
|
|
18
|
+
* maps to a real schema field (item-view columns may reference metadata
|
|
19
|
+
* like `status` or `updated_at` which ride on the document row and don't
|
|
20
|
+
* need a projection entry)
|
|
32
21
|
*/
|
|
33
22
|
export declare function resolveRelationProjection(sourceField: RelationField, targetDef: CollectionDefinition | null, targetAdmin: CollectionAdminConfig | null): string[];
|
|
34
23
|
export type RelationTargetResolver = (targetPath: string) => {
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Relation projection helpers.
|
|
10
|
+
*
|
|
11
|
+
* Translate a source collection's relation fields into a `PopulateMap`
|
|
12
|
+
* projection that fetches just enough data from each target to render
|
|
13
|
+
* a relation-summary tile on the admin edit view (picker columns,
|
|
14
|
+
* `useAsTitle`, plus an optional source-side `displayField` override).
|
|
15
|
+
*
|
|
16
|
+
* Consumed by the admin webapp's edit-route server fn so relation tiles
|
|
17
|
+
* arrive pre-hydrated on first paint — no client-side fetch per relation.
|
|
18
|
+
*/
|
|
19
|
+
import { resolveItemViewColumns } from '../config/config.js';
|
|
8
20
|
/**
|
|
9
21
|
* Union of the target-collection field names needed to render a
|
|
10
22
|
* relation-summary tile for the given source relation field.
|
|
@@ -12,10 +24,10 @@
|
|
|
12
24
|
* - `sourceField.displayField` (explicit per-relation override)
|
|
13
25
|
* - target's `useAsTitle`
|
|
14
26
|
* - target's first declared text field (safety fallback)
|
|
15
|
-
* - every `
|
|
16
|
-
* to a real schema field (
|
|
17
|
-
* like `status` or `updated_at` which ride on the document row
|
|
18
|
-
*
|
|
27
|
+
* - every item-view column `fieldName` on the target admin config that
|
|
28
|
+
* maps to a real schema field (item-view columns may reference metadata
|
|
29
|
+
* like `status` or `updated_at` which ride on the document row and don't
|
|
30
|
+
* need a projection entry)
|
|
19
31
|
*/
|
|
20
32
|
export function resolveRelationProjection(sourceField, targetDef, targetAdmin) {
|
|
21
33
|
const out = new Set();
|
|
@@ -26,8 +38,9 @@ export function resolveRelationProjection(sourceField, targetDef, targetAdmin) {
|
|
|
26
38
|
const firstText = targetDef?.fields.find((f) => f.type === 'text')?.name;
|
|
27
39
|
if (firstText)
|
|
28
40
|
out.add(firstText);
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
const itemViewColumns = resolveItemViewColumns(targetAdmin);
|
|
42
|
+
if (itemViewColumns) {
|
|
43
|
+
for (const col of itemViewColumns) {
|
|
31
44
|
const name = String(col.fieldName);
|
|
32
45
|
if (targetDef?.fields.some((f) => f.name === name))
|
|
33
46
|
out.add(name);
|
|
@@ -0,0 +1,28 @@
|
|
|
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 type { CollectionDefinition } from '../@types/collection-types.js';
|
|
9
|
+
/** Whether a `SearchProvider` is registered on `ServerConfig.search`. */
|
|
10
|
+
export interface SearchProviderPresence {
|
|
11
|
+
/** `ServerConfig.search != null` */
|
|
12
|
+
provider: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate search configuration across every collection. Throws when any
|
|
16
|
+
* collection opts into search (`CollectionDefinition.search`) but no
|
|
17
|
+
* `SearchProvider` is registered on `ServerConfig.search` — otherwise
|
|
18
|
+
* indexing and `client.search()` would silently no-op.
|
|
19
|
+
*
|
|
20
|
+
* Called once at `initBylineCore()` time, right after the richText field
|
|
21
|
+
* validation. Fail-fast at boot is the right posture; the alternative is a
|
|
22
|
+
* collection that declares it's searchable but never gets indexed.
|
|
23
|
+
*
|
|
24
|
+
* Note: a missing provider is only an error when at least one collection
|
|
25
|
+
* actually opts in. Installations that don't use search leave
|
|
26
|
+
* `ServerConfig.search` unset and pass cleanly.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateSearchConfig(collections: CollectionDefinition[], adapters: SearchProviderPresence): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Validate search configuration across every collection. Throws when any
|
|
10
|
+
* collection opts into search (`CollectionDefinition.search`) but no
|
|
11
|
+
* `SearchProvider` is registered on `ServerConfig.search` — otherwise
|
|
12
|
+
* indexing and `client.search()` would silently no-op.
|
|
13
|
+
*
|
|
14
|
+
* Called once at `initBylineCore()` time, right after the richText field
|
|
15
|
+
* validation. Fail-fast at boot is the right posture; the alternative is a
|
|
16
|
+
* collection that declares it's searchable but never gets indexed.
|
|
17
|
+
*
|
|
18
|
+
* Note: a missing provider is only an error when at least one collection
|
|
19
|
+
* actually opts in. Installations that don't use search leave
|
|
20
|
+
* `ServerConfig.search` unset and pass cleanly.
|
|
21
|
+
*/
|
|
22
|
+
export function validateSearchConfig(collections, adapters) {
|
|
23
|
+
if (adapters.provider)
|
|
24
|
+
return;
|
|
25
|
+
const optedIn = collections.filter((def) => def.search != null).map((def) => def.path);
|
|
26
|
+
if (optedIn.length === 0)
|
|
27
|
+
return;
|
|
28
|
+
throw new Error(`initBylineCore: ${optedIn.length} collection(s) opt into search ` +
|
|
29
|
+
`(${optedIn.join(', ')}) but no search provider is registered. ` +
|
|
30
|
+
`Wire one via ServerConfig.search — see \`@byline/search-postgres\` → ` +
|
|
31
|
+
`\`postgresSearch()\` for the built-in Postgres full-text driver.`);
|
|
32
|
+
}
|
|
@@ -89,7 +89,7 @@ describe('fingerprintCollection', () => {
|
|
|
89
89
|
it('ignores search / showStats (admin UX only)', async () => {
|
|
90
90
|
const a = await fingerprintCollection(baseCollection());
|
|
91
91
|
const b = baseCollection();
|
|
92
|
-
b.search = {
|
|
92
|
+
b.search = { body: ['title', 'body.heading'] };
|
|
93
93
|
b.showStats = true;
|
|
94
94
|
expect(await fingerprintCollection(b)).toBe(a);
|
|
95
95
|
});
|
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": "3.
|
|
5
|
+
"version": "3.15.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"sharp": "^0.34.5",
|
|
80
80
|
"uuid": "^14.0.0",
|
|
81
81
|
"zod": "^4.4.3",
|
|
82
|
-
"@byline/auth": "3.
|
|
82
|
+
"@byline/auth": "3.15.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|