@contrail/cli 3.5.0-alpha.dd20a19 → 3.5.1-alpha.68d57e0
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 +1 -33
- package/node_modules/@contrail/loader/lib/color-processor.js +9 -23
- package/node_modules/@contrail/loader/lib/color-processor.spec.js +112 -62
- package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.d.ts +1 -1
- package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.js +2 -0
- package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.spec.js +4 -0
- package/node_modules/@contrail/loader/lib/common/color-validation.d.ts +26 -0
- package/node_modules/@contrail/loader/lib/common/color-validation.js +163 -0
- package/node_modules/@contrail/loader/lib/common/color-validation.spec.js +102 -0
- package/node_modules/@contrail/loader/lib/common/size-range-helpers.js +0 -1
- package/node_modules/@contrail/loader/lib/constants.d.ts +0 -3
- package/node_modules/@contrail/loader/lib/constants.js +1 -4
- package/node_modules/@contrail/loader/lib/entities-v2/colors-v2/color-processor-v2.js +9 -23
- package/node_modules/@contrail/loader/lib/entities-v2/colors-v2/color-processor-v2.spec.js +112 -62
- package/node_modules/@contrail/loader/lib/entities-v2/entities-helper.js +0 -1
- package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/assortment-processor.js +0 -1
- package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/item-processor.js +8 -22
- package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/item-processor.spec.js +43 -1
- package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.d.ts +5 -0
- package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.js +562 -0
- package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.spec.js +231 -0
- package/node_modules/@contrail/loader/lib/pipeline/item-execute-helpers.js +77 -3
- package/node_modules/@contrail/loader/lib/pipeline/item-execute-helpers.spec.js +82 -0
- package/node_modules/@contrail/loader/lib/pipeline/preview.js +0 -5
- package/node_modules/@contrail/loader/lib/pipeline/preview.spec.js +2 -2
- package/node_modules/@contrail/loader/lib/pipeline/project-item-execute-helpers.js +1 -1
- package/node_modules/@contrail/loader/lib/pipeline/transform.js +18 -0
- package/node_modules/@contrail/loader/lib/pipeline/transform.spec.js +3 -2
- package/node_modules/@contrail/loader/lib/pipeline/types.d.ts +5 -2
- package/node_modules/@contrail/loader/lib/pipeline/types.js +2 -0
- package/node_modules/@contrail/loader/lib/pipeline/validate.js +184 -437
- package/node_modules/@contrail/loader/lib/pipeline/validate.spec.js +157 -53
- package/node_modules/@contrail/loader/package.json +3 -2
- package/node_modules/@contrail/sdk/.github/workflows/publish-to-npm.yml +120 -0
- package/node_modules/@contrail/sdk/lib/auth/login.js +42 -3
- package/node_modules/@contrail/sdk/lib/core/request.d.ts +3 -0
- package/node_modules/@contrail/sdk/lib/core/request.js +100 -4
- package/node_modules/@contrail/sdk/lib/entities/entities.client.js +1 -1
- package/node_modules/@contrail/sdk/lib/entities/entity.resource.registry.js +1 -0
- package/node_modules/@contrail/sdk/lib/files/files.d.ts +13 -3
- package/node_modules/@contrail/sdk/lib/files/files.js +66 -25
- package/node_modules/@contrail/sdk/package.json +4 -1
- package/node_modules/validate-color/LICENSE +21 -0
- package/node_modules/validate-color/README.md +265 -0
- package/node_modules/validate-color/lib/index.d.ts +10 -0
- package/node_modules/validate-color/lib/index.js +1 -0
- package/node_modules/validate-color/package.json +91 -0
- package/node_modules/validate-color/test/redos.js +29 -0
- package/oclif.manifest.json +1 -79
- package/package.json +3 -6
- package/dist/api/dash0/client.d.ts +0 -13
- package/dist/api/dash0/client.js +0 -53
- package/dist/api/dash0/spans.d.ts +0 -8
- package/dist/api/dash0/spans.js +0 -91
- package/dist/api/dash0/types.d.ts +0 -84
- package/dist/api/dash0/types.js +0 -4
- package/dist/commands/event-workflows/assess-impact.d.ts +0 -19
- package/dist/commands/event-workflows/assess-impact.js +0 -104
- package/dist/helpers/event-workflow-impact/aggregate.d.ts +0 -23
- package/dist/helpers/event-workflow-impact/aggregate.js +0 -49
- package/dist/helpers/event-workflow-impact/aggregate.spec.js +0 -109
- package/dist/helpers/event-workflow-impact/classify.d.ts +0 -13
- package/dist/helpers/event-workflow-impact/classify.js +0 -135
- package/dist/helpers/event-workflow-impact/classify.spec.js +0 -138
- package/dist/helpers/event-workflow-impact/render.d.ts +0 -12
- package/dist/helpers/event-workflow-impact/render.js +0 -62
- package/dist/helpers/event-workflow-impact/subtree.d.ts +0 -20
- package/dist/helpers/event-workflow-impact/subtree.js +0 -93
- package/dist/helpers/event-workflow-impact/subtree.spec.d.ts +0 -1
- package/dist/helpers/event-workflow-impact/subtree.spec.js +0 -103
- /package/{dist/helpers/event-workflow-impact/aggregate.spec.d.ts → node_modules/@contrail/loader/lib/common/color-validation.spec.d.ts} +0 -0
- /package/{dist/helpers/event-workflow-impact/classify.spec.d.ts → node_modules/@contrail/loader/lib/pipeline/identifier-resolver.spec.d.ts} +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const identifier_resolver_1 = require("./identifier-resolver");
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
describe('resolveIdentifiers', () => {
|
|
6
|
+
describe('validateAssortmentColumns', () => {
|
|
7
|
+
it('skips for non-ASSORTMENT loads', async () => {
|
|
8
|
+
const config = { loadType: [types_1.LoadType.ITEM] };
|
|
9
|
+
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A' }];
|
|
10
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
11
|
+
const assortmentIssue = issues.find((i) => i.columnName?.includes('assortmentId'));
|
|
12
|
+
expect(assortmentIssue).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
it('errors when no rows have assortmentId or assortmentIdentifier', async () => {
|
|
15
|
+
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
16
|
+
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A' }];
|
|
17
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
18
|
+
const issue = issues.find((i) => i.message.includes('ASSORTMENT loads require'));
|
|
19
|
+
expect(issue).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
it('errors when rows have both assortmentId and assortmentIdentifier', async () => {
|
|
22
|
+
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
23
|
+
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'id-1', assortmentIdentifier: 'ident-1' }];
|
|
24
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
25
|
+
const issue = issues.find((i) => i.message.includes('both "assortmentId" and "assortmentIdentifier"'));
|
|
26
|
+
expect(issue).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
it('errors when assortmentId values are empty', async () => {
|
|
29
|
+
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
30
|
+
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: '' }];
|
|
31
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
32
|
+
const issue = issues.find((i) => i.message.includes('"assortmentId" cannot be empty'));
|
|
33
|
+
expect(issue).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
it('does not error when rows have valid assortmentId (no client)', async () => {
|
|
36
|
+
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
37
|
+
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' }];
|
|
38
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
39
|
+
const columnIssue = issues.find((i) => i.message.includes('ASSORTMENT loads require'));
|
|
40
|
+
expect(columnIssue).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
it('does not error on rows missing assortmentId when derived from conditional columns with conditions', async () => {
|
|
43
|
+
const config = {
|
|
44
|
+
loadType: [types_1.LoadType.ASSORTMENT],
|
|
45
|
+
conditionalColumns: [
|
|
46
|
+
{ toProperty: 'assortmentId', conditions: [{ conditional: "{gender} === 'mens'", value: 'assort-1' }] },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
const rows = [
|
|
50
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' },
|
|
51
|
+
{ itemFamilyFederatedId: 'fam-2', name: 'B' },
|
|
52
|
+
];
|
|
53
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
54
|
+
const emptyIssue = issues.find((i) => i.message.includes('"assortmentId" cannot be empty'));
|
|
55
|
+
expect(emptyIssue).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('combo mode: PROJECT_ITEM + ASSORTMENT', () => {
|
|
59
|
+
const buildClient = (handler) => ({
|
|
60
|
+
get: jest.fn(async (params) => handler(params)),
|
|
61
|
+
create: jest.fn(),
|
|
62
|
+
update: jest.fn(),
|
|
63
|
+
delete: jest.fn(),
|
|
64
|
+
batchCreate: jest.fn(),
|
|
65
|
+
batchUpdate: jest.fn(),
|
|
66
|
+
batchDelete: jest.fn(),
|
|
67
|
+
getAllReferences: jest.fn(),
|
|
68
|
+
});
|
|
69
|
+
it('stamps projectId on rows from the assortment workspace when assortmentId is per-row', async () => {
|
|
70
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
71
|
+
const rows = [
|
|
72
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' },
|
|
73
|
+
{ itemFamilyFederatedId: 'fam-2', name: 'B', assortmentId: 'assort-1' },
|
|
74
|
+
];
|
|
75
|
+
const client = buildClient((params) => {
|
|
76
|
+
if (params.entityName === 'assortment' && params.id === 'assort-1') {
|
|
77
|
+
return { id: 'assort-1', name: 'Assort 1', workspaceId: 'ws-1' };
|
|
78
|
+
}
|
|
79
|
+
if (params.entityName === 'workspace' && params.id === 'ws-1') {
|
|
80
|
+
return { id: 'ws-1', project: { id: 'proj-1', name: 'Project 1' } };
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
});
|
|
84
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
85
|
+
expect(issues).toHaveLength(0);
|
|
86
|
+
expect(rows[0].projectId).toBe('proj-1');
|
|
87
|
+
expect(rows[1].projectId).toBe('proj-1');
|
|
88
|
+
expect(rows[0].projectName).toBe('Project 1');
|
|
89
|
+
expect(rows[0].assortmentWorkspaceId).toBe('ws-1');
|
|
90
|
+
});
|
|
91
|
+
it('resolves assortmentIdentifier → assortment → workspace → project', async () => {
|
|
92
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
93
|
+
const rows = [
|
|
94
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentIdentifier: 'NORD-2026' },
|
|
95
|
+
];
|
|
96
|
+
const client = buildClient((params) => {
|
|
97
|
+
if (params.entityName === 'assortment' && params.criteria?.identifier === 'NORD-2026') {
|
|
98
|
+
return [{ id: 'assort-9', name: 'Nord 2026', workspaceId: 'ws-9' }];
|
|
99
|
+
}
|
|
100
|
+
if (params.entityName === 'workspace' && params.id === 'ws-9') {
|
|
101
|
+
return { id: 'ws-9', project: { id: 'proj-9', name: 'Nord Project' } };
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
});
|
|
105
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
106
|
+
expect(issues).toHaveLength(0);
|
|
107
|
+
expect(rows[0].assortmentId).toBe('assort-9');
|
|
108
|
+
expect(rows[0].projectId).toBe('proj-9');
|
|
109
|
+
expect(rows[0].projectName).toBe('Nord Project');
|
|
110
|
+
expect(rows[0].assortmentIdentifier).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
it('reports assortment-only error when a row is missing assortmentId (no project/workspace mention)', async () => {
|
|
113
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
114
|
+
const rows = [
|
|
115
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A' },
|
|
116
|
+
];
|
|
117
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
118
|
+
const projectIssue = issues.find((i) => i.message.includes('workspaceIdentifier') || i.message.includes('projectId'));
|
|
119
|
+
expect(projectIssue).toBeUndefined();
|
|
120
|
+
const assortmentIssue = issues.find((i) => i.message.includes('ASSORTMENT loads require'));
|
|
121
|
+
expect(assortmentIssue).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
it('errors when the assortment has no workspaceId (assortment-scoped error)', async () => {
|
|
124
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
125
|
+
const rows = [
|
|
126
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-x' },
|
|
127
|
+
];
|
|
128
|
+
const client = buildClient((params) => {
|
|
129
|
+
if (params.entityName === 'assortment' && params.id === 'assort-x') {
|
|
130
|
+
return { id: 'assort-x', name: 'X' };
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
});
|
|
134
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
135
|
+
const issue = issues.find((i) => i.message.includes('has no workspaceId'));
|
|
136
|
+
expect(issue).toBeDefined();
|
|
137
|
+
expect(issue?.columnName).toBe('assortmentId');
|
|
138
|
+
});
|
|
139
|
+
it('errors when the assortment workspace has no project relation', async () => {
|
|
140
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
141
|
+
const rows = [
|
|
142
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-y' },
|
|
143
|
+
];
|
|
144
|
+
const client = buildClient((params) => {
|
|
145
|
+
if (params.entityName === 'assortment' && params.id === 'assort-y') {
|
|
146
|
+
return { id: 'assort-y', name: 'Y', workspaceId: 'ws-y' };
|
|
147
|
+
}
|
|
148
|
+
if (params.entityName === 'workspace' && params.id === 'ws-y') {
|
|
149
|
+
return { id: 'ws-y' };
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
});
|
|
153
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
154
|
+
const issue = issues.find((i) => i.message.includes('has no associated project'));
|
|
155
|
+
expect(issue).toBeDefined();
|
|
156
|
+
expect(issue?.columnName).toBe('assortmentId');
|
|
157
|
+
expect(rows[0].projectId).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
it('does not require workspaceIdentifier or projectId on rows in combo mode', async () => {
|
|
160
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
161
|
+
const rows = [
|
|
162
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' },
|
|
163
|
+
];
|
|
164
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
165
|
+
const projectItemIssue = issues.find((i) => i.message.includes('PROJECT_ITEM loads require'));
|
|
166
|
+
expect(projectItemIssue).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
it('does NOT trigger combo mode when rows already have a workspaceIdentifier', async () => {
|
|
169
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
170
|
+
const rows = [
|
|
171
|
+
{
|
|
172
|
+
itemFamilyFederatedId: 'fam-1',
|
|
173
|
+
name: 'A',
|
|
174
|
+
assortmentId: 'assort-1',
|
|
175
|
+
workspaceIdentifier: 'project_one',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
const client = buildClient((params) => {
|
|
179
|
+
if (params.entityName === 'workspace' && params.criteria?.identifier === 'project_one') {
|
|
180
|
+
return [{ id: 'ws-1' }];
|
|
181
|
+
}
|
|
182
|
+
if (params.entityName === 'workspace' && params.id === 'ws-1') {
|
|
183
|
+
return { id: 'ws-1', project: { id: 'proj-1', name: 'Project 1' } };
|
|
184
|
+
}
|
|
185
|
+
if (params.entityName === 'assortment' && params.id === 'assort-1') {
|
|
186
|
+
return { id: 'assort-1', name: 'A1', workspaceId: 'totally-bogus-id' };
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
});
|
|
190
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
191
|
+
expect(issues).toHaveLength(0);
|
|
192
|
+
expect(rows[0].projectId).toBe('proj-1');
|
|
193
|
+
expect(rows[0].workspaceIdentifier).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
it('does NOT trigger combo mode when rows already have a projectId', async () => {
|
|
196
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM, types_1.LoadType.ASSORTMENT] };
|
|
197
|
+
const rows = [
|
|
198
|
+
{
|
|
199
|
+
itemFamilyFederatedId: 'fam-1',
|
|
200
|
+
name: 'A',
|
|
201
|
+
assortmentId: 'assort-1',
|
|
202
|
+
projectId: 'proj-explicit',
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
const client = buildClient((params) => {
|
|
206
|
+
if (params.entityName === 'project' && params.id === 'proj-explicit') {
|
|
207
|
+
return { id: 'proj-explicit', name: 'Explicit Project' };
|
|
208
|
+
}
|
|
209
|
+
if (params.entityName === 'assortment' && params.id === 'assort-1') {
|
|
210
|
+
return { id: 'assort-1', name: 'A1', workspaceId: 'totally-bogus-id' };
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
});
|
|
214
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config, client);
|
|
215
|
+
expect(issues).toHaveLength(0);
|
|
216
|
+
expect(rows[0].projectId).toBe('proj-explicit');
|
|
217
|
+
expect(rows[0].projectName).toBe('Explicit Project');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe('PROJECT_ITEM-only loads (no assortment)', () => {
|
|
221
|
+
it('still requires workspaceIdentifier or projectId on rows', async () => {
|
|
222
|
+
const config = { loadType: [types_1.LoadType.PROJECT_ITEM] };
|
|
223
|
+
const rows = [
|
|
224
|
+
{ itemFamilyFederatedId: 'fam-1', name: 'A' },
|
|
225
|
+
];
|
|
226
|
+
const { issues } = await (0, identifier_resolver_1.resolveIdentifiers)(rows, config);
|
|
227
|
+
const issue = issues.find((i) => i.message.includes('PROJECT_ITEM loads require'));
|
|
228
|
+
expect(issue).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -11,23 +11,97 @@ const pLimit = require('p-limit');
|
|
|
11
11
|
const DEFAULT_BATCH_SIZE = 50;
|
|
12
12
|
const DEFAULT_CONCURRENCY = 20;
|
|
13
13
|
const DEFAULT_MAX_RETRIES = 3;
|
|
14
|
+
function findUniquenessPayload(node, depth = 0) {
|
|
15
|
+
if (!node || depth > 6)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (Array.isArray(node?.failedEntities))
|
|
18
|
+
return node;
|
|
19
|
+
if (typeof node === 'string') {
|
|
20
|
+
const trimmed = node.trim();
|
|
21
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
22
|
+
try {
|
|
23
|
+
return findUniquenessPayload(JSON.parse(trimmed), depth + 1);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(node)) {
|
|
32
|
+
for (const child of node) {
|
|
33
|
+
const found = findUniquenessPayload(child, depth + 1);
|
|
34
|
+
if (found)
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (typeof node === 'object') {
|
|
40
|
+
for (const key of ['error', 'response', 'message', 'data', 'body']) {
|
|
41
|
+
if (node[key] !== undefined) {
|
|
42
|
+
const found = findUniquenessPayload(node[key], depth + 1);
|
|
43
|
+
if (found)
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
14
50
|
function extractApiErrorMessage(err) {
|
|
15
|
-
const validationErrors = err?.error?.response?.validationSummary?.validationErrors
|
|
51
|
+
const validationErrors = err?.error?.response?.validationSummary?.validationErrors ??
|
|
52
|
+
err?.error?.response?.validationErrors;
|
|
16
53
|
if (validationErrors && typeof validationErrors === 'object') {
|
|
17
54
|
const messages = [];
|
|
18
55
|
for (const [field, errors] of Object.entries(validationErrors)) {
|
|
19
56
|
if (Array.isArray(errors)) {
|
|
20
57
|
for (const e of errors) {
|
|
21
|
-
|
|
58
|
+
const isObj = typeof e === 'object' && e !== null;
|
|
59
|
+
const valuePart = isObj && 'value' in e ? ` (value=${JSON.stringify(e.value)})` : '';
|
|
60
|
+
const msg = isObj ? e.message || JSON.stringify(e) : String(e);
|
|
61
|
+
messages.push(`${field}${valuePart}: ${msg}`);
|
|
22
62
|
}
|
|
23
63
|
}
|
|
24
64
|
}
|
|
25
65
|
if (messages.length > 0)
|
|
26
|
-
return messages.join('; ')
|
|
66
|
+
return `Validation failed — ${messages.join('; ')}`;
|
|
67
|
+
}
|
|
68
|
+
const uniquePayload = findUniquenessPayload(err);
|
|
69
|
+
if (uniquePayload?.failedEntities?.length) {
|
|
70
|
+
const parts = uniquePayload.failedEntities.map((f) => {
|
|
71
|
+
const label = f.propertyLabel ?? f.propertyName ?? '<unknown>';
|
|
72
|
+
const value = f.propertyValue ?? '';
|
|
73
|
+
return `${label}=${JSON.stringify(value)}`;
|
|
74
|
+
});
|
|
75
|
+
const offending = uniquePayload.propertiesMustBeUnique?.length
|
|
76
|
+
? ` (one of ${uniquePayload.propertiesMustBeUnique.join(', ')} is already taken or duplicated in batch)`
|
|
77
|
+
: '';
|
|
78
|
+
return `Uniqueness conflict — attempted: ${parts.join(', ')}${offending}`;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
console.warn('[extractApiErrorMessage] unknown error shape:', summarizeErrorShape(err));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
27
84
|
}
|
|
28
85
|
return err?.error?.message ?? err?.error?.response?.message ?? String(err?.error);
|
|
29
86
|
}
|
|
30
87
|
exports.extractApiErrorMessage = extractApiErrorMessage;
|
|
88
|
+
function summarizeErrorShape(err) {
|
|
89
|
+
const truncate = (s, max = 200) => {
|
|
90
|
+
if (s === undefined || s === null)
|
|
91
|
+
return undefined;
|
|
92
|
+
const str = typeof s === 'string' ? s : JSON.stringify(s);
|
|
93
|
+
return str.length > max ? `${str.slice(0, max)}…` : str;
|
|
94
|
+
};
|
|
95
|
+
const errInner = err?.error;
|
|
96
|
+
return {
|
|
97
|
+
topLevelKeys: err && typeof err === 'object' ? Object.keys(err) : undefined,
|
|
98
|
+
errorKeys: errInner && typeof errInner === 'object' ? Object.keys(errInner) : undefined,
|
|
99
|
+
status: errInner?.status ?? errInner?.response?.status,
|
|
100
|
+
name: errInner?.name,
|
|
101
|
+
message: truncate(errInner?.message),
|
|
102
|
+
responseMessage: truncate(errInner?.response?.message),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
31
105
|
function extractFailureId(err) {
|
|
32
106
|
return (err?.data?.federatedId ??
|
|
33
107
|
err?.entity?.federatedId ??
|
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const types_1 = require("@contrail/types");
|
|
4
4
|
const item_execute_helpers_1 = require("./item-execute-helpers");
|
|
5
|
+
describe('extractApiErrorMessage', () => {
|
|
6
|
+
it('formats current-shape validation errors (response.validationErrors)', () => {
|
|
7
|
+
const err = {
|
|
8
|
+
entity: { federatedId: 'TB0A41MG' },
|
|
9
|
+
error: {
|
|
10
|
+
response: {
|
|
11
|
+
validationErrorCount: 1,
|
|
12
|
+
validationErrors: {
|
|
13
|
+
vfAdvConceptsTech: [
|
|
14
|
+
{ value: 'GRIPMAX SLIP RESISTANT', message: 'The value provided is not a valid option for this property.' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
status: 400,
|
|
19
|
+
message: 'Bad Request Exception',
|
|
20
|
+
name: 'BadRequestException',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const msg = (0, item_execute_helpers_1.extractApiErrorMessage)(err);
|
|
24
|
+
expect(msg).toContain('Validation failed');
|
|
25
|
+
expect(msg).toContain('vfAdvConceptsTech');
|
|
26
|
+
expect(msg).toContain('GRIPMAX SLIP RESISTANT');
|
|
27
|
+
expect(msg).toContain('The value provided is not a valid option');
|
|
28
|
+
});
|
|
29
|
+
it('formats older-shape validation errors (validationSummary.validationErrors)', () => {
|
|
30
|
+
const err = {
|
|
31
|
+
error: {
|
|
32
|
+
response: {
|
|
33
|
+
validationSummary: {
|
|
34
|
+
validationErrors: {
|
|
35
|
+
name: [{ message: 'Required.' }],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
expect((0, item_execute_helpers_1.extractApiErrorMessage)(err)).toBe('Validation failed — name: Required.');
|
|
42
|
+
});
|
|
43
|
+
it('does not throw when a validation-error array contains primitive entries', () => {
|
|
44
|
+
const err = {
|
|
45
|
+
error: {
|
|
46
|
+
response: {
|
|
47
|
+
validationErrors: { someField: ['oops'] },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
expect(() => (0, item_execute_helpers_1.extractApiErrorMessage)(err)).not.toThrow();
|
|
52
|
+
expect((0, item_execute_helpers_1.extractApiErrorMessage)(err)).toContain('someField');
|
|
53
|
+
});
|
|
54
|
+
describe('uniqueness conflict payloads', () => {
|
|
55
|
+
const sample = [
|
|
56
|
+
{
|
|
57
|
+
failedEntities: [
|
|
58
|
+
{ propertyName: 'federatedId', propertyLabel: 'Federated Id', propertyValue: '0A294C', entityReference: 'item:abc' },
|
|
59
|
+
{ propertyName: 'vfStyleNumber', propertyLabel: 'Style Number', propertyValue: '0A294C', entityReference: 'item:abc' },
|
|
60
|
+
],
|
|
61
|
+
propertiesMustBeUnique: ['federatedId', 'vfStyleNumber'],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
it('finds the payload nested under err.error.response.message (object form)', () => {
|
|
65
|
+
const msg = (0, item_execute_helpers_1.extractApiErrorMessage)({ error: { response: { message: sample } } });
|
|
66
|
+
expect(msg).toContain('Uniqueness conflict');
|
|
67
|
+
expect(msg).toContain('Style Number="0A294C"');
|
|
68
|
+
expect(msg).toContain('vfStyleNumber');
|
|
69
|
+
});
|
|
70
|
+
it('parses the payload when err.error.response.message is a JSON-stringified array', () => {
|
|
71
|
+
const msg = (0, item_execute_helpers_1.extractApiErrorMessage)({ error: { response: { message: JSON.stringify(sample) } } });
|
|
72
|
+
expect(msg).toContain('Uniqueness conflict');
|
|
73
|
+
expect(msg).toContain('Federated Id="0A294C"');
|
|
74
|
+
});
|
|
75
|
+
it('finds the payload at the top level', () => {
|
|
76
|
+
expect((0, item_execute_helpers_1.extractApiErrorMessage)(sample[0])).toContain('Uniqueness conflict');
|
|
77
|
+
});
|
|
78
|
+
it('falls back to a redacted summary for unknown shapes', () => {
|
|
79
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
80
|
+
const result = (0, item_execute_helpers_1.extractApiErrorMessage)({ error: { something: 'else', message: 'mystery' } });
|
|
81
|
+
expect(result).toBe('mystery');
|
|
82
|
+
expect(warn).toHaveBeenCalledWith('[extractApiErrorMessage] unknown error shape:', expect.objectContaining({ message: 'mystery' }));
|
|
83
|
+
warn.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
5
87
|
describe('splitIntoFamiliesAndOptions', () => {
|
|
6
88
|
const makeTypesByPath = (typeProperties) => ({
|
|
7
89
|
item: {
|
|
@@ -21,11 +21,6 @@ async function preview(client, rows, config, columnAnalysis) {
|
|
|
21
21
|
const itemsByFederatedId = entityType !== types_1.EntityType.ITEM ? { ...sharedItemsByFederatedId } : {};
|
|
22
22
|
const clonedRows = rows.map(row => ({ ...row }));
|
|
23
23
|
const result = await entity_processor_v2_1.EntityProcessorV2.setupEntities(client, entityType, clonedRows, undefined, loaderProcess, itemsByFederatedId, doConstructEntities, columnAnalysis.typesByPath ? buildTypesByTypePathCache(columnAnalysis) : undefined);
|
|
24
|
-
if (result.generalWarnings?.length) {
|
|
25
|
-
for (const warning of result.generalWarnings) {
|
|
26
|
-
allWarnings.push(warning.message);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
24
|
if (result.badEntities?.length) {
|
|
30
25
|
for (let i = 0; i < result.badEntities.length; i++) {
|
|
31
26
|
const badEntity = result.badEntities[i];
|
|
@@ -122,7 +122,7 @@ describe('preview (non-item entity types)', () => {
|
|
|
122
122
|
expect(result.staleRows[0].details?.incomingTimestamp).toBe('2024-01-01T00:00:00Z');
|
|
123
123
|
expect(result.staleRows[0].details?.existingTimestamp).toBe('2024-06-01T00:00:00Z');
|
|
124
124
|
});
|
|
125
|
-
it('should
|
|
125
|
+
it('should not duplicate object-reference resolution warnings (now surfaced by validate)', async () => {
|
|
126
126
|
entity_processor_v2_1.EntityProcessorV2.setupEntities.mockResolvedValue({
|
|
127
127
|
incomingEntities: [],
|
|
128
128
|
badEntities: [],
|
|
@@ -135,7 +135,7 @@ describe('preview (non-item entity types)', () => {
|
|
|
135
135
|
});
|
|
136
136
|
const rows = [{ federatedId: '1' }];
|
|
137
137
|
const result = await (0, preview_1.preview)(mockClient, rows, baseConfig, baseAnalysis);
|
|
138
|
-
expect(result.warnings).toContain('ObjectReference not found for vendor with value acme');
|
|
138
|
+
expect(result.warnings).not.toContain('ObjectReference not found for vendor with value acme');
|
|
139
139
|
});
|
|
140
140
|
it('should store constructed entities for execute stage', async () => {
|
|
141
141
|
entity_processor_v2_1.EntityProcessorV2.setupEntities.mockResolvedValue({
|
|
@@ -232,7 +232,7 @@ async function executeProjectItemEntities(client, entitySet, config, options) {
|
|
|
232
232
|
failures: [{
|
|
233
233
|
federatedId: 'N/A',
|
|
234
234
|
operation: 'create',
|
|
235
|
-
error: 'Could not resolve a target Project\'s ID
|
|
235
|
+
error: 'Could not resolve a target Project\'s ID. For combined PROJECT_ITEM + ASSORTMENT loads, ensure each row has an assortmentId so the project can be derived from the assortment\'s workspace. Otherwise, include or map a workspaceIdentifier column, or a projectId column with the project\'s ID directly.',
|
|
236
236
|
}],
|
|
237
237
|
}],
|
|
238
238
|
skippedRows: [],
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.transform = void 0;
|
|
4
4
|
const types_1 = require("./types");
|
|
5
|
+
const identifier_resolver_1 = require("./identifier-resolver");
|
|
5
6
|
const data_transformer_functions_1 = require("../common/data-transformer-functions");
|
|
6
7
|
const type_helper_1 = require("../common/type-helper");
|
|
7
8
|
const types_2 = require("./types");
|
|
@@ -127,6 +128,22 @@ async function transform(client, rows, config) {
|
|
|
127
128
|
typeAware.generalErrors.push({ message: errorMessage, issueType: types_1.IssueType.SYSTEM_ERROR });
|
|
128
129
|
}
|
|
129
130
|
const { columns, unmappedTypeProperties } = analyzeColumns(processedData, typePropertyMaps, config);
|
|
131
|
+
let identifierIssues = [];
|
|
132
|
+
try {
|
|
133
|
+
const resolved = await (0, identifier_resolver_1.resolveIdentifiers)(processedData, config, client);
|
|
134
|
+
identifierIssues = resolved.issues;
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
const errorMessage = `Error in transform step "Resolve Identifiers": ${e.message}`;
|
|
138
|
+
console.error(errorMessage);
|
|
139
|
+
typeAware.generalErrors.push({ message: errorMessage, issueType: types_1.IssueType.SYSTEM_ERROR });
|
|
140
|
+
}
|
|
141
|
+
typeAware.stepResults.push({
|
|
142
|
+
stepName: 'Resolve Identifiers',
|
|
143
|
+
rowCountAfter: processedData.length,
|
|
144
|
+
errorCount: identifierIssues.filter((i) => i.severity === 'error').length,
|
|
145
|
+
warningCount: identifierIssues.filter((i) => i.severity === 'warning').length,
|
|
146
|
+
});
|
|
130
147
|
const merged = mergeResults(structural, typeAware);
|
|
131
148
|
merged.stepResults.push({
|
|
132
149
|
stepName: 'Analyze Columns',
|
|
@@ -140,6 +157,7 @@ async function transform(client, rows, config) {
|
|
|
140
157
|
typesByPath,
|
|
141
158
|
columns,
|
|
142
159
|
unmappedTypeProperties,
|
|
160
|
+
identifierIssues,
|
|
143
161
|
...collectIssues(merged),
|
|
144
162
|
};
|
|
145
163
|
}
|
|
@@ -63,10 +63,10 @@ describe('transform', () => {
|
|
|
63
63
|
expect(result.stepResults).toEqual([]);
|
|
64
64
|
expect(result.errors).toEqual([]);
|
|
65
65
|
});
|
|
66
|
-
it('should run all
|
|
66
|
+
it('should run all 9 transform steps (including type fetch, identifier resolution, and column analysis)', async () => {
|
|
67
67
|
const rows = [{ sku: 'ABC-123', name: 'Test', active: 'true' }];
|
|
68
68
|
const result = await (0, transform_1.transform)(mockClient, rows, baseConfig);
|
|
69
|
-
expect(result.stepResults).toHaveLength(
|
|
69
|
+
expect(result.stepResults).toHaveLength(9);
|
|
70
70
|
expect(result.stepResults.map((s) => s.stepName)).toEqual([
|
|
71
71
|
'Evaluate Conditional Columns',
|
|
72
72
|
'Remove Unwanted Properties',
|
|
@@ -75,6 +75,7 @@ describe('transform', () => {
|
|
|
75
75
|
'Fetch Type Definitions',
|
|
76
76
|
'Transform Data Based on Types',
|
|
77
77
|
'Coerce Values by Type',
|
|
78
|
+
'Resolve Identifiers',
|
|
78
79
|
'Analyze Columns',
|
|
79
80
|
]);
|
|
80
81
|
});
|
|
@@ -41,7 +41,9 @@ export declare enum PreValidationCategory {
|
|
|
41
41
|
SELF_REFERENCE = "SELF_REFERENCE",
|
|
42
42
|
UNMAPPED_COLUMN = "UNMAPPED_COLUMN",
|
|
43
43
|
UNMAPPED_TYPE_PROPERTY = "UNMAPPED_TYPE_PROPERTY",
|
|
44
|
-
FORMAT_NUMBER_PRECISION = "FORMAT_NUMBER_PRECISION"
|
|
44
|
+
FORMAT_NUMBER_PRECISION = "FORMAT_NUMBER_PRECISION",
|
|
45
|
+
INVALID_DATA = "INVALID_DATA",
|
|
46
|
+
DUPLICATE_FEDERATED_ID = "DUPLICATE_FEDERATED_ID"
|
|
45
47
|
}
|
|
46
48
|
export declare enum PreValidationSeverity {
|
|
47
49
|
ERROR = "error",
|
|
@@ -133,8 +135,9 @@ export interface TransformResult {
|
|
|
133
135
|
generalErrors: GeneralIssueDetails[];
|
|
134
136
|
generalWarnings: GeneralIssueDetails[];
|
|
135
137
|
stepResults: PreprocessingStepSummary[];
|
|
138
|
+
identifierIssues?: PreValidationIssue[];
|
|
136
139
|
}
|
|
137
|
-
export declare type ColumnAnalysisResult = Pick<TransformResult, 'columns' | 'unmappedTypeProperties' | 'typePropertyMaps' | 'typesByPath'
|
|
140
|
+
export declare type ColumnAnalysisResult = Pick<TransformResult, 'columns' | 'unmappedTypeProperties' | 'typePropertyMaps' | 'typesByPath' | 'identifierIssues'> & Partial<Pick<TransformResult, 'warnings' | 'errors'>>;
|
|
138
141
|
export interface ColumnMetaData {
|
|
139
142
|
csvHeader: string;
|
|
140
143
|
matchedSlug?: string;
|
|
@@ -49,6 +49,8 @@ var PreValidationCategory;
|
|
|
49
49
|
PreValidationCategory["UNMAPPED_COLUMN"] = "UNMAPPED_COLUMN";
|
|
50
50
|
PreValidationCategory["UNMAPPED_TYPE_PROPERTY"] = "UNMAPPED_TYPE_PROPERTY";
|
|
51
51
|
PreValidationCategory["FORMAT_NUMBER_PRECISION"] = "FORMAT_NUMBER_PRECISION";
|
|
52
|
+
PreValidationCategory["INVALID_DATA"] = "INVALID_DATA";
|
|
53
|
+
PreValidationCategory["DUPLICATE_FEDERATED_ID"] = "DUPLICATE_FEDERATED_ID";
|
|
52
54
|
})(PreValidationCategory = exports.PreValidationCategory || (exports.PreValidationCategory = {}));
|
|
53
55
|
var PreValidationSeverity;
|
|
54
56
|
(function (PreValidationSeverity) {
|