@workbench-kit/field-remap 0.0.1-prototype.0 → 0.0.2-prototype.0.2.5
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 +115 -115
- package/package.json +1 -1
- package/src/domain/constants.ts +14 -14
- package/src/domain/document/fieldRemapDocument.ts +129 -129
- package/src/domain/document/mappingEdge.ts +136 -136
- package/src/domain/ingest/sourceFieldsFromPlainObject.ts +103 -103
- package/src/domain/ingest/targetSlotsFromPlainObject.ts +41 -41
- package/src/domain/mapping/convertItemEdges.ts +93 -93
- package/src/domain/mapping/dateFormat.ts +102 -102
- package/src/domain/mapping/mappingConflicts.ts +90 -90
- package/src/domain/mapping/pathUtils.ts +156 -156
- package/src/domain/mapping/resolveMappedValue.ts +96 -96
- package/src/domain/mapping/transformOptions.ts +184 -184
- package/src/domain/mapping/treeUtils.ts +32 -32
- package/src/domain/shapes/conversionDefinition.ts +95 -95
- package/src/domain/shapes/convertToShape.ts +187 -187
- package/src/domain/shapes/dataShape.ts +109 -109
- package/src/domain/types.ts +182 -182
- package/src/index.ts +137 -137
- package/src/registry/builtinTransforms.ts +243 -243
- package/src/registry/createValueTransformRegistry.ts +194 -194
package/README.md
CHANGED
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
# `@workbench-kit/field-remap`
|
|
2
|
-
|
|
3
|
-
Field remap **runtime**: reshape structure A into structure B with mapping edges and `convertToShape`.
|
|
4
|
-
|
|
5
|
-
This package does **not** ship a mapping UI. Hosts adapt a tree or table UI into `MappingEdge[]`
|
|
6
|
-
and call `convertToShape`. The workbench sample (**Field Remap → A → B**) demonstrates a nested
|
|
7
|
-
tree mapper with list context; flat OSS adapters (for example `react-table-mapping`) remain useful
|
|
8
|
-
for leaf-only hosts.
|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
```powershell
|
|
13
|
-
pnpm add @workbench-kit/field-remap@prototype
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Capabilities
|
|
17
|
-
|
|
18
|
-
| Pattern | Support |
|
|
19
|
-
| ----------------------------------------- | -------------------------------------------------------------------- |
|
|
20
|
-
| Leaf → leaf rename | Yes |
|
|
21
|
-
| Nested object paths | Yes (`path` + `writeObjectPath`) |
|
|
22
|
-
| Array whole copy | Yes (`identity`) |
|
|
23
|
-
| Array item projection | Yes (`itemSourcePath`) |
|
|
24
|
-
| Array → scalar reduce | Yes (`array:first`, `array:join`) |
|
|
25
|
-
| String format chain | Yes (`string:trim` / `upper` / `lower` / `prefix` / `suffix`, max 3) |
|
|
26
|
-
| Array<object> → Array<object> | Yes (`itemEdges` list context) |
|
|
27
|
-
| Index / wildcard paths | No (P2) |
|
|
28
|
-
|
|
29
|
-
Middle “graph nodes” in the sample UI are just `MappingEdge.transformIds` steps
|
|
30
|
-
(plus optional `transformOptionSteps`), not a separate document type. The workbench
|
|
31
|
-
sample renders them with `@xyflow/react` (source out → transform → target in).
|
|
32
|
-
|
|
33
|
-
## Quick start
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import {
|
|
37
|
-
convertToShape,
|
|
38
|
-
createBuiltinValueTransformRegistry,
|
|
39
|
-
defineConversion,
|
|
40
|
-
defineDataShape,
|
|
41
|
-
sourceFieldsFromPlainObject,
|
|
42
|
-
targetSlotsFromPlainObject,
|
|
43
|
-
} from '@workbench-kit/field-remap';
|
|
44
|
-
|
|
45
|
-
const structureA = {
|
|
46
|
-
user_name: 'Ada',
|
|
47
|
-
tags: [{ name: 'math' }, { name: 'computing' }],
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const shapes = [
|
|
51
|
-
defineDataShape({
|
|
52
|
-
id: 'a',
|
|
53
|
-
label: 'A',
|
|
54
|
-
role: 'source',
|
|
55
|
-
fields: sourceFieldsFromPlainObject(structureA, { idPrefix: 'a' }),
|
|
56
|
-
}),
|
|
57
|
-
defineDataShape({
|
|
58
|
-
id: 'b',
|
|
59
|
-
label: 'B',
|
|
60
|
-
role: 'target',
|
|
61
|
-
fields: targetSlotsFromPlainObject({ name: '', labels: [{ title: '' }] }, { idPrefix: 'b' }),
|
|
62
|
-
}),
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const conversion = defineConversion({
|
|
66
|
-
id: 'a→b',
|
|
67
|
-
sourceShapeIds: ['a'],
|
|
68
|
-
targetShapeId: 'b',
|
|
69
|
-
edges: [
|
|
70
|
-
{
|
|
71
|
-
id: 'e-name',
|
|
72
|
-
sourceFieldId: 'a.user_name',
|
|
73
|
-
targetSlotId: 'b.name',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
id: 'e-tags',
|
|
77
|
-
sourceFieldId: 'a.tags',
|
|
78
|
-
targetSlotId: 'b.labels',
|
|
79
|
-
itemEdges: [
|
|
80
|
-
{
|
|
81
|
-
id: 'e-title',
|
|
82
|
-
sourceFieldId: 'a.tags.item.name',
|
|
83
|
-
targetSlotId: 'b.labels.item.title',
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const { output } = convertToShape({
|
|
91
|
-
conversion,
|
|
92
|
-
shapes,
|
|
93
|
-
inputs: { a: structureA },
|
|
94
|
-
transforms: createBuiltinValueTransformRegistry(),
|
|
95
|
-
});
|
|
96
|
-
// { name: 'Ada', labels: [{ title: 'math' }, { title: 'computing' }] }
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Hosts may `registry.register()` additional transforms (the sample registers `expr:jsonata` via
|
|
100
|
-
[jsonata](https://jsonata.org/)).
|
|
101
|
-
|
|
102
|
-
## Layout
|
|
103
|
-
|
|
104
|
-
```text
|
|
105
|
-
src/
|
|
106
|
-
domain/document/ edges + FieldRemapDocument
|
|
107
|
-
domain/shapes/ DataShape, ConversionDefinition, convertToShape
|
|
108
|
-
domain/ingest/ plain object → fields / slots
|
|
109
|
-
domain/mapping/ path helpers, list context, conflicts
|
|
110
|
-
registry/ ValueTransform registry (identity, array:first, array:join)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Stability
|
|
114
|
-
|
|
115
|
-
Published on the npm `@prototype` tag. Prefer the root export; deep paths are unsupported.
|
|
1
|
+
# `@workbench-kit/field-remap`
|
|
2
|
+
|
|
3
|
+
Field remap **runtime**: reshape structure A into structure B with mapping edges and `convertToShape`.
|
|
4
|
+
|
|
5
|
+
This package does **not** ship a mapping UI. Hosts adapt a tree or table UI into `MappingEdge[]`
|
|
6
|
+
and call `convertToShape`. The workbench sample (**Field Remap → A → B**) demonstrates a nested
|
|
7
|
+
tree mapper with list context; flat OSS adapters (for example `react-table-mapping`) remain useful
|
|
8
|
+
for leaf-only hosts.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```powershell
|
|
13
|
+
pnpm add @workbench-kit/field-remap@prototype
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Capabilities
|
|
17
|
+
|
|
18
|
+
| Pattern | Support |
|
|
19
|
+
| ----------------------------------------- | -------------------------------------------------------------------- |
|
|
20
|
+
| Leaf → leaf rename | Yes |
|
|
21
|
+
| Nested object paths | Yes (`path` + `writeObjectPath`) |
|
|
22
|
+
| Array whole copy | Yes (`identity`) |
|
|
23
|
+
| Array item projection | Yes (`itemSourcePath`) |
|
|
24
|
+
| Array → scalar reduce | Yes (`array:first`, `array:join`) |
|
|
25
|
+
| String format chain | Yes (`string:trim` / `upper` / `lower` / `prefix` / `suffix`, max 3) |
|
|
26
|
+
| Array<object> → Array<object> | Yes (`itemEdges` list context) |
|
|
27
|
+
| Index / wildcard paths | No (P2) |
|
|
28
|
+
|
|
29
|
+
Middle “graph nodes” in the sample UI are just `MappingEdge.transformIds` steps
|
|
30
|
+
(plus optional `transformOptionSteps`), not a separate document type. The workbench
|
|
31
|
+
sample renders them with `@xyflow/react` (source out → transform → target in).
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
convertToShape,
|
|
38
|
+
createBuiltinValueTransformRegistry,
|
|
39
|
+
defineConversion,
|
|
40
|
+
defineDataShape,
|
|
41
|
+
sourceFieldsFromPlainObject,
|
|
42
|
+
targetSlotsFromPlainObject,
|
|
43
|
+
} from '@workbench-kit/field-remap';
|
|
44
|
+
|
|
45
|
+
const structureA = {
|
|
46
|
+
user_name: 'Ada',
|
|
47
|
+
tags: [{ name: 'math' }, { name: 'computing' }],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const shapes = [
|
|
51
|
+
defineDataShape({
|
|
52
|
+
id: 'a',
|
|
53
|
+
label: 'A',
|
|
54
|
+
role: 'source',
|
|
55
|
+
fields: sourceFieldsFromPlainObject(structureA, { idPrefix: 'a' }),
|
|
56
|
+
}),
|
|
57
|
+
defineDataShape({
|
|
58
|
+
id: 'b',
|
|
59
|
+
label: 'B',
|
|
60
|
+
role: 'target',
|
|
61
|
+
fields: targetSlotsFromPlainObject({ name: '', labels: [{ title: '' }] }, { idPrefix: 'b' }),
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const conversion = defineConversion({
|
|
66
|
+
id: 'a→b',
|
|
67
|
+
sourceShapeIds: ['a'],
|
|
68
|
+
targetShapeId: 'b',
|
|
69
|
+
edges: [
|
|
70
|
+
{
|
|
71
|
+
id: 'e-name',
|
|
72
|
+
sourceFieldId: 'a.user_name',
|
|
73
|
+
targetSlotId: 'b.name',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'e-tags',
|
|
77
|
+
sourceFieldId: 'a.tags',
|
|
78
|
+
targetSlotId: 'b.labels',
|
|
79
|
+
itemEdges: [
|
|
80
|
+
{
|
|
81
|
+
id: 'e-title',
|
|
82
|
+
sourceFieldId: 'a.tags.item.name',
|
|
83
|
+
targetSlotId: 'b.labels.item.title',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { output } = convertToShape({
|
|
91
|
+
conversion,
|
|
92
|
+
shapes,
|
|
93
|
+
inputs: { a: structureA },
|
|
94
|
+
transforms: createBuiltinValueTransformRegistry(),
|
|
95
|
+
});
|
|
96
|
+
// { name: 'Ada', labels: [{ title: 'math' }, { title: 'computing' }] }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Hosts may `registry.register()` additional transforms (the sample registers `expr:jsonata` via
|
|
100
|
+
[jsonata](https://jsonata.org/)).
|
|
101
|
+
|
|
102
|
+
## Layout
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
src/
|
|
106
|
+
domain/document/ edges + FieldRemapDocument
|
|
107
|
+
domain/shapes/ DataShape, ConversionDefinition, convertToShape
|
|
108
|
+
domain/ingest/ plain object → fields / slots
|
|
109
|
+
domain/mapping/ path helpers, list context, conflicts
|
|
110
|
+
registry/ ValueTransform registry (identity, array:first, array:join)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Stability
|
|
114
|
+
|
|
115
|
+
Published on the npm `@prototype` tag. Prefer the root export; deep paths are unsupported.
|
package/package.json
CHANGED
package/src/domain/constants.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/** Maximum ordered transforms applied on a single edge. */
|
|
2
|
-
export const MAX_TRANSFORM_CHAIN = 3;
|
|
3
|
-
|
|
4
|
-
/** Built-in pass-through transform id (kept here to avoid import cycles). */
|
|
5
|
-
export const IDENTITY_TRANSFORM_ID = 'identity';
|
|
6
|
-
|
|
7
|
-
/** Legacy id aliases → canonical ids (empty until hosts need migration). */
|
|
8
|
-
export const TRANSFORM_ID_ALIASES: Readonly<Record<string, string>> = {};
|
|
9
|
-
|
|
10
|
-
/** Map a legacy or canonical transform id to its canonical form. */
|
|
11
|
-
export function canonicalizeTransformId(id: string): string {
|
|
12
|
-
const trimmed = id.trim();
|
|
13
|
-
return TRANSFORM_ID_ALIASES[trimmed] ?? trimmed;
|
|
14
|
-
}
|
|
1
|
+
/** Maximum ordered transforms applied on a single edge. */
|
|
2
|
+
export const MAX_TRANSFORM_CHAIN = 3;
|
|
3
|
+
|
|
4
|
+
/** Built-in pass-through transform id (kept here to avoid import cycles). */
|
|
5
|
+
export const IDENTITY_TRANSFORM_ID = 'identity';
|
|
6
|
+
|
|
7
|
+
/** Legacy id aliases → canonical ids (empty until hosts need migration). */
|
|
8
|
+
export const TRANSFORM_ID_ALIASES: Readonly<Record<string, string>> = {};
|
|
9
|
+
|
|
10
|
+
/** Map a legacy or canonical transform id to its canonical form. */
|
|
11
|
+
export function canonicalizeTransformId(id: string): string {
|
|
12
|
+
const trimmed = id.trim();
|
|
13
|
+
return TRANSFORM_ID_ALIASES[trimmed] ?? trimmed;
|
|
14
|
+
}
|
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
import { normalizeMappingEdges } from './mappingEdge.js';
|
|
2
|
-
import type { MappingEdge, FieldRemapDocument } from '../types.js';
|
|
3
|
-
|
|
4
|
-
export const FIELD_REMAP_DOCUMENT_VERSION = 1 as const;
|
|
5
|
-
|
|
6
|
-
/** Thrown when parse/deserialize sees a document `version` other than the supported constant. */
|
|
7
|
-
export class UnsupportedFieldRemapDocumentVersionError extends Error {
|
|
8
|
-
readonly version: unknown;
|
|
9
|
-
readonly expectedVersion: typeof FIELD_REMAP_DOCUMENT_VERSION;
|
|
10
|
-
|
|
11
|
-
constructor(version: unknown) {
|
|
12
|
-
super(
|
|
13
|
-
`Unsupported field remap document version ${String(version)}; expected ${FIELD_REMAP_DOCUMENT_VERSION}.`,
|
|
14
|
-
);
|
|
15
|
-
this.name = 'UnsupportedFieldRemapDocumentVersionError';
|
|
16
|
-
this.version = version;
|
|
17
|
-
this.expectedVersion = FIELD_REMAP_DOCUMENT_VERSION;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Thrown when parse/deserialize receives a value that is not a mapping document. */
|
|
22
|
-
export class InvalidFieldRemapDocumentError extends Error {
|
|
23
|
-
constructor(message: string) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = 'InvalidFieldRemapDocumentError';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Build a versioned, normalized mapping document for host persistence. */
|
|
30
|
-
export function createFieldRemapDocument(edges: readonly MappingEdge[]): FieldRemapDocument {
|
|
31
|
-
return {
|
|
32
|
-
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
33
|
-
edges: normalizeMappingEdges(edges),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Normalize edges (including legacy transform id aliases) on a persisted document. */
|
|
38
|
-
export function normalizeFieldRemapDocument(document: FieldRemapDocument): FieldRemapDocument {
|
|
39
|
-
if (document.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
40
|
-
throw new UnsupportedFieldRemapDocumentVersionError(document.version);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
44
|
-
edges: normalizeMappingEdges(document.edges),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Stable JSON serialization for persistence / clipboard.
|
|
50
|
-
* Always emits the current document version with normalized edges.
|
|
51
|
-
*/
|
|
52
|
-
export function serializeFieldRemapDocument(
|
|
53
|
-
document: FieldRemapDocument | readonly MappingEdge[],
|
|
54
|
-
): string {
|
|
55
|
-
const doc = Array.isArray(document)
|
|
56
|
-
? createFieldRemapDocument(document)
|
|
57
|
-
: createFieldRemapDocument((document as FieldRemapDocument).edges);
|
|
58
|
-
return JSON.stringify(doc);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Parse an unknown JSON value into a normalized `FieldRemapDocument`.
|
|
63
|
-
* Rejects unsupported versions and malformed shapes with typed errors.
|
|
64
|
-
*/
|
|
65
|
-
export function parseFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
66
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
67
|
-
throw new InvalidFieldRemapDocumentError(
|
|
68
|
-
'Expected a field remap document object with version and edges.',
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const record = input as Record<string, unknown>;
|
|
73
|
-
if (!('version' in record)) {
|
|
74
|
-
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
75
|
-
}
|
|
76
|
-
if (record.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
77
|
-
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(record.edges)) {
|
|
80
|
-
throw new InvalidFieldRemapDocumentError('Field remap document edges must be an array.');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return normalizeFieldRemapDocument({
|
|
84
|
-
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
85
|
-
edges: record.edges as MappingEdge[],
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** JSON.parse + {@link parseFieldRemapDocument}. */
|
|
90
|
-
export function deserializeFieldRemapDocument(json: string): FieldRemapDocument {
|
|
91
|
-
let parsed: unknown;
|
|
92
|
-
try {
|
|
93
|
-
parsed = JSON.parse(json) as unknown;
|
|
94
|
-
} catch (error) {
|
|
95
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
-
throw new InvalidFieldRemapDocumentError(`Field remap document JSON is invalid: ${detail}`);
|
|
97
|
-
}
|
|
98
|
-
return parseFieldRemapDocument(parsed);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Migrate an unknown persisted value to the current {@link FieldRemapDocument}.
|
|
103
|
-
*
|
|
104
|
-
* Hosts should call this (or `parseFieldRemapDocument`) at load time so future
|
|
105
|
-
* document versions can be rewritten here without changing call sites.
|
|
106
|
-
*
|
|
107
|
-
* **v1:** passthrough normalize (legacy transform id aliases rewritten).
|
|
108
|
-
* Future versions: add `case` branches that rewrite into v1 shape, then normalize.
|
|
109
|
-
*/
|
|
110
|
-
export function migrateFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
111
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
112
|
-
throw new InvalidFieldRemapDocumentError(
|
|
113
|
-
'Expected a field remap document object with version and edges.',
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const record = input as Record<string, unknown>;
|
|
118
|
-
if (!('version' in record)) {
|
|
119
|
-
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
switch (record.version) {
|
|
123
|
-
case FIELD_REMAP_DOCUMENT_VERSION:
|
|
124
|
-
return parseFieldRemapDocument(input);
|
|
125
|
-
// Future: case 2: return parseFieldRemapDocument(migrateV2ToV1(record));
|
|
126
|
-
default:
|
|
127
|
-
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
1
|
+
import { normalizeMappingEdges } from './mappingEdge.js';
|
|
2
|
+
import type { MappingEdge, FieldRemapDocument } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export const FIELD_REMAP_DOCUMENT_VERSION = 1 as const;
|
|
5
|
+
|
|
6
|
+
/** Thrown when parse/deserialize sees a document `version` other than the supported constant. */
|
|
7
|
+
export class UnsupportedFieldRemapDocumentVersionError extends Error {
|
|
8
|
+
readonly version: unknown;
|
|
9
|
+
readonly expectedVersion: typeof FIELD_REMAP_DOCUMENT_VERSION;
|
|
10
|
+
|
|
11
|
+
constructor(version: unknown) {
|
|
12
|
+
super(
|
|
13
|
+
`Unsupported field remap document version ${String(version)}; expected ${FIELD_REMAP_DOCUMENT_VERSION}.`,
|
|
14
|
+
);
|
|
15
|
+
this.name = 'UnsupportedFieldRemapDocumentVersionError';
|
|
16
|
+
this.version = version;
|
|
17
|
+
this.expectedVersion = FIELD_REMAP_DOCUMENT_VERSION;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Thrown when parse/deserialize receives a value that is not a mapping document. */
|
|
22
|
+
export class InvalidFieldRemapDocumentError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'InvalidFieldRemapDocumentError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Build a versioned, normalized mapping document for host persistence. */
|
|
30
|
+
export function createFieldRemapDocument(edges: readonly MappingEdge[]): FieldRemapDocument {
|
|
31
|
+
return {
|
|
32
|
+
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
33
|
+
edges: normalizeMappingEdges(edges),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Normalize edges (including legacy transform id aliases) on a persisted document. */
|
|
38
|
+
export function normalizeFieldRemapDocument(document: FieldRemapDocument): FieldRemapDocument {
|
|
39
|
+
if (document.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
40
|
+
throw new UnsupportedFieldRemapDocumentVersionError(document.version);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
44
|
+
edges: normalizeMappingEdges(document.edges),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stable JSON serialization for persistence / clipboard.
|
|
50
|
+
* Always emits the current document version with normalized edges.
|
|
51
|
+
*/
|
|
52
|
+
export function serializeFieldRemapDocument(
|
|
53
|
+
document: FieldRemapDocument | readonly MappingEdge[],
|
|
54
|
+
): string {
|
|
55
|
+
const doc = Array.isArray(document)
|
|
56
|
+
? createFieldRemapDocument(document)
|
|
57
|
+
: createFieldRemapDocument((document as FieldRemapDocument).edges);
|
|
58
|
+
return JSON.stringify(doc);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse an unknown JSON value into a normalized `FieldRemapDocument`.
|
|
63
|
+
* Rejects unsupported versions and malformed shapes with typed errors.
|
|
64
|
+
*/
|
|
65
|
+
export function parseFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
66
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
67
|
+
throw new InvalidFieldRemapDocumentError(
|
|
68
|
+
'Expected a field remap document object with version and edges.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const record = input as Record<string, unknown>;
|
|
73
|
+
if (!('version' in record)) {
|
|
74
|
+
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
75
|
+
}
|
|
76
|
+
if (record.version !== FIELD_REMAP_DOCUMENT_VERSION) {
|
|
77
|
+
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(record.edges)) {
|
|
80
|
+
throw new InvalidFieldRemapDocumentError('Field remap document edges must be an array.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalizeFieldRemapDocument({
|
|
84
|
+
version: FIELD_REMAP_DOCUMENT_VERSION,
|
|
85
|
+
edges: record.edges as MappingEdge[],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** JSON.parse + {@link parseFieldRemapDocument}. */
|
|
90
|
+
export function deserializeFieldRemapDocument(json: string): FieldRemapDocument {
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(json) as unknown;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw new InvalidFieldRemapDocumentError(`Field remap document JSON is invalid: ${detail}`);
|
|
97
|
+
}
|
|
98
|
+
return parseFieldRemapDocument(parsed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Migrate an unknown persisted value to the current {@link FieldRemapDocument}.
|
|
103
|
+
*
|
|
104
|
+
* Hosts should call this (or `parseFieldRemapDocument`) at load time so future
|
|
105
|
+
* document versions can be rewritten here without changing call sites.
|
|
106
|
+
*
|
|
107
|
+
* **v1:** passthrough normalize (legacy transform id aliases rewritten).
|
|
108
|
+
* Future versions: add `case` branches that rewrite into v1 shape, then normalize.
|
|
109
|
+
*/
|
|
110
|
+
export function migrateFieldRemapDocument(input: unknown): FieldRemapDocument {
|
|
111
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
112
|
+
throw new InvalidFieldRemapDocumentError(
|
|
113
|
+
'Expected a field remap document object with version and edges.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const record = input as Record<string, unknown>;
|
|
118
|
+
if (!('version' in record)) {
|
|
119
|
+
throw new InvalidFieldRemapDocumentError('Field remap document is missing version.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch (record.version) {
|
|
123
|
+
case FIELD_REMAP_DOCUMENT_VERSION:
|
|
124
|
+
return parseFieldRemapDocument(input);
|
|
125
|
+
// Future: case 2: return parseFieldRemapDocument(migrateV2ToV1(record));
|
|
126
|
+
default:
|
|
127
|
+
throw new UnsupportedFieldRemapDocumentVersionError(record.version);
|
|
128
|
+
}
|
|
129
|
+
}
|