@contrail/cli 3.5.0 → 3.5.1-alpha.64bf880
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 -25
- package/dist/api/types.d.ts +0 -5
- package/dist/api/types.js +1 -36
- package/dist/commands/types/getAll.js +0 -18
- package/dist/commands/types/loadAll.js +0 -2
- package/dist/commands/types/mergeTypeProperties.js +0 -5
- package/dist/commands/types/uploadOptionSetHierarchies.js +0 -117
- package/dist/commands/types/uploadOptionSets.js +1 -1
- package/dist/commands/types/uploadRuleSets.js +2 -85
- package/dist/commands/types/uploadTypeOverrides.js +10 -2
- package/dist/commands/types/uploadTypePolicies.js +2 -86
- package/dist/commands/types/uploadTypePropertyPolicies.d.ts +0 -1
- package/dist/commands/types/uploadTypePropertyPolicies.js +3 -139
- package/dist/helpers/types/type-uploader.js +1 -1
- package/dist/interfaces.d.ts +0 -25
- package/dist/templates/typescript/tsup.config.ts +0 -0
- 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.d.ts +1 -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 -52
- package/package.json +3 -3
- package/dist/commands/types/uploadUsageInfo.d.ts +0 -15
- package/dist/commands/types/uploadUsageInfo.js +0 -180
- package/dist/commands/types/uploadUsageInfo.spec.js +0 -249
- /package/{dist/commands/types/uploadUsageInfo.spec.d.ts → node_modules/@contrail/loader/lib/common/color-validation.spec.d.ts} +0 -0
|
@@ -118,6 +118,59 @@ describe('validate', () => {
|
|
|
118
118
|
expect(multiSelectIssues).toHaveLength(1);
|
|
119
119
|
expect(multiSelectIssues[0].suggestedFix?.type).toBe(types_3.SuggestedFixType.CHANGE_SEPARATOR);
|
|
120
120
|
});
|
|
121
|
+
it('should validate Family/Option-suffixed columns against the base option set', async () => {
|
|
122
|
+
const analysis = makeAnalysis({
|
|
123
|
+
optionSetUsingTypePropertiesBySlug: {
|
|
124
|
+
vfAdvConceptsTech: {
|
|
125
|
+
slug: 'vfAdvConceptsTech',
|
|
126
|
+
label: 'Advanced Concepts Tech',
|
|
127
|
+
propertyType: 'multi_select',
|
|
128
|
+
typePropertyOptionSetId: 'os-suffix',
|
|
129
|
+
options: [
|
|
130
|
+
{ value: 'ortholite', display: 'Ortholite' },
|
|
131
|
+
{ value: 'hoverSpring', display: 'Hover Spring' },
|
|
132
|
+
],
|
|
133
|
+
optionLookupByValue: {
|
|
134
|
+
ortholite: { value: 'ortholite', display: 'Ortholite' },
|
|
135
|
+
hoverSpring: { value: 'hoverSpring', display: 'Hover Spring' },
|
|
136
|
+
},
|
|
137
|
+
optionLookupByDisplay: {
|
|
138
|
+
Ortholite: { value: 'ortholite', display: 'Ortholite' },
|
|
139
|
+
'Hover Spring': { value: 'hoverSpring', display: 'Hover Spring' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const rows = [
|
|
145
|
+
{ federatedId: '1', name: 'A', vfAdvConceptsTechFamily: 'ortholite,GRIPMAX SLIP RESISTANT,hoverSpring' },
|
|
146
|
+
];
|
|
147
|
+
const result = await (0, validate_1.validate)(rows, baseConfig, analysis);
|
|
148
|
+
expect(result.valid).toBe(false);
|
|
149
|
+
const issues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.OPTION_SET && i.columnName === 'vfAdvConceptsTechFamily');
|
|
150
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
151
|
+
const offending = issues.find((i) => i.message.includes('GRIPMAX SLIP RESISTANT'));
|
|
152
|
+
expect(offending).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
it('flags missing option set on suffixed columns (no typePropertyOptionSetId)', async () => {
|
|
155
|
+
const analysis = makeAnalysis({
|
|
156
|
+
optionSetUsingTypePropertiesBySlug: {
|
|
157
|
+
lifecycleStage: {
|
|
158
|
+
slug: 'lifecycleStage',
|
|
159
|
+
label: 'Lifecycle Stage',
|
|
160
|
+
propertyType: 'choice',
|
|
161
|
+
typePropertyOptionSetId: undefined,
|
|
162
|
+
options: [],
|
|
163
|
+
optionLookupByValue: {},
|
|
164
|
+
optionLookupByDisplay: {},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const rows = [{ federatedId: '1', name: 'A', lifecycleStageOption: 'concept' }];
|
|
169
|
+
const result = await (0, validate_1.validate)(rows, baseConfig, analysis);
|
|
170
|
+
const issues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.OPTION_SET && i.columnName === 'lifecycleStageOption');
|
|
171
|
+
expect(issues.length).toBe(1);
|
|
172
|
+
expect(issues[0].message).toContain('no option set attached');
|
|
173
|
+
});
|
|
121
174
|
it('should skip empty values in option set validation', async () => {
|
|
122
175
|
const analysis = makeAnalysis({
|
|
123
176
|
optionSetUsingTypePropertiesBySlug: {
|
|
@@ -531,59 +584,6 @@ describe('validate', () => {
|
|
|
531
584
|
expect(conflict).toBeUndefined();
|
|
532
585
|
});
|
|
533
586
|
});
|
|
534
|
-
describe('validateAssortmentColumns', () => {
|
|
535
|
-
it('should skip for non-ASSORTMENT loads', async () => {
|
|
536
|
-
const config = { loadType: [types_1.LoadType.ITEM] };
|
|
537
|
-
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A' }];
|
|
538
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
539
|
-
const assortmentIssue = result.issues.find((i) => i.columnName?.includes('assortmentId'));
|
|
540
|
-
expect(assortmentIssue).toBeUndefined();
|
|
541
|
-
});
|
|
542
|
-
it('should error when no rows have assortmentId or assortmentIdentifier', async () => {
|
|
543
|
-
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
544
|
-
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A' }];
|
|
545
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
546
|
-
const issue = result.issues.find((i) => i.message.includes('ASSORTMENT loads require'));
|
|
547
|
-
expect(issue).toBeDefined();
|
|
548
|
-
expect(result.valid).toBe(false);
|
|
549
|
-
});
|
|
550
|
-
it('should error when rows have both assortmentId and assortmentIdentifier', async () => {
|
|
551
|
-
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
552
|
-
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'id-1', assortmentIdentifier: 'ident-1' }];
|
|
553
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
554
|
-
const issue = result.issues.find((i) => i.message.includes('both "assortmentId" and "assortmentIdentifier"'));
|
|
555
|
-
expect(issue).toBeDefined();
|
|
556
|
-
});
|
|
557
|
-
it('should error when assortmentId values are empty', async () => {
|
|
558
|
-
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
559
|
-
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: '' }];
|
|
560
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
561
|
-
const issue = result.issues.find((i) => i.message.includes('"assortmentId" cannot be empty'));
|
|
562
|
-
expect(issue).toBeDefined();
|
|
563
|
-
});
|
|
564
|
-
it('should not error when rows have valid assortmentId', async () => {
|
|
565
|
-
const config = { loadType: [types_1.LoadType.ASSORTMENT] };
|
|
566
|
-
const rows = [{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' }];
|
|
567
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
568
|
-
const columnIssue = result.issues.find((i) => i.message.includes('ASSORTMENT loads require'));
|
|
569
|
-
expect(columnIssue).toBeUndefined();
|
|
570
|
-
});
|
|
571
|
-
it('should not error on rows missing assortmentId when derived from conditional columns with conditions', async () => {
|
|
572
|
-
const config = {
|
|
573
|
-
loadType: [types_1.LoadType.ASSORTMENT],
|
|
574
|
-
conditionalColumns: [
|
|
575
|
-
{ toProperty: 'assortmentId', conditions: [{ conditional: "{gender} === 'mens'", value: 'assort-1' }] },
|
|
576
|
-
],
|
|
577
|
-
};
|
|
578
|
-
const rows = [
|
|
579
|
-
{ itemFamilyFederatedId: 'fam-1', name: 'A', assortmentId: 'assort-1' },
|
|
580
|
-
{ itemFamilyFederatedId: 'fam-2', name: 'B' },
|
|
581
|
-
];
|
|
582
|
-
const result = await (0, validate_1.validate)(rows, config, makeAnalysis());
|
|
583
|
-
const emptyIssue = result.issues.find((i) => i.message.includes('"assortmentId" cannot be empty'));
|
|
584
|
-
expect(emptyIssue).toBeUndefined();
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
587
|
describe('validatePartialAssortmentConfig', () => {
|
|
588
588
|
it('should skip for non-ASSORTMENT loads', async () => {
|
|
589
589
|
const config = { loadType: [types_1.LoadType.ITEM], partialAssortmentUpdate: true };
|
|
@@ -1211,4 +1211,108 @@ describe('validate', () => {
|
|
|
1211
1211
|
expect(unmappedPropIssues[0].message).toContain('noLabel');
|
|
1212
1212
|
});
|
|
1213
1213
|
});
|
|
1214
|
+
describe('object reference resolution validation', () => {
|
|
1215
|
+
const colorConfig = {
|
|
1216
|
+
loadType: [types_1.LoadType.COLOR],
|
|
1217
|
+
federatedMappings: { federatedId: 'federatedId', name: 'name' },
|
|
1218
|
+
};
|
|
1219
|
+
const typesByPathWithVendorRef = {
|
|
1220
|
+
color: {
|
|
1221
|
+
type: { typeProperties: [{ slug: 'vendor', propertyType: 'object_reference' }] },
|
|
1222
|
+
allPropertiesSlugs: ['vendor'],
|
|
1223
|
+
typePropertyToValueType: {},
|
|
1224
|
+
objectReferenceProperties: [
|
|
1225
|
+
{
|
|
1226
|
+
slug: 'vendor',
|
|
1227
|
+
propertyType: 'object_reference',
|
|
1228
|
+
referencedTypeRootSlug: 'vendor',
|
|
1229
|
+
referencedTypePath: 'vendor',
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
userListProperties: [],
|
|
1233
|
+
},
|
|
1234
|
+
};
|
|
1235
|
+
it('should emit one warning per unique missing value with affected rows', async () => {
|
|
1236
|
+
const mockClient = {
|
|
1237
|
+
get: jest.fn().mockResolvedValue([{ federatedId: 'acme', id: 'vendor-acme' }]),
|
|
1238
|
+
};
|
|
1239
|
+
const rows = [
|
|
1240
|
+
{ federatedId: 'c-1', name: 'Red', vendor: 'acme' },
|
|
1241
|
+
{ federatedId: 'c-2', name: 'Blue', vendor: 'missing-vendor' },
|
|
1242
|
+
{ federatedId: 'c-3', name: 'Green', vendor: 'another-missing' },
|
|
1243
|
+
{ federatedId: 'c-4', name: 'Yellow', vendor: 'missing-vendor' },
|
|
1244
|
+
];
|
|
1245
|
+
const analysis = makeAnalysis({}, typesByPathWithVendorRef);
|
|
1246
|
+
const result = await (0, validate_1.validate)(rows, colorConfig, analysis, mockClient);
|
|
1247
|
+
const refIssues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.REFERENCE_DATA_NOT_FOUND &&
|
|
1248
|
+
i.columnName === 'vendor');
|
|
1249
|
+
expect(refIssues).toHaveLength(2);
|
|
1250
|
+
for (const issue of refIssues) {
|
|
1251
|
+
expect(issue.severity).toBe(types_3.PreValidationSeverity.WARNING);
|
|
1252
|
+
expect(issue.propertyType).toBe('object_reference');
|
|
1253
|
+
}
|
|
1254
|
+
const byValue = new Map(refIssues.map((i) => [i.affectedRows[0].value, i]));
|
|
1255
|
+
expect(byValue.get('missing-vendor').affectedRows.map((r) => r.row).sort()).toEqual([3, 5]);
|
|
1256
|
+
expect(byValue.get('missing-vendor').message).toContain('"missing-vendor"');
|
|
1257
|
+
expect(byValue.get('another-missing').affectedRows.map((r) => r.row)).toEqual([4]);
|
|
1258
|
+
expect(byValue.get('another-missing').message).toContain('"another-missing"');
|
|
1259
|
+
});
|
|
1260
|
+
it('should not emit a warning when all references resolve', async () => {
|
|
1261
|
+
const mockClient = {
|
|
1262
|
+
get: jest.fn().mockResolvedValue([
|
|
1263
|
+
{ federatedId: 'acme', id: 'vendor-acme' },
|
|
1264
|
+
{ federatedId: 'foo', id: 'vendor-foo' },
|
|
1265
|
+
]),
|
|
1266
|
+
};
|
|
1267
|
+
const rows = [
|
|
1268
|
+
{ federatedId: 'c-1', name: 'Red', vendor: 'acme' },
|
|
1269
|
+
{ federatedId: 'c-2', name: 'Blue', vendor: 'foo' },
|
|
1270
|
+
];
|
|
1271
|
+
const analysis = makeAnalysis({}, typesByPathWithVendorRef);
|
|
1272
|
+
const result = await (0, validate_1.validate)(rows, colorConfig, analysis, mockClient);
|
|
1273
|
+
const refIssues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.REFERENCE_DATA_NOT_FOUND);
|
|
1274
|
+
expect(refIssues).toHaveLength(0);
|
|
1275
|
+
});
|
|
1276
|
+
it('should skip the lookup entirely when no client is provided', async () => {
|
|
1277
|
+
const rows = [{ federatedId: 'c-1', name: 'Red', vendor: 'missing' }];
|
|
1278
|
+
const analysis = makeAnalysis({}, typesByPathWithVendorRef);
|
|
1279
|
+
const result = await (0, validate_1.validate)(rows, colorConfig, analysis);
|
|
1280
|
+
const refIssues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.REFERENCE_DATA_NOT_FOUND);
|
|
1281
|
+
expect(refIssues).toHaveLength(0);
|
|
1282
|
+
});
|
|
1283
|
+
it('should prefix custom-entity referenced fed ids when looking up', async () => {
|
|
1284
|
+
const customEntityTypesByPath = {
|
|
1285
|
+
color: {
|
|
1286
|
+
type: { typeProperties: [{ slug: 'category', propertyType: 'object_reference' }] },
|
|
1287
|
+
allPropertiesSlugs: ['category'],
|
|
1288
|
+
typePropertyToValueType: {},
|
|
1289
|
+
objectReferenceProperties: [
|
|
1290
|
+
{
|
|
1291
|
+
slug: 'category',
|
|
1292
|
+
propertyType: 'object_reference',
|
|
1293
|
+
referencedTypeRootSlug: 'custom-entity',
|
|
1294
|
+
referencedTypePath: 'custom-entity:category',
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
userListProperties: [],
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
const mockClient = {
|
|
1301
|
+
get: jest.fn().mockResolvedValue([
|
|
1302
|
+
{ federatedId: 'custom-entity:category:resolved', id: 'cat-1' },
|
|
1303
|
+
]),
|
|
1304
|
+
};
|
|
1305
|
+
const rows = [
|
|
1306
|
+
{ federatedId: 'c-1', name: 'Red', category: 'resolved' },
|
|
1307
|
+
{ federatedId: 'c-2', name: 'Blue', category: 'unresolved' },
|
|
1308
|
+
];
|
|
1309
|
+
const analysis = makeAnalysis({}, customEntityTypesByPath);
|
|
1310
|
+
const result = await (0, validate_1.validate)(rows, colorConfig, analysis, mockClient);
|
|
1311
|
+
const refIssues = result.issues.filter((i) => i.category === types_3.PreValidationCategory.REFERENCE_DATA_NOT_FOUND &&
|
|
1312
|
+
i.columnName === 'category');
|
|
1313
|
+
expect(refIssues).toHaveLength(1);
|
|
1314
|
+
expect(refIssues[0].affectedRows).toHaveLength(1);
|
|
1315
|
+
expect(refIssues[0].affectedRows[0].value).toBe('unresolved');
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1214
1318
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/loader",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Client library for helping with loader jobs.",
|
|
5
5
|
"homepage": "https://archergreysorg.github.io/contrail-loader/",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"form-data": "^4.0.0",
|
|
37
37
|
"lodash": "^4.17.21",
|
|
38
38
|
"node-fetch": "^2.6.0",
|
|
39
|
-
"p-limit": "^3.0.0"
|
|
39
|
+
"p-limit": "^3.0.0",
|
|
40
|
+
"validate-color": "^2.2.4"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/jest": "^29.5.2",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- '.github/**'
|
|
8
|
+
pull_request:
|
|
9
|
+
paths-ignore:
|
|
10
|
+
- '.github/**'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check-version:
|
|
14
|
+
name: Verify version is available on npm
|
|
15
|
+
if: github.event_name == 'pull_request'
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Check version not already published
|
|
21
|
+
run: |
|
|
22
|
+
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
23
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
24
|
+
echo "Checking if ${PACKAGE_NAME}@${VERSION} exists on npm..."
|
|
25
|
+
if npm view "${PACKAGE_NAME}@${VERSION}" version 2>/dev/null; then
|
|
26
|
+
echo "::error::Version ${VERSION} is already published on npm. Bump the version in package.json before merging."
|
|
27
|
+
exit 1
|
|
28
|
+
else
|
|
29
|
+
echo "Version ${VERSION} is available."
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
check-dependencies:
|
|
33
|
+
name: Verify no private dependencies
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Verify no private dependencies
|
|
39
|
+
run: |
|
|
40
|
+
node -e "
|
|
41
|
+
const deps = require('./package.json').dependencies || {};
|
|
42
|
+
const { execSync } = require('child_process');
|
|
43
|
+
let failed = false;
|
|
44
|
+
for (const pkg of Object.keys(deps)) {
|
|
45
|
+
try {
|
|
46
|
+
execSync('npm view ' + pkg, { stdio: 'pipe' });
|
|
47
|
+
} catch {
|
|
48
|
+
console.error('::error::Dependency ' + pkg + ' is not publicly accessible on npm');
|
|
49
|
+
failed = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (failed) process.exit(1);
|
|
53
|
+
console.log('All ' + Object.keys(deps).length + ' dependencies are public.');
|
|
54
|
+
"
|
|
55
|
+
|
|
56
|
+
publish-alpha:
|
|
57
|
+
name: Publish alpha
|
|
58
|
+
needs: check-dependencies
|
|
59
|
+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
permissions:
|
|
62
|
+
id-token: write
|
|
63
|
+
contents: read
|
|
64
|
+
outputs:
|
|
65
|
+
version: ${{ steps.publish.outputs.version }}
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
|
|
69
|
+
- uses: actions/setup-node@v4
|
|
70
|
+
with:
|
|
71
|
+
node-version: '24'
|
|
72
|
+
registry-url: 'https://registry.npmjs.org'
|
|
73
|
+
|
|
74
|
+
- name: Install and build
|
|
75
|
+
run: npm ci && npm run build
|
|
76
|
+
|
|
77
|
+
- name: Publish alpha
|
|
78
|
+
id: publish
|
|
79
|
+
run: |
|
|
80
|
+
SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)
|
|
81
|
+
VERSION=$(node -p "require('./package.json').version")-alpha.${SHORT_SHA}
|
|
82
|
+
npm version $VERSION --no-git-tag-version
|
|
83
|
+
OUTPUT=$(npm publish --tag alpha 2>&1) && {
|
|
84
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
85
|
+
} || {
|
|
86
|
+
if echo "$OUTPUT" | grep -q "You cannot publish over the previously published versions"; then
|
|
87
|
+
echo "::warning::Alpha ${VERSION} already published, skipping."
|
|
88
|
+
else
|
|
89
|
+
echo "::error::Failed to publish alpha ${VERSION}"
|
|
90
|
+
echo "$OUTPUT"
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
publish-release:
|
|
96
|
+
name: Publish release
|
|
97
|
+
needs: check-dependencies
|
|
98
|
+
if: github.event_name == 'push'
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
permissions:
|
|
101
|
+
id-token: write
|
|
102
|
+
contents: read
|
|
103
|
+
outputs:
|
|
104
|
+
version: ${{ steps.publish.outputs.version }}
|
|
105
|
+
steps:
|
|
106
|
+
- uses: actions/checkout@v4
|
|
107
|
+
|
|
108
|
+
- uses: actions/setup-node@v4
|
|
109
|
+
with:
|
|
110
|
+
node-version: '24'
|
|
111
|
+
registry-url: 'https://registry.npmjs.org'
|
|
112
|
+
|
|
113
|
+
- name: Install and build
|
|
114
|
+
run: npm ci && npm run build
|
|
115
|
+
|
|
116
|
+
- name: Publish release
|
|
117
|
+
id: publish
|
|
118
|
+
run: |
|
|
119
|
+
npm publish
|
|
120
|
+
echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
@@ -93,8 +93,16 @@ function usernameAuthentication(email, password, orgSlug) {
|
|
|
93
93
|
cognitoUser.authenticateUser(authDetails, {
|
|
94
94
|
onSuccess: (result) => __awaiter(this, void 0, void 0, function* () {
|
|
95
95
|
const user = result;
|
|
96
|
-
const
|
|
97
|
-
|
|
96
|
+
const cognitoAccessToken = user.idToken.jwtToken;
|
|
97
|
+
try {
|
|
98
|
+
const apiTokens = yield exchangeCognitoTokensForApiTokens(cognitoAccessToken);
|
|
99
|
+
const config = yield setAuthContext(apiTokens.accessToken, orgSlug, apiTokens.refreshToken);
|
|
100
|
+
resolve(config);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error('Failed to process Cognito token:', error);
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
98
106
|
}),
|
|
99
107
|
onFailure: (err) => {
|
|
100
108
|
reject(err);
|
|
@@ -103,11 +111,42 @@ function usernameAuthentication(email, password, orgSlug) {
|
|
|
103
111
|
});
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
|
-
function
|
|
114
|
+
function exchangeCognitoTokensForApiTokens(cognitoAccessToken) {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
const apiGateway = CONFIGURATION.apiGateway;
|
|
117
|
+
const url = apiGateway + '/auth/cognito/';
|
|
118
|
+
const response = yield fetch(url, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
Accept: 'application/json',
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
accessToken: cognitoAccessToken,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`Failed to authenticate with Cognito token: ${response.status}`);
|
|
130
|
+
}
|
|
131
|
+
const data = yield response.json();
|
|
132
|
+
if (!data.accessToken) {
|
|
133
|
+
throw new Error('Response from /auth/cognito/ missing required field: accessToken');
|
|
134
|
+
}
|
|
135
|
+
if (!data.refreshToken) {
|
|
136
|
+
throw new Error('Response from /auth/cognito/ missing required field: refreshToken');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
accessToken: data.accessToken,
|
|
140
|
+
refreshToken: data.refreshToken,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function setAuthContext(bearerToken, orgSlug, apiRefreshToken) {
|
|
107
145
|
return __awaiter(this, void 0, void 0, function* () {
|
|
108
146
|
let apiGateway = CONFIGURATION.apiGateway;
|
|
109
147
|
auth_1.Auth.config = {
|
|
110
148
|
apiUserToken: bearerToken,
|
|
149
|
+
apiRefreshToken: apiRefreshToken,
|
|
111
150
|
apiGateway,
|
|
112
151
|
orgSlug,
|
|
113
152
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { InvocationContext } from '../config/config';
|
|
2
2
|
export declare const AUTH_TOKEN_REQUIRED = "Authentication token must be set to make a contrail call.";
|
|
3
|
+
export declare function _resetTokenRefreshPromise(): void;
|
|
3
4
|
export declare class Request {
|
|
4
5
|
private static MAX_RETRIES;
|
|
5
6
|
private static RETRY_DELAY;
|
|
6
7
|
static request(resource: any, params?: any, ctx?: InvocationContext): Promise<any>;
|
|
8
|
+
private static requestWithRetries;
|
|
9
|
+
private static isTokenExpiredError;
|
|
7
10
|
private static delay;
|
|
8
11
|
static logError(error: {}, resource: any, params?: {}): void;
|
|
9
12
|
static sanitizeParametersString(params: any): string;
|
|
@@ -12,19 +12,108 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.Request = exports.AUTH_TOKEN_REQUIRED = void 0;
|
|
15
|
+
exports.Request = exports._resetTokenRefreshPromise = exports.AUTH_TOKEN_REQUIRED = void 0;
|
|
16
16
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
17
17
|
const config_1 = require("../config/config");
|
|
18
|
+
const auth_1 = require("../auth/auth");
|
|
18
19
|
exports.AUTH_TOKEN_REQUIRED = 'Authentication token must be set to make a contrail call.';
|
|
19
20
|
class ApiError extends Error {
|
|
20
|
-
constructor(message, details) {
|
|
21
|
+
constructor(message, details, errorCode) {
|
|
21
22
|
super(message);
|
|
22
23
|
this.details = details;
|
|
24
|
+
this.errorCode = errorCode;
|
|
23
25
|
this.details = details;
|
|
26
|
+
this.errorCode = errorCode;
|
|
24
27
|
}
|
|
25
28
|
}
|
|
29
|
+
let tokenRefreshPromise = null;
|
|
30
|
+
function performTokenRefresh() {
|
|
31
|
+
var _a, _b, _c;
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
const apiRefreshToken = (_a = auth_1.Auth.config) === null || _a === void 0 ? void 0 : _a.apiRefreshToken;
|
|
34
|
+
const apiGateway = (_b = auth_1.Auth.config) === null || _b === void 0 ? void 0 : _b.apiGateway;
|
|
35
|
+
if (!apiRefreshToken) {
|
|
36
|
+
throw new Error('No API refresh token available. Please login again.');
|
|
37
|
+
}
|
|
38
|
+
if (!apiGateway) {
|
|
39
|
+
throw new Error('API Gateway not configured. Ensure the application environment is properly configured.');
|
|
40
|
+
}
|
|
41
|
+
const url = apiGateway + '/auth/jwt_refresh/';
|
|
42
|
+
try {
|
|
43
|
+
const response = yield (0, node_fetch_1.default)(url, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
refreshToken: apiRefreshToken,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const errorText = yield response.text();
|
|
55
|
+
throw new Error(`Token refresh failed with status ${response.status}: ${errorText}`);
|
|
56
|
+
}
|
|
57
|
+
const data = yield response.json();
|
|
58
|
+
if (!data.accessToken) {
|
|
59
|
+
throw new Error(`Token refresh response did not contain a new access token.`);
|
|
60
|
+
}
|
|
61
|
+
auth_1.Auth.config.apiUserToken = data.accessToken;
|
|
62
|
+
(0, config_1.setConfig)(auth_1.Auth.config);
|
|
63
|
+
console.log('Access token refreshed successfully');
|
|
64
|
+
return data.accessToken;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
throw new Error(`Token refresh failed: ${(_c = error === null || error === void 0 ? void 0 : error.message) !== null && _c !== void 0 ? _c : String(error)}. Please login again with your credentials.`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function refreshAccessToken() {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
if (tokenRefreshPromise) {
|
|
74
|
+
console.log('Token refresh already in progress, waiting for completion...');
|
|
75
|
+
return tokenRefreshPromise;
|
|
76
|
+
}
|
|
77
|
+
tokenRefreshPromise = performTokenRefresh();
|
|
78
|
+
try {
|
|
79
|
+
const newToken = yield tokenRefreshPromise;
|
|
80
|
+
return newToken;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
tokenRefreshPromise = null;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function _resetTokenRefreshPromise() {
|
|
88
|
+
tokenRefreshPromise = null;
|
|
89
|
+
}
|
|
90
|
+
exports._resetTokenRefreshPromise = _resetTokenRefreshPromise;
|
|
26
91
|
class Request {
|
|
27
92
|
static request(resource, params = { method: 'get' }, ctx) {
|
|
93
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
params.headers = Object.assign(Object.assign({}, (params.headers || {})), (yield Request.getHeaders()));
|
|
95
|
+
try {
|
|
96
|
+
return yield this.requestWithRetries(resource, params, ctx);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (this.isTokenExpiredError(error)) {
|
|
100
|
+
console.log(`[SDK Request: Token Expired] Attempting to refresh access token...`);
|
|
101
|
+
try {
|
|
102
|
+
yield refreshAccessToken();
|
|
103
|
+
params.headers = Object.assign(Object.assign({}, (params.headers || {})), (yield Request.getHeaders()));
|
|
104
|
+
console.log(`[SDK Request: Token Refreshed] Retrying request...`);
|
|
105
|
+
return yield this.requestWithRetries(resource, params, ctx);
|
|
106
|
+
}
|
|
107
|
+
catch (refreshError) {
|
|
108
|
+
console.error(`[SDK Request: Token Refresh Failed] ${refreshError === null || refreshError === void 0 ? void 0 : refreshError.message}`, refreshError);
|
|
109
|
+
throw new ApiError('Access token refresh failed after TOKEN_EXPIRED error. Unable to proceed with request', refreshError);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
static requestWithRetries(resource, params, ctx) {
|
|
28
117
|
var _a;
|
|
29
118
|
return __awaiter(this, void 0, void 0, function* () {
|
|
30
119
|
const userCtx = ctx || (0, config_1.getGlobalContext)();
|
|
@@ -33,7 +122,6 @@ class Request {
|
|
|
33
122
|
const url = API_GATEWAY + resource;
|
|
34
123
|
const method = (params === null || params === void 0 ? void 0 : params.method) ? params.method.toUpperCase() : 'GET';
|
|
35
124
|
const fullResource = `${method} ${url.split('/')[3]}${resource}`;
|
|
36
|
-
params.headers = Object.assign(Object.assign({}, (params.headers || {})), (yield Request.getHeaders()));
|
|
37
125
|
let res;
|
|
38
126
|
let attempt = 0;
|
|
39
127
|
while (attempt <= this.MAX_RETRIES) {
|
|
@@ -72,11 +160,16 @@ class Request {
|
|
|
72
160
|
else {
|
|
73
161
|
const error = yield res.text();
|
|
74
162
|
const errorJson = this.tryParseStringIntoObject(error);
|
|
163
|
+
const isTokenExpired = errorJson && typeof errorJson === 'object' && errorJson.errorCode === 'TOKEN_EXPIRED';
|
|
164
|
+
if (isTokenExpired) {
|
|
165
|
+
throw new ApiError('Token expired', errorJson, 'TOKEN_EXPIRED');
|
|
166
|
+
}
|
|
75
167
|
if (errorHandler) {
|
|
76
168
|
const retryCallback = () => __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
params.headers = Object.assign(Object.assign({}, (params.headers || {})), (yield Request.getHeaders()));
|
|
77
170
|
return yield (0, node_fetch_1.default)(url, params);
|
|
78
171
|
});
|
|
79
|
-
console.log(`[###### SDK Request: ERROR] code ${
|
|
172
|
+
console.log(`[###### SDK Request: ERROR] code ${res.status} resource: ${fullResource}`);
|
|
80
173
|
console.log(`[###### SDK Request: ERROR] params ${Request.sanitizeParametersString(params)}`);
|
|
81
174
|
const request = Object.assign(Object.assign({}, params), { url, method });
|
|
82
175
|
return yield errorHandler.handle(res, errorJson, retryCallback, request);
|
|
@@ -97,6 +190,9 @@ class Request {
|
|
|
97
190
|
}
|
|
98
191
|
});
|
|
99
192
|
}
|
|
193
|
+
static isTokenExpiredError(error) {
|
|
194
|
+
return error instanceof ApiError && error.errorCode === 'TOKEN_EXPIRED';
|
|
195
|
+
}
|
|
100
196
|
static delay(ms) {
|
|
101
197
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
198
|
}
|
|
@@ -131,7 +131,7 @@ class Entities {
|
|
|
131
131
|
resource += `skipHydrateRelationships=${options.skipHydrateRelationships}&`;
|
|
132
132
|
}
|
|
133
133
|
if (options.federatedIds) {
|
|
134
|
-
resource += `ids=${options.federatedIds.map((id) => id.replace(/,/g, '_COMMA_')).join(',')}&`;
|
|
134
|
+
resource += `ids=${options.federatedIds.map((id) => encodeURIComponent(id.replace(/,/g, '_COMMA_'))).join(',')}&`;
|
|
135
135
|
}
|
|
136
136
|
if (options.criteria) {
|
|
137
137
|
resource += 'criteria=' + JSURL.stringify(options.criteria);
|
|
@@ -55,3 +55,4 @@ EntityResourceRegistry.registerEntityResourceName('open-search', 'open-search');
|
|
|
55
55
|
EntityResourceRegistry.registerEntityResourceName('project-configurator', 'project-configurator');
|
|
56
56
|
EntityResourceRegistry.registerEntityResourceName('identity', 'identities');
|
|
57
57
|
EntityResourceRegistry.registerEntityResourceName('entity-policy', 'entity-policies');
|
|
58
|
+
EntityResourceRegistry.registerEntityResourceName('measurement-spec-entry', 'measurement-spec-entries');
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
export declare class Files {
|
|
2
3
|
private ctx;
|
|
3
4
|
constructor(ctx?: any);
|
|
4
5
|
createAndUploadFile(file: File, ownedByReference?: string, ttl?: number): Promise<any>;
|
|
5
|
-
createAndUploadFileFromBuffer(buffer:
|
|
6
|
-
uploadFile(uploadPost:
|
|
7
|
-
|
|
6
|
+
createAndUploadFileFromBuffer(buffer: Buffer, contentType: string, fileName: string, ownedByReference?: string, ttl?: number): Promise<any>;
|
|
7
|
+
uploadFile(uploadPost: {
|
|
8
|
+
url: string;
|
|
9
|
+
fields: Record<string, string>;
|
|
10
|
+
}, file: File): Promise<void>;
|
|
11
|
+
uploadFileAsStream(uploadPost: {
|
|
12
|
+
url: string;
|
|
13
|
+
fields: Record<string, string>;
|
|
14
|
+
}, buffer: Buffer | File, options?: {
|
|
15
|
+
contentType?: string;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
private withRetry;
|
|
8
18
|
private appendUploadHeaders;
|
|
9
19
|
}
|