@contrail/flexplm 1.4.0 → 1.5.0-alpha.0d65410

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.
@@ -6,6 +6,18 @@ import { Logger } from '@contrail/app-framework';
6
6
  import { ObjectDiff, ObjectUtil } from '@contrail/util';
7
7
  import { TypeConversionUtils } from './type-conversion-utils';
8
8
  import { MapUtil } from './map-utils';
9
+ import { ConfigDefaults } from './config-defaults';
10
+
11
+ interface ObjectReferenceContext {
12
+ entityType: string;
13
+ entityTypePath: string;
14
+ rootTypeCriteria: Record<string, any>;
15
+ typeCriteria: Record<string, any>;
16
+ combinedCriteria: Record<string, any>;
17
+ cacheKey: string;
18
+ useIdentityService: boolean;
19
+ identityLookupKeys: string[];
20
+ }
9
21
 
10
22
  export class DataConverter {
11
23
  private typeUtils: TypeUtils;
@@ -287,7 +299,7 @@ export class DataConverter {
287
299
  const type = await this.typeUtils.getByRootAndPath(tco);
288
300
  const typePath = type['typePath'];
289
301
  if(typePath && (typePath.startsWith('item') || typePath.startsWith('project-item'))){
290
- if(['LCSProduct', 'LCSProductSeasonLink'].includes(objectClass)){
302
+ if(['LCSProduct', 'LCSProductSeasonLink', 'LCSMaterial'].includes(objectClass)){
291
303
  entityValues['roles'] = ['family'];
292
304
  }else{
293
305
  entityValues['roles'] = ['color', 'option'];
@@ -426,33 +438,60 @@ export class DataConverter {
426
438
  * @returns the object reference id from VibeIQ
427
439
  */
428
440
  async setObjectReferenceValue(prop, nd) {
429
- let objectReferenceId = "";
430
- if(!nd) {
431
- return objectReferenceId;
441
+ if (!nd) {
442
+ return "";
432
443
  }
433
- if(this.transformMapFile){
434
- const mapKey = await TypeConversionUtils.getMapKeyFromObject(this.transformMapFile, this.mapFileUtil, nd, TypeConversionUtils.FLEX2VIBE_DIRECTION);
435
- nd = await MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, nd, mapKey, TypeConversionUtils.FLEX2VIBE_DIRECTION)
444
+
445
+ nd = await this.applyInboundTransformMap(nd);
446
+
447
+ const ctx = await this.buildObjectReferenceContext(prop, nd);
448
+ if (!ctx) {
449
+ return "";
450
+ }
451
+
452
+ if (this.objRefCache[ctx.cacheKey]) {
453
+ if (Logger.isDebugOn()) {
454
+ console.debug(`object reference cache hit: ${ctx.cacheKey}`);
455
+ }
456
+ return this.objRefCache[ctx.cacheKey];
436
457
  }
437
458
 
459
+ const objectReferenceId = ctx.useIdentityService
460
+ ? await this.lookupObjectReferenceViaIdentityService(ctx, nd)
461
+ : await this.lookupObjectReferenceViaQuery(ctx);
462
+
463
+ if (objectReferenceId) {
464
+ this.objRefCache[ctx.cacheKey] = objectReferenceId;
465
+ }
466
+ return objectReferenceId;
467
+ }
468
+
469
+ private async applyInboundTransformMap(nd: any): Promise<any> {
470
+ if (!this.transformMapFile) {
471
+ return nd;
472
+ }
473
+ const mapKey = await TypeConversionUtils.getMapKeyFromObject(this.transformMapFile, this.mapFileUtil, nd, TypeConversionUtils.FLEX2VIBE_DIRECTION);
474
+ return MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, nd, mapKey as string, TypeConversionUtils.FLEX2VIBE_DIRECTION);
475
+ }
476
+
477
+ private async buildObjectReferenceContext(prop: any, nd: any): Promise<ObjectReferenceContext | null> {
438
478
  const entityType = prop['referencedTypeRootSlug'];
439
479
  const entityTypePath = prop['referencedTypePath'];
440
480
  const entityKeys = Object.keys(nd);
441
- const identifierKeys = await TypeConversionUtils.getIdentifierPropertiesFromObject(this.transformMapFile, this.mapFileUtil, nd)
481
+ const identifierKeys = await TypeConversionUtils.getIdentifierPropertiesFromObject(this.transformMapFile, this.mapFileUtil, nd);
442
482
  const hasAllIdentifiers = identifierKeys.every(key => entityKeys.includes(key));
443
- if(identifierKeys.length == 0 || !hasAllIdentifiers){
483
+ if (identifierKeys.length === 0 || !hasAllIdentifiers) {
444
484
  console.warn(`The inbound ${entityType} for prop '${prop['slug']}' doesnt have all "identifier" properties, so there is no processing. Identifier properties: ${identifierKeys}`);
445
- return objectReferenceId;
485
+ return null;
446
486
  }
447
-
487
+
448
488
  const rootType = await this.typeUtils.getByRootAndPath({root: entityType});
449
489
  const rootTypeProps = this.typeUtils.filterTypeProperties(rootType, nd);
450
490
 
451
- let rootTypeCriteria = {};
452
- let typeCriteria = { };
491
+ const rootTypeCriteria: Record<string, any> = {};
492
+ const typeCriteria: Record<string, any> = {};
453
493
  identifierKeys.forEach(keyName => {
454
- const foundInObjects = rootTypeProps.some(obj => obj.slug === keyName);
455
- if (foundInObjects) {
494
+ if (rootTypeProps.some(obj => obj.slug === keyName)) {
456
495
  rootTypeCriteria[keyName] = nd[keyName];
457
496
  } else {
458
497
  typeCriteria[keyName] = nd[keyName];
@@ -461,35 +500,49 @@ export class DataConverter {
461
500
 
462
501
  const combinedCriteria = { ...rootTypeCriteria, ...typeCriteria, typePath: entityTypePath };
463
502
  const cacheKey = Object.values(combinedCriteria).join('_');
503
+ const rolesIsTypeDiscriminator = entityType === 'item' || entityType === 'project-item';
504
+ const identityLookupKeys = rolesIsTypeDiscriminator
505
+ ? identifierKeys.filter(key => key !== 'roles')
506
+ : identifierKeys;
507
+ const useIdentityService = ConfigDefaults.isPropertyTrue(this.config?.search?.[entityType]?.useIdentityServiceForInboundData)
508
+ && identityLookupKeys.length === 1;
509
+
510
+ return { entityType, entityTypePath, rootTypeCriteria, typeCriteria, combinedCriteria, cacheKey, useIdentityService, identityLookupKeys };
511
+ }
464
512
 
465
- if (this.objRefCache[cacheKey]) {
466
- if (Logger.isDebugOn()) {
467
- console.debug(`object reference cache hit: ${cacheKey}`);
468
- }
469
- return this.objRefCache[cacheKey];
470
- }
513
+ private async lookupObjectReferenceViaIdentityService(ctx: ObjectReferenceContext, nd: any): Promise<string> {
514
+ const propertyName = ctx.identityLookupKeys[0];
515
+ const propertyValue = nd[propertyName];
516
+ const poolKey = await TypeConversionUtils.getUniquenessPoolKeyFromObject(this.transformMapFile, this.mapFileUtil, nd);
471
517
 
472
- let arrObjectReferences = await this.getAllObjectReferences(entityType, rootTypeCriteria);
473
- if(entityType !== entityTypePath) {
474
- arrObjectReferences = this.checkKeysAndValues(typeCriteria, arrObjectReferences, entityTypePath);
475
- }
518
+ const identityResults = await new Entities().get({
519
+ entityName: 'identity',
520
+ criteria: { poolKey, propertyName, propertyValue }
521
+ }) ?? [];
476
522
 
477
- if(arrObjectReferences.length) {
478
- if(arrObjectReferences.length === 1) {
479
- objectReferenceId = arrObjectReferences[0].id;
480
- } else {
481
- console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
482
- }
523
+ const match = this.pickSingleResult(identityResults, ctx.combinedCriteria);
524
+ return match ? match.entityReference.split(':')[1] : "";
525
+ }
526
+
527
+ private async lookupObjectReferenceViaQuery(ctx: ObjectReferenceContext): Promise<string> {
528
+ let arrObjectReferences = await this.getAllObjectReferences(ctx.entityType, ctx.rootTypeCriteria);
529
+ if (ctx.entityType !== ctx.entityTypePath) {
530
+ arrObjectReferences = this.checkKeysAndValues(ctx.typeCriteria, arrObjectReferences, ctx.entityTypePath);
483
531
  }
532
+ const match = this.pickSingleResult(arrObjectReferences, ctx.combinedCriteria);
533
+ return match ? match.id : "";
534
+ }
484
535
 
485
- if (!objectReferenceId) {
536
+ private pickSingleResult(results: any[], combinedCriteria: any): any | undefined {
537
+ if (!results.length) {
486
538
  console.warn(`The passed in object reference criteria ${JSON.stringify(combinedCriteria)} didn't match any entities.`);
487
- return objectReferenceId;
539
+ return undefined;
488
540
  }
489
-
490
- this.objRefCache[cacheKey] = objectReferenceId;
491
-
492
- return objectReferenceId;
541
+ if (results.length > 1) {
542
+ console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
543
+ return undefined;
544
+ }
545
+ return results[0];
493
546
  }
494
547
 
495
548
  /**
@@ -485,6 +485,30 @@ describe('Type Defaults', () =>{
485
485
  expect(entityClass).toBe('custom-entity');
486
486
  });
487
487
 
488
+ it('item - LCSMaterial - processAsItem=true', () =>{
489
+ const object = {
490
+ flexPLMObjectClass: 'LCSMaterial'
491
+ };
492
+
493
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
494
+ try {
495
+ const entityClass = TypeDefaults.getDefaultEntityClass(object);
496
+ expect(entityClass).toBe('item');
497
+ } finally {
498
+ TypeDefaults.applyConfig({});
499
+ }
500
+ });
501
+
502
+ it('custom-entity - LCSMaterial - processAsItem=false (default)', () =>{
503
+ const object = {
504
+ flexPLMObjectClass: 'LCSMaterial'
505
+ };
506
+
507
+ const entityClass = TypeDefaults.getDefaultEntityClass(object);
508
+
509
+ expect(entityClass).toBe('custom-entity');
510
+ });
511
+
488
512
  });//getDefaultEntityClass
489
513
 
490
514
  describe('getDefaultEntityTypePath', () =>{
@@ -549,6 +573,28 @@ describe('Type Defaults', () =>{
549
573
  const typePath = TypeDefaults.getDefaultEntityTypePath(object);
550
574
  expect(typePath).toBe('assortment');
551
575
  });
576
+
577
+ it('LCSMaterial - processAsItem=true', () =>{
578
+ const object = {
579
+ flexPLMObjectClass: 'LCSMaterial'
580
+ };
581
+
582
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
583
+ try {
584
+ const typePath = TypeDefaults.getDefaultEntityTypePath(object);
585
+ expect(typePath).toBe('item:material');
586
+ } finally {
587
+ TypeDefaults.applyConfig({});
588
+ }
589
+ });
590
+
591
+ it('LCSMaterial - processAsItem=false (default) throws', () =>{
592
+ const object = {
593
+ flexPLMObjectClass: 'LCSMaterial'
594
+ };
595
+
596
+ expect(() => TypeDefaults.getDefaultEntityTypePath(object)).toThrowError(TypeDefaults.NO_TYPE_PATH);
597
+ });
552
598
  });//getDefaultEntityTypePath
553
599
 
554
600
  describe('getDefaultIdentifierPropertiesFromObject', () =>{
@@ -622,6 +668,30 @@ describe('Type Defaults', () =>{
622
668
  expect(defaultIdentifiers).toHaveLength(1);
623
669
  });
624
670
 
671
+ it('LCSMaterial - processAsItem=true', () =>{
672
+ const object = {
673
+ flexPLMObjectClass: 'LCSMaterial'
674
+ };
675
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
676
+ try {
677
+ const defaultIdentifiers = TypeDefaults.getDefaultIdentifierPropertiesFromObject(object);
678
+ expect(defaultIdentifiers).toContain('itemNumber');
679
+ expect(defaultIdentifiers).toHaveLength(1);
680
+ } finally {
681
+ TypeDefaults.applyConfig({});
682
+ }
683
+ });
684
+
685
+ it('LCSMaterial - processAsItem=false (default)', () =>{
686
+ const object = {
687
+ flexPLMObjectClass: 'LCSMaterial'
688
+ };
689
+ const defaultIdentifiers = TypeDefaults.getDefaultIdentifierPropertiesFromObject(object);
690
+
691
+ expect(defaultIdentifiers).toContain('name');
692
+ expect(defaultIdentifiers).toHaveLength(1);
693
+ });
694
+
625
695
  });//getDefaultIdentifierPropertiesFromObject
626
696
 
627
697
  describe('getDefaultInformationalPropertiesFromObject', () =>{
@@ -665,5 +735,63 @@ describe('Type Defaults', () =>{
665
735
  expect(defaultIdentifiers).toHaveLength(1);
666
736
  });
667
737
 
738
+ it('LCSMaterial - processAsItem=true', () =>{
739
+ const object = {
740
+ flexPLMObjectClass: 'LCSMaterial'
741
+ };
742
+
743
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
744
+ try {
745
+ const defaultIdentifiers = TypeDefaults.getDefaultInformationalPropertiesFromObject(object);
746
+ expect(defaultIdentifiers).toContain('name');
747
+ expect(defaultIdentifiers).toHaveLength(1);
748
+ } finally {
749
+ TypeDefaults.applyConfig({});
750
+ }
751
+ });
752
+
753
+ it('LCSMaterial - processAsItem=false (default) yields no informational props', () =>{
754
+ const object = {
755
+ flexPLMObjectClass: 'LCSMaterial'
756
+ };
757
+
758
+ const defaultIdentifiers = TypeDefaults.getDefaultInformationalPropertiesFromObject(object);
759
+ expect(defaultIdentifiers).toHaveLength(0);
760
+ });
761
+
668
762
  });//getDefaultInformationalPropertiesFromObject
763
+
764
+ describe('applyConfig', () => {
765
+ afterEach(() => {
766
+ TypeDefaults.applyConfig({});
767
+ });
768
+
769
+ it('sets processLCSMaterialAsItem=true when processAsItem=true', () => {
770
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
771
+ expect(TypeDefaults.processLCSMaterialAsItem).toBe(true);
772
+ });
773
+
774
+ it('sets processLCSMaterialAsItem=true when processAsItem=\"true\" (string)', () => {
775
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: 'true' } });
776
+ expect(TypeDefaults.processLCSMaterialAsItem).toBe(true);
777
+ });
778
+
779
+ it('sets processLCSMaterialAsItem=false when processAsItem=false', () => {
780
+ TypeDefaults.processLCSMaterialAsItem = true;
781
+ TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: false } });
782
+ expect(TypeDefaults.processLCSMaterialAsItem).toBe(false);
783
+ });
784
+
785
+ it('sets processLCSMaterialAsItem=false when LCSMaterial missing', () => {
786
+ TypeDefaults.processLCSMaterialAsItem = true;
787
+ TypeDefaults.applyConfig({});
788
+ expect(TypeDefaults.processLCSMaterialAsItem).toBe(false);
789
+ });
790
+
791
+ it('sets processLCSMaterialAsItem=false when config is null/undefined', () => {
792
+ TypeDefaults.processLCSMaterialAsItem = true;
793
+ TypeDefaults.applyConfig(undefined);
794
+ expect(TypeDefaults.processLCSMaterialAsItem).toBe(false);
795
+ });
796
+ });
669
797
  });
@@ -2,8 +2,25 @@ export class TypeDefaults {
2
2
  static NO_ENTITY_TYPE = 'Not able to determine the entity type of the entity object';
3
3
  static NO_OBJECT_CLASS = 'Please ensure that the flexPLMObjectClass property is provided.';
4
4
  static NO_TYPE_PATH = 'Please ensure that the flexPLMTypePath property is provided.';
5
+ static processLCSMaterialAsItem = false;
5
6
  constructor() {
6
7
  }
8
+
9
+ /** Applies values from the resolved config to TypeDefaults static state.
10
+ * Currently toggles whether LCSMaterial is treated as an item (new) or a
11
+ * custom-entity (old) based on config.LCSMaterial.processAsItem.
12
+ * Technically this could cause side effects if different parts of the code
13
+ * expect different behavior, but in practice * @param config will be
14
+ * consistent across the app and this is simpler than passing this config
15
+ * through multiple layers of function calls.
16
+ */
17
+ static applyConfig(config: any): void {
18
+ TypeDefaults.processLCSMaterialAsItem = TypeDefaults.isPropertyTrue(config?.LCSMaterial?.processAsItem);
19
+ }
20
+
21
+ static isPropertyTrue(value: any): boolean {
22
+ return value === true || (typeof value === 'string' && value.toLowerCase() === 'true');
23
+ }
7
24
  /**Takes in full entity and returs the default FlexPLM
8
25
  * object class.
9
26
  *
@@ -87,7 +104,7 @@ export class TypeDefaults {
87
104
  */
88
105
 
89
106
  static getDefaultIdentifierProperties(entity): string[] {
90
- const identifierProps = [];
107
+ const identifierProps: string[] = [];
91
108
  const entityType = this.getEntityType(entity);
92
109
 
93
110
  switch (entityType) {
@@ -168,7 +185,14 @@ export class TypeDefaults {
168
185
  static getDefaultEntityClass(object): string {
169
186
  let entityClass = '';
170
187
  let objectClass = TypeDefaults.getObjectClass(object);
171
- if(['LCSProduct', 'LCSSKU'].includes(objectClass)){
188
+ const itemClasses = TypeDefaults.processLCSMaterialAsItem
189
+ ? ['LCSProduct', 'LCSSKU', 'LCSMaterial']
190
+ : ['LCSProduct', 'LCSSKU'];
191
+ const customEntityClasses = TypeDefaults.processLCSMaterialAsItem
192
+ ? ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast']
193
+ : ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', 'LCSMaterial'];
194
+
195
+ if(itemClasses.includes(objectClass)){
172
196
  entityClass = 'item';
173
197
  }else if(['LCSProductSeasonLink', 'LCSSKUSeasonLink'].includes(objectClass)){
174
198
  entityClass = 'project-item'
@@ -176,7 +200,7 @@ export class TypeDefaults {
176
200
  entityClass = 'color';
177
201
  } else if(['LCSSeason', 'SeasonGroup'].includes(objectClass)) {
178
202
  entityClass = 'assortment';
179
- } else if(['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', 'LCSMaterial'].includes(objectClass)) {
203
+ } else if(customEntityClasses.includes(objectClass)) {
180
204
  entityClass = 'custom-entity';
181
205
  }
182
206
 
@@ -201,6 +225,11 @@ export class TypeDefaults {
201
225
  case 'LCSSKU':
202
226
  typePath = 'item';
203
227
  break;
228
+ case 'LCSMaterial':
229
+ if (TypeDefaults.processLCSMaterialAsItem) {
230
+ typePath = 'item:material';
231
+ }
232
+ break;
204
233
  case 'LCSProductSeasonLink':
205
234
  case 'LCSSKUSeasonLink':
206
235
  typePath = 'project-item';
@@ -229,7 +258,7 @@ export class TypeDefaults {
229
258
  */
230
259
 
231
260
  static getDefaultIdentifierPropertiesFromObject(object): string[] {
232
- const identifierProps = [];
261
+ const identifierProps: string[] = [];
233
262
  const objectClass = TypeDefaults.getObjectClass(object);
234
263
 
235
264
  switch (objectClass) {
@@ -237,6 +266,13 @@ export class TypeDefaults {
237
266
  case 'LCSSKU':
238
267
  identifierProps.push('itemNumber');
239
268
  break;
269
+ case 'LCSMaterial':
270
+ if (TypeDefaults.processLCSMaterialAsItem) {
271
+ identifierProps.push('itemNumber');
272
+ } else {
273
+ identifierProps.push('name');
274
+ }
275
+ break;
240
276
  case 'LCSSeason':
241
277
  identifierProps.push('flexPLMSeasonName');
242
278
  break;
@@ -247,7 +283,6 @@ export class TypeDefaults {
247
283
  case 'LCSRevisableEntity':
248
284
  case 'LCSLifecycleManaged':
249
285
  case 'LCSLast':
250
- case 'LCSMaterial':
251
286
  identifierProps.push('name');
252
287
  break;
253
288
  }
@@ -265,7 +300,11 @@ export class TypeDefaults {
265
300
  static getDefaultInformationalPropertiesFromObject(object): string[] {
266
301
  const objectClass = TypeDefaults.getObjectClass(object);
267
302
  let properties:string[] = [];
268
- if ('LCSProduct' === objectClass) {
303
+ const itemClasses = TypeDefaults.processLCSMaterialAsItem
304
+ ? ['LCSProduct', 'LCSMaterial']
305
+ : ['LCSProduct'];
306
+
307
+ if (itemClasses.includes(objectClass)) {
269
308
  properties.push('name');
270
309
  } else if ('LCSSKU' === objectClass) {
271
310
  properties.push('optionName');