@workbench-kit/field-remap 0.0.1-prototype.0 → 0.0.2-prototype.0.2.10
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 +288 -115
- package/package.json +1 -1
- package/src/domain/abort.ts +29 -0
- package/src/domain/constants.ts +14 -14
- package/src/domain/document/fieldRemapDocument.ts +163 -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 +98 -93
- package/src/domain/mapping/dateFormat.ts +102 -102
- package/src/domain/mapping/mappingConflicts.ts +90 -90
- package/src/domain/mapping/mappingOperators.ts +297 -0
- package/src/domain/mapping/objectPathSafety.ts +141 -0
- package/src/domain/mapping/pathUtils.ts +302 -156
- package/src/domain/mapping/resolveMappedValue.ts +98 -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/convertMappedInputs.ts +158 -0
- package/src/domain/shapes/convertToShape.ts +205 -187
- package/src/domain/shapes/dataShape.ts +109 -109
- package/src/domain/shapes/projectShapes.ts +116 -0
- package/src/domain/shapes/shapeEdit.ts +114 -0
- package/src/domain/types.ts +242 -182
- package/src/index.ts +197 -137
- package/src/registry/builtinTransforms.ts +243 -243
- package/src/registry/createValueTransformRegistry.ts +235 -194
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
import { isPlainObject } from '../mapping/pathUtils.js';
|
|
2
|
-
import type { FieldDataType, SourceField } from '../types.js';
|
|
3
|
-
|
|
4
|
-
export interface SourceFieldsFromPlainObjectOptions {
|
|
5
|
-
/** Prefix for generated field ids (default `src`). */
|
|
6
|
-
readonly idPrefix?: string;
|
|
7
|
-
/** Max nesting depth for object children (default 4). */
|
|
8
|
-
readonly maxDepth?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function inferDataType(value: unknown): FieldDataType {
|
|
12
|
-
if (value === null || value === undefined) {
|
|
13
|
-
return 'unknown';
|
|
14
|
-
}
|
|
15
|
-
if (typeof value === 'string') {
|
|
16
|
-
return 'string';
|
|
17
|
-
}
|
|
18
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
19
|
-
return 'number';
|
|
20
|
-
}
|
|
21
|
-
if (typeof value === 'boolean') {
|
|
22
|
-
return 'boolean';
|
|
23
|
-
}
|
|
24
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
25
|
-
return 'datetime';
|
|
26
|
-
}
|
|
27
|
-
if (Array.isArray(value)) {
|
|
28
|
-
return 'array';
|
|
29
|
-
}
|
|
30
|
-
if (isPlainObject(value)) {
|
|
31
|
-
return 'object';
|
|
32
|
-
}
|
|
33
|
-
return 'unknown';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function fieldFromEntry(
|
|
37
|
-
key: string,
|
|
38
|
-
value: unknown,
|
|
39
|
-
path: string,
|
|
40
|
-
idPrefix: string,
|
|
41
|
-
depth: number,
|
|
42
|
-
maxDepth: number,
|
|
43
|
-
): SourceField {
|
|
44
|
-
const id = path ? `${idPrefix}.${path}` : `${idPrefix}.${key}`;
|
|
45
|
-
const dataType = inferDataType(value);
|
|
46
|
-
const base: SourceField = {
|
|
47
|
-
id,
|
|
48
|
-
label: key,
|
|
49
|
-
path,
|
|
50
|
-
dataType,
|
|
51
|
-
sampleValue: value,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
if (depth >= maxDepth) {
|
|
55
|
-
return base;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (isPlainObject(value)) {
|
|
59
|
-
const children = Object.entries(value).map(([childKey, childValue]) =>
|
|
60
|
-
fieldFromEntry(
|
|
61
|
-
childKey,
|
|
62
|
-
childValue,
|
|
63
|
-
path ? `${path}.${childKey}` : childKey,
|
|
64
|
-
idPrefix,
|
|
65
|
-
depth + 1,
|
|
66
|
-
maxDepth,
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
return children.length > 0 ? { ...base, children } : base;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (Array.isArray(value) && value.length > 0 && isPlainObject(value[0])) {
|
|
73
|
-
const sampleItem = value[0] as Record<string, unknown>;
|
|
74
|
-
const children = Object.entries(sampleItem).map(([childKey, childValue]) =>
|
|
75
|
-
fieldFromEntry(childKey, childValue, childKey, `${id}.item`, depth + 1, maxDepth),
|
|
76
|
-
);
|
|
77
|
-
return children.length > 0 ? { ...base, children } : base;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return base;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Infer a shallow/nested `SourceField` tree from a plain sample object.
|
|
85
|
-
* Useful for demos and hosts that have a sample payload but no formal schema.
|
|
86
|
-
*
|
|
87
|
-
* - Top-level keys become root fields.
|
|
88
|
-
* - Nested plain objects become `children` (up to `maxDepth`).
|
|
89
|
-
* - Arrays of objects expose item-key children for projection pickers.
|
|
90
|
-
*/
|
|
91
|
-
export function sourceFieldsFromPlainObject(
|
|
92
|
-
sample: unknown,
|
|
93
|
-
options: SourceFieldsFromPlainObjectOptions = {},
|
|
94
|
-
): SourceField[] {
|
|
95
|
-
if (!isPlainObject(sample)) {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
const idPrefix = options.idPrefix?.trim() || 'src';
|
|
99
|
-
const maxDepth = Math.max(0, options.maxDepth ?? 4);
|
|
100
|
-
return Object.entries(sample).map(([key, value]) =>
|
|
101
|
-
fieldFromEntry(key, value, key, idPrefix, 0, maxDepth),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
1
|
+
import { isPlainObject } from '../mapping/pathUtils.js';
|
|
2
|
+
import type { FieldDataType, SourceField } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export interface SourceFieldsFromPlainObjectOptions {
|
|
5
|
+
/** Prefix for generated field ids (default `src`). */
|
|
6
|
+
readonly idPrefix?: string;
|
|
7
|
+
/** Max nesting depth for object children (default 4). */
|
|
8
|
+
readonly maxDepth?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function inferDataType(value: unknown): FieldDataType {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
return 'string';
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
19
|
+
return 'number';
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'boolean') {
|
|
22
|
+
return 'boolean';
|
|
23
|
+
}
|
|
24
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
25
|
+
return 'datetime';
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return 'array';
|
|
29
|
+
}
|
|
30
|
+
if (isPlainObject(value)) {
|
|
31
|
+
return 'object';
|
|
32
|
+
}
|
|
33
|
+
return 'unknown';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fieldFromEntry(
|
|
37
|
+
key: string,
|
|
38
|
+
value: unknown,
|
|
39
|
+
path: string,
|
|
40
|
+
idPrefix: string,
|
|
41
|
+
depth: number,
|
|
42
|
+
maxDepth: number,
|
|
43
|
+
): SourceField {
|
|
44
|
+
const id = path ? `${idPrefix}.${path}` : `${idPrefix}.${key}`;
|
|
45
|
+
const dataType = inferDataType(value);
|
|
46
|
+
const base: SourceField = {
|
|
47
|
+
id,
|
|
48
|
+
label: key,
|
|
49
|
+
path,
|
|
50
|
+
dataType,
|
|
51
|
+
sampleValue: value,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (depth >= maxDepth) {
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isPlainObject(value)) {
|
|
59
|
+
const children = Object.entries(value).map(([childKey, childValue]) =>
|
|
60
|
+
fieldFromEntry(
|
|
61
|
+
childKey,
|
|
62
|
+
childValue,
|
|
63
|
+
path ? `${path}.${childKey}` : childKey,
|
|
64
|
+
idPrefix,
|
|
65
|
+
depth + 1,
|
|
66
|
+
maxDepth,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return children.length > 0 ? { ...base, children } : base;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(value) && value.length > 0 && isPlainObject(value[0])) {
|
|
73
|
+
const sampleItem = value[0] as Record<string, unknown>;
|
|
74
|
+
const children = Object.entries(sampleItem).map(([childKey, childValue]) =>
|
|
75
|
+
fieldFromEntry(childKey, childValue, childKey, `${id}.item`, depth + 1, maxDepth),
|
|
76
|
+
);
|
|
77
|
+
return children.length > 0 ? { ...base, children } : base;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return base;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Infer a shallow/nested `SourceField` tree from a plain sample object.
|
|
85
|
+
* Useful for demos and hosts that have a sample payload but no formal schema.
|
|
86
|
+
*
|
|
87
|
+
* - Top-level keys become root fields.
|
|
88
|
+
* - Nested plain objects become `children` (up to `maxDepth`).
|
|
89
|
+
* - Arrays of objects expose item-key children for projection pickers.
|
|
90
|
+
*/
|
|
91
|
+
export function sourceFieldsFromPlainObject(
|
|
92
|
+
sample: unknown,
|
|
93
|
+
options: SourceFieldsFromPlainObjectOptions = {},
|
|
94
|
+
): SourceField[] {
|
|
95
|
+
if (!isPlainObject(sample)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const idPrefix = options.idPrefix?.trim() || 'src';
|
|
99
|
+
const maxDepth = Math.max(0, options.maxDepth ?? 4);
|
|
100
|
+
return Object.entries(sample).map(([key, value]) =>
|
|
101
|
+
fieldFromEntry(key, value, key, idPrefix, 0, maxDepth),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { sourceFieldsFromPlainObject } from './sourceFieldsFromPlainObject.js';
|
|
2
|
-
import type { SourceField, TargetSlot } from '../types.js';
|
|
3
|
-
|
|
4
|
-
export interface TargetSlotsFromPlainObjectOptions {
|
|
5
|
-
/** Prefix for generated slot ids (default `tgt`). */
|
|
6
|
-
readonly idPrefix?: string;
|
|
7
|
-
/** Max nesting depth for object children (default 4). */
|
|
8
|
-
readonly maxDepth?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function sourceFieldToTargetSlot(field: SourceField): TargetSlot {
|
|
12
|
-
const slot: TargetSlot = {
|
|
13
|
-
id: field.id,
|
|
14
|
-
label: field.label,
|
|
15
|
-
...(field.path ? { path: field.path } : {}),
|
|
16
|
-
...(field.dataType ? { dataType: field.dataType } : {}),
|
|
17
|
-
};
|
|
18
|
-
if (field.children && field.children.length > 0) {
|
|
19
|
-
return {
|
|
20
|
-
...slot,
|
|
21
|
-
children: field.children.map(sourceFieldToTargetSlot),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
return slot;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Infer a nested `TargetSlot` tree from a plain sample object (or target shape).
|
|
29
|
-
* Mirrors {@link sourceFieldsFromPlainObject}: same nesting / array-of-object rules,
|
|
30
|
-
* keeping `path` for `convertToShape` output assembly (no `sampleValue`).
|
|
31
|
-
*/
|
|
32
|
-
export function targetSlotsFromPlainObject(
|
|
33
|
-
sample: unknown,
|
|
34
|
-
options: TargetSlotsFromPlainObjectOptions = {},
|
|
35
|
-
): TargetSlot[] {
|
|
36
|
-
const idPrefix = options.idPrefix?.trim() || 'tgt';
|
|
37
|
-
return sourceFieldsFromPlainObject(sample, {
|
|
38
|
-
idPrefix,
|
|
39
|
-
maxDepth: options.maxDepth,
|
|
40
|
-
}).map(sourceFieldToTargetSlot);
|
|
41
|
-
}
|
|
1
|
+
import { sourceFieldsFromPlainObject } from './sourceFieldsFromPlainObject.js';
|
|
2
|
+
import type { SourceField, TargetSlot } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export interface TargetSlotsFromPlainObjectOptions {
|
|
5
|
+
/** Prefix for generated slot ids (default `tgt`). */
|
|
6
|
+
readonly idPrefix?: string;
|
|
7
|
+
/** Max nesting depth for object children (default 4). */
|
|
8
|
+
readonly maxDepth?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sourceFieldToTargetSlot(field: SourceField): TargetSlot {
|
|
12
|
+
const slot: TargetSlot = {
|
|
13
|
+
id: field.id,
|
|
14
|
+
label: field.label,
|
|
15
|
+
...(field.path ? { path: field.path } : {}),
|
|
16
|
+
...(field.dataType ? { dataType: field.dataType } : {}),
|
|
17
|
+
};
|
|
18
|
+
if (field.children && field.children.length > 0) {
|
|
19
|
+
return {
|
|
20
|
+
...slot,
|
|
21
|
+
children: field.children.map(sourceFieldToTargetSlot),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return slot;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Infer a nested `TargetSlot` tree from a plain sample object (or target shape).
|
|
29
|
+
* Mirrors {@link sourceFieldsFromPlainObject}: same nesting / array-of-object rules,
|
|
30
|
+
* keeping `path` for `convertToShape` output assembly (no `sampleValue`).
|
|
31
|
+
*/
|
|
32
|
+
export function targetSlotsFromPlainObject(
|
|
33
|
+
sample: unknown,
|
|
34
|
+
options: TargetSlotsFromPlainObjectOptions = {},
|
|
35
|
+
): TargetSlot[] {
|
|
36
|
+
const idPrefix = options.idPrefix?.trim() || 'tgt';
|
|
37
|
+
return sourceFieldsFromPlainObject(sample, {
|
|
38
|
+
idPrefix,
|
|
39
|
+
maxDepth: options.maxDepth,
|
|
40
|
+
}).map(sourceFieldToTargetSlot);
|
|
41
|
+
}
|
|
@@ -1,93 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
readonly
|
|
39
|
-
readonly
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
1
|
+
import { throwIfAborted } from '../abort.js';
|
|
2
|
+
import { edgeTransformIds } from '../document/mappingEdge.js';
|
|
3
|
+
import { applyTransformChain } from '../../registry/createValueTransformRegistry.js';
|
|
4
|
+
import { BUILTIN_TRANSFORM_IDS } from '../../registry/builtinTransforms.js';
|
|
5
|
+
import { resolveOptionSteps } from './transformOptions.js';
|
|
6
|
+
import { isPlainObject, readObjectPath, writeObjectPath } from './pathUtils.js';
|
|
7
|
+
import { findTargetSlot, flattenSourceFields, flattenTargetSlots } from './treeUtils.js';
|
|
8
|
+
import type {
|
|
9
|
+
MappingEdge,
|
|
10
|
+
SourceField,
|
|
11
|
+
TargetSlot,
|
|
12
|
+
TransformContext,
|
|
13
|
+
ValueTransformRegistry,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
|
|
16
|
+
function findSourceField(fields: readonly SourceField[], fieldId: string): SourceField | undefined {
|
|
17
|
+
return flattenSourceFields(fields).find((field) => field.id === fieldId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function itemRelativePath(
|
|
21
|
+
node: { readonly path?: string; readonly label: string; readonly id: string } | undefined,
|
|
22
|
+
): string {
|
|
23
|
+
if (!node) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
const path = node.path?.trim();
|
|
27
|
+
if (path) {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
return node.label.trim() || node.id.split('.').pop() || '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert each object in a source array through list-context `itemEdges`.
|
|
35
|
+
* Child field paths are treated as item-relative (ingest array children).
|
|
36
|
+
*/
|
|
37
|
+
export async function convertArrayWithItemEdges(input: {
|
|
38
|
+
readonly items: unknown;
|
|
39
|
+
readonly itemEdges: readonly MappingEdge[];
|
|
40
|
+
readonly sources: readonly SourceField[];
|
|
41
|
+
readonly targets: readonly TargetSlot[];
|
|
42
|
+
readonly transforms: ValueTransformRegistry;
|
|
43
|
+
readonly context?: TransformContext;
|
|
44
|
+
}): Promise<unknown[]> {
|
|
45
|
+
if (!Array.isArray(input.items)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const targetLeaves = flattenTargetSlots(input.targets);
|
|
50
|
+
|
|
51
|
+
return Promise.all(
|
|
52
|
+
input.items.map(async (rawItem) => {
|
|
53
|
+
const item = isPlainObject(rawItem) ? rawItem : {};
|
|
54
|
+
let outItem: Record<string, unknown> = {};
|
|
55
|
+
|
|
56
|
+
for (const edge of input.itemEdges) {
|
|
57
|
+
throwIfAborted(input.context?.signal);
|
|
58
|
+
|
|
59
|
+
const sourceField = findSourceField(input.sources, edge.sourceFieldId);
|
|
60
|
+
const targetSlot =
|
|
61
|
+
findTargetSlot(input.targets, edge.targetSlotId) ??
|
|
62
|
+
targetLeaves.find((slot) => slot.id === edge.targetSlotId);
|
|
63
|
+
if (!sourceField || !targetSlot) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sourcePath = itemRelativePath(sourceField);
|
|
68
|
+
const sourceValue = sourcePath ? readObjectPath(item, sourcePath) : undefined;
|
|
69
|
+
|
|
70
|
+
const chain = edgeTransformIds(edge);
|
|
71
|
+
let value: unknown;
|
|
72
|
+
if (chain.length === 0) {
|
|
73
|
+
const identity = input.transforms.get(BUILTIN_TRANSFORM_IDS.identity);
|
|
74
|
+
value = identity
|
|
75
|
+
? await identity.apply(sourceValue, { ...input.context, sampleValue: sourceValue })
|
|
76
|
+
: sourceValue;
|
|
77
|
+
} else {
|
|
78
|
+
const steps = resolveOptionSteps(chain, edge.transformOptionSteps, edge.transformOptions);
|
|
79
|
+
value = await applyTransformChain(
|
|
80
|
+
input.transforms,
|
|
81
|
+
chain,
|
|
82
|
+
sourceValue,
|
|
83
|
+
{ ...input.context, sampleValue: sourceValue },
|
|
84
|
+
steps,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const targetPath = itemRelativePath(targetSlot);
|
|
89
|
+
if (!targetPath) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
outItem = writeObjectPath(outItem, targetPath, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return outItem;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|