@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.
@@ -14,7 +14,7 @@ export interface FCConfig {
14
14
  urlContext: string;
15
15
  vibeEventEndpoint: string;
16
16
  csrfEndpoint: string;
17
- useDistinctRestEndPointForImages: boolean;
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) || !pcd.releasedForDevelopmentItemIds.includes(itemId)) {
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.log('the useDistinctRestEndPointForImages is --> ' + this.config.useDistinctRestEndPointForImages);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.3-alpha.b33e995",
3
+ "version": "1.7.3-alpha.cf8b99d",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",