@byline/core 3.20.3 → 3.20.4
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.
|
@@ -993,9 +993,7 @@ export interface CollectionDefinition {
|
|
|
993
993
|
* Text fields contribute their value; `richText` fields are extracted to
|
|
994
994
|
* plain text via the registered `fields.richText.toText` seam. Each entry
|
|
995
995
|
* is a field path, or `{ field, boost }` to weight it for scoring
|
|
996
|
-
* providers that support `capabilities.weighting`.
|
|
997
|
-
* list-view search box (its `store_text` subset). Falls back to the
|
|
998
|
-
* identity field (`useAsTitle`) when omitted.
|
|
996
|
+
* providers that support `capabilities.weighting`.
|
|
999
997
|
* - `facets` — relation field paths to controlled-vocabulary collections.
|
|
1000
998
|
* Core resolves each target's `counter` field (the stable aggregation id)
|
|
1001
999
|
* and its `useAsTitle` (the term, folded into searchable text). `{ field,
|
|
@@ -1015,6 +1013,27 @@ export interface CollectionDefinition {
|
|
|
1015
1013
|
filters?: string[];
|
|
1016
1014
|
zones?: string[];
|
|
1017
1015
|
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Admin list-view quick-search fields — the top-level text-store field
|
|
1018
|
+
* names (`text` / `textArea` / `select`) the list route's search box
|
|
1019
|
+
* matches with substring (`ILIKE`) queries against `store_text`.
|
|
1020
|
+
*
|
|
1021
|
+
* Deliberately separate from `search`, which configures provider indexing
|
|
1022
|
+
* for site search: the two answer different questions ("find the row I
|
|
1023
|
+
* mean" vs. "rank relevant published content") and need not name the same
|
|
1024
|
+
* fields — a collection with a six-field weighted `search.body` typically
|
|
1025
|
+
* wants only its identity field (plus maybe a serial/code field) here.
|
|
1026
|
+
* Declaring one without the other is equally valid: `listSearch` with no
|
|
1027
|
+
* `search` keeps the list box working on an unindexed collection.
|
|
1028
|
+
*
|
|
1029
|
+
* Falls back to the identity field (`useAsTitle`, else the first declared
|
|
1030
|
+
* text field) when omitted — most collections need no declaration.
|
|
1031
|
+
*
|
|
1032
|
+
* Lives on the schema (not admin config) for the same reason as
|
|
1033
|
+
* `useAsTitle`: the query layer (`findDocuments` in the db adapter)
|
|
1034
|
+
* resolves it server-side from the `CollectionDefinition`.
|
|
1035
|
+
*/
|
|
1036
|
+
listSearch?: string[];
|
|
1018
1037
|
/**
|
|
1019
1038
|
* The field that represents this document's identity — used anywhere a
|
|
1020
1039
|
* single-line label for the document is needed: form headings, relation
|
|
@@ -33,6 +33,9 @@ export declare const RESERVED_FIELD_NAMES: ReadonlySet<string>;
|
|
|
33
33
|
* - A collection may not set both `tree: true` and `orderable: true`. A
|
|
34
34
|
* document-tree owns ordering per-parent on the tree edge, so
|
|
35
35
|
* `byline_documents.order_key` is inert for it.
|
|
36
|
+
* - Each `listSearch` entry must name an existing top-level field whose
|
|
37
|
+
* type is persisted to the text store (the admin list-view search box
|
|
38
|
+
* is an `ILIKE` over `store_text`), and the field may not be virtual.
|
|
36
39
|
* - `virtual` fields must satisfy the constraints in
|
|
37
40
|
* {@link validateVirtualFields} (optional-or-default, no counters, no
|
|
38
41
|
* upload fields, not referenced by useAsTitle / useAsPath / search).
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { fieldTypeToStore } from '../storage/field-store-map.js';
|
|
8
9
|
/**
|
|
9
10
|
* Field names that cannot be declared in a collection schema because they
|
|
10
11
|
* collide with system-managed attributes on `documentVersions`. Exported
|
|
@@ -21,6 +22,15 @@ const RESERVED_FIELD_HINTS = {
|
|
|
21
22
|
path: "Use `useAsPath: '<sourceField>'` on the collection definition instead.",
|
|
22
23
|
availableLocales: 'Use `advertiseLocales: true` on the collection definition instead.',
|
|
23
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Field types `listSearch` may name — the types persisted to the text
|
|
27
|
+
* store, since the admin list-view search box is an `ILIKE` over
|
|
28
|
+
* `store_text`. Derived from the canonical field→store mapping so the two
|
|
29
|
+
* can't drift.
|
|
30
|
+
*/
|
|
31
|
+
const LIST_SEARCH_SOURCE_TYPES = new Set(Object.entries(fieldTypeToStore)
|
|
32
|
+
.filter(([, mapping]) => mapping?.storeType === 'text')
|
|
33
|
+
.map(([type]) => type));
|
|
24
34
|
const USE_AS_PATH_SOURCE_TYPES = new Set([
|
|
25
35
|
'text',
|
|
26
36
|
'textArea',
|
|
@@ -180,6 +190,9 @@ function validateVirtualFields(collection) {
|
|
|
180
190
|
* - A collection may not set both `tree: true` and `orderable: true`. A
|
|
181
191
|
* document-tree owns ordering per-parent on the tree edge, so
|
|
182
192
|
* `byline_documents.order_key` is inert for it.
|
|
193
|
+
* - Each `listSearch` entry must name an existing top-level field whose
|
|
194
|
+
* type is persisted to the text store (the admin list-view search box
|
|
195
|
+
* is an `ILIKE` over `store_text`), and the field may not be virtual.
|
|
183
196
|
* - `virtual` fields must satisfy the constraints in
|
|
184
197
|
* {@link validateVirtualFields} (optional-or-default, no counters, no
|
|
185
198
|
* upload fields, not referenced by useAsTitle / useAsPath / search).
|
|
@@ -205,6 +218,18 @@ export function validateCollections(collections) {
|
|
|
205
218
|
throw new Error(`Collection "${collection.path}" sets \`useAsPath: '${collection.useAsPath}'\` but field "${collection.useAsPath}" has type "${source.type}". Supported source types: ${[...USE_AS_PATH_SOURCE_TYPES].join(', ')}.`);
|
|
206
219
|
}
|
|
207
220
|
}
|
|
221
|
+
for (const name of collection.listSearch ?? []) {
|
|
222
|
+
const source = collection.fields.find((f) => 'name' in f && f.name === name);
|
|
223
|
+
if (source == null) {
|
|
224
|
+
throw new Error(`Collection "${collection.path}" names '${name}' in \`listSearch\` but no top-level field with that name exists.`);
|
|
225
|
+
}
|
|
226
|
+
if (!LIST_SEARCH_SOURCE_TYPES.has(source.type)) {
|
|
227
|
+
throw new Error(`Collection "${collection.path}" names '${name}' in \`listSearch\` but field "${name}" has type "${source.type}". The list-view search box matches text-store fields only (${[...LIST_SEARCH_SOURCE_TYPES].join(', ')}).`);
|
|
228
|
+
}
|
|
229
|
+
if (source.virtual === true) {
|
|
230
|
+
throw new Error(`Collection "${collection.path}" names virtual field '${name}' in \`listSearch\` — list-view search reads persisted values, which virtual fields never have.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
208
233
|
if (collection.advertiseLocales === true && !hasLocalizedField(collection.fields)) {
|
|
209
234
|
throw new Error(`Collection "${collection.path}" sets \`advertiseLocales: true\` but has no localized fields. The available-locales control advertises content locales, which is only meaningful when at least one field is \`localized\`.`);
|
|
210
235
|
}
|
|
@@ -410,4 +410,46 @@ describe('validateCollections', () => {
|
|
|
410
410
|
};
|
|
411
411
|
expect(() => validateCollections([collection])).not.toThrow();
|
|
412
412
|
});
|
|
413
|
+
it('accepts listSearch naming text-store fields', () => {
|
|
414
|
+
const collection = {
|
|
415
|
+
...baseCollection,
|
|
416
|
+
listSearch: ['title', 'summary', 'kind'],
|
|
417
|
+
fields: [
|
|
418
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
419
|
+
{ name: 'summary', label: 'Summary', type: 'textArea' },
|
|
420
|
+
{
|
|
421
|
+
name: 'kind',
|
|
422
|
+
label: 'Kind',
|
|
423
|
+
type: 'select',
|
|
424
|
+
options: [{ label: 'A', value: 'a' }],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
expect(() => validateCollections([collection])).not.toThrow();
|
|
429
|
+
});
|
|
430
|
+
it('rejects a listSearch entry referencing a missing field', () => {
|
|
431
|
+
expect(() => validateCollections([{ ...baseCollection, listSearch: ['nonexistent'] }])).toThrow(/listSearch.*no top-level field/s);
|
|
432
|
+
});
|
|
433
|
+
it('rejects a listSearch entry referencing a non-text-store field', () => {
|
|
434
|
+
const collection = {
|
|
435
|
+
...baseCollection,
|
|
436
|
+
listSearch: ['count'],
|
|
437
|
+
fields: [
|
|
438
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
439
|
+
{ name: 'count', label: 'Count', type: 'integer' },
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
expect(() => validateCollections([collection])).toThrow(/text-store fields only/);
|
|
443
|
+
});
|
|
444
|
+
it('rejects a listSearch entry referencing a virtual field', () => {
|
|
445
|
+
const collection = {
|
|
446
|
+
...baseCollection,
|
|
447
|
+
listSearch: ['ephemeral'],
|
|
448
|
+
fields: [
|
|
449
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
450
|
+
{ name: 'ephemeral', label: 'Ephemeral', type: 'text', virtual: true, optional: true },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
expect(() => validateCollections([collection])).toThrow(/listSearch.*virtual/s);
|
|
454
|
+
});
|
|
413
455
|
});
|
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.20.
|
|
5
|
+
"version": "3.20.4",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"pino": "^10.3.1",
|
|
77
77
|
"sharp": "^0.35.3",
|
|
78
78
|
"zod": "^4.4.3",
|
|
79
|
-
"@byline/auth": "3.20.
|
|
79
|
+
"@byline/auth": "3.20.4"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@biomejs/biome": "2.5.2",
|