@focus-reactive/payload-plugin-translator 0.4.0 → 0.5.1
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/README.md +44 -29
- package/dist/client/shared/ui/Button/Button.d.ts +4 -4
- package/dist/client/shared/ui/Button/Button.js +18 -18
- package/dist/client/shared/ui/Button/styles.module.scss +1 -1
- package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
- package/dist/client/shared/ui/Popup/Popup.js +4 -3
- package/dist/client/shared/ui/Select/Select.d.ts +9 -7
- package/dist/client/shared/ui/Select/Select.js +18 -17
- package/dist/client/shared/ui/Select/styles.module.scss +5 -0
- package/dist/index.d.ts +16 -14
- package/dist/index.js +9 -7
- package/dist/plugin.d.ts +15 -4
- package/dist/plugin.js +34 -57
- package/dist/server/features/cancel/handler.d.ts +2 -2
- package/dist/server/features/cancel/handler.js +3 -1
- package/dist/server/features/cancel/route.d.ts +4 -4
- package/dist/server/features/cancel/route.js +4 -4
- package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
- package/dist/server/features/cancel-by-collection/handler.js +1 -1
- package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
- package/dist/server/features/cancel-by-collection/route.js +4 -4
- package/dist/server/features/createTranslationRoutes.d.ts +23 -0
- package/dist/server/features/createTranslationRoutes.js +27 -0
- package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
- package/dist/server/features/enqueue-translation/handler.js +1 -1
- package/dist/server/features/enqueue-translation/route.d.ts +5 -5
- package/dist/server/features/enqueue-translation/route.js +4 -4
- package/dist/server/features/get-collection-status/handler.d.ts +3 -3
- package/dist/server/features/get-collection-status/handler.js +1 -1
- package/dist/server/features/get-collection-status/route.d.ts +5 -5
- package/dist/server/features/get-collection-status/route.js +4 -4
- package/dist/server/features/get-document-status/handler.d.ts +3 -3
- package/dist/server/features/get-document-status/handler.js +1 -1
- package/dist/server/features/get-document-status/route.d.ts +5 -5
- package/dist/server/features/get-document-status/route.js +4 -4
- package/dist/server/features/index.d.ts +7 -6
- package/dist/server/features/index.js +7 -6
- package/dist/server/features/run-translation/handler.d.ts +2 -2
- package/dist/server/features/run-translation/route.d.ts +4 -4
- package/dist/server/features/run-translation/route.js +4 -4
- package/dist/server/features/translate-document/handler.js +7 -11
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
- package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +18 -1
- package/dist/server/modules/task-runner/index.d.ts +6 -6
- package/dist/server/modules/task-runner/index.js +2 -2
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +16 -1
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
- package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
- package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
- package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
- package/dist/server/modules/translation-levels/documentLevel.js +28 -0
- package/dist/server/modules/translation-levels/index.d.ts +3 -0
- package/dist/server/modules/translation-levels/index.js +4 -0
- package/dist/server/modules/translation-levels/types.d.ts +42 -0
- package/dist/server/modules/translation-levels/types.js +10 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
- package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
- package/dist/server/modules/translation-pipeline/index.js +3 -2
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
- package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
- package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
- package/dist/server/shared/field-traversal/index.d.ts +5 -0
- package/dist/server/shared/field-traversal/index.js +5 -0
- package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
- package/dist/server/shared/field-traversal/kernel.js +145 -0
- package/dist/server/shared/field-traversal/types.d.ts +191 -0
- package/dist/server/shared/field-traversal/types.js +41 -0
- package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
- package/dist/server/shared/field-traversal/walkFields.js +164 -0
- package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
- package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
- package/dist/types/AccessGuard.d.ts +24 -1
- package/dist/types/AccessGuard.js +12 -1
- package/package.json +2 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { classifyField, tabScopes } from "./kernel";
|
|
2
|
+
/**
|
|
3
|
+
* Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
|
|
4
|
+
* with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
|
|
5
|
+
* (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
|
|
6
|
+
* path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
|
|
7
|
+
* in one place.
|
|
8
|
+
*
|
|
9
|
+
* The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
|
|
10
|
+
* a field's config is shared across array items, so indices never appear in the schema).
|
|
11
|
+
*
|
|
12
|
+
* @param fields - The schema level to search.
|
|
13
|
+
* @param segments - Remaining path segments (names) to match, head-first.
|
|
14
|
+
* @returns A {@link FieldPathResult}.
|
|
15
|
+
* @public
|
|
16
|
+
*/ export function findFieldByPath(fields, segments) {
|
|
17
|
+
const [head, ...rest] = segments;
|
|
18
|
+
if (head === undefined) return {
|
|
19
|
+
status: "not-found"
|
|
20
|
+
};
|
|
21
|
+
for (const field of fields){
|
|
22
|
+
const structure = classifyField(field);
|
|
23
|
+
switch(structure.kind){
|
|
24
|
+
case "tabs":
|
|
25
|
+
{
|
|
26
|
+
for (const scope of tabScopes(structure.field)){
|
|
27
|
+
if (scope.named) {
|
|
28
|
+
if (scope.tab.name === head) {
|
|
29
|
+
return rest.length === 0 ? {
|
|
30
|
+
status: "container"
|
|
31
|
+
} : findFieldByPath(scope.tab.fields, rest);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const found = findFieldByPath(scope.fields, segments); // unnamed tab → same path scope
|
|
35
|
+
if (found.status !== "not-found") return found;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "transparent":
|
|
41
|
+
{
|
|
42
|
+
const found = findFieldByPath(structure.fields, segments); // row/collapsible/unnamed group → same scope
|
|
43
|
+
if (found.status !== "not-found") return found;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "presentational":
|
|
47
|
+
break;
|
|
48
|
+
case "group":
|
|
49
|
+
case "array":
|
|
50
|
+
{
|
|
51
|
+
if (structure.name !== head) break;
|
|
52
|
+
return rest.length === 0 ? {
|
|
53
|
+
status: "container"
|
|
54
|
+
} : findFieldByPath(structure.fields, rest);
|
|
55
|
+
}
|
|
56
|
+
case "blocks":
|
|
57
|
+
{
|
|
58
|
+
if (structure.name !== head) break;
|
|
59
|
+
return rest.length === 0 ? {
|
|
60
|
+
status: "container"
|
|
61
|
+
} : {
|
|
62
|
+
status: "inside-blocks"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
case "leaf":
|
|
66
|
+
{
|
|
67
|
+
if (structure.name !== head) break;
|
|
68
|
+
return rest.length === 0 ? {
|
|
69
|
+
status: "leaf",
|
|
70
|
+
field: structure.field
|
|
71
|
+
} : {
|
|
72
|
+
status: "not-found"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
{
|
|
77
|
+
// Exhaustiveness guard: a new FieldStructure kind would error here at compile time.
|
|
78
|
+
const exhaustive = structure;
|
|
79
|
+
throw new Error(`unhandled field structure: ${String(exhaustive)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
status: "not-found"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=findFieldByPath.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { findFieldByPath } from "./findFieldByPath";
|
|
2
|
+
export type { FieldPathResult } from "./findFieldByPath";
|
|
3
|
+
export { classifyField, resolveBlockFields, tabScopes } from "./kernel";
|
|
4
|
+
export type { ChildCursor, ChildOutput, ContainerInfo, FieldStructure, FieldWalker, LeafField, TabScope, WalkSignal } from "./types";
|
|
5
|
+
export { walkFields } from "./walkFields";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { BlocksField, Field, TabsField } from "payload";
|
|
2
|
+
import type { FieldStructure, TabScope } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
|
|
5
|
+
* one place that encodes how Payload field types map onto data boundaries.
|
|
6
|
+
*
|
|
7
|
+
* Traversals switch on the result instead of re-deriving the predicate chain
|
|
8
|
+
* (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
|
|
9
|
+
* field type — lives in exactly one place.
|
|
10
|
+
*
|
|
11
|
+
* @param field - Any field from a collection/global `fields` array (original, un-sanitized
|
|
12
|
+
* config).
|
|
13
|
+
* @returns The field's {@link FieldStructure}:
|
|
14
|
+
* - `tabs` — a `tabs` field (expand with {@link tabScopes}).
|
|
15
|
+
* - `transparent` — a presentational container that opens **no** data boundary (`row`,
|
|
16
|
+
* `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
|
|
17
|
+
* - `presentational` — a `ui` field: no data, no subfields.
|
|
18
|
+
* - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
|
|
19
|
+
* except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
|
|
20
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
21
|
+
*
|
|
22
|
+
* Dispatch order is significant: some fields satisfy more than one predicate and only the
|
|
23
|
+
* order disambiguates them.
|
|
24
|
+
* - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
|
|
25
|
+
* so a later check would mislabel it `presentational`.
|
|
26
|
+
* - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
|
|
27
|
+
* `transparent`, never a `group` with `name: undefined`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const structure = classifyField(field);
|
|
32
|
+
* switch (structure.kind) {
|
|
33
|
+
* case "group": descend(structure.fields, data[structure.name]); break;
|
|
34
|
+
* case "leaf": translate(structure.field, structure.name); break;
|
|
35
|
+
* // ...handle the remaining kinds
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @see {@link walkFields} — the recursive engine built on this classification.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export declare function classifyField(field: Field): FieldStructure;
|
|
43
|
+
/**
|
|
44
|
+
* Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
|
|
45
|
+
* `hasFields` handling that field traversals would otherwise each repeat.
|
|
46
|
+
*
|
|
47
|
+
* @param field - A Payload `tabs` field.
|
|
48
|
+
* @returns One {@link TabScope} per tab that has fields, in declaration order:
|
|
49
|
+
* - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
|
|
50
|
+
* the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
|
|
51
|
+
* - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
|
|
52
|
+
* scope, so only the `fields` are needed.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* for (const scope of tabScopes(tabsField)) {
|
|
57
|
+
* if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
|
|
58
|
+
* else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export declare function tabScopes(field: TabsField): TabScope[];
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the child `fields` for a single `blocks` element by matching its `blockType`
|
|
66
|
+
* against the field's block definitions.
|
|
67
|
+
*
|
|
68
|
+
* @param field - A Payload `blocks` field carrying inline block definitions.
|
|
69
|
+
* @param item - The data for one block element (expected to carry a `blockType` string).
|
|
70
|
+
* @returns The matched block's `fields`, or `null` when `item` is not a block object or its
|
|
71
|
+
* `blockType` matches no defined block.
|
|
72
|
+
*
|
|
73
|
+
* Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
|
|
74
|
+
* `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
|
|
75
|
+
* against the top-level config — not handled here by design, since this package traverses
|
|
76
|
+
* the original schema to preserve nested `localized` flags.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const fields = resolveBlockFields(blocksField, item);
|
|
81
|
+
* if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
|
|
82
|
+
* ```
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveBlockFields(field: BlocksField, item: unknown): Field[] | null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
|
|
2
|
+
import { hasFields, isBlockItem, isTabsField } from "../guards";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
|
|
5
|
+
* one place that encodes how Payload field types map onto data boundaries.
|
|
6
|
+
*
|
|
7
|
+
* Traversals switch on the result instead of re-deriving the predicate chain
|
|
8
|
+
* (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
|
|
9
|
+
* field type — lives in exactly one place.
|
|
10
|
+
*
|
|
11
|
+
* @param field - Any field from a collection/global `fields` array (original, un-sanitized
|
|
12
|
+
* config).
|
|
13
|
+
* @returns The field's {@link FieldStructure}:
|
|
14
|
+
* - `tabs` — a `tabs` field (expand with {@link tabScopes}).
|
|
15
|
+
* - `transparent` — a presentational container that opens **no** data boundary (`row`,
|
|
16
|
+
* `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
|
|
17
|
+
* - `presentational` — a `ui` field: no data, no subfields.
|
|
18
|
+
* - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
|
|
19
|
+
* except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
|
|
20
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
21
|
+
*
|
|
22
|
+
* Dispatch order is significant: some fields satisfy more than one predicate and only the
|
|
23
|
+
* order disambiguates them.
|
|
24
|
+
* - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
|
|
25
|
+
* so a later check would mislabel it `presentational`.
|
|
26
|
+
* - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
|
|
27
|
+
* `transparent`, never a `group` with `name: undefined`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const structure = classifyField(field);
|
|
32
|
+
* switch (structure.kind) {
|
|
33
|
+
* case "group": descend(structure.fields, data[structure.name]); break;
|
|
34
|
+
* case "leaf": translate(structure.field, structure.name); break;
|
|
35
|
+
* // ...handle the remaining kinds
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @see {@link walkFields} — the recursive engine built on this classification.
|
|
40
|
+
* @public
|
|
41
|
+
*/ export function classifyField(field) {
|
|
42
|
+
if (isTabsField(field)) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "tabs",
|
|
45
|
+
field
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!fieldAffectsData(field)) {
|
|
49
|
+
return hasFields(field) ? {
|
|
50
|
+
kind: "transparent",
|
|
51
|
+
fields: field.fields
|
|
52
|
+
} : {
|
|
53
|
+
kind: "presentational"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (fieldIsGroupType(field)) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "group",
|
|
59
|
+
name: field.name,
|
|
60
|
+
fields: field.fields,
|
|
61
|
+
field
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (fieldIsArrayType(field)) {
|
|
65
|
+
return {
|
|
66
|
+
kind: "array",
|
|
67
|
+
name: field.name,
|
|
68
|
+
fields: field.fields,
|
|
69
|
+
field
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (fieldIsBlockType(field)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "blocks",
|
|
75
|
+
name: field.name,
|
|
76
|
+
field
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const leaf = field;
|
|
80
|
+
return {
|
|
81
|
+
kind: "leaf",
|
|
82
|
+
name: leaf.name,
|
|
83
|
+
field: leaf
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
|
|
88
|
+
* `hasFields` handling that field traversals would otherwise each repeat.
|
|
89
|
+
*
|
|
90
|
+
* @param field - A Payload `tabs` field.
|
|
91
|
+
* @returns One {@link TabScope} per tab that has fields, in declaration order:
|
|
92
|
+
* - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
|
|
93
|
+
* the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
|
|
94
|
+
* - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
|
|
95
|
+
* scope, so only the `fields` are needed.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* for (const scope of tabScopes(tabsField)) {
|
|
100
|
+
* if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
|
|
101
|
+
* else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
* @public
|
|
105
|
+
*/ export function tabScopes(field) {
|
|
106
|
+
const scopes = [];
|
|
107
|
+
for (const tab of field.tabs){
|
|
108
|
+
if (tabHasName(tab)) scopes.push({
|
|
109
|
+
named: true,
|
|
110
|
+
tab
|
|
111
|
+
});
|
|
112
|
+
else if (hasFields(tab)) scopes.push({
|
|
113
|
+
named: false,
|
|
114
|
+
fields: tab.fields
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return scopes;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the child `fields` for a single `blocks` element by matching its `blockType`
|
|
121
|
+
* against the field's block definitions.
|
|
122
|
+
*
|
|
123
|
+
* @param field - A Payload `blocks` field carrying inline block definitions.
|
|
124
|
+
* @param item - The data for one block element (expected to carry a `blockType` string).
|
|
125
|
+
* @returns The matched block's `fields`, or `null` when `item` is not a block object or its
|
|
126
|
+
* `blockType` matches no defined block.
|
|
127
|
+
*
|
|
128
|
+
* Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
|
|
129
|
+
* `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
|
|
130
|
+
* against the top-level config — not handled here by design, since this package traverses
|
|
131
|
+
* the original schema to preserve nested `localized` flags.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const fields = resolveBlockFields(blocksField, item);
|
|
136
|
+
* if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
|
|
137
|
+
* ```
|
|
138
|
+
* @public
|
|
139
|
+
*/ export function resolveBlockFields(field, item) {
|
|
140
|
+
if (!isBlockItem(item)) return null;
|
|
141
|
+
const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
|
|
142
|
+
return block ? block.fields : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=kernel.js.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ArrayField, BlocksField, Field, FieldAffectingData, NamedGroupField, NamedTab, TabAsField, TabsField } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* A data-affecting field the engine routes to `leaf`: a scalar/relational leaf, never a
|
|
4
|
+
* container (`group`/`array`/`blocks`) and never a `TabAsField`. Narrowing `FieldAffectingData`
|
|
5
|
+
* down to these members is what guarantees `name: string` — the raw union does NOT, because
|
|
6
|
+
* an unnamed `TabAsField` carries `name?: string`.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type LeafField = Exclude<FieldAffectingData, ArrayField | BlocksField | NamedGroupField | TabAsField>;
|
|
11
|
+
/**
|
|
12
|
+
* Structural classification of a single Payload field — the one place that encodes
|
|
13
|
+
* how Payload field types map onto data boundaries (the dispatch order
|
|
14
|
+
* `tabs → transparent → group → array → blocks → leaf`).
|
|
15
|
+
*
|
|
16
|
+
* Consumed by {@link FieldWalker}/`walkFields` (the exhaustive, data-parallel walks)
|
|
17
|
+
* and by path-navigation; the four hand-rolled traversals collapse onto it so the
|
|
18
|
+
* dispatch lives in exactly one place.
|
|
19
|
+
*
|
|
20
|
+
* - `transparent` — presentational container that does NOT open a data boundary
|
|
21
|
+
* (row, collapsible, unnamed group). Its `fields` live in the SAME data scope as
|
|
22
|
+
* the parent.
|
|
23
|
+
* - `presentational` — a `ui` leaf: no data, no subfields.
|
|
24
|
+
* - `group`/`array`/`blocks` — open a data boundary at `name`.
|
|
25
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
26
|
+
*
|
|
27
|
+
* @see {@link classifyField} — produces this; {@link walkFields} — consumes it.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type FieldStructure = {
|
|
31
|
+
kind: "tabs";
|
|
32
|
+
field: TabsField;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "transparent";
|
|
35
|
+
fields: Field[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "presentational";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "group";
|
|
40
|
+
name: string;
|
|
41
|
+
fields: Field[];
|
|
42
|
+
field: NamedGroupField;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "array";
|
|
45
|
+
name: string;
|
|
46
|
+
fields: Field[];
|
|
47
|
+
field: ArrayField;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "blocks";
|
|
50
|
+
name: string;
|
|
51
|
+
field: BlocksField;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "leaf";
|
|
54
|
+
name: string;
|
|
55
|
+
field: LeafField;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A tab flattened by `tabScopes`. A named tab opens a data boundary, so its `NamedTab`
|
|
59
|
+
* is surfaced (the walker passes it to `enterObject`, and `tab.name` is the data key).
|
|
60
|
+
* An unnamed tab flattens into the parent data scope, so only its `fields` are needed.
|
|
61
|
+
*
|
|
62
|
+
* @see {@link tabScopes}
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type TabScope = {
|
|
66
|
+
named: true;
|
|
67
|
+
tab: NamedTab;
|
|
68
|
+
} | {
|
|
69
|
+
named: false;
|
|
70
|
+
fields: Field[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Control signal a walker callback can return instead of descending.
|
|
74
|
+
* - `skip` — do not descend this branch; continue with siblings.
|
|
75
|
+
* - `stop` — halt the whole walk (used for path-style navigation / early-exit).
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export type WalkSignal = "skip" | "stop";
|
|
80
|
+
/**
|
|
81
|
+
* One child to recurse under an array/blocks boundary. The caller derives these from its own
|
|
82
|
+
* data (it alone knows how many elements there are and their shape) and returns them from
|
|
83
|
+
* {@link FieldWalker.enterList}.
|
|
84
|
+
*
|
|
85
|
+
* @template Cursor - The walker's data-position type (see {@link FieldWalker}).
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export interface ChildCursor<Cursor extends object> {
|
|
89
|
+
cursor: Cursor;
|
|
90
|
+
/** Child fields to walk under this item. For `blocks`, the resolved `block.fields`. */
|
|
91
|
+
fields: Field[];
|
|
92
|
+
/** Index/key this child sits at; surfaced back to `combine`. */
|
|
93
|
+
key: string | number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The container the engine asks the walker to assemble, bottom-up.
|
|
97
|
+
*
|
|
98
|
+
* - `root` — the top-level field list (`field` is `null`).
|
|
99
|
+
* - `object` — a `group` or a named tab.
|
|
100
|
+
* - `list` — an `array` or `blocks` collection (children are its elements, keyed by index).
|
|
101
|
+
* - `element` — a single element of a list (children are its fields). `field` is the
|
|
102
|
+
* parent list field; distinguish array vs blocks via `field.type`.
|
|
103
|
+
*
|
|
104
|
+
* @see {@link FieldWalker.combine}
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export type ContainerInfo = {
|
|
108
|
+
kind: "root";
|
|
109
|
+
field: null;
|
|
110
|
+
} | {
|
|
111
|
+
kind: "object";
|
|
112
|
+
field: NamedGroupField | NamedTab;
|
|
113
|
+
key: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "list";
|
|
116
|
+
field: ArrayField | BlocksField;
|
|
117
|
+
key: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: "element";
|
|
120
|
+
field: ArrayField | BlocksField;
|
|
121
|
+
key: string | number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A child's assembled output, tagged with the key it sat at within its parent. Passed to
|
|
125
|
+
* {@link FieldWalker.combine} as the `children` list.
|
|
126
|
+
*
|
|
127
|
+
* @template Out - The walker's per-node output type (see {@link FieldWalker}).
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
export interface ChildOutput<Out> {
|
|
131
|
+
key: string | number;
|
|
132
|
+
out: Out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
136
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
137
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
138
|
+
*
|
|
139
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
140
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
141
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
142
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
143
|
+
*
|
|
144
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
145
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
146
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
147
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
148
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
152
|
+
* ```ts
|
|
153
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
154
|
+
* const found: string[] = [];
|
|
155
|
+
*
|
|
156
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
157
|
+
* enterObject: (field, c) =>
|
|
158
|
+
* isRecord(c.data[field.name])
|
|
159
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
160
|
+
* : "skip",
|
|
161
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
162
|
+
* leaf: (field, c) => {
|
|
163
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
164
|
+
* return undefined;
|
|
165
|
+
* },
|
|
166
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
167
|
+
* };
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @see {@link walkFields}
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
export interface FieldWalker<Cursor extends object, Out> {
|
|
174
|
+
/** Enter a single-object boundary (named `group` or named tab). Return the child cursor, or a signal. */
|
|
175
|
+
enterObject(field: NamedGroupField | NamedTab, cursor: Cursor): Cursor | WalkSignal;
|
|
176
|
+
/** Enter an `array`/`blocks` boundary. Return one {@link ChildCursor} per element, or a signal. */
|
|
177
|
+
enterList(field: ArrayField | BlocksField, cursor: Cursor): ChildCursor<Cursor>[] | WalkSignal;
|
|
178
|
+
/**
|
|
179
|
+
* Visit a data-affecting leaf. `field` is a {@link LeafField} — the engine has already
|
|
180
|
+
* resolved it via the `fieldAffectsData` guard and excluded containers/tabs, so `field.name`
|
|
181
|
+
* is always present and callers never touch the raw `Field` union. Return its output, or
|
|
182
|
+
* `undefined` to drop it.
|
|
183
|
+
*/
|
|
184
|
+
leaf(field: LeafField, cursor: Cursor): Out | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Assemble a container from its children's outputs (called bottom-up). Build-tree
|
|
187
|
+
* callers reconstruct here; collectors return `undefined`. Return `undefined` to drop
|
|
188
|
+
* the container from its parent (e.g. an empty object after filtering).
|
|
189
|
+
*/
|
|
190
|
+
combine(container: ContainerInfo, children: ChildOutput<Out>[], cursor: Cursor): Out | undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
3
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
4
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
5
|
+
*
|
|
6
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
7
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
8
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
9
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
10
|
+
*
|
|
11
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
12
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
13
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
14
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
15
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
19
|
+
* ```ts
|
|
20
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
21
|
+
* const found: string[] = [];
|
|
22
|
+
*
|
|
23
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
24
|
+
* enterObject: (field, c) =>
|
|
25
|
+
* isRecord(c.data[field.name])
|
|
26
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
27
|
+
* : "skip",
|
|
28
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
29
|
+
* leaf: (field, c) => {
|
|
30
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
31
|
+
* return undefined;
|
|
32
|
+
* },
|
|
33
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
34
|
+
* };
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @see {@link walkFields}
|
|
38
|
+
* @public
|
|
39
|
+
*/ export { };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { FieldWalker } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Depth-first walk of a Payload field **schema**, driving a caller-supplied
|
|
5
|
+
* {@link FieldWalker}. The single traversal engine this package's data operations build on
|
|
6
|
+
* (filtering, reconciling, collecting, navigating).
|
|
7
|
+
*
|
|
8
|
+
* The engine owns structural dispatch (via {@link classifyField} / {@link tabScopes}) and the
|
|
9
|
+
* recursion; the caller owns the **data** through the opaque `Cursor` and decides what to
|
|
10
|
+
* produce. The engine never reads the cursor — so the caller can thread one, two, or more
|
|
11
|
+
* parallel data trees (plus a path) inside it. Containers are assembled bottom-up via
|
|
12
|
+
* `combine`, after their children.
|
|
13
|
+
*
|
|
14
|
+
* @template Cursor - The caller's data position threaded through the walk (e.g. the current
|
|
15
|
+
* data object, a `{ source, target }` pair, plus a path). Constrained to `object` so a cursor
|
|
16
|
+
* can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
17
|
+
* @template Out - What the walker produces per node and assembles per container (e.g. a
|
|
18
|
+
* rebuilt data subtree, or `void` for a collect-only walk).
|
|
19
|
+
*
|
|
20
|
+
* @param fields - The schema to walk (a collection/global/group `fields` array).
|
|
21
|
+
* @param root - The initial `Cursor` paired with the top-level `fields`.
|
|
22
|
+
* @param walker - The behavior to drive: `enterObject` / `enterList` derive child cursors (or
|
|
23
|
+
* return a {@link WalkSignal} to `'skip'` a branch or `'stop'` the whole walk), `leaf`
|
|
24
|
+
* produces a value per data-affecting leaf, and `combine` assembles each container from its
|
|
25
|
+
* children.
|
|
26
|
+
* @returns The root `combine` output. `undefined` if the walk was halted by `'stop'` before the
|
|
27
|
+
* root assembled, OR if the root `combine` itself returned `undefined` (e.g. a collect-only
|
|
28
|
+
* walker). To distinguish "stopped" from "empty result", track it via the cursor, not this value.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* Keep only localized leaves and rebuild the data tree (a "filter" walk):
|
|
32
|
+
* ```ts
|
|
33
|
+
* type Cursor = { data: Record<string, unknown> };
|
|
34
|
+
*
|
|
35
|
+
* const filtered = walkFields<Cursor, unknown>(schema, { data }, {
|
|
36
|
+
* enterObject: (field, c) =>
|
|
37
|
+
* isRecord(c.data[field.name]) ? { data: c.data[field.name] } : "skip",
|
|
38
|
+
* enterList: (field, c) => toItemCursors(field, c), // one ChildCursor per element
|
|
39
|
+
* leaf: (field, c) => (field.localized ? c.data[field.name] : undefined),
|
|
40
|
+
* combine: (container, children) => {
|
|
41
|
+
* if (children.length === 0) return undefined; // drop empty containers
|
|
42
|
+
* if (container.kind === "list") return children.map((ch) => ch.out);
|
|
43
|
+
* return Object.fromEntries(children.map((ch) => [ch.key, ch.out]));
|
|
44
|
+
* },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see {@link FieldWalker} for the full visitor contract and the caller shapes it subsumes.
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare function walkFields<Cursor extends object, Out>(fields: Field[], root: Cursor, walker: FieldWalker<Cursor, Out>): Out | undefined;
|