@contrail/flexplm 1.7.3-alpha.b33e995 → 1.7.3-alpha.cf8b99d
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/lib/interfaces/interfaces.d.ts +1 -1
- package/lib/interfaces/item-family-changes.d.ts +1 -0
- package/lib/interfaces/item-family-changes.js +2 -0
- package/lib/publish/base-process-publish-assortment.js +23 -1
- package/lib/publish/base-process-publish-assortment.spec.js +144 -0
- package/lib/util/config-defaults.d.ts +2 -0
- package/lib/util/config-defaults.js +22 -8
- package/lib/util/config-defaults.spec.js +14 -0
- package/lib/util/data-converter.spec.js +15 -30
- package/lib/util/thumbnail-util.js +6 -8
- package/lib/util/thumbnail-util.spec.js +35 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ export interface FCConfig {
|
|
|
14
14
|
urlContext: string;
|
|
15
15
|
vibeEventEndpoint: string;
|
|
16
16
|
csrfEndpoint: string;
|
|
17
|
-
useDistinctRestEndPointForImages
|
|
17
|
+
useDistinctRestEndPointForImages?: boolean;
|
|
18
18
|
itemPreDevelopmentLifecycleStages: string[];
|
|
19
19
|
identifierAtts?: {
|
|
20
20
|
[key: string]: string[];
|
|
@@ -12,6 +12,7 @@ export declare class ItemFamilyChanges {
|
|
|
12
12
|
colorDeletes: string[];
|
|
13
13
|
colorUpdates: string[];
|
|
14
14
|
colorUnchanged: string[];
|
|
15
|
+
colorSuppressed: string[];
|
|
15
16
|
assortmentItemFullChangeMap: Map<string, any>;
|
|
16
17
|
assortmentItemDeleteMap: Map<string, any>;
|
|
17
18
|
itemToFederatedIdMapping: Map<string, string>;
|
|
@@ -14,6 +14,7 @@ class ItemFamilyChanges {
|
|
|
14
14
|
this.colorDeletes = [];
|
|
15
15
|
this.colorUpdates = [];
|
|
16
16
|
this.colorUnchanged = [];
|
|
17
|
+
this.colorSuppressed = []; // changed this window, but excluded by the lifecycle filter
|
|
17
18
|
this.assortmentItemFullChangeMap = new Map();
|
|
18
19
|
this.assortmentItemDeleteMap = new Map();
|
|
19
20
|
this.itemToFederatedIdMapping = new Map();
|
|
@@ -32,6 +33,7 @@ class ItemFamilyChanges {
|
|
|
32
33
|
s += ' colorDeletes: [' + this.colorDeletes + ']\n';
|
|
33
34
|
s += ' colorUpdates: [' + this.colorUpdates + ']\n';
|
|
34
35
|
s += ' colorUnchanged: [' + this.colorUnchanged + ']\n';
|
|
36
|
+
s += ' colorSuppressed: [' + this.colorSuppressed + ']\n';
|
|
35
37
|
s += 'assortmentItemFullChangeMap:\n';
|
|
36
38
|
s += 'size: ' + this.assortmentItemFullChangeMap.size + '\n[';
|
|
37
39
|
for (const key of this.assortmentItemFullChangeMap.keys()) {
|
|
@@ -730,7 +730,7 @@ class BaseProcessPublishAssortment {
|
|
|
730
730
|
for (const [itemId, aItem] of assortmentItemFullChangeMap) {
|
|
731
731
|
const projectItem = aItem?.projectItem;
|
|
732
732
|
const itemFamilyId = aItem?.item?.itemFamilyId;
|
|
733
|
-
if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)
|
|
733
|
+
if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)) {
|
|
734
734
|
continue;
|
|
735
735
|
}
|
|
736
736
|
const ifc = itemFamilyChanges.get(itemFamilyId) || new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
|
|
@@ -742,6 +742,20 @@ class BaseProcessPublishAssortment {
|
|
|
742
742
|
if (pcd.itemToFederatedIdMapping.has(itemId)) {
|
|
743
743
|
ifc.itemToFederatedIdMapping.set(itemId, pcd.itemToFederatedIdMapping.get(itemId));
|
|
744
744
|
}
|
|
745
|
+
// ---- NEW Step to enable ItemFamily publish ET-243: option is pre-development. It must not produce an
|
|
746
|
+
// LCSSKUSeasonLink, but a change to it still makes the family publishable. ----
|
|
747
|
+
if (!pcd.releasedForDevelopmentItemIds.includes(itemId)) {
|
|
748
|
+
const changedInWindow = addIds.includes(itemId)
|
|
749
|
+
|| deleteIds.includes(itemId)
|
|
750
|
+
|| updateIds.includes(itemId)
|
|
751
|
+
|| this.updatedSinceDate(projectItem, pcd.sinceDate)
|
|
752
|
+
|| this.updatedSinceDate(aItem?.item, pcd.sinceDate);
|
|
753
|
+
if (changedInWindow) {
|
|
754
|
+
ifc.colorSuppressed.push(itemId);
|
|
755
|
+
}
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
// NEW Step - ends
|
|
745
759
|
if (itemId === itemFamilyId) {
|
|
746
760
|
if (addIds.includes(itemId))
|
|
747
761
|
ifc.familyAdd = true;
|
|
@@ -757,6 +771,12 @@ class BaseProcessPublishAssortment {
|
|
|
757
771
|
ifc.familyUpdate = true;
|
|
758
772
|
}
|
|
759
773
|
}
|
|
774
|
+
// NEW Step to enable ItemFamily publish ET-243: item-level edits (incl. lifecycleStage) must publish
|
|
775
|
+
// the family. Guarded so the "first option added" flow stays suppressed. ----
|
|
776
|
+
if (!ifc.familyItemRemoved && this.updatedSinceDate(aItem?.item, pcd.sinceDate)) {
|
|
777
|
+
ifc.familyUpdate = true;
|
|
778
|
+
}
|
|
779
|
+
// NEW Step - ends
|
|
760
780
|
if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
|
|
761
781
|
projectItem.roles = aItem?.item?.roles;
|
|
762
782
|
}
|
|
@@ -855,8 +875,10 @@ class BaseProcessPublishAssortment {
|
|
|
855
875
|
const LCSSeason = Object.assign({}, seasonFed);
|
|
856
876
|
const assortment = await this.getAssortment(assortmentId);
|
|
857
877
|
const projectItem = this.getProjectItem(itemFamilyChanges, itemFamilyChanges.itemFamilyId);
|
|
878
|
+
// ---- NEW Step to enable ItemFamily publish ET-243: added color suppressed length to existing condition, no other changes on familyAssortmentItem boolean
|
|
858
879
|
const familyAssortmentItem = itemFamilyChanges.familyAdd || itemFamilyChanges.familyUpdate || itemFamilyChanges.familyDelete
|
|
859
880
|
|| itemFamilyChanges.colorAdds.length > 0 || itemFamilyChanges.colorUpdates.length > 0 || itemFamilyChanges.colorDeletes.length > 0
|
|
881
|
+
|| itemFamilyChanges.colorSuppressed.length > 0
|
|
860
882
|
|| (projectItem && this.updatedSinceDate(projectItem, itemFamilyChanges.sinceDate));
|
|
861
883
|
//familyItemRemoved is used when adding the first option to an assortment
|
|
862
884
|
//and will have updates for the family item.
|
|
@@ -1337,6 +1337,150 @@ describe('getItemFamilyChanges', () => {
|
|
|
1337
1337
|
expect(ifc.colorDeletes.length).toEqual(1);
|
|
1338
1338
|
});
|
|
1339
1339
|
});
|
|
1340
|
+
describe('getItemFamilyChanges - lifecycle-suppressed options', () => {
|
|
1341
|
+
const familyId = 'AERfdvdAt1HHEVvQ';
|
|
1342
|
+
const maroonId = 'YwWKVoL_vGN8khLy';
|
|
1343
|
+
const limeId = 'rsmWhWiXRNv_XBJG';
|
|
1344
|
+
const sinceDate = new Date('2026-08-01T00:00:00.000Z');
|
|
1345
|
+
const afterSince = '2026-08-26T00:00:00.000Z';
|
|
1346
|
+
const beforeSince = '2026-05-11T00:00:00.000Z';
|
|
1347
|
+
const noDetailChanges = () => ({ adds: [], deletes: [], updates: [], familyItemsRemoved: [] });
|
|
1348
|
+
const buildPpa = () => {
|
|
1349
|
+
const config = {};
|
|
1350
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1351
|
+
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1352
|
+
return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1353
|
+
};
|
|
1354
|
+
const buildPcd = (releasedIds) => {
|
|
1355
|
+
const seasonFed = {
|
|
1356
|
+
entityReference: 'assortment:11EZML5dMn3tREuT',
|
|
1357
|
+
objectClass: 'LCSSeason'
|
|
1358
|
+
};
|
|
1359
|
+
const pcd = new publish_change_data_1.PublishChangeData('11EZML5dMn3tREuT', seasonFed, 'Pg3y2x5IclYxk3hM', sinceDate);
|
|
1360
|
+
pcd.itemToFederatedIdMapping = new Map();
|
|
1361
|
+
releasedIds.forEach(id => pcd.releasedForDevelopmentItemIds.push(id));
|
|
1362
|
+
return pcd;
|
|
1363
|
+
};
|
|
1364
|
+
const familyItem = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
|
|
1365
|
+
id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant', updatedOn
|
|
1366
|
+
});
|
|
1367
|
+
const familyRow = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
|
|
1368
|
+
itemId: familyId,
|
|
1369
|
+
item: familyItem(updatedOn, lifecycleStage),
|
|
1370
|
+
projectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
|
|
1371
|
+
});
|
|
1372
|
+
const optionRow = (id, updatedOn, lifecycleStage = 'concept') => ({
|
|
1373
|
+
itemId: id,
|
|
1374
|
+
item: { id, itemFamilyId: familyId, lifecycleStage, updatedOn, itemFamily: familyItem() },
|
|
1375
|
+
projectItem: { id: 'pi-' + id, updatedOn: beforeSince },
|
|
1376
|
+
familyProjectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
|
|
1377
|
+
});
|
|
1378
|
+
it('records changed concept options as suppressed, not as colour updates', () => {
|
|
1379
|
+
const ppa = buildPpa();
|
|
1380
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1381
|
+
assortmentItems: [familyRow(), optionRow(maroonId, afterSince), optionRow(limeId, afterSince)]
|
|
1382
|
+
});
|
|
1383
|
+
const pcd = buildPcd([familyId]);
|
|
1384
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1385
|
+
expect(pcd.itemFamilyChanges.size).toEqual(1);
|
|
1386
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1387
|
+
expect(ifc.colorSuppressed).toEqual([maroonId, limeId]);
|
|
1388
|
+
expect(ifc.colorAdds.length).toEqual(0);
|
|
1389
|
+
expect(ifc.colorUpdates.length).toEqual(0);
|
|
1390
|
+
expect(ifc.colorDeletes.length).toEqual(0);
|
|
1391
|
+
expect(ifc.familyUpdate).toBe(false); // family itself unchanged
|
|
1392
|
+
});
|
|
1393
|
+
it('keeps the family reachable when only concept option rows are in the baseline', () => {
|
|
1394
|
+
const ppa = buildPpa();
|
|
1395
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1396
|
+
assortmentItems: [optionRow(maroonId, afterSince), optionRow(limeId, afterSince)]
|
|
1397
|
+
});
|
|
1398
|
+
const pcd = buildPcd([familyId]);
|
|
1399
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1400
|
+
expect(pcd.itemFamilyChanges.size).toEqual(1);
|
|
1401
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1402
|
+
expect(ifc.itemFamilyObject.id).toEqual(familyId);
|
|
1403
|
+
expect(ifc.colorSuppressed.length).toEqual(2);
|
|
1404
|
+
// the suppressed option must stay in the map so the family's project item is reachable
|
|
1405
|
+
expect(ppa.getProjectItem(ifc, familyId)).toEqual({ id: 'pi-' + familyId, updatedOn: beforeSince });
|
|
1406
|
+
});
|
|
1407
|
+
it('does not suppress-record an unchanged concept option', () => {
|
|
1408
|
+
const ppa = buildPpa();
|
|
1409
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1410
|
+
assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
|
|
1411
|
+
});
|
|
1412
|
+
const pcd = buildPcd([familyId]);
|
|
1413
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1414
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1415
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1416
|
+
});
|
|
1417
|
+
it('picks up a concept option listed in the change detail even when timestamps are stale', () => {
|
|
1418
|
+
const ppa = buildPpa();
|
|
1419
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1420
|
+
assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
|
|
1421
|
+
});
|
|
1422
|
+
const details = noDetailChanges();
|
|
1423
|
+
details.updates = [{ id: maroonId }];
|
|
1424
|
+
const pcd = buildPcd([familyId]);
|
|
1425
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1426
|
+
expect(pcd.itemFamilyChanges.get(familyId).colorSuppressed).toEqual([maroonId]);
|
|
1427
|
+
});
|
|
1428
|
+
it('skips everything when the family itself is pre-development', () => {
|
|
1429
|
+
const ppa = buildPpa();
|
|
1430
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1431
|
+
assortmentItems: [familyRow(afterSince, 'concept'), optionRow(maroonId, afterSince)]
|
|
1432
|
+
});
|
|
1433
|
+
const pcd = buildPcd([]); // nothing released
|
|
1434
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1435
|
+
expect(pcd.itemFamilyChanges.size).toEqual(0);
|
|
1436
|
+
});
|
|
1437
|
+
it('still classifies released options normally', () => {
|
|
1438
|
+
const ppa = buildPpa();
|
|
1439
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1440
|
+
assortmentItems: [familyRow(), optionRow(maroonId, afterSince, 'released')]
|
|
1441
|
+
});
|
|
1442
|
+
const details = noDetailChanges();
|
|
1443
|
+
details.adds = [{ id: maroonId }];
|
|
1444
|
+
const pcd = buildPcd([familyId, maroonId]);
|
|
1445
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1446
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1447
|
+
expect(ifc.colorAdds).toEqual([maroonId]);
|
|
1448
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1449
|
+
});
|
|
1450
|
+
it('publishes a family-only row whose lifecycle just flipped (item-level edit)', () => {
|
|
1451
|
+
const ppa = buildPpa();
|
|
1452
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1453
|
+
assortmentItems: [familyRow(afterSince)] // item.updatedOn moved, projectItem did not
|
|
1454
|
+
});
|
|
1455
|
+
const pcd = buildPcd([familyId]);
|
|
1456
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1457
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1458
|
+
expect(ifc.familyUpdate).toBe(true); // fails without Step 3
|
|
1459
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1460
|
+
});
|
|
1461
|
+
it('leaves an unchanged family-only row with no flags', () => {
|
|
1462
|
+
const ppa = buildPpa();
|
|
1463
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow()] });
|
|
1464
|
+
const pcd = buildPcd([familyId]);
|
|
1465
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1466
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1467
|
+
expect(ifc.familyAdd).toBe(false);
|
|
1468
|
+
expect(ifc.familyUpdate).toBe(false);
|
|
1469
|
+
expect(ifc.familyDelete).toBe(false);
|
|
1470
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1471
|
+
});
|
|
1472
|
+
it('does not set familyUpdate when the family item was removed', () => {
|
|
1473
|
+
const ppa = buildPpa();
|
|
1474
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow(afterSince)] });
|
|
1475
|
+
const details = noDetailChanges();
|
|
1476
|
+
details.familyItemsRemoved = [{ itemId: familyId }]; // note: itemId, not id
|
|
1477
|
+
const pcd = buildPcd([familyId]);
|
|
1478
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1479
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1480
|
+
expect(ifc.familyItemRemoved).toBe(true);
|
|
1481
|
+
expect(ifc.familyUpdate).toBe(false); // fails if the !familyItemRemoved guard is dropped
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1340
1484
|
describe('getProjectItem', () => {
|
|
1341
1485
|
const config = {
|
|
1342
1486
|
identifierAtts: {
|
|
@@ -2,6 +2,8 @@ import { FCConfig } from '../interfaces/interfaces';
|
|
|
2
2
|
export declare class ConfigDefaults {
|
|
3
3
|
static NEED_CONFIG_VALUES: string;
|
|
4
4
|
static STATIC_CONFIG_CACHE: {};
|
|
5
|
+
static PROTO_KEYS: string[];
|
|
6
|
+
static stripProtoKeys(obj: any): any;
|
|
5
7
|
static setConfigDefaults(config: any): Promise<FCConfig>;
|
|
6
8
|
static getDefaultConfig(): {
|
|
7
9
|
urlContext: string;
|
|
@@ -5,16 +5,29 @@ const sdk_1 = require("@contrail/sdk");
|
|
|
5
5
|
const util_1 = require("@contrail/util");
|
|
6
6
|
const type_defaults_1 = require("./type-defaults");
|
|
7
7
|
class ConfigDefaults {
|
|
8
|
+
static stripProtoKeys(obj) {
|
|
9
|
+
if (obj && typeof obj === 'object' && !(obj instanceof Date)) {
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
if (ConfigDefaults.PROTO_KEYS.includes(key)) {
|
|
12
|
+
delete obj[key];
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
ConfigDefaults.stripProtoKeys(obj[key]); // recurse into nested payloads
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
8
21
|
static async setConfigDefaults(config) {
|
|
9
|
-
//Validate config
|
|
22
|
+
// Validate config
|
|
10
23
|
if (!config.apiHost || !config.userName || !config.password) {
|
|
11
24
|
throw new Error(ConfigDefaults.NEED_CONFIG_VALUES);
|
|
12
25
|
}
|
|
13
26
|
if (config.complexConfig && typeof config.complexConfig === 'object') {
|
|
14
|
-
Object.assign(config, config.complexConfig);
|
|
27
|
+
Object.assign(config, ConfigDefaults.stripProtoKeys(config.complexConfig));
|
|
15
28
|
delete config.complexConfig;
|
|
16
29
|
}
|
|
17
|
-
//List will be comma separated list in UI, so convert to array
|
|
30
|
+
// List will be comma separated list in UI, so convert to array
|
|
18
31
|
if (config?.itemPreDevelopmentLifecycleStages && !(config?.itemPreDevelopmentLifecycleStages instanceof Array)) {
|
|
19
32
|
config.itemPreDevelopmentLifecycleStages = config.itemPreDevelopmentLifecycleStages.split(',');
|
|
20
33
|
}
|
|
@@ -32,7 +45,7 @@ class ConfigDefaults {
|
|
|
32
45
|
const pass = outputConfig.password;
|
|
33
46
|
outputConfig.userName = () => uName;
|
|
34
47
|
outputConfig.password = () => pass;
|
|
35
|
-
//Don't allow overwriting this.
|
|
48
|
+
// Don't allow overwriting this.
|
|
36
49
|
outputConfig['OOBvibeEventEndpoint'] = '/rfa/vibeiq/vibeEvents';
|
|
37
50
|
type_defaults_1.TypeDefaults.applyConfig(outputConfig);
|
|
38
51
|
console.log('outputConfig: ' + JSON.stringify(outputConfig));
|
|
@@ -42,21 +55,21 @@ class ConfigDefaults {
|
|
|
42
55
|
return {
|
|
43
56
|
urlContext: '/Windchill',
|
|
44
57
|
sendMode: {
|
|
45
|
-
ASYNC_PUBLISH_SEASON: 'vibeiqfile'
|
|
58
|
+
ASYNC_PUBLISH_SEASON: 'vibeiqfile',
|
|
46
59
|
},
|
|
47
60
|
itemPreDevelopmentLifecycleStages: ['concept'],
|
|
48
61
|
identifierAtts: {
|
|
49
62
|
LCSProduct: ['itemNumber'],
|
|
50
63
|
LCSSeason: ['flexPLMSeasonName'],
|
|
51
|
-
LCSSKU: ['itemNumber']
|
|
64
|
+
LCSSKU: ['itemNumber'],
|
|
52
65
|
},
|
|
53
66
|
LCSMaterial: {
|
|
54
|
-
processAsItem: false
|
|
67
|
+
processAsItem: false,
|
|
55
68
|
},
|
|
56
69
|
useDistinctRestEndPointForImages: false,
|
|
57
70
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
58
71
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
59
|
-
payloadDefaultAsArray: true
|
|
72
|
+
payloadDefaultAsArray: true,
|
|
60
73
|
};
|
|
61
74
|
}
|
|
62
75
|
static async getConfigFile(fileId) {
|
|
@@ -101,3 +114,4 @@ class ConfigDefaults {
|
|
|
101
114
|
exports.ConfigDefaults = ConfigDefaults;
|
|
102
115
|
ConfigDefaults.NEED_CONFIG_VALUES = 'To connect to FlexPLM all these APP values need to be set apiHost, userName, and password';
|
|
103
116
|
ConfigDefaults.STATIC_CONFIG_CACHE = {};
|
|
117
|
+
ConfigDefaults.PROTO_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
@@ -378,4 +378,18 @@ describe('all tests', () => {
|
|
|
378
378
|
expect(Object.keys(config).length).toEqual(0);
|
|
379
379
|
});
|
|
380
380
|
});
|
|
381
|
+
describe('prototype pollution', () => {
|
|
382
|
+
const config = {
|
|
383
|
+
apiHost: 'http://test.com',
|
|
384
|
+
userName: 'vibeiq',
|
|
385
|
+
password: 'vibeiq'
|
|
386
|
+
};
|
|
387
|
+
it('does not allow prototype pollution via complexConfig', async () => {
|
|
388
|
+
const startConfig = Object.assign({}, config, {
|
|
389
|
+
complexConfig: JSON.parse('{"__proto__":{"polluted":true}}')
|
|
390
|
+
});
|
|
391
|
+
await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
|
|
392
|
+
expect({}.polluted).toBeUndefined();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
381
395
|
});
|
|
@@ -38,8 +38,7 @@ describe('getFlexPLMValue multi_select', () => {
|
|
|
38
38
|
urlContext: 'xxx',
|
|
39
39
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
40
40
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
41
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
42
|
-
useDistinctRestEndPointForImages: false
|
|
41
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
43
42
|
};
|
|
44
43
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
45
44
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -97,8 +96,7 @@ describe('getEnumerationValue', () => {
|
|
|
97
96
|
urlContext: 'xxx',
|
|
98
97
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
99
98
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
100
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
101
|
-
useDistinctRestEndPointForImages: false
|
|
99
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
102
100
|
};
|
|
103
101
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
104
102
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -155,8 +153,7 @@ describe('getEnumerationValue multi_select', () => {
|
|
|
155
153
|
urlContext: 'xxx',
|
|
156
154
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
157
155
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
158
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
159
|
-
useDistinctRestEndPointForImages: false
|
|
156
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
160
157
|
};
|
|
161
158
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
162
159
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -216,8 +213,7 @@ describe('getPersistableChanges', () => {
|
|
|
216
213
|
urlContext: 'xxx',
|
|
217
214
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
218
215
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
219
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
220
|
-
useDistinctRestEndPointForImages: false
|
|
216
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
221
217
|
};
|
|
222
218
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
223
219
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -302,8 +298,7 @@ describe('getObjectReferenceValue cache', () => {
|
|
|
302
298
|
urlContext: 'xxx',
|
|
303
299
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
304
300
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
305
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
306
|
-
useDistinctRestEndPointForImages: false
|
|
301
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
307
302
|
};
|
|
308
303
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
309
304
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -361,8 +356,7 @@ describe('getObjectReferenceValue bad value', () => {
|
|
|
361
356
|
urlContext: 'xxx',
|
|
362
357
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
363
358
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
364
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
365
|
-
useDistinctRestEndPointForImages: false
|
|
359
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
366
360
|
};
|
|
367
361
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
368
362
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -468,8 +462,7 @@ describe('setObjectReferenceValue - identity service', () => {
|
|
|
468
462
|
urlContext: 'xxx',
|
|
469
463
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
470
464
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
471
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
472
|
-
useDistinctRestEndPointForImages: false
|
|
465
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
473
466
|
});
|
|
474
467
|
const refProp = {
|
|
475
468
|
id: 'cJoZQvoj7dkfCBJq',
|
|
@@ -927,8 +920,7 @@ describe('getEntityValues', () => {
|
|
|
927
920
|
urlContext: 'xxx',
|
|
928
921
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
929
922
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
930
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
931
|
-
useDistinctRestEndPointForImages: false
|
|
923
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
932
924
|
};
|
|
933
925
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
934
926
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -969,8 +961,7 @@ describe('setEnumerationKeys', () => {
|
|
|
969
961
|
urlContext: 'xxx',
|
|
970
962
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
971
963
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
972
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
973
|
-
useDistinctRestEndPointForImages: false
|
|
964
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
974
965
|
};
|
|
975
966
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
976
967
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1034,8 +1025,7 @@ describe('checkKeysAndValues', () => {
|
|
|
1034
1025
|
urlContext: 'xxx',
|
|
1035
1026
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1036
1027
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1037
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1038
|
-
useDistinctRestEndPointForImages: false
|
|
1028
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1039
1029
|
};
|
|
1040
1030
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1041
1031
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1112,8 +1102,7 @@ describe('filterOutArchivedAndTrashedEntities', () => {
|
|
|
1112
1102
|
urlContext: 'xxx',
|
|
1113
1103
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1114
1104
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1115
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1116
|
-
useDistinctRestEndPointForImages: false
|
|
1105
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1117
1106
|
};
|
|
1118
1107
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1119
1108
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1153,8 +1142,7 @@ describe('setUserListValue', () => {
|
|
|
1153
1142
|
urlContext: 'xxx',
|
|
1154
1143
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1155
1144
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1156
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1157
|
-
useDistinctRestEndPointForImages: false
|
|
1145
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1158
1146
|
};
|
|
1159
1147
|
const userListProp1 = {};
|
|
1160
1148
|
const userEmailMapping = [
|
|
@@ -1250,8 +1238,7 @@ describe('getUserListValue', () => {
|
|
|
1250
1238
|
urlContext: 'xxx',
|
|
1251
1239
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1252
1240
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1253
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1254
|
-
useDistinctRestEndPointForImages: false
|
|
1241
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1255
1242
|
};
|
|
1256
1243
|
const userListProp1 = {
|
|
1257
1244
|
slug: 'userList1'
|
|
@@ -1353,8 +1340,7 @@ describe('getFlexPLMValue size_range', () => {
|
|
|
1353
1340
|
urlContext: 'xxx',
|
|
1354
1341
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1355
1342
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1356
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1357
|
-
useDistinctRestEndPointForImages: false
|
|
1343
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1358
1344
|
};
|
|
1359
1345
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1360
1346
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -1387,8 +1373,7 @@ describe('getEntityValue size_range', () => {
|
|
|
1387
1373
|
urlContext: 'xxx',
|
|
1388
1374
|
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
1389
1375
|
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
1390
|
-
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1391
|
-
useDistinctRestEndPointForImages: false
|
|
1376
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
1392
1377
|
};
|
|
1393
1378
|
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1394
1379
|
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
@@ -181,7 +181,7 @@ class ThumbnailUtil {
|
|
|
181
181
|
const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
|
|
182
182
|
const encodedUrl = urlParts.map(part => encodeURIComponent(part)).join('/');
|
|
183
183
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
184
|
-
console.
|
|
184
|
+
console.debug('the useDistinctRestEndPointForImages is --> ' + this.config.useDistinctRestEndPointForImages);
|
|
185
185
|
let response;
|
|
186
186
|
if (this.config.useDistinctRestEndPointForImages) {
|
|
187
187
|
// Route through the connector endpoint (Basic auth + VIBEIQGROUP enforced there).
|
|
@@ -200,17 +200,15 @@ class ThumbnailUtil {
|
|
|
200
200
|
returnFullResponse: true,
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
|
-
// Ensure we actually receive an image content type.
|
|
204
|
-
const contTypeHeader = response.headers.get('content-type');
|
|
205
|
-
const contType = contTypeHeader ? contTypeHeader.split(';')[0] : 'application/octet-stream';
|
|
206
|
-
if (!contType.startsWith('image/')) {
|
|
207
|
-
const message = `Expected image content from FlexPLM but received '${contType}' for ${thumbnailUrl}`;
|
|
208
|
-
throw new Error(message);
|
|
209
|
-
}
|
|
210
203
|
const fileBuffer = await response.arrayBuffer();
|
|
211
204
|
const buffer = Buffer.from(fileBuffer);
|
|
212
205
|
const contentTypeHeader = response.headers.get('content-type');
|
|
213
206
|
const contentType = contentTypeHeader ? contentTypeHeader.split(';')[0] : 'application/octet-stream';
|
|
207
|
+
// Ensure we actually receive an image content type.
|
|
208
|
+
if (!contentType.startsWith('image/')) {
|
|
209
|
+
const message = `Expected image content from FlexPLM but received '${contentType}' for ${thumbnailUrl}`;
|
|
210
|
+
throw new Error(message);
|
|
211
|
+
}
|
|
214
212
|
const contentHolderReference = `${entityName}:${entityId}`;
|
|
215
213
|
const content = await new sdk_1.Content().create({
|
|
216
214
|
fileBuffer: buffer,
|
|
@@ -437,6 +437,41 @@ describe('ThumbnailUtil Tests', () => {
|
|
|
437
437
|
expect(mockEntitiesDelete).not.toHaveBeenCalled();
|
|
438
438
|
});
|
|
439
439
|
});
|
|
440
|
+
describe('syncThumbnailToVibeIQ - useDistinctRestEndPointForImages enabled', () => {
|
|
441
|
+
let tu;
|
|
442
|
+
const distinctConfig = { useDistinctRestEndPointForImages: true };
|
|
443
|
+
beforeEach(() => {
|
|
444
|
+
jest.clearAllMocks();
|
|
445
|
+
tu = new thumbnail_util_1.ThumbnailUtil(distinctConfig);
|
|
446
|
+
mockEntitiesGet.mockImplementation((opts) => {
|
|
447
|
+
if (opts.entityName === 'content-custom-size')
|
|
448
|
+
return Promise.resolve([]);
|
|
449
|
+
return Promise.resolve({});
|
|
450
|
+
});
|
|
451
|
+
mockEntitiesUpdate.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
|
|
452
|
+
mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
|
|
453
|
+
});
|
|
454
|
+
it('routes through the distinct image endpoint with includeUrlContext true', async () => {
|
|
455
|
+
const mockResponse = {
|
|
456
|
+
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
|
|
457
|
+
headers: { get: jest.fn().mockReturnValue('image/png') },
|
|
458
|
+
};
|
|
459
|
+
mockGetRequest.mockResolvedValue(mockResponse);
|
|
460
|
+
mockContentCreate.mockResolvedValue({
|
|
461
|
+
id: 'distinctContent1', contentType: 'image/png', fileName: 'thumb.png',
|
|
462
|
+
primaryFileUrl: 'https://files/primary.png', largeViewableUrl: null,
|
|
463
|
+
mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
|
|
464
|
+
});
|
|
465
|
+
const thumbnailUrl = '/rest/thumbnail/thumb.png';
|
|
466
|
+
const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: thumbnailUrl } };
|
|
467
|
+
await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
|
|
468
|
+
expect(mockGetRequest).toHaveBeenCalledWith({
|
|
469
|
+
urlPath: '/servlet/rest/rfa/vibeiq/image?path=' + encodeURIComponent(thumbnailUrl),
|
|
470
|
+
includeUrlContext: true,
|
|
471
|
+
returnFullResponse: true,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
440
475
|
describe('ThumbnailUtil - iteratedThumbnailId (THUMBNAIL key)', () => {
|
|
441
476
|
const config = {};
|
|
442
477
|
let tu;
|