@contrail/flexplm 1.5.0-alpha.6fc44c4 → 1.5.0-alpha.aaef470

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
@@ -8,6 +8,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
  ## [Unreleased]
9
9
  ### Added
10
10
  - Added support for Inbound `LCSMaterial` to sync to the entity class `item` with type path `item:material` and `itemNumber` as identifier. This is controlled by an `LCSMaterial.processAsItem` (default `false`) config default.
11
+ - Added optional identity-service lookup in `DataConverter.setObjectReferenceValue` for resolving inbound `object_reference` values. Enabled per referenced entity type via `config.search.<entityType>.useIdentityServiceForInboundData`. When enabled the reference is resolved via the identity service using a uniqueness pool key; otherwise behavior falls through to the existing `getAllObjectReferences` query path.
11
12
 
12
13
  ## [1.4.0] - 2026-05-06
13
14
  ### Added
@@ -4,6 +4,7 @@ const transform_data_1 = require("@contrail/transform-data");
4
4
  const identifier_conversion_1 = require("./identifier-conversion");
5
5
  const data_converter_1 = require("../util/data-converter");
6
6
  const sdk_1 = require("@contrail/sdk");
7
+ const type_defaults_1 = require("../util/type-defaults");
7
8
  const mapFile1Data = require('./identifier-conversion-spec-mockData');
8
9
  const mapFile1Mappings = mapFile1Data?.mapping;
9
10
  const mapFile2Mappings = mapFile1Data?.mapping2;
@@ -312,6 +313,7 @@ describe('getItemCriteriaFromObject', () => {
312
313
  itemNumber: 'MAT-100'
313
314
  };
314
315
  let getEntityValuesSpyOn = undefined;
316
+ type_defaults_1.TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
315
317
  try {
316
318
  getEntityValuesSpyOn = jest.spyOn(dc, 'getEntityValues');
317
319
  const result = await identifier_conversion_1.IdentifierConversion.getItemCriteriaFromObject(transformMapFile1, mapFileUtil, dc, object);
@@ -322,6 +324,7 @@ describe('getItemCriteriaFromObject', () => {
322
324
  if (getEntityValuesSpyOn) {
323
325
  getEntityValuesSpyOn.mockRestore();
324
326
  }
327
+ type_defaults_1.TypeDefaults.applyConfig({});
325
328
  }
326
329
  });
327
330
  it('should return the item option criteria from the object -uniqueIdentifierA, uniqueIdentifierB', async () => {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConfigDefaults = void 0;
4
4
  const sdk_1 = require("@contrail/sdk");
5
5
  const util_1 = require("@contrail/util");
6
+ const type_defaults_1 = require("./type-defaults");
6
7
  class ConfigDefaults {
7
8
  static async setConfigDefaults(config) {
8
9
  if (!config.apiHost || !config.userName || !config.password) {
@@ -26,6 +27,7 @@ class ConfigDefaults {
26
27
  outputConfig.userName = () => uName;
27
28
  outputConfig.password = () => pass;
28
29
  outputConfig['OOBvibeEventEndpoint'] = '/rfa/vibeiq/vibeEvents';
30
+ type_defaults_1.TypeDefaults.applyConfig(outputConfig);
29
31
  console.log('outputConfig: ' + JSON.stringify(outputConfig));
30
32
  return outputConfig;
31
33
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config_defaults_1 = require("./config-defaults");
4
+ const type_defaults_1 = require("./type-defaults");
4
5
  let entityObject = {};
5
6
  jest.mock('@contrail/sdk', () => {
6
7
  return {
@@ -328,6 +329,19 @@ describe('all tests', () => {
328
329
  const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
329
330
  expect(fcConfig.LCSMaterial.processAsItem).toBe(true);
330
331
  });
332
+ it('applies LCSMaterial.processAsItem to TypeDefaults', async () => {
333
+ try {
334
+ const startConfig = Object.assign({}, config, { LCSMaterial: { processAsItem: true } });
335
+ await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
336
+ expect(type_defaults_1.TypeDefaults.processLCSMaterialAsItem).toBe(true);
337
+ const defaultStart = Object.assign({}, config);
338
+ await config_defaults_1.ConfigDefaults.setConfigDefaults(defaultStart);
339
+ expect(type_defaults_1.TypeDefaults.processLCSMaterialAsItem).toBe(false);
340
+ }
341
+ finally {
342
+ type_defaults_1.TypeDefaults.applyConfig({});
343
+ }
344
+ });
331
345
  });
332
346
  describe('getConfigFile', () => {
333
347
  beforeEach(() => {
@@ -28,6 +28,11 @@ export declare class DataConverter {
28
28
  setEnumerationKeys(prop: any, nd: any, matchByDisplay: any): any;
29
29
  getPersistableChanges(entity: object, changes: object): object;
30
30
  setObjectReferenceValue(prop: any, nd: any): Promise<any>;
31
+ private applyInboundTransformMap;
32
+ private buildObjectReferenceContext;
33
+ private lookupObjectReferenceViaIdentityService;
34
+ private lookupObjectReferenceViaQuery;
35
+ private assertSingleObjectReference;
31
36
  getAllObjectReferences(entityType: string, rootTypeCriteria: any, postProcessCriteria?: any): Promise<any[]>;
32
37
  checkKeysAndValues(criteria: any, arrayOfObjects: any, entityTypePath: any): any[];
33
38
  filterOutArchivedAndTrashedEntities(entities: any[]): any[];
@@ -7,6 +7,7 @@ const app_framework_1 = require("@contrail/app-framework");
7
7
  const util_1 = require("@contrail/util");
8
8
  const type_conversion_utils_1 = require("./type-conversion-utils");
9
9
  const map_utils_1 = require("./map-utils");
10
+ const config_defaults_1 = require("./config-defaults");
10
11
  class DataConverter {
11
12
  static clearStaticUserCache() {
12
13
  DataConverter.staticUserCache = {};
@@ -347,30 +348,51 @@ class DataConverter {
347
348
  return diffValues;
348
349
  }
349
350
  async setObjectReferenceValue(prop, nd) {
350
- let objectReferenceId = "";
351
351
  if (!nd) {
352
- return objectReferenceId;
352
+ return "";
353
+ }
354
+ nd = await this.applyInboundTransformMap(nd);
355
+ const ctx = await this.buildObjectReferenceContext(prop, nd);
356
+ if (!ctx) {
357
+ return "";
358
+ }
359
+ if (this.objRefCache[ctx.cacheKey]) {
360
+ if (app_framework_1.Logger.isDebugOn()) {
361
+ console.debug(`object reference cache hit: ${ctx.cacheKey}`);
362
+ }
363
+ return this.objRefCache[ctx.cacheKey];
353
364
  }
354
- if (this.transformMapFile) {
355
- const mapKey = await type_conversion_utils_1.TypeConversionUtils.getMapKeyFromObject(this.transformMapFile, this.mapFileUtil, nd, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION);
356
- nd = await map_utils_1.MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, nd, mapKey, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION);
365
+ const objectReferenceId = ctx.useIdentityService
366
+ ? await this.lookupObjectReferenceViaIdentityService(ctx, nd)
367
+ : await this.lookupObjectReferenceViaQuery(ctx);
368
+ if (objectReferenceId) {
369
+ this.objRefCache[ctx.cacheKey] = objectReferenceId;
357
370
  }
371
+ return objectReferenceId;
372
+ }
373
+ async applyInboundTransformMap(nd) {
374
+ if (!this.transformMapFile) {
375
+ return nd;
376
+ }
377
+ const mapKey = await type_conversion_utils_1.TypeConversionUtils.getMapKeyFromObject(this.transformMapFile, this.mapFileUtil, nd, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION);
378
+ return map_utils_1.MapUtil.applyTransformMap(this.transformMapFile, this.mapFileUtil, nd, mapKey, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION);
379
+ }
380
+ async buildObjectReferenceContext(prop, nd) {
358
381
  const entityType = prop['referencedTypeRootSlug'];
359
382
  const entityTypePath = prop['referencedTypePath'];
360
383
  const entityKeys = Object.keys(nd);
361
384
  const identifierKeys = await type_conversion_utils_1.TypeConversionUtils.getIdentifierPropertiesFromObject(this.transformMapFile, this.mapFileUtil, nd);
362
385
  const hasAllIdentifiers = identifierKeys.every(key => entityKeys.includes(key));
363
- if (identifierKeys.length == 0 || !hasAllIdentifiers) {
386
+ if (identifierKeys.length === 0 || !hasAllIdentifiers) {
364
387
  console.warn(`The inbound ${entityType} for prop '${prop['slug']}' doesnt have all "identifier" properties, so there is no processing. Identifier properties: ${identifierKeys}`);
365
- return objectReferenceId;
388
+ return null;
366
389
  }
367
390
  const rootType = await this.typeUtils.getByRootAndPath({ root: entityType });
368
391
  const rootTypeProps = this.typeUtils.filterTypeProperties(rootType, nd);
369
- let rootTypeCriteria = {};
370
- let typeCriteria = {};
392
+ const rootTypeCriteria = {};
393
+ const typeCriteria = {};
371
394
  identifierKeys.forEach(keyName => {
372
- const foundInObjects = rootTypeProps.some(obj => obj.slug === keyName);
373
- if (foundInObjects) {
395
+ if (rootTypeProps.some(obj => obj.slug === keyName)) {
374
396
  rootTypeCriteria[keyName] = nd[keyName];
375
397
  }
376
398
  else {
@@ -379,30 +401,41 @@ class DataConverter {
379
401
  });
380
402
  const combinedCriteria = { ...rootTypeCriteria, ...typeCriteria, typePath: entityTypePath };
381
403
  const cacheKey = Object.values(combinedCriteria).join('_');
382
- if (this.objRefCache[cacheKey]) {
383
- if (app_framework_1.Logger.isDebugOn()) {
384
- console.debug(`object reference cache hit: ${cacheKey}`);
385
- }
386
- return this.objRefCache[cacheKey];
387
- }
388
- let arrObjectReferences = await this.getAllObjectReferences(entityType, rootTypeCriteria);
389
- if (entityType !== entityTypePath) {
390
- arrObjectReferences = this.checkKeysAndValues(typeCriteria, arrObjectReferences, entityTypePath);
391
- }
392
- if (arrObjectReferences.length) {
393
- if (arrObjectReferences.length === 1) {
394
- objectReferenceId = arrObjectReferences[0].id;
395
- }
396
- else {
397
- console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
398
- }
404
+ const rolesIsTypeDiscriminator = entityType === 'item' || entityType === 'project-item';
405
+ const identityLookupKeys = rolesIsTypeDiscriminator
406
+ ? identifierKeys.filter(key => key !== 'roles')
407
+ : identifierKeys;
408
+ const useIdentityService = config_defaults_1.ConfigDefaults.isPropertyTrue(this.config?.search?.[entityType]?.useIdentityServiceForInboundData)
409
+ && identityLookupKeys.length === 1;
410
+ return { entityType, entityTypePath, rootTypeCriteria, typeCriteria, combinedCriteria, cacheKey, useIdentityService, identityLookupKeys };
411
+ }
412
+ async lookupObjectReferenceViaIdentityService(ctx, nd) {
413
+ const propertyName = ctx.identityLookupKeys[0];
414
+ const propertyValue = nd[propertyName];
415
+ const poolKey = await type_conversion_utils_1.TypeConversionUtils.getUniquenessPoolKeyFromObject(this.transformMapFile, this.mapFileUtil, nd);
416
+ const identityResults = await new sdk_1.Entities().get({
417
+ entityName: 'identity',
418
+ criteria: { poolKey, propertyName, propertyValue }
419
+ }) ?? [];
420
+ return this.assertSingleObjectReference(identityResults, ctx.combinedCriteria, (r) => r.entityReference.split(':')[1]);
421
+ }
422
+ async lookupObjectReferenceViaQuery(ctx) {
423
+ let arrObjectReferences = await this.getAllObjectReferences(ctx.entityType, ctx.rootTypeCriteria);
424
+ if (ctx.entityType !== ctx.entityTypePath) {
425
+ arrObjectReferences = this.checkKeysAndValues(ctx.typeCriteria, arrObjectReferences, ctx.entityTypePath);
399
426
  }
400
- if (!objectReferenceId) {
427
+ return this.assertSingleObjectReference(arrObjectReferences, ctx.combinedCriteria, (r) => r.id);
428
+ }
429
+ assertSingleObjectReference(results, combinedCriteria, getId) {
430
+ if (!results.length) {
401
431
  console.warn(`The passed in object reference criteria ${JSON.stringify(combinedCriteria)} didn't match any entities.`);
402
- return objectReferenceId;
432
+ return "";
403
433
  }
404
- this.objRefCache[cacheKey] = objectReferenceId;
405
- return objectReferenceId;
434
+ if (results.length > 1) {
435
+ console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
436
+ return "";
437
+ }
438
+ return getId(results[0]);
406
439
  }
407
440
  async getAllObjectReferences(entityType, rootTypeCriteria, postProcessCriteria = null) {
408
441
  const entities = new sdk_1.Entities();
@@ -454,6 +454,393 @@ describe('getObjectReferenceValue bad value', () => {
454
454
  }
455
455
  });
456
456
  });
457
+ describe('setObjectReferenceValue - identity service', () => {
458
+ const baseConfig = () => ({
459
+ apiHost: 'host',
460
+ userName: () => 'user',
461
+ password: () => 'pass',
462
+ urlContext: 'xxx',
463
+ vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
464
+ csrfEndpoint: '/servlet/rest/security/csrf',
465
+ itemPreDevelopmentLifecycleStages: ['concept']
466
+ });
467
+ const refProp = {
468
+ id: 'cJoZQvoj7dkfCBJq',
469
+ propertyType: 'object_reference',
470
+ slug: 'material',
471
+ label: 'Material',
472
+ referencedTypeRootSlug: 'item',
473
+ referencedTypePath: 'item:material'
474
+ };
475
+ let identifierSpy;
476
+ let poolKeySpy;
477
+ let getByRootAndPathSpy;
478
+ let filterTypePropertiesSpy;
479
+ const setupTypeUtilsSpies = (dc, rootPropSlugs = ['itemNumber']) => {
480
+ getByRootAndPathSpy = jest.spyOn(dc['typeUtils'], 'getByRootAndPath')
481
+ .mockImplementation(async () => ({ typePath: 'item', typeProperties: [] }));
482
+ filterTypePropertiesSpy = jest.spyOn(dc['typeUtils'], 'filterTypeProperties')
483
+ .mockImplementation(() => rootPropSlugs.map(slug => ({ slug })));
484
+ };
485
+ afterEach(() => {
486
+ identifierSpy?.mockRestore();
487
+ poolKeySpy?.mockRestore();
488
+ getByRootAndPathSpy?.mockRestore();
489
+ filterTypePropertiesSpy?.mockRestore();
490
+ mockGetFunction.mockReset();
491
+ mockGetFunction.mockImplementation((options) => ((options?.criteria?.id === 1234) ? [mockObj1234] : [mockObj2222]));
492
+ });
493
+ it('flag off - uses getAllObjectReferences path', async () => {
494
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
495
+ setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
496
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
497
+ .mockImplementation(async () => ['itemNumber', 'roles']);
498
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
499
+ .mockImplementation(async () => 'item:material');
500
+ mockGetFunction.mockClear();
501
+ mockGetFunction.mockImplementation(() => [{ id: 'abc123', typePath: 'item:material' }]);
502
+ const nd = { itemNumber: 'MAT-100', roles: ['family'] };
503
+ const result = await dc.setObjectReferenceValue(refProp, nd);
504
+ expect(mockGetFunction).toHaveBeenCalled();
505
+ const callArg = mockGetFunction.mock.calls[0][0];
506
+ expect(callArg.entityName).toEqual('item');
507
+ expect(callArg.entityName).not.toEqual('identity');
508
+ expect(result).toEqual('abc123');
509
+ expect(poolKeySpy).not.toHaveBeenCalled();
510
+ });
511
+ it('flag on, single identity match - returns parsed id', async () => {
512
+ const config = baseConfig();
513
+ config.search = { item: { useIdentityServiceForInboundData: true } };
514
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
515
+ setupTypeUtilsSpies(dc);
516
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
517
+ .mockImplementation(async () => ['itemNumber', 'roles']);
518
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
519
+ .mockImplementation(async () => 'item:material');
520
+ mockGetFunction.mockClear();
521
+ mockGetFunction.mockImplementation(() => [{ entityReference: 'item:abc123' }]);
522
+ const nd = { itemNumber: 'MAT-100', roles: ['family'] };
523
+ const result = await dc.setObjectReferenceValue(refProp, nd);
524
+ expect(mockGetFunction).toHaveBeenCalledTimes(1);
525
+ const callArg = mockGetFunction.mock.calls[0][0];
526
+ expect(callArg.entityName).toEqual('identity');
527
+ expect(callArg.criteria).toEqual({
528
+ poolKey: 'item:material',
529
+ propertyName: 'itemNumber',
530
+ propertyValue: 'MAT-100'
531
+ });
532
+ expect(result).toEqual('abc123');
533
+ });
534
+ it('flag on, no identity match - returns empty and warns', async () => {
535
+ const config = baseConfig();
536
+ config.search = { item: { useIdentityServiceForInboundData: true } };
537
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
538
+ setupTypeUtilsSpies(dc);
539
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
540
+ .mockImplementation(async () => ['itemNumber', 'roles']);
541
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
542
+ .mockImplementation(async () => 'item:material');
543
+ mockGetFunction.mockClear();
544
+ mockGetFunction.mockImplementation(() => []);
545
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
546
+ const getAllSpy = jest.spyOn(dc, 'getAllObjectReferences');
547
+ try {
548
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
549
+ expect(result).toEqual('');
550
+ expect(warnSpy).toHaveBeenCalled();
551
+ expect(getAllSpy).not.toHaveBeenCalled();
552
+ }
553
+ finally {
554
+ warnSpy.mockRestore();
555
+ getAllSpy.mockRestore();
556
+ }
557
+ });
558
+ it('flag on, multiple identity matches - returns empty and does not throw', async () => {
559
+ const config = baseConfig();
560
+ config.search = { item: { useIdentityServiceForInboundData: true } };
561
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
562
+ setupTypeUtilsSpies(dc);
563
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
564
+ .mockImplementation(async () => ['itemNumber', 'roles']);
565
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
566
+ .mockImplementation(async () => 'item:material');
567
+ mockGetFunction.mockClear();
568
+ mockGetFunction.mockImplementation(() => [
569
+ { entityReference: 'item:abc123' },
570
+ { entityReference: 'item:def456' }
571
+ ]);
572
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
573
+ try {
574
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
575
+ expect(result).toEqual('');
576
+ expect(warnSpy).toHaveBeenCalled();
577
+ }
578
+ finally {
579
+ warnSpy.mockRestore();
580
+ }
581
+ });
582
+ it('flag on, ambiguous identifier set - falls back to getAllObjectReferences', async () => {
583
+ const config = baseConfig();
584
+ config.search = { item: { useIdentityServiceForInboundData: true } };
585
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
586
+ setupTypeUtilsSpies(dc, ['itemNumber', 'season', 'roles']);
587
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
588
+ .mockImplementation(async () => ['itemNumber', 'season', 'roles']);
589
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
590
+ .mockImplementation(async () => 'item:material');
591
+ mockGetFunction.mockClear();
592
+ mockGetFunction.mockImplementation(() => [{ id: 'abc123', typePath: 'item:material' }]);
593
+ const nd = { itemNumber: 'MAT-100', season: 'SS24', roles: ['family'] };
594
+ const result = await dc.setObjectReferenceValue(refProp, nd);
595
+ expect(mockGetFunction).toHaveBeenCalled();
596
+ const callArg = mockGetFunction.mock.calls[0][0];
597
+ expect(callArg.entityName).toEqual('item');
598
+ expect(callArg.entityName).not.toEqual('identity');
599
+ expect(result).toEqual('abc123');
600
+ expect(poolKeySpy).not.toHaveBeenCalled();
601
+ });
602
+ it('flag on, cache hit - second call short-circuits', async () => {
603
+ const config = baseConfig();
604
+ config.search = { item: { useIdentityServiceForInboundData: true } };
605
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
606
+ setupTypeUtilsSpies(dc);
607
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
608
+ .mockImplementation(async () => ['itemNumber', 'roles']);
609
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
610
+ .mockImplementation(async () => 'item:material');
611
+ mockGetFunction.mockClear();
612
+ mockGetFunction.mockImplementation(() => [{ entityReference: 'item:abc123' }]);
613
+ const nd = { itemNumber: 'MAT-100', roles: ['family'] };
614
+ const r1 = await dc.setObjectReferenceValue(refProp, nd);
615
+ const r2 = await dc.setObjectReferenceValue(refProp, nd);
616
+ expect(r1).toEqual('abc123');
617
+ expect(r2).toEqual('abc123');
618
+ expect(mockGetFunction).toHaveBeenCalledTimes(1);
619
+ });
620
+ it('flag on, project-item entityType - roles is filtered, identity path used', async () => {
621
+ const projectItemProp = {
622
+ ...refProp,
623
+ referencedTypeRootSlug: 'project-item',
624
+ referencedTypePath: 'project-item'
625
+ };
626
+ const config = baseConfig();
627
+ config.search = { 'project-item': { useIdentityServiceForInboundData: true } };
628
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
629
+ setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
630
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
631
+ .mockImplementation(async () => ['itemNumber', 'roles']);
632
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
633
+ .mockImplementation(async () => 'project-item');
634
+ mockGetFunction.mockClear();
635
+ mockGetFunction.mockImplementation(() => [{ entityReference: 'project-item:pi-789' }]);
636
+ const result = await dc.setObjectReferenceValue(projectItemProp, { itemNumber: 'X1', roles: ['family'] });
637
+ expect(mockGetFunction).toHaveBeenCalledTimes(1);
638
+ const callArg = mockGetFunction.mock.calls[0][0];
639
+ expect(callArg.entityName).toEqual('identity');
640
+ expect(callArg.criteria.propertyName).toEqual('itemNumber');
641
+ expect(result).toEqual('pi-789');
642
+ });
643
+ it('flag on, non-item entityType - roles is NOT filtered, falls back to query path', async () => {
644
+ const colorProp = {
645
+ ...refProp,
646
+ referencedTypeRootSlug: 'color',
647
+ referencedTypePath: 'color'
648
+ };
649
+ const config = baseConfig();
650
+ config.search = { color: { useIdentityServiceForInboundData: true } };
651
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
652
+ setupTypeUtilsSpies(dc, ['colorNumber', 'roles']);
653
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
654
+ .mockImplementation(async () => ['colorNumber', 'roles']);
655
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
656
+ .mockImplementation(async () => 'color');
657
+ mockGetFunction.mockClear();
658
+ mockGetFunction.mockImplementation(() => [{ id: 'col-1', typePath: 'color' }]);
659
+ const result = await dc.setObjectReferenceValue(colorProp, { colorNumber: 'C1', roles: ['family'] });
660
+ expect(mockGetFunction).toHaveBeenCalled();
661
+ const callArg = mockGetFunction.mock.calls[0][0];
662
+ expect(callArg.entityName).toEqual('color');
663
+ expect(callArg.entityName).not.toEqual('identity');
664
+ expect(result).toEqual('col-1');
665
+ expect(poolKeySpy).not.toHaveBeenCalled();
666
+ });
667
+ it('query path, multiple matches - returns empty with single warn (no double warn)', async () => {
668
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
669
+ setupTypeUtilsSpies(dc, ['itemNumber']);
670
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
671
+ .mockImplementation(async () => ['itemNumber']);
672
+ mockGetFunction.mockClear();
673
+ mockGetFunction.mockImplementation(() => [
674
+ { id: 'a1', typePath: 'item:material' },
675
+ { id: 'a2', typePath: 'item:material' }
676
+ ]);
677
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
678
+ try {
679
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
680
+ expect(result).toEqual('');
681
+ expect(warnSpy).toHaveBeenCalledTimes(1);
682
+ expect(warnSpy.mock.calls[0][0]).toMatch(/duplicate records/);
683
+ }
684
+ finally {
685
+ warnSpy.mockRestore();
686
+ }
687
+ });
688
+ it('flag on, identity returns null - normalized to empty array, returns empty', async () => {
689
+ const config = baseConfig();
690
+ config.search = { item: { useIdentityServiceForInboundData: true } };
691
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
692
+ setupTypeUtilsSpies(dc);
693
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
694
+ .mockImplementation(async () => ['itemNumber', 'roles']);
695
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
696
+ .mockImplementation(async () => 'item:material');
697
+ mockGetFunction.mockClear();
698
+ mockGetFunction.mockImplementation(() => null);
699
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
700
+ try {
701
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
702
+ expect(result).toEqual('');
703
+ expect(warnSpy).toHaveBeenCalled();
704
+ }
705
+ finally {
706
+ warnSpy.mockRestore();
707
+ }
708
+ });
709
+ it('null nd - returns empty string and does not query', async () => {
710
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
711
+ mockGetFunction.mockClear();
712
+ const result = await dc.setObjectReferenceValue(refProp, null);
713
+ expect(result).toEqual('');
714
+ expect(mockGetFunction).not.toHaveBeenCalled();
715
+ });
716
+ it('zero identifier keys - warns and returns empty string', async () => {
717
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
718
+ setupTypeUtilsSpies(dc);
719
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
720
+ .mockImplementation(async () => []);
721
+ mockGetFunction.mockClear();
722
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
723
+ try {
724
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
725
+ expect(result).toEqual('');
726
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/doesnt have all "identifier" properties/));
727
+ expect(mockGetFunction).not.toHaveBeenCalled();
728
+ }
729
+ finally {
730
+ warnSpy.mockRestore();
731
+ }
732
+ });
733
+ it('missing identifier keys on nd - warns and returns empty string', async () => {
734
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
735
+ setupTypeUtilsSpies(dc);
736
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
737
+ .mockImplementation(async () => ['itemNumber', 'season']);
738
+ mockGetFunction.mockClear();
739
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
740
+ try {
741
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
742
+ expect(result).toEqual('');
743
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/doesnt have all "identifier" properties/));
744
+ expect(mockGetFunction).not.toHaveBeenCalled();
745
+ }
746
+ finally {
747
+ warnSpy.mockRestore();
748
+ }
749
+ });
750
+ it('transformMapFile set - applyInboundTransformMap is invoked before context build', async () => {
751
+ const config = baseConfig();
752
+ config['transformMapFile'] = 'file1';
753
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
754
+ setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
755
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
756
+ .mockImplementation(async () => ['itemNumber', 'roles']);
757
+ const mapKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKeyFromObject')
758
+ .mockImplementation(async () => 'LCSMaterial');
759
+ const applyMapSpy = jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap')
760
+ .mockImplementation(async (...args) => args[2]);
761
+ mockGetFunction.mockClear();
762
+ mockGetFunction.mockImplementation(() => [{ id: 'q-1', typePath: 'item:material' }]);
763
+ try {
764
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
765
+ expect(mapKeySpy).toHaveBeenCalledTimes(1);
766
+ expect(applyMapSpy).toHaveBeenCalledTimes(1);
767
+ expect(applyMapSpy.mock.calls[0][3]).toEqual('LCSMaterial');
768
+ expect(result).toEqual('q-1');
769
+ }
770
+ finally {
771
+ mapKeySpy.mockRestore();
772
+ applyMapSpy.mockRestore();
773
+ }
774
+ });
775
+ it('transformMapFile unset - applyInboundTransformMap does not invoke map utilities', async () => {
776
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
777
+ setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
778
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
779
+ .mockImplementation(async () => ['itemNumber', 'roles']);
780
+ const mapKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getMapKeyFromObject');
781
+ const applyMapSpy = jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap');
782
+ mockGetFunction.mockClear();
783
+ mockGetFunction.mockImplementation(() => [{ id: 'q-2', typePath: 'item:material' }]);
784
+ try {
785
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
786
+ expect(mapKeySpy).not.toHaveBeenCalled();
787
+ expect(applyMapSpy).not.toHaveBeenCalled();
788
+ expect(result).toEqual('q-2');
789
+ }
790
+ finally {
791
+ mapKeySpy.mockRestore();
792
+ applyMapSpy.mockRestore();
793
+ }
794
+ });
795
+ it('query path success - writes id to cache (second call short-circuits)', async () => {
796
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
797
+ setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
798
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
799
+ .mockImplementation(async () => ['itemNumber', 'roles']);
800
+ mockGetFunction.mockClear();
801
+ mockGetFunction.mockImplementation(() => [{ id: 'cached-q', typePath: 'item:material' }]);
802
+ const nd = { itemNumber: 'MAT-100', roles: ['family'] };
803
+ const r1 = await dc.setObjectReferenceValue(refProp, nd);
804
+ const callsAfterFirst = mockGetFunction.mock.calls.length;
805
+ const r2 = await dc.setObjectReferenceValue(refProp, nd);
806
+ expect(r1).toEqual('cached-q');
807
+ expect(r2).toEqual('cached-q');
808
+ expect(mockGetFunction.mock.calls.length).toEqual(callsAfterFirst);
809
+ });
810
+ it('query path with subtype filter - applies checkKeysAndValues when entityType !== entityTypePath', async () => {
811
+ const dc = new data_converter_1.DataConverter(baseConfig(), new transform_data_1.MapFileUtil(new sdk_1.Entities()));
812
+ setupTypeUtilsSpies(dc, ['itemNumber']);
813
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
814
+ .mockImplementation(async () => ['itemNumber', 'season']);
815
+ const checkSpy = jest.spyOn(dc, 'checkKeysAndValues')
816
+ .mockImplementation((_criteria, arr) => arr);
817
+ mockGetFunction.mockClear();
818
+ mockGetFunction.mockImplementation(() => [{ id: 'sub-1', typePath: 'item:material' }]);
819
+ try {
820
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', season: 'SS24' });
821
+ expect(checkSpy).toHaveBeenCalledTimes(1);
822
+ expect(checkSpy.mock.calls[0][2]).toEqual('item:material');
823
+ expect(result).toEqual('sub-1');
824
+ }
825
+ finally {
826
+ checkSpy.mockRestore();
827
+ }
828
+ });
829
+ it('assertSingleObjectReference single result - returns id via callback', async () => {
830
+ const config = baseConfig();
831
+ config.search = { item: { useIdentityServiceForInboundData: true } };
832
+ const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
833
+ setupTypeUtilsSpies(dc);
834
+ identifierSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierPropertiesFromObject')
835
+ .mockImplementation(async () => ['itemNumber', 'roles']);
836
+ poolKeySpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
837
+ .mockImplementation(async () => 'item:material');
838
+ mockGetFunction.mockClear();
839
+ mockGetFunction.mockImplementation(() => [{ entityReference: 'item:parsed-id-from-ref' }]);
840
+ const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
841
+ expect(result).toEqual('parsed-id-from-ref');
842
+ });
843
+ });
457
844
  describe('getObjectReferenceValue - use mapping', () => {
458
845
  const maps = require('./data-converter-spec-mockData');
459
846
  const mapping = maps['mapping'];
@@ -2,7 +2,10 @@ export declare class TypeDefaults {
2
2
  static NO_ENTITY_TYPE: string;
3
3
  static NO_OBJECT_CLASS: string;
4
4
  static NO_TYPE_PATH: string;
5
+ static processLCSMaterialAsItem: boolean;
5
6
  constructor();
7
+ static applyConfig(config: any): void;
8
+ static isPropertyTrue(value: any): boolean;
6
9
  static getDefaultObjectClass(entity: any): string;
7
10
  static getDefaultObjectTypePath(entity: any): string;
8
11
  static getDefaultIdentifierProperties(entity: any): string[];