@byline/core 3.21.0 → 4.1.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/collection-types.d.ts +66 -31
- package/dist/@types/db-types.d.ts +93 -47
- package/dist/@types/field-types.d.ts +25 -1
- package/dist/@types/query-predicate.d.ts +3 -3
- package/dist/@types/search-types.d.ts +3 -4
- package/dist/@types/site-config.d.ts +51 -12
- package/dist/auth/apply-before-read.d.ts +23 -11
- package/dist/auth/apply-before-read.js +139 -33
- package/dist/auth/apply-before-read.test.node.js +241 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/read-context-scope.d.ts +20 -0
- package/dist/auth/read-context-scope.js +48 -0
- package/dist/codegen/index.js +34 -3
- package/dist/codegen/index.test.node.js +20 -2
- package/dist/config/attach-hooks.d.ts +25 -0
- package/dist/config/attach-hooks.js +130 -0
- package/dist/config/attach-hooks.test.node.d.ts +1 -0
- package/dist/config/attach-hooks.test.node.js +173 -0
- package/dist/config/config-hooks.test.node.d.ts +1 -0
- package/dist/config/config-hooks.test.node.js +56 -0
- package/dist/config/config.d.ts +9 -5
- package/dist/config/config.js +20 -2
- package/dist/config/routes.d.ts +5 -5
- package/dist/config/routes.js +42 -9
- package/dist/config/routes.test.node.d.ts +1 -0
- package/dist/config/routes.test.node.js +152 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +20 -14
- package/dist/core.test.node.d.ts +1 -0
- package/dist/core.test.node.js +28 -0
- package/dist/host/host-request-bridge.d.ts +62 -0
- package/dist/host/host-request-bridge.js +38 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +14 -0
- package/dist/query/parse-where.d.ts +9 -0
- package/dist/query/parse-where.js +146 -2
- package/dist/query/parse-where.test.node.js +60 -1
- package/dist/services/collection-bootstrap.test.node.js +30 -59
- package/dist/services/discover-counter-groups.test.node.js +23 -0
- package/dist/services/document-lifecycle/audit.d.ts +22 -1
- package/dist/services/document-lifecycle/audit.js +32 -1
- package/dist/services/document-lifecycle/create.js +13 -6
- package/dist/services/document-lifecycle/delete.d.ts +17 -1
- package/dist/services/document-lifecycle/delete.js +91 -26
- package/dist/services/document-lifecycle/index.d.ts +1 -1
- package/dist/services/document-lifecycle/internals.d.ts +0 -20
- package/dist/services/document-lifecycle/internals.js +22 -46
- package/dist/services/document-lifecycle/status.d.ts +1 -1
- package/dist/services/document-lifecycle/status.js +20 -3
- package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
- package/dist/services/document-lifecycle/system-fields.js +112 -74
- package/dist/services/document-lifecycle/tree.d.ts +22 -24
- package/dist/services/document-lifecycle/tree.js +244 -123
- package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/tree.test.node.js +663 -0
- package/dist/services/document-lifecycle/update.js +2 -1
- package/dist/services/document-lifecycle.test.node.js +360 -16
- package/dist/services/document-read.d.ts +14 -6
- package/dist/services/document-read.js +47 -9
- package/dist/services/field-upload.test.node.js +70 -0
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +2 -2
- package/dist/services/populate.d.ts +7 -3
- package/dist/services/populate.js +180 -28
- package/dist/services/populate.test.node.js +59 -0
- package/dist/services/richtext-embed.d.ts +39 -2
- package/dist/services/richtext-embed.js +4 -34
- package/dist/services/richtext-embed.test.node.js +15 -0
- package/dist/services/richtext-populate.d.ts +20 -2
- package/dist/services/richtext-populate.js +160 -19
- package/dist/services/richtext-populate.test.node.js +523 -2
- package/dist/utils/root-relative-redirect.d.ts +6 -0
- package/dist/utils/root-relative-redirect.js +37 -0
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* branch C (per-leaf error swallow). The visitor-side branches (A / B /
|
|
12
12
|
* found) live in `@byline/richtext-lexical` and have their own suite.
|
|
13
13
|
*/
|
|
14
|
+
import { createSuperAdminContext } from '@byline/auth';
|
|
14
15
|
import { describe, expect, it, vi } from 'vitest';
|
|
15
16
|
import { embedRichTextFields } from './richtext-embed.js';
|
|
16
17
|
const noopLogger = {
|
|
@@ -24,6 +25,11 @@ const noopLogger = {
|
|
|
24
25
|
silent: vi.fn(),
|
|
25
26
|
};
|
|
26
27
|
const fakeReadContext = {};
|
|
28
|
+
const authOptions = {
|
|
29
|
+
requestContext: createSuperAdminContext(),
|
|
30
|
+
readMode: 'published',
|
|
31
|
+
readDocuments: vi.fn(),
|
|
32
|
+
};
|
|
27
33
|
const richTextValue = (label) => ({
|
|
28
34
|
root: { type: 'root', children: [], _label: label },
|
|
29
35
|
});
|
|
@@ -56,9 +62,15 @@ describe('embedRichTextFields', () => {
|
|
|
56
62
|
data,
|
|
57
63
|
embed,
|
|
58
64
|
readContext: fakeReadContext,
|
|
65
|
+
...authOptions,
|
|
59
66
|
logger: noopLogger,
|
|
60
67
|
});
|
|
61
68
|
expect(embed).toHaveBeenCalledTimes(3);
|
|
69
|
+
expect(embed).toHaveBeenCalledWith(expect.objectContaining({
|
|
70
|
+
requestContext: authOptions.requestContext,
|
|
71
|
+
readMode: 'published',
|
|
72
|
+
readDocuments: authOptions.readDocuments,
|
|
73
|
+
}));
|
|
62
74
|
const paths = embed.mock.calls.map((call) => call[0].fieldPath);
|
|
63
75
|
expect(paths.sort()).toEqual(['body', 'faq.0.answer', 'meta.summary']);
|
|
64
76
|
});
|
|
@@ -83,6 +95,7 @@ describe('embedRichTextFields', () => {
|
|
|
83
95
|
data,
|
|
84
96
|
embed,
|
|
85
97
|
readContext: fakeReadContext,
|
|
98
|
+
...authOptions,
|
|
86
99
|
logger: noopLogger,
|
|
87
100
|
});
|
|
88
101
|
expect(embed).toHaveBeenCalledTimes(1);
|
|
@@ -116,6 +129,7 @@ describe('embedRichTextFields', () => {
|
|
|
116
129
|
data,
|
|
117
130
|
embed,
|
|
118
131
|
readContext: fakeReadContext,
|
|
132
|
+
...authOptions,
|
|
119
133
|
logger,
|
|
120
134
|
})).resolves.toBeUndefined();
|
|
121
135
|
// All three leaves were attempted — the failure in 'second' did not
|
|
@@ -142,6 +156,7 @@ describe('embedRichTextFields', () => {
|
|
|
142
156
|
data,
|
|
143
157
|
embed,
|
|
144
158
|
readContext: fakeReadContext,
|
|
159
|
+
...authOptions,
|
|
145
160
|
logger: noopLogger,
|
|
146
161
|
});
|
|
147
162
|
expect(embed).not.toHaveBeenCalled();
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
* each call has a precise `fieldPath` for error messages and so future
|
|
24
24
|
* adapters can implement per-leaf caching if needed.
|
|
25
25
|
*/
|
|
26
|
-
import
|
|
26
|
+
import type { RequestContext } from '@byline/auth';
|
|
27
|
+
import { type FieldSet, type RichTextField, type RichTextPopulateFn, type RichTextReadDocumentsFn } from '../@types/field-types.js';
|
|
27
28
|
import type { CollectionDefinition } from '../@types/collection-types.js';
|
|
28
|
-
import type { ReadContext } from '../@types/index.js';
|
|
29
|
+
import type { IDbAdapter, ReadContext, ReadMode } from '../@types/index.js';
|
|
29
30
|
/**
|
|
30
31
|
* One rich-text leaf yielded by `collectRichTextLeaves`. The walker hands
|
|
31
32
|
* back a reference to the *parent container* (`parent[key]`) rather than
|
|
@@ -61,6 +62,9 @@ export interface PopulateRichTextFieldsOptions {
|
|
|
61
62
|
/** Registered server-side populate function from `ServerConfig`. */
|
|
62
63
|
populate: RichTextPopulateFn;
|
|
63
64
|
readContext: ReadContext;
|
|
65
|
+
requestContext: RequestContext;
|
|
66
|
+
readMode: ReadMode;
|
|
67
|
+
readDocuments: RichTextReadDocumentsFn;
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
66
70
|
* Resolve the effective `populateRelationsOnRead` for a richText field.
|
|
@@ -76,6 +80,20 @@ export declare function resolvePopulateOnRead(field: RichTextField): boolean;
|
|
|
76
80
|
* is `true`. Mutates document `fields` in place.
|
|
77
81
|
*/
|
|
78
82
|
export declare function populateRichTextFields(options: PopulateRichTextFieldsOptions): Promise<void>;
|
|
83
|
+
/** Build the secure batch reader exposed to editor-agnostic adapters. */
|
|
84
|
+
export declare function createRichTextDocumentReader(options: {
|
|
85
|
+
db: IDbAdapter;
|
|
86
|
+
collections: readonly CollectionDefinition[];
|
|
87
|
+
requestContext: RequestContext;
|
|
88
|
+
readContext: ReadContext;
|
|
89
|
+
readMode: ReadMode;
|
|
90
|
+
locale?: string;
|
|
91
|
+
bypassBeforeRead?: true;
|
|
92
|
+
/** Private cache domain explicitly shared with the originating client read. */
|
|
93
|
+
securityDomain?: object;
|
|
94
|
+
/** Adapter reused recursively for rich-text fields on target documents. */
|
|
95
|
+
richTextPopulate?: RichTextPopulateFn;
|
|
96
|
+
}): RichTextReadDocumentsFn;
|
|
79
97
|
/**
|
|
80
98
|
* Which richtext server adapters the host has registered. Pass both
|
|
81
99
|
* flags so the validator can fail-fast on each missing-adapter case
|
|
@@ -5,25 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Richtext populate service — walks a reconstructed document, finds every
|
|
10
|
-
* rich-text leaf (including those nested inside `group` / `array` /
|
|
11
|
-
* `blocks` structures), gates each leaf by its `populateRelationsOnRead`
|
|
12
|
-
* flag, and dispatches to the registered richtext populate adapter.
|
|
13
|
-
*
|
|
14
|
-
* Slots into the read pipeline alongside `populateDocuments`:
|
|
15
|
-
*
|
|
16
|
-
* findDocuments → reconstruct → populateDocuments → populateRichTextFields → afterRead
|
|
17
|
-
*
|
|
18
|
-
* The same `ReadContext` flows through both populate phases, so dedup /
|
|
19
|
-
* cycle protection / read-budget enforcement covers rich-text fan-out
|
|
20
|
-
* automatically and any nested reads the adapter performs.
|
|
21
|
-
*
|
|
22
|
-
* The adapter is invoked once per leaf rather than once per document so
|
|
23
|
-
* each call has a precise `fieldPath` for error messages and so future
|
|
24
|
-
* adapters can implement per-leaf caching if needed.
|
|
25
|
-
*/
|
|
26
8
|
import { isArrayField, isBlocksField, isGroupField, } from '../@types/field-types.js';
|
|
9
|
+
import { bindReadContextAuthority, compileBeforeReadFilters } from '../auth/apply-before-read.js';
|
|
10
|
+
import { assertActorCanPerform } from '../auth/assert-actor-can-perform.js';
|
|
11
|
+
import { resolveReadContextRoot } from '../auth/read-context-scope.js';
|
|
12
|
+
import { ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION } from '../lib/errors.js';
|
|
13
|
+
import { applyAfterRead } from './document-read.js';
|
|
27
14
|
import { walkFieldTree } from './walk-field-tree.js';
|
|
28
15
|
/**
|
|
29
16
|
* Walk a field set and a matching reconstructed data tree in lockstep,
|
|
@@ -66,7 +53,7 @@ export function resolvePopulateOnRead(field) {
|
|
|
66
53
|
* is `true`. Mutates document `fields` in place.
|
|
67
54
|
*/
|
|
68
55
|
export async function populateRichTextFields(options) {
|
|
69
|
-
const { fields, collectionPath, documents, populate, readContext } = options;
|
|
56
|
+
const { fields, collectionPath, documents, populate, readContext, requestContext, readMode, readDocuments, } = options;
|
|
70
57
|
for (const doc of documents) {
|
|
71
58
|
const docFields = (doc.fields ?? {});
|
|
72
59
|
for (const leaf of collectRichTextLeaves(fields, docFields)) {
|
|
@@ -77,10 +64,164 @@ export async function populateRichTextFields(options) {
|
|
|
77
64
|
fieldPath: leaf.fieldPath,
|
|
78
65
|
collectionPath,
|
|
79
66
|
readContext,
|
|
67
|
+
requestContext,
|
|
68
|
+
readMode,
|
|
69
|
+
readDocuments,
|
|
80
70
|
});
|
|
81
71
|
}
|
|
82
72
|
}
|
|
83
73
|
}
|
|
74
|
+
/** Build the secure batch reader exposed to editor-agnostic adapters. */
|
|
75
|
+
export function createRichTextDocumentReader(options) {
|
|
76
|
+
const { db, collections, requestContext, readContext, readMode, locale, bypassBeforeRead, richTextPopulate, } = options;
|
|
77
|
+
const securityDomain = options.securityDomain ?? {};
|
|
78
|
+
const state = getRichTextReaderState(readContext);
|
|
79
|
+
const read = async ({ collectionPath, documentIds, fields }) => {
|
|
80
|
+
bindReadContextAuthority(readContext, requestContext);
|
|
81
|
+
if (documentIds.length === 0)
|
|
82
|
+
return [];
|
|
83
|
+
const definition = collections.find((candidate) => candidate.path === collectionPath);
|
|
84
|
+
if (definition == null) {
|
|
85
|
+
throw ERR_VALIDATION({
|
|
86
|
+
message: `richtext target collection '${collectionPath}' is not registered`,
|
|
87
|
+
details: { collectionPath },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
assertActorCanPerform(requestContext, collectionPath, 'read');
|
|
91
|
+
let filters;
|
|
92
|
+
if (!bypassBeforeRead) {
|
|
93
|
+
filters = await compileBeforeReadFilters({
|
|
94
|
+
definition,
|
|
95
|
+
requestContext,
|
|
96
|
+
readContext,
|
|
97
|
+
securityDomain,
|
|
98
|
+
parseContext: {
|
|
99
|
+
collections,
|
|
100
|
+
resolveCollectionId: async (path) => {
|
|
101
|
+
const row = await db.queries.collections.getCollectionByPath(path);
|
|
102
|
+
return row?.id ?? '';
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const collection = await db.queries.collections.getCollectionByPath(collectionPath);
|
|
108
|
+
if (collection?.id == null) {
|
|
109
|
+
throw ERR_VALIDATION({
|
|
110
|
+
message: `richtext target collection '${collectionPath}' is not available`,
|
|
111
|
+
details: { collectionPath },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const collectionId = collection.id;
|
|
115
|
+
const projection = fields == null ? '*' : [...fields].sort().join(',');
|
|
116
|
+
const resultById = new Map();
|
|
117
|
+
const idsToFetch = [];
|
|
118
|
+
for (const documentId of new Set(documentIds)) {
|
|
119
|
+
const key = richTextMaterializationKey(collectionId, documentId, requestContext.requestId, locale, readMode, projection);
|
|
120
|
+
if (state.cache.has(key)) {
|
|
121
|
+
const cached = state.cache.get(key);
|
|
122
|
+
if (cached != null)
|
|
123
|
+
resultById.set(documentId, cached);
|
|
124
|
+
}
|
|
125
|
+
else if (!state.active.has(`${collectionId}:${documentId}`)) {
|
|
126
|
+
idsToFetch.push(documentId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (idsToFetch.length > 0) {
|
|
130
|
+
const fetched = (await db.queries.documents.getDocumentsByDocumentIds({
|
|
131
|
+
collection_id: collectionId,
|
|
132
|
+
document_ids: idsToFetch,
|
|
133
|
+
fields,
|
|
134
|
+
readMode,
|
|
135
|
+
locale,
|
|
136
|
+
filters: filters && filters.length > 0 ? filters : undefined,
|
|
137
|
+
}));
|
|
138
|
+
const fetchedById = new Map(fetched
|
|
139
|
+
.filter((doc) => typeof doc.document_id === 'string')
|
|
140
|
+
.map((doc) => [doc.document_id, doc]));
|
|
141
|
+
for (const documentId of idsToFetch) {
|
|
142
|
+
const materializationKey = richTextMaterializationKey(collectionId, documentId, requestContext.requestId, locale, readMode, projection);
|
|
143
|
+
// Earlier items in this batch may recursively populate a later target.
|
|
144
|
+
// Honour that completed/active state rather than reprocessing the stale
|
|
145
|
+
// raw row captured by the outer batch and overwriting its redacted cache.
|
|
146
|
+
if (state.cache.has(materializationKey)) {
|
|
147
|
+
const cached = state.cache.get(materializationKey);
|
|
148
|
+
if (cached != null)
|
|
149
|
+
resultById.set(documentId, cached);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const documentKey = `${collectionId}:${documentId}`;
|
|
153
|
+
if (state.active.has(documentKey))
|
|
154
|
+
continue;
|
|
155
|
+
const doc = fetchedById.get(documentId);
|
|
156
|
+
if (doc == null) {
|
|
157
|
+
state.cache.set(materializationKey, null);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
state.active.add(documentKey);
|
|
161
|
+
readContext.visited.add(documentKey);
|
|
162
|
+
try {
|
|
163
|
+
readContext.readCount += 1;
|
|
164
|
+
if (readContext.readCount > readContext.maxReads) {
|
|
165
|
+
throw ERR_READ_BUDGET_EXCEEDED({
|
|
166
|
+
message: `richtext populate exceeded read budget (maxReads=${readContext.maxReads})`,
|
|
167
|
+
details: {
|
|
168
|
+
readCount: readContext.readCount,
|
|
169
|
+
maxReads: readContext.maxReads,
|
|
170
|
+
targetCollectionId: collectionId,
|
|
171
|
+
targetDocumentId: documentId,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (richTextPopulate) {
|
|
176
|
+
await populateRichTextFields({
|
|
177
|
+
fields: definition.fields,
|
|
178
|
+
collectionPath,
|
|
179
|
+
documents: [doc],
|
|
180
|
+
populate: richTextPopulate,
|
|
181
|
+
readContext,
|
|
182
|
+
requestContext,
|
|
183
|
+
readMode,
|
|
184
|
+
readDocuments: read,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
await applyAfterRead({
|
|
188
|
+
doc,
|
|
189
|
+
definition,
|
|
190
|
+
readContext,
|
|
191
|
+
requestContext,
|
|
192
|
+
locale,
|
|
193
|
+
readMode,
|
|
194
|
+
projection: fields,
|
|
195
|
+
materialization: 'richtext-target',
|
|
196
|
+
});
|
|
197
|
+
state.cache.set(materializationKey, doc);
|
|
198
|
+
resultById.set(documentId, doc);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
state.active.delete(documentKey);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return documentIds.flatMap((documentId) => {
|
|
206
|
+
const doc = resultById.get(documentId);
|
|
207
|
+
return doc == null ? [] : [doc];
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
return read;
|
|
211
|
+
}
|
|
212
|
+
const richTextReaderStates = new WeakMap();
|
|
213
|
+
function getRichTextReaderState(readContext) {
|
|
214
|
+
const root = resolveReadContextRoot(readContext);
|
|
215
|
+
const existing = richTextReaderStates.get(root);
|
|
216
|
+
if (existing)
|
|
217
|
+
return existing;
|
|
218
|
+
const state = { active: new Set(), cache: new Map() };
|
|
219
|
+
richTextReaderStates.set(root, state);
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
function richTextMaterializationKey(collectionId, documentId, requestId, locale, readMode, projection) {
|
|
223
|
+
return `${collectionId}:${documentId}:${requestId}:${locale ?? 'all'}:${readMode}:${projection}`;
|
|
224
|
+
}
|
|
84
225
|
// ---------------------------------------------------------------------------
|
|
85
226
|
// Boot-time validation
|
|
86
227
|
// ---------------------------------------------------------------------------
|