@contrail/flexplm 1.3.1 → 1.3.2-alpha.30ca8bf

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/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.2] - 2026-04-24
11
+ ### Added
12
+ - Added `getEntityUsingIdentityService` method to `BaseEntityProcessor` for looking up entities via the identity service using a pool key and property criteria. Returns the resolved entity from the identity's `entityReference`, `undefined` if not found, or throws if multiple matches exist.
13
+
10
14
  ## [1.3.0] - 2026-04-15
11
15
  ### Added
12
16
  - Added inbound thumbnail/primary content syncing from FlexPLM to VibeIQ via `ThumbnailUtil.syncThumbnailToVibeIQ`.
@@ -19,6 +19,16 @@ export declare abstract class BaseEntityProcessor {
19
19
  inbound(event: EntityPayloadType): Promise<any>;
20
20
  handleIncomingUpsert(event: EntityPayloadType): Promise<any>;
21
21
  getInboundStatusMessage(statusObject: any): string;
22
+ getIdentityEntity(params: {
23
+ poolKey: string;
24
+ propertyName: string;
25
+ propertyValue: string;
26
+ }): Promise<any | undefined>;
27
+ getEntityUsingIdentityService(params: {
28
+ poolKey: string;
29
+ propertyName: string;
30
+ propertyValue: string;
31
+ }): Promise<any | undefined>;
22
32
  queryEntityWithSubTypeCriteria(entityType: string, entityTypePath: string, propertyCriteria: any): Promise<any[]>;
23
33
  getCriteriaForEntity(entityType: string, entityTypePath: string, propertyCriteria: any): Promise<any>;
24
34
  getRootTypePropertyKeys(rootType: any, propertyCriteria?: any): string[];
@@ -136,6 +136,40 @@ class BaseEntityProcessor {
136
136
  + ', federatedId: ' + statusObject.federatedId
137
137
  + ', orgSlug: ' + this.orgSlug;
138
138
  }
139
+ async getIdentityEntity(params) {
140
+ const { poolKey, propertyName, propertyValue } = params;
141
+ if (!poolKey || !propertyName || !propertyValue) {
142
+ throw new Error('poolKey, propertyName, and propertyValue must be defined');
143
+ }
144
+ const identityEntities = await this.entities.get({
145
+ entityName: 'identity',
146
+ criteria: {
147
+ poolKey,
148
+ propertyName,
149
+ propertyValue
150
+ }
151
+ });
152
+ if (!identityEntities || (Array.isArray(identityEntities) && identityEntities.length === 0)) {
153
+ return undefined;
154
+ }
155
+ if (Array.isArray(identityEntities) && identityEntities.length > 1) {
156
+ throw new Error('Multiple identity entities found for poolKey: ' + poolKey + ', ' + propertyName + ': ' + propertyValue);
157
+ }
158
+ return Array.isArray(identityEntities) ? identityEntities[0] : identityEntities;
159
+ }
160
+ async getEntityUsingIdentityService(params) {
161
+ const identityEntity = await this.getIdentityEntity(params);
162
+ if (!identityEntity) {
163
+ return undefined;
164
+ }
165
+ const entityReference = identityEntity.entityReference;
166
+ const [entityName, id] = entityReference.split(':');
167
+ const entity = await this.entities.get({
168
+ entityName,
169
+ id
170
+ });
171
+ return entity;
172
+ }
139
173
  async queryEntityWithSubTypeCriteria(entityType, entityTypePath, propertyCriteria) {
140
174
  if (!entityType || !entityTypePath) {
141
175
  throw new Error('type and entityTypePath must be defined');
@@ -394,4 +394,195 @@ describe('BaseEntityProcessor', () => {
394
394
  expect(result).toEqual({ status: 200, data: { message: 'No Changes to persist for entity: existing-1' } });
395
395
  });
396
396
  });
397
+ describe('getIdentityEntity', () => {
398
+ const config = {};
399
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
400
+ const dc = new data_converter_1.DataConverter(config, mapFileUtil);
401
+ let btep;
402
+ let mockEntitiesGet;
403
+ beforeEach(() => {
404
+ btep = new TestBaseEntityProcessor(config, dc, mapFileUtil, 'item');
405
+ mockEntitiesGet = jest.fn();
406
+ btep.entities = { get: mockEntitiesGet };
407
+ });
408
+ it('should throw error when poolKey is missing', async () => {
409
+ await expect(btep.getIdentityEntity({
410
+ poolKey: '',
411
+ propertyName: 'itemNumber',
412
+ propertyValue: '12345'
413
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
414
+ });
415
+ it('should throw error when propertyName is missing', async () => {
416
+ await expect(btep.getIdentityEntity({
417
+ poolKey: 'item',
418
+ propertyName: '',
419
+ propertyValue: '12345'
420
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
421
+ });
422
+ it('should throw error when propertyValue is missing', async () => {
423
+ await expect(btep.getIdentityEntity({
424
+ poolKey: 'item',
425
+ propertyName: 'itemNumber',
426
+ propertyValue: ''
427
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
428
+ });
429
+ it('should return undefined when no identity entities are found (empty array)', async () => {
430
+ mockEntitiesGet.mockResolvedValue([]);
431
+ const result = await btep.getIdentityEntity({
432
+ poolKey: 'item',
433
+ propertyName: 'itemNumber',
434
+ propertyValue: '12345'
435
+ });
436
+ expect(result).toBeUndefined();
437
+ expect(mockEntitiesGet).toHaveBeenCalledWith({
438
+ entityName: 'identity',
439
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
440
+ });
441
+ });
442
+ it('should return undefined when identity entities result is null', async () => {
443
+ mockEntitiesGet.mockResolvedValue(null);
444
+ const result = await btep.getIdentityEntity({
445
+ poolKey: 'item',
446
+ propertyName: 'itemNumber',
447
+ propertyValue: '12345'
448
+ });
449
+ expect(result).toBeUndefined();
450
+ });
451
+ it('should throw error when multiple identity entities are found', async () => {
452
+ mockEntitiesGet.mockResolvedValue([
453
+ { entityReference: 'item:1' },
454
+ { entityReference: 'item:2' }
455
+ ]);
456
+ await expect(btep.getIdentityEntity({
457
+ poolKey: 'item',
458
+ propertyName: 'itemNumber',
459
+ propertyValue: '12345'
460
+ })).rejects.toThrow('Multiple identity entities found for poolKey: item, itemNumber: 12345');
461
+ });
462
+ it('should return the identity entity when one is found (array)', async () => {
463
+ const identityEntity = { entityReference: 'item:1' };
464
+ mockEntitiesGet.mockResolvedValue([identityEntity]);
465
+ const result = await btep.getIdentityEntity({
466
+ poolKey: 'item',
467
+ propertyName: 'itemNumber',
468
+ propertyValue: '12345'
469
+ });
470
+ expect(result).toEqual(identityEntity);
471
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(1);
472
+ });
473
+ it('should return the identity entity when result is a single object (not array)', async () => {
474
+ const identityEntity = { entityReference: 'item:5' };
475
+ mockEntitiesGet.mockResolvedValue(identityEntity);
476
+ const result = await btep.getIdentityEntity({
477
+ poolKey: 'item:material',
478
+ propertyName: 'materialNumber',
479
+ propertyValue: 'MAT-001'
480
+ });
481
+ expect(result).toEqual(identityEntity);
482
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(1);
483
+ });
484
+ });
485
+ describe('getEntityUsingIdentityService', () => {
486
+ const config = {};
487
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
488
+ const dc = new data_converter_1.DataConverter(config, mapFileUtil);
489
+ let btep;
490
+ let mockEntitiesGet;
491
+ beforeEach(() => {
492
+ btep = new TestBaseEntityProcessor(config, dc, mapFileUtil, 'item');
493
+ mockEntitiesGet = jest.fn();
494
+ btep.entities = { get: mockEntitiesGet };
495
+ });
496
+ it('should throw error when poolKey is missing', async () => {
497
+ await expect(btep.getEntityUsingIdentityService({
498
+ poolKey: '',
499
+ propertyName: 'itemNumber',
500
+ propertyValue: '12345'
501
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
502
+ });
503
+ it('should throw error when propertyName is missing', async () => {
504
+ await expect(btep.getEntityUsingIdentityService({
505
+ poolKey: 'item',
506
+ propertyName: '',
507
+ propertyValue: '12345'
508
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
509
+ });
510
+ it('should throw error when propertyValue is missing', async () => {
511
+ await expect(btep.getEntityUsingIdentityService({
512
+ poolKey: 'item',
513
+ propertyName: 'itemNumber',
514
+ propertyValue: ''
515
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
516
+ });
517
+ it('should return undefined when no identity entities are found (empty array)', async () => {
518
+ mockEntitiesGet.mockResolvedValue([]);
519
+ const result = await btep.getEntityUsingIdentityService({
520
+ poolKey: 'item',
521
+ propertyName: 'itemNumber',
522
+ propertyValue: '12345'
523
+ });
524
+ expect(result).toBeUndefined();
525
+ expect(mockEntitiesGet).toHaveBeenCalledWith({
526
+ entityName: 'identity',
527
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
528
+ });
529
+ });
530
+ it('should return undefined when identity entities result is null', async () => {
531
+ mockEntitiesGet.mockResolvedValue(null);
532
+ const result = await btep.getEntityUsingIdentityService({
533
+ poolKey: 'item',
534
+ propertyName: 'itemNumber',
535
+ propertyValue: '12345'
536
+ });
537
+ expect(result).toBeUndefined();
538
+ });
539
+ it('should throw error when multiple identity entities are found', async () => {
540
+ mockEntitiesGet.mockResolvedValue([
541
+ { entityReference: 'item:1' },
542
+ { entityReference: 'item:2' }
543
+ ]);
544
+ await expect(btep.getEntityUsingIdentityService({
545
+ poolKey: 'item',
546
+ propertyName: 'itemNumber',
547
+ propertyValue: '12345'
548
+ })).rejects.toThrow('Multiple identity entities found for poolKey: item, itemNumber: 12345');
549
+ });
550
+ it('should return the entity when one identity entity is found (array)', async () => {
551
+ const mockEntity = { id: '1', name: 'Test Item' };
552
+ mockEntitiesGet
553
+ .mockResolvedValueOnce([{ entityReference: 'item:1' }])
554
+ .mockResolvedValueOnce(mockEntity);
555
+ const result = await btep.getEntityUsingIdentityService({
556
+ poolKey: 'item',
557
+ propertyName: 'itemNumber',
558
+ propertyValue: '12345'
559
+ });
560
+ expect(result).toEqual(mockEntity);
561
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(2);
562
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(1, {
563
+ entityName: 'identity',
564
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
565
+ });
566
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(2, {
567
+ entityName: 'item',
568
+ id: '1'
569
+ });
570
+ });
571
+ it('should return the entity when identity result is a single object (not array)', async () => {
572
+ const mockEntity = { id: '5', name: 'Test Material' };
573
+ mockEntitiesGet
574
+ .mockResolvedValueOnce({ entityReference: 'item:5' })
575
+ .mockResolvedValueOnce(mockEntity);
576
+ const result = await btep.getEntityUsingIdentityService({
577
+ poolKey: 'item:material',
578
+ propertyName: 'materialNumber',
579
+ propertyValue: 'MAT-001'
580
+ });
581
+ expect(result).toEqual(mockEntity);
582
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(2, {
583
+ entityName: 'item',
584
+ id: '5'
585
+ });
586
+ });
587
+ });
397
588
  });
@@ -4,5 +4,6 @@ export declare class ConfigDefaults {
4
4
  static STATIC_CONFIG_CACHE: {};
5
5
  static setConfigDefaults(config: any): Promise<FCConfig>;
6
6
  static getConfigFile(fileId: string): Promise<any>;
7
+ static isPropertyTrue(value: any): boolean;
7
8
  static clearConfigCache(): void;
8
9
  }
@@ -76,6 +76,9 @@ class ConfigDefaults {
76
76
  return {};
77
77
  }
78
78
  }
79
+ static isPropertyTrue(value) {
80
+ return value === true || (typeof value === 'string' && value.toLowerCase() === 'true');
81
+ }
79
82
  static clearConfigCache() {
80
83
  ConfigDefaults.STATIC_CONFIG_CACHE = {};
81
84
  }
@@ -264,6 +264,38 @@ describe('all tests', () => {
264
264
  }
265
265
  });
266
266
  });
267
+ describe('isPropertyTrue', () => {
268
+ it('returns true for boolean true', () => {
269
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(true)).toBe(true);
270
+ });
271
+ it('returns true for string true', () => {
272
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue('true')).toBe(true);
273
+ });
274
+ it('returns true for string TRUE', () => {
275
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue('TRUE')).toBe(true);
276
+ });
277
+ it('returns true for string True', () => {
278
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue('True')).toBe(true);
279
+ });
280
+ it('returns false for boolean false', () => {
281
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(false)).toBe(false);
282
+ });
283
+ it('returns false for string false', () => {
284
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue('false')).toBe(false);
285
+ });
286
+ it('returns false for null', () => {
287
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(null)).toBe(false);
288
+ });
289
+ it('returns false for undefined', () => {
290
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(undefined)).toBe(false);
291
+ });
292
+ it('returns false for empty string', () => {
293
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue('')).toBe(false);
294
+ });
295
+ it('returns false for number 1', () => {
296
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(1)).toBe(false);
297
+ });
298
+ });
267
299
  describe('getConfigFile', () => {
268
300
  beforeEach(() => {
269
301
  config_defaults_1.ConfigDefaults.clearConfigCache();
@@ -207,6 +207,7 @@ exports.mapping = {
207
207
  }
208
208
  },
209
209
  catName: {
210
+ uniquenessPool: 'catName-pool',
210
211
  getIdentifierProperties: () => ['catName', 'catNumber'],
211
212
  getInformationalProperties: () => ['longName'],
212
213
  vibe2flex: {
@@ -11,6 +11,7 @@ export declare class TypeConversionUtils {
11
11
  static getMapKey(transformMapFile: any, mapFileUtil: MapFileUtil, entity: any, direction: string): Promise<string>;
12
12
  static getEntityType(entity: any): any;
13
13
  static getEntityClassFromObject(fileId: any, mapFileUtil: any, object: any): Promise<string>;
14
+ static getUniquenessPoolKeyFromObject(fileId: any, mapFileUtil: any, object: any): Promise<string>;
14
15
  static getEntityTypePathFromOjbect(fileId: any, mapFileUtil: any, object: any): Promise<string>;
15
16
  static getIdentifierPropertiesFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any): Promise<string[]>;
16
17
  static getInformationalPropertiesFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any): Promise<string[]>;
@@ -118,6 +118,22 @@ class TypeConversionUtils {
118
118
  }
119
119
  return type_defaults_1.TypeDefaults.getDefaultEntityClass(object);
120
120
  }
121
+ static async getUniquenessPoolKeyFromObject(fileId, mapFileUtil, object) {
122
+ let uniquenessPool;
123
+ if (fileId) {
124
+ const mapKey = await this.getMapKeyFromObject(fileId, mapFileUtil, object, TypeConversionUtils.FLEX2VIBE_DIRECTION);
125
+ if (mapKey) {
126
+ const mapData = await map_utils_1.MapUtil.getFullMapSection(fileId, mapFileUtil, mapKey);
127
+ if (mapData && mapData['uniquenessPool']) {
128
+ uniquenessPool = mapData['uniquenessPool'];
129
+ }
130
+ }
131
+ }
132
+ if (uniquenessPool) {
133
+ return uniquenessPool;
134
+ }
135
+ return type_defaults_1.TypeDefaults.getDefaultEntityClass(object);
136
+ }
121
137
  static async getEntityTypePathFromOjbect(fileId, mapFileUtil, object) {
122
138
  let typePath = object['vibeIQTypePath'];
123
139
  if (typePath) {
@@ -328,6 +328,65 @@ describe('conversion-utils', () => {
328
328
  }
329
329
  });
330
330
  });
331
+ describe('getUniquenessPoolKeyFromObject', () => {
332
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
333
+ it('uses mapping-catName', async () => {
334
+ const expectedPool = 'catName-pool';
335
+ const object = {
336
+ flexPLMObjectClass: 'LCSLast',
337
+ flexPLMTypePath: 'Last\\catName'
338
+ };
339
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
340
+ .mockImplementation(async () => {
341
+ return mapping;
342
+ });
343
+ try {
344
+ const results = await type_conversion_utils_1.TypeConversionUtils.getUniquenessPoolKeyFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object);
345
+ expect(results).toEqual(expectedPool);
346
+ }
347
+ finally {
348
+ spy.mockRestore();
349
+ }
350
+ });
351
+ it('uses default-noMap', async () => {
352
+ const expectedClass = 'color';
353
+ const object = {
354
+ flexPLMObjectClass: 'LCSColor',
355
+ flexPLMTypePath: 'Color'
356
+ };
357
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
358
+ .mockImplementation(async () => {
359
+ return mapping;
360
+ });
361
+ const spyDefaults = jest.spyOn(type_defaults_1.TypeDefaults, 'getDefaultEntityClass')
362
+ .mockImplementation(() => expectedClass);
363
+ try {
364
+ const results = await type_conversion_utils_1.TypeConversionUtils.getUniquenessPoolKeyFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object);
365
+ expect(results).toEqual(expectedClass);
366
+ expect(spyDefaults).toBeCalledTimes(1);
367
+ }
368
+ finally {
369
+ spy.mockRestore();
370
+ spyDefaults.mockRestore();
371
+ }
372
+ });
373
+ it('uses default-noFileId', async () => {
374
+ const expectedClass = 'color';
375
+ const object = {
376
+ flexPLMObjectClass: 'LCSColor',
377
+ };
378
+ const spyDefaults = jest.spyOn(type_defaults_1.TypeDefaults, 'getDefaultEntityClass')
379
+ .mockImplementation(() => expectedClass);
380
+ try {
381
+ const results = await type_conversion_utils_1.TypeConversionUtils.getUniquenessPoolKeyFromObject(null, mapFileUtil, object);
382
+ expect(results).toEqual(expectedClass);
383
+ expect(spyDefaults).toBeCalledTimes(1);
384
+ }
385
+ finally {
386
+ spyDefaults.mockRestore();
387
+ }
388
+ });
389
+ });
331
390
  describe('getEntityTypePathFromOjbect', () => {
332
391
  const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
333
392
  it('vibeIQTypePath', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.3.1",
3
+ "version": "1.3.2-alpha.30ca8bf",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -457,4 +457,233 @@ describe('BaseEntityProcessor', () =>{
457
457
  });
458
458
  });
459
459
 
460
+ describe('getIdentityEntity', () => {
461
+ const config = {} as FCConfig;
462
+ const mapFileUtil = new MapFileUtil(new Entities());
463
+ const dc = new DataConverter(config, mapFileUtil);
464
+ let btep: TestBaseEntityProcessor;
465
+ let mockEntitiesGet: jest.Mock;
466
+
467
+ beforeEach(() => {
468
+ btep = new TestBaseEntityProcessor(config, dc, mapFileUtil, 'item');
469
+ mockEntitiesGet = jest.fn();
470
+ (btep as any).entities = { get: mockEntitiesGet };
471
+ });
472
+
473
+ it('should throw error when poolKey is missing', async () => {
474
+ await expect(btep.getIdentityEntity({
475
+ poolKey: '',
476
+ propertyName: 'itemNumber',
477
+ propertyValue: '12345'
478
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
479
+ });
480
+
481
+ it('should throw error when propertyName is missing', async () => {
482
+ await expect(btep.getIdentityEntity({
483
+ poolKey: 'item',
484
+ propertyName: '',
485
+ propertyValue: '12345'
486
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
487
+ });
488
+
489
+ it('should throw error when propertyValue is missing', async () => {
490
+ await expect(btep.getIdentityEntity({
491
+ poolKey: 'item',
492
+ propertyName: 'itemNumber',
493
+ propertyValue: ''
494
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
495
+ });
496
+
497
+ it('should return undefined when no identity entities are found (empty array)', async () => {
498
+ mockEntitiesGet.mockResolvedValue([]);
499
+
500
+ const result = await btep.getIdentityEntity({
501
+ poolKey: 'item',
502
+ propertyName: 'itemNumber',
503
+ propertyValue: '12345'
504
+ });
505
+
506
+ expect(result).toBeUndefined();
507
+ expect(mockEntitiesGet).toHaveBeenCalledWith({
508
+ entityName: 'identity',
509
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
510
+ });
511
+ });
512
+
513
+ it('should return undefined when identity entities result is null', async () => {
514
+ mockEntitiesGet.mockResolvedValue(null);
515
+
516
+ const result = await btep.getIdentityEntity({
517
+ poolKey: 'item',
518
+ propertyName: 'itemNumber',
519
+ propertyValue: '12345'
520
+ });
521
+
522
+ expect(result).toBeUndefined();
523
+ });
524
+
525
+ it('should throw error when multiple identity entities are found', async () => {
526
+ mockEntitiesGet.mockResolvedValue([
527
+ { entityReference: 'item:1' },
528
+ { entityReference: 'item:2' }
529
+ ]);
530
+
531
+ await expect(btep.getIdentityEntity({
532
+ poolKey: 'item',
533
+ propertyName: 'itemNumber',
534
+ propertyValue: '12345'
535
+ })).rejects.toThrow('Multiple identity entities found for poolKey: item, itemNumber: 12345');
536
+ });
537
+
538
+ it('should return the identity entity when one is found (array)', async () => {
539
+ const identityEntity = { entityReference: 'item:1' };
540
+ mockEntitiesGet.mockResolvedValue([identityEntity]);
541
+
542
+ const result = await btep.getIdentityEntity({
543
+ poolKey: 'item',
544
+ propertyName: 'itemNumber',
545
+ propertyValue: '12345'
546
+ });
547
+
548
+ expect(result).toEqual(identityEntity);
549
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(1);
550
+ });
551
+
552
+ it('should return the identity entity when result is a single object (not array)', async () => {
553
+ const identityEntity = { entityReference: 'item:5' };
554
+ mockEntitiesGet.mockResolvedValue(identityEntity);
555
+
556
+ const result = await btep.getIdentityEntity({
557
+ poolKey: 'item:material',
558
+ propertyName: 'materialNumber',
559
+ propertyValue: 'MAT-001'
560
+ });
561
+
562
+ expect(result).toEqual(identityEntity);
563
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(1);
564
+ });
565
+ });
566
+
567
+ describe('getEntityUsingIdentityService', () => {
568
+ const config = {} as FCConfig;
569
+ const mapFileUtil = new MapFileUtil(new Entities());
570
+ const dc = new DataConverter(config, mapFileUtil);
571
+ let btep: TestBaseEntityProcessor;
572
+ let mockEntitiesGet: jest.Mock;
573
+
574
+ beforeEach(() => {
575
+ btep = new TestBaseEntityProcessor(config, dc, mapFileUtil, 'item');
576
+ mockEntitiesGet = jest.fn();
577
+ (btep as any).entities = { get: mockEntitiesGet };
578
+ });
579
+
580
+ it('should throw error when poolKey is missing', async () => {
581
+ await expect(btep.getEntityUsingIdentityService({
582
+ poolKey: '',
583
+ propertyName: 'itemNumber',
584
+ propertyValue: '12345'
585
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
586
+ });
587
+
588
+ it('should throw error when propertyName is missing', async () => {
589
+ await expect(btep.getEntityUsingIdentityService({
590
+ poolKey: 'item',
591
+ propertyName: '',
592
+ propertyValue: '12345'
593
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
594
+ });
595
+
596
+ it('should throw error when propertyValue is missing', async () => {
597
+ await expect(btep.getEntityUsingIdentityService({
598
+ poolKey: 'item',
599
+ propertyName: 'itemNumber',
600
+ propertyValue: ''
601
+ })).rejects.toThrow('poolKey, propertyName, and propertyValue must be defined');
602
+ });
603
+
604
+ it('should return undefined when no identity entities are found (empty array)', async () => {
605
+ mockEntitiesGet.mockResolvedValue([]);
606
+
607
+ const result = await btep.getEntityUsingIdentityService({
608
+ poolKey: 'item',
609
+ propertyName: 'itemNumber',
610
+ propertyValue: '12345'
611
+ });
612
+
613
+ expect(result).toBeUndefined();
614
+ expect(mockEntitiesGet).toHaveBeenCalledWith({
615
+ entityName: 'identity',
616
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
617
+ });
618
+ });
619
+
620
+ it('should return undefined when identity entities result is null', async () => {
621
+ mockEntitiesGet.mockResolvedValue(null);
622
+
623
+ const result = await btep.getEntityUsingIdentityService({
624
+ poolKey: 'item',
625
+ propertyName: 'itemNumber',
626
+ propertyValue: '12345'
627
+ });
628
+
629
+ expect(result).toBeUndefined();
630
+ });
631
+
632
+ it('should throw error when multiple identity entities are found', async () => {
633
+ mockEntitiesGet.mockResolvedValue([
634
+ { entityReference: 'item:1' },
635
+ { entityReference: 'item:2' }
636
+ ]);
637
+
638
+ await expect(btep.getEntityUsingIdentityService({
639
+ poolKey: 'item',
640
+ propertyName: 'itemNumber',
641
+ propertyValue: '12345'
642
+ })).rejects.toThrow('Multiple identity entities found for poolKey: item, itemNumber: 12345');
643
+ });
644
+
645
+ it('should return the entity when one identity entity is found (array)', async () => {
646
+ const mockEntity = { id: '1', name: 'Test Item' };
647
+ mockEntitiesGet
648
+ .mockResolvedValueOnce([{ entityReference: 'item:1' }])
649
+ .mockResolvedValueOnce(mockEntity);
650
+
651
+ const result = await btep.getEntityUsingIdentityService({
652
+ poolKey: 'item',
653
+ propertyName: 'itemNumber',
654
+ propertyValue: '12345'
655
+ });
656
+
657
+ expect(result).toEqual(mockEntity);
658
+ expect(mockEntitiesGet).toHaveBeenCalledTimes(2);
659
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(1, {
660
+ entityName: 'identity',
661
+ criteria: { poolKey: 'item', propertyName: 'itemNumber', propertyValue: '12345' }
662
+ });
663
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(2, {
664
+ entityName: 'item',
665
+ id: '1'
666
+ });
667
+ });
668
+
669
+ it('should return the entity when identity result is a single object (not array)', async () => {
670
+ const mockEntity = { id: '5', name: 'Test Material' };
671
+ mockEntitiesGet
672
+ .mockResolvedValueOnce({ entityReference: 'item:5' })
673
+ .mockResolvedValueOnce(mockEntity);
674
+
675
+ const result = await btep.getEntityUsingIdentityService({
676
+ poolKey: 'item:material',
677
+ propertyName: 'materialNumber',
678
+ propertyValue: 'MAT-001'
679
+ });
680
+
681
+ expect(result).toEqual(mockEntity);
682
+ expect(mockEntitiesGet).toHaveBeenNthCalledWith(2, {
683
+ entityName: 'item',
684
+ id: '5'
685
+ });
686
+ });
687
+ });
688
+
460
689
  });
@@ -163,6 +163,74 @@ export abstract class BaseEntityProcessor {
163
163
  + ', orgSlug: ' + this.orgSlug;
164
164
  }
165
165
 
166
+ /** Looks up an identity record from the identity service based on the passed in criteria.
167
+ * If no identity record is found, returns undefined. If multiple are found, throws an error.
168
+ *
169
+ * @param params.poolKey the key to use for the identity service pool. This will be the subtype uniqueness is defined on, typically the root type. Ex: 'item' or 'item:material'
170
+ * @param params.propertyName the name of the property to use for the criteria. Ex: 'itemNumber'
171
+ * @param params.propertyValue the value of the property to use for the criteria. Ex: '12345'
172
+ * @returns the identity entity, or undefined if no identity record is found
173
+ * @throws error if multiple identity entities are found, or if required parameters are missing
174
+ */
175
+ async getIdentityEntity(params: {
176
+ poolKey: string,
177
+ propertyName: string,
178
+ propertyValue: string
179
+ }): Promise<any | undefined> {
180
+ const {poolKey, propertyName, propertyValue} = params;
181
+ if(!poolKey || !propertyName || !propertyValue){
182
+ throw new Error('poolKey, propertyName, and propertyValue must be defined');
183
+ }
184
+
185
+ const identityEntities = await this.entities.get({
186
+ entityName: 'identity',
187
+ criteria: {
188
+ poolKey,
189
+ propertyName,
190
+ propertyValue
191
+ }
192
+ });
193
+
194
+ if(!identityEntities || (Array.isArray(identityEntities) && identityEntities.length === 0)){
195
+ return undefined;
196
+ }
197
+
198
+ if(Array.isArray(identityEntities) && identityEntities.length > 1){
199
+ throw new Error('Multiple identity entities found for poolKey: ' + poolKey + ', ' + propertyName + ': ' + propertyValue);
200
+ }
201
+
202
+ return Array.isArray(identityEntities) ? identityEntities[0] : identityEntities;
203
+ }
204
+
205
+ /** Looks up an entity via the identity service. Uses {@link getIdentityEntity} to find the identity record,
206
+ * then resolves the entity reference to fetch and return the actual entity.
207
+ *
208
+ * @param params.poolKey the key to use for the identity service pool. This will be the subtype uniqueness is defined on, typically the root type. Ex: 'item' or 'item:material'
209
+ * @param params.propertyName the name of the property to use for the criteria. Ex: 'itemNumber'
210
+ * @param params.propertyValue the value of the property to use for the criteria. Ex: '12345'
211
+ * @returns the resolved entity, or undefined if no identity record is found
212
+ * @throws error if multiple identity entities are found, or if required parameters are missing
213
+ */
214
+ async getEntityUsingIdentityService(params: {
215
+ poolKey: string,
216
+ propertyName: string,
217
+ propertyValue: string
218
+ }): Promise<any | undefined> {
219
+ const identityEntity = await this.getIdentityEntity(params);
220
+ if(!identityEntity){
221
+ return undefined;
222
+ }
223
+
224
+ const entityReference = identityEntity.entityReference;
225
+ const [entityName, id] = entityReference.split(':');
226
+
227
+ const entity = await this.entities.get({
228
+ entityName,
229
+ id
230
+ });
231
+
232
+ return entity;
233
+ }
166
234
  /**This will query for the entity, and handle post-processing
167
235
  * of any critieria that is defined at the sub-type level.
168
236
  * Because sub-type criteria can't be used in the search done
@@ -314,6 +314,48 @@ describe('all tests', () => {
314
314
 
315
315
  });
316
316
 
317
+ describe('isPropertyTrue', () => {
318
+ it('returns true for boolean true', () => {
319
+ expect(ConfigDefaults.isPropertyTrue(true)).toBe(true);
320
+ });
321
+
322
+ it('returns true for string true', () => {
323
+ expect(ConfigDefaults.isPropertyTrue('true')).toBe(true);
324
+ });
325
+
326
+ it('returns true for string TRUE', () => {
327
+ expect(ConfigDefaults.isPropertyTrue('TRUE')).toBe(true);
328
+ });
329
+
330
+ it('returns true for string True', () => {
331
+ expect(ConfigDefaults.isPropertyTrue('True')).toBe(true);
332
+ });
333
+
334
+ it('returns false for boolean false', () => {
335
+ expect(ConfigDefaults.isPropertyTrue(false)).toBe(false);
336
+ });
337
+
338
+ it('returns false for string false', () => {
339
+ expect(ConfigDefaults.isPropertyTrue('false')).toBe(false);
340
+ });
341
+
342
+ it('returns false for null', () => {
343
+ expect(ConfigDefaults.isPropertyTrue(null)).toBe(false);
344
+ });
345
+
346
+ it('returns false for undefined', () => {
347
+ expect(ConfigDefaults.isPropertyTrue(undefined)).toBe(false);
348
+ });
349
+
350
+ it('returns false for empty string', () => {
351
+ expect(ConfigDefaults.isPropertyTrue('')).toBe(false);
352
+ });
353
+
354
+ it('returns false for number 1', () => {
355
+ expect(ConfigDefaults.isPropertyTrue(1)).toBe(false);
356
+ });
357
+ });
358
+
317
359
  describe('getConfigFile', () => {
318
360
  beforeEach(() => {
319
361
  ConfigDefaults.clearConfigCache();
@@ -87,6 +87,10 @@ export class ConfigDefaults {
87
87
  }
88
88
  }
89
89
 
90
+ static isPropertyTrue(value: any): boolean {
91
+ return value === true || (typeof value === 'string' && value.toLowerCase() === 'true');
92
+ }
93
+
90
94
  static clearConfigCache(){
91
95
  ConfigDefaults.STATIC_CONFIG_CACHE = {};
92
96
  }
@@ -214,6 +214,7 @@ exports.mapping = {
214
214
  }
215
215
  },
216
216
  catName: {
217
+ uniquenessPool: 'catName-pool',
217
218
  getIdentifierProperties: () => ['catName', 'catNumber'],
218
219
  getInformationalProperties: () => ['longName'],
219
220
  vibe2flex: {
@@ -360,6 +360,69 @@ describe('conversion-utils', () => {
360
360
  });
361
361
  });
362
362
 
363
+ describe('getUniquenessPoolKeyFromObject', () =>{
364
+ const mapFileUtil = new MapFileUtil(new Entities());
365
+
366
+ it('uses mapping-catName', async () =>{
367
+ const expectedPool = 'catName-pool';
368
+ const object = {
369
+ flexPLMObjectClass: 'LCSLast',
370
+ flexPLMTypePath: 'Last\\catName'
371
+ };
372
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
373
+ .mockImplementation(async () =>{
374
+ return mapping;
375
+ });
376
+ try{
377
+ const results = await TypeConversionUtils.getUniquenessPoolKeyFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object);
378
+ expect(results).toEqual(expectedPool);
379
+
380
+ } finally {
381
+ spy.mockRestore();
382
+ }
383
+ });
384
+
385
+ it('uses default-noMap', async () =>{
386
+ const expectedClass = 'color';
387
+ const object = {
388
+ flexPLMObjectClass: 'LCSColor',
389
+ flexPLMTypePath: 'Color'
390
+ };
391
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
392
+ .mockImplementation(async () =>{
393
+ return mapping;
394
+ });
395
+ const spyDefaults = jest.spyOn(TypeDefaults, 'getDefaultEntityClass')
396
+ .mockImplementation(() => expectedClass);
397
+ try{
398
+ const results = await TypeConversionUtils.getUniquenessPoolKeyFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object);
399
+ expect(results).toEqual(expectedClass);
400
+ expect(spyDefaults).toBeCalledTimes(1);
401
+
402
+ } finally {
403
+ spy.mockRestore();
404
+ spyDefaults.mockRestore();
405
+ }
406
+ });
407
+
408
+ it('uses default-noFileId', async () =>{
409
+ const expectedClass = 'color';
410
+ const object = {
411
+ flexPLMObjectClass: 'LCSColor',
412
+ };
413
+ const spyDefaults = jest.spyOn(TypeDefaults, 'getDefaultEntityClass')
414
+ .mockImplementation(() => expectedClass);
415
+ try{
416
+ const results = await TypeConversionUtils.getUniquenessPoolKeyFromObject(null, mapFileUtil, object);
417
+ expect(results).toEqual(expectedClass);
418
+ expect(spyDefaults).toBeCalledTimes(1);
419
+
420
+ } finally {
421
+ spyDefaults.mockRestore();
422
+ }
423
+ });
424
+ });
425
+
363
426
  describe('getEntityTypePathFromOjbect', () =>{
364
427
  const mapFileUtil = new MapFileUtil(new Entities());
365
428
 
@@ -224,6 +224,36 @@ export class TypeConversionUtils {
224
224
  return TypeDefaults.getDefaultEntityClass(object);
225
225
  }
226
226
 
227
+ /** Takes in a FlexPLM object and returns the correct VibeIQ uniqueness
228
+ * pool key. Order of precedence:
229
+ * Map file entry in 'typeConversion:flex2vibe:<value>:getUniquenessPool()'
230
+ * for value from 'objectClass'
231
+ * TypeDefaults.getDefaultEntityClass() function
232
+ *
233
+ * @param fileId id for mapFile
234
+ * @param mapFileUtil class to get mapfile
235
+ * @param object FlexPLM object
236
+ * @returns Promise<string>
237
+ */
238
+ static async getUniquenessPoolKeyFromObject(fileId, mapFileUtil, object): Promise<string>{
239
+ let uniquenessPool;
240
+
241
+ if(fileId){
242
+ const mapKey = await this.getMapKeyFromObject(fileId, mapFileUtil, object, TypeConversionUtils.FLEX2VIBE_DIRECTION);
243
+ if(mapKey){
244
+ const mapData = await MapUtil.getFullMapSection(fileId, mapFileUtil, mapKey);
245
+ if(mapData && mapData['uniquenessPool']){
246
+ uniquenessPool = mapData['uniquenessPool'];
247
+ }
248
+ }
249
+ }
250
+
251
+ if(uniquenessPool){
252
+ return uniquenessPool;
253
+ }
254
+ return TypeDefaults.getDefaultEntityClass(object);
255
+ }
256
+
227
257
  /** Takes in a FlexPLM object and returns the correct VibeIQ
228
258
  * type associated to the object. Order of precedence
229
259
  * Property 'vibeIQTypePath'