@contrail/flexplm 1.5.0-alpha.6d9ffc4 → 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 +1 -0
- package/lib/util/data-converter.d.ts +5 -0
- package/lib/util/data-converter.js +65 -32
- package/lib/util/data-converter.spec.js +387 -0
- package/lib/util/type-defaults.d.ts +1 -0
- package/lib/util/type-defaults.js +4 -3
- package/package.json +1 -1
- package/src/util/data-converter.spec.ts +457 -0
- package/src/util/data-converter.ts +87 -36
- package/src/util/type-defaults.ts +7 -3
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
|
|
@@ -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
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
392
|
+
const rootTypeCriteria = {};
|
|
393
|
+
const typeCriteria = {};
|
|
371
394
|
identifierKeys.forEach(keyName => {
|
|
372
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
|
432
|
+
return "";
|
|
403
433
|
}
|
|
404
|
-
|
|
405
|
-
|
|
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'];
|
|
@@ -5,6 +5,7 @@ export declare class TypeDefaults {
|
|
|
5
5
|
static processLCSMaterialAsItem: boolean;
|
|
6
6
|
constructor();
|
|
7
7
|
static applyConfig(config: any): void;
|
|
8
|
+
static isPropertyTrue(value: any): boolean;
|
|
8
9
|
static getDefaultObjectClass(entity: any): string;
|
|
9
10
|
static getDefaultObjectTypePath(entity: any): string;
|
|
10
11
|
static getDefaultIdentifierProperties(entity: any): string[];
|
|
@@ -5,9 +5,10 @@ class TypeDefaults {
|
|
|
5
5
|
constructor() {
|
|
6
6
|
}
|
|
7
7
|
static applyConfig(config) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
TypeDefaults.processLCSMaterialAsItem = TypeDefaults.isPropertyTrue(config?.LCSMaterial?.processAsItem);
|
|
9
|
+
}
|
|
10
|
+
static isPropertyTrue(value) {
|
|
11
|
+
return value === true || (typeof value === 'string' && value.toLowerCase() === 'true');
|
|
11
12
|
}
|
|
12
13
|
static getDefaultObjectClass(entity) {
|
|
13
14
|
const entityType = this.getEntityType(entity);
|
package/package.json
CHANGED
|
@@ -509,6 +509,463 @@ describe('getObjectReferenceValue bad value', () =>{
|
|
|
509
509
|
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
describe('setObjectReferenceValue - identity service', () => {
|
|
513
|
+
const baseConfig = (): FCConfig => ({
|
|
514
|
+
apiHost: 'host',
|
|
515
|
+
userName: () => 'user',
|
|
516
|
+
password: () => 'pass',
|
|
517
|
+
urlContext: 'xxx',
|
|
518
|
+
vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
|
|
519
|
+
csrfEndpoint: '/servlet/rest/security/csrf',
|
|
520
|
+
itemPreDevelopmentLifecycleStages: ['concept']
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const refProp = {
|
|
524
|
+
id: 'cJoZQvoj7dkfCBJq',
|
|
525
|
+
propertyType: 'object_reference',
|
|
526
|
+
slug: 'material',
|
|
527
|
+
label: 'Material',
|
|
528
|
+
referencedTypeRootSlug: 'item',
|
|
529
|
+
referencedTypePath: 'item:material'
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
let identifierSpy: jest.SpyInstance;
|
|
533
|
+
let poolKeySpy: jest.SpyInstance;
|
|
534
|
+
let getByRootAndPathSpy: jest.SpyInstance;
|
|
535
|
+
let filterTypePropertiesSpy: jest.SpyInstance;
|
|
536
|
+
|
|
537
|
+
const setupTypeUtilsSpies = (dc: DataConverter, rootPropSlugs: string[] = ['itemNumber']) => {
|
|
538
|
+
getByRootAndPathSpy = jest.spyOn(dc['typeUtils'], 'getByRootAndPath')
|
|
539
|
+
.mockImplementation(async () => ({ typePath: 'item', typeProperties: [] }) as any);
|
|
540
|
+
filterTypePropertiesSpy = jest.spyOn(dc['typeUtils'], 'filterTypeProperties')
|
|
541
|
+
.mockImplementation(() => rootPropSlugs.map(slug => ({ slug })) as any);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
afterEach(() => {
|
|
545
|
+
identifierSpy?.mockRestore();
|
|
546
|
+
poolKeySpy?.mockRestore();
|
|
547
|
+
getByRootAndPathSpy?.mockRestore();
|
|
548
|
+
filterTypePropertiesSpy?.mockRestore();
|
|
549
|
+
mockGetFunction.mockReset();
|
|
550
|
+
mockGetFunction.mockImplementation((options) =>
|
|
551
|
+
((options?.criteria?.id === 1234) ? [mockObj1234] : [mockObj2222]) as any
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('flag off - uses getAllObjectReferences path', async () => {
|
|
556
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
557
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
|
|
558
|
+
|
|
559
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
560
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
561
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
562
|
+
.mockImplementation(async () => 'item:material');
|
|
563
|
+
|
|
564
|
+
mockGetFunction.mockClear();
|
|
565
|
+
mockGetFunction.mockImplementation(() => [{ id: 'abc123', typePath: 'item:material' }] as any);
|
|
566
|
+
|
|
567
|
+
const nd = { itemNumber: 'MAT-100', roles: ['family'] };
|
|
568
|
+
const result = await dc.setObjectReferenceValue(refProp, nd);
|
|
569
|
+
|
|
570
|
+
expect(mockGetFunction).toHaveBeenCalled();
|
|
571
|
+
const callArg = mockGetFunction.mock.calls[0][0];
|
|
572
|
+
expect(callArg.entityName).toEqual('item');
|
|
573
|
+
expect(callArg.entityName).not.toEqual('identity');
|
|
574
|
+
expect(result).toEqual('abc123');
|
|
575
|
+
expect(poolKeySpy).not.toHaveBeenCalled();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('flag on, single identity match - returns parsed id', async () => {
|
|
579
|
+
const config = baseConfig();
|
|
580
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
581
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
582
|
+
setupTypeUtilsSpies(dc);
|
|
583
|
+
|
|
584
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
585
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
586
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
587
|
+
.mockImplementation(async () => 'item:material');
|
|
588
|
+
|
|
589
|
+
mockGetFunction.mockClear();
|
|
590
|
+
mockGetFunction.mockImplementation(() => [{ entityReference: 'item:abc123' }] as any);
|
|
591
|
+
|
|
592
|
+
const nd = { itemNumber: 'MAT-100', roles: ['family'] };
|
|
593
|
+
const result = await dc.setObjectReferenceValue(refProp, nd);
|
|
594
|
+
|
|
595
|
+
expect(mockGetFunction).toHaveBeenCalledTimes(1);
|
|
596
|
+
const callArg = mockGetFunction.mock.calls[0][0];
|
|
597
|
+
expect(callArg.entityName).toEqual('identity');
|
|
598
|
+
expect(callArg.criteria).toEqual({
|
|
599
|
+
poolKey: 'item:material',
|
|
600
|
+
propertyName: 'itemNumber',
|
|
601
|
+
propertyValue: 'MAT-100'
|
|
602
|
+
});
|
|
603
|
+
expect(result).toEqual('abc123');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('flag on, no identity match - returns empty and warns', async () => {
|
|
607
|
+
const config = baseConfig();
|
|
608
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
609
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
610
|
+
setupTypeUtilsSpies(dc);
|
|
611
|
+
|
|
612
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
613
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
614
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
615
|
+
.mockImplementation(async () => 'item:material');
|
|
616
|
+
|
|
617
|
+
mockGetFunction.mockClear();
|
|
618
|
+
mockGetFunction.mockImplementation(() => [] as any);
|
|
619
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
620
|
+
const getAllSpy = jest.spyOn(dc, 'getAllObjectReferences');
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
624
|
+
expect(result).toEqual('');
|
|
625
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
626
|
+
expect(getAllSpy).not.toHaveBeenCalled();
|
|
627
|
+
} finally {
|
|
628
|
+
warnSpy.mockRestore();
|
|
629
|
+
getAllSpy.mockRestore();
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('flag on, multiple identity matches - returns empty and does not throw', async () => {
|
|
634
|
+
const config = baseConfig();
|
|
635
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
636
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
637
|
+
setupTypeUtilsSpies(dc);
|
|
638
|
+
|
|
639
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
640
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
641
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
642
|
+
.mockImplementation(async () => 'item:material');
|
|
643
|
+
|
|
644
|
+
mockGetFunction.mockClear();
|
|
645
|
+
mockGetFunction.mockImplementation(() => [
|
|
646
|
+
{ entityReference: 'item:abc123' },
|
|
647
|
+
{ entityReference: 'item:def456' }
|
|
648
|
+
] as any);
|
|
649
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
653
|
+
expect(result).toEqual('');
|
|
654
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
655
|
+
} finally {
|
|
656
|
+
warnSpy.mockRestore();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('flag on, ambiguous identifier set - falls back to getAllObjectReferences', async () => {
|
|
661
|
+
const config = baseConfig();
|
|
662
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
663
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
664
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'season', 'roles']);
|
|
665
|
+
|
|
666
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
667
|
+
.mockImplementation(async () => ['itemNumber', 'season', 'roles']);
|
|
668
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
669
|
+
.mockImplementation(async () => 'item:material');
|
|
670
|
+
|
|
671
|
+
mockGetFunction.mockClear();
|
|
672
|
+
mockGetFunction.mockImplementation(() => [{ id: 'abc123', typePath: 'item:material' }] as any);
|
|
673
|
+
|
|
674
|
+
const nd = { itemNumber: 'MAT-100', season: 'SS24', roles: ['family'] };
|
|
675
|
+
const result = await dc.setObjectReferenceValue(refProp, nd);
|
|
676
|
+
|
|
677
|
+
expect(mockGetFunction).toHaveBeenCalled();
|
|
678
|
+
const callArg = mockGetFunction.mock.calls[0][0];
|
|
679
|
+
expect(callArg.entityName).toEqual('item');
|
|
680
|
+
expect(callArg.entityName).not.toEqual('identity');
|
|
681
|
+
expect(result).toEqual('abc123');
|
|
682
|
+
expect(poolKeySpy).not.toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('flag on, cache hit - second call short-circuits', async () => {
|
|
686
|
+
const config = baseConfig();
|
|
687
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
688
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
689
|
+
setupTypeUtilsSpies(dc);
|
|
690
|
+
|
|
691
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
692
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
693
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
694
|
+
.mockImplementation(async () => 'item:material');
|
|
695
|
+
|
|
696
|
+
mockGetFunction.mockClear();
|
|
697
|
+
mockGetFunction.mockImplementation(() => [{ entityReference: 'item:abc123' }] as any);
|
|
698
|
+
|
|
699
|
+
const nd = { itemNumber: 'MAT-100', roles: ['family'] };
|
|
700
|
+
const r1 = await dc.setObjectReferenceValue(refProp, nd);
|
|
701
|
+
const r2 = await dc.setObjectReferenceValue(refProp, nd);
|
|
702
|
+
|
|
703
|
+
expect(r1).toEqual('abc123');
|
|
704
|
+
expect(r2).toEqual('abc123');
|
|
705
|
+
expect(mockGetFunction).toHaveBeenCalledTimes(1);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('flag on, project-item entityType - roles is filtered, identity path used', async () => {
|
|
709
|
+
const projectItemProp = {
|
|
710
|
+
...refProp,
|
|
711
|
+
referencedTypeRootSlug: 'project-item',
|
|
712
|
+
referencedTypePath: 'project-item'
|
|
713
|
+
};
|
|
714
|
+
const config = baseConfig();
|
|
715
|
+
config.search = { 'project-item': { useIdentityServiceForInboundData: true } };
|
|
716
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
717
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
|
|
718
|
+
|
|
719
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
720
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
721
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
722
|
+
.mockImplementation(async () => 'project-item');
|
|
723
|
+
|
|
724
|
+
mockGetFunction.mockClear();
|
|
725
|
+
mockGetFunction.mockImplementation(() => [{ entityReference: 'project-item:pi-789' }] as any);
|
|
726
|
+
|
|
727
|
+
const result = await dc.setObjectReferenceValue(projectItemProp, { itemNumber: 'X1', roles: ['family'] });
|
|
728
|
+
|
|
729
|
+
expect(mockGetFunction).toHaveBeenCalledTimes(1);
|
|
730
|
+
const callArg = mockGetFunction.mock.calls[0][0];
|
|
731
|
+
expect(callArg.entityName).toEqual('identity');
|
|
732
|
+
expect(callArg.criteria.propertyName).toEqual('itemNumber');
|
|
733
|
+
expect(result).toEqual('pi-789');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('flag on, non-item entityType - roles is NOT filtered, falls back to query path', async () => {
|
|
737
|
+
const colorProp = {
|
|
738
|
+
...refProp,
|
|
739
|
+
referencedTypeRootSlug: 'color',
|
|
740
|
+
referencedTypePath: 'color'
|
|
741
|
+
};
|
|
742
|
+
const config = baseConfig();
|
|
743
|
+
config.search = { color: { useIdentityServiceForInboundData: true } };
|
|
744
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
745
|
+
setupTypeUtilsSpies(dc, ['colorNumber', 'roles']);
|
|
746
|
+
|
|
747
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
748
|
+
.mockImplementation(async () => ['colorNumber', 'roles']);
|
|
749
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
750
|
+
.mockImplementation(async () => 'color');
|
|
751
|
+
|
|
752
|
+
mockGetFunction.mockClear();
|
|
753
|
+
mockGetFunction.mockImplementation(() => [{ id: 'col-1', typePath: 'color' }] as any);
|
|
754
|
+
|
|
755
|
+
const result = await dc.setObjectReferenceValue(colorProp, { colorNumber: 'C1', roles: ['family'] });
|
|
756
|
+
|
|
757
|
+
expect(mockGetFunction).toHaveBeenCalled();
|
|
758
|
+
const callArg = mockGetFunction.mock.calls[0][0];
|
|
759
|
+
expect(callArg.entityName).toEqual('color');
|
|
760
|
+
expect(callArg.entityName).not.toEqual('identity');
|
|
761
|
+
expect(result).toEqual('col-1');
|
|
762
|
+
expect(poolKeySpy).not.toHaveBeenCalled();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('query path, multiple matches - returns empty with single warn (no double warn)', async () => {
|
|
766
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
767
|
+
setupTypeUtilsSpies(dc, ['itemNumber']);
|
|
768
|
+
|
|
769
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
770
|
+
.mockImplementation(async () => ['itemNumber']);
|
|
771
|
+
|
|
772
|
+
mockGetFunction.mockClear();
|
|
773
|
+
mockGetFunction.mockImplementation(() => [
|
|
774
|
+
{ id: 'a1', typePath: 'item:material' },
|
|
775
|
+
{ id: 'a2', typePath: 'item:material' }
|
|
776
|
+
] as any);
|
|
777
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
|
|
781
|
+
expect(result).toEqual('');
|
|
782
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
783
|
+
expect(warnSpy.mock.calls[0][0]).toMatch(/duplicate records/);
|
|
784
|
+
} finally {
|
|
785
|
+
warnSpy.mockRestore();
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('flag on, identity returns null - normalized to empty array, returns empty', async () => {
|
|
790
|
+
const config = baseConfig();
|
|
791
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
792
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
793
|
+
setupTypeUtilsSpies(dc);
|
|
794
|
+
|
|
795
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
796
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
797
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
798
|
+
.mockImplementation(async () => 'item:material');
|
|
799
|
+
|
|
800
|
+
mockGetFunction.mockClear();
|
|
801
|
+
mockGetFunction.mockImplementation(() => null as any);
|
|
802
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
806
|
+
expect(result).toEqual('');
|
|
807
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
808
|
+
} finally {
|
|
809
|
+
warnSpy.mockRestore();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('null nd - returns empty string and does not query', async () => {
|
|
814
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
815
|
+
mockGetFunction.mockClear();
|
|
816
|
+
const result = await dc.setObjectReferenceValue(refProp, null);
|
|
817
|
+
expect(result).toEqual('');
|
|
818
|
+
expect(mockGetFunction).not.toHaveBeenCalled();
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('zero identifier keys - warns and returns empty string', async () => {
|
|
822
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
823
|
+
setupTypeUtilsSpies(dc);
|
|
824
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
825
|
+
.mockImplementation(async () => []);
|
|
826
|
+
mockGetFunction.mockClear();
|
|
827
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
|
|
831
|
+
expect(result).toEqual('');
|
|
832
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/doesnt have all "identifier" properties/));
|
|
833
|
+
expect(mockGetFunction).not.toHaveBeenCalled();
|
|
834
|
+
} finally {
|
|
835
|
+
warnSpy.mockRestore();
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('missing identifier keys on nd - warns and returns empty string', async () => {
|
|
840
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
841
|
+
setupTypeUtilsSpies(dc);
|
|
842
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
843
|
+
.mockImplementation(async () => ['itemNumber', 'season']);
|
|
844
|
+
mockGetFunction.mockClear();
|
|
845
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100' });
|
|
849
|
+
expect(result).toEqual('');
|
|
850
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/doesnt have all "identifier" properties/));
|
|
851
|
+
expect(mockGetFunction).not.toHaveBeenCalled();
|
|
852
|
+
} finally {
|
|
853
|
+
warnSpy.mockRestore();
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('transformMapFile set - applyInboundTransformMap is invoked before context build', async () => {
|
|
858
|
+
const config = baseConfig();
|
|
859
|
+
config['transformMapFile'] = 'file1';
|
|
860
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
861
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
|
|
862
|
+
|
|
863
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
864
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
865
|
+
const mapKeySpy = jest.spyOn(TypeConversionUtils, 'getMapKeyFromObject')
|
|
866
|
+
.mockImplementation(async () => 'LCSMaterial');
|
|
867
|
+
const applyMapSpy = jest.spyOn(MapUtil, 'applyTransformMap')
|
|
868
|
+
.mockImplementation(async (...args) => args[2]);
|
|
869
|
+
|
|
870
|
+
mockGetFunction.mockClear();
|
|
871
|
+
mockGetFunction.mockImplementation(() => [{ id: 'q-1', typePath: 'item:material' }] as any);
|
|
872
|
+
|
|
873
|
+
try {
|
|
874
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
875
|
+
expect(mapKeySpy).toHaveBeenCalledTimes(1);
|
|
876
|
+
expect(applyMapSpy).toHaveBeenCalledTimes(1);
|
|
877
|
+
expect(applyMapSpy.mock.calls[0][3]).toEqual('LCSMaterial');
|
|
878
|
+
expect(result).toEqual('q-1');
|
|
879
|
+
} finally {
|
|
880
|
+
mapKeySpy.mockRestore();
|
|
881
|
+
applyMapSpy.mockRestore();
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('transformMapFile unset - applyInboundTransformMap does not invoke map utilities', async () => {
|
|
886
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
887
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
|
|
888
|
+
|
|
889
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
890
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
891
|
+
const mapKeySpy = jest.spyOn(TypeConversionUtils, 'getMapKeyFromObject');
|
|
892
|
+
const applyMapSpy = jest.spyOn(MapUtil, 'applyTransformMap');
|
|
893
|
+
|
|
894
|
+
mockGetFunction.mockClear();
|
|
895
|
+
mockGetFunction.mockImplementation(() => [{ id: 'q-2', typePath: 'item:material' }] as any);
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
899
|
+
expect(mapKeySpy).not.toHaveBeenCalled();
|
|
900
|
+
expect(applyMapSpy).not.toHaveBeenCalled();
|
|
901
|
+
expect(result).toEqual('q-2');
|
|
902
|
+
} finally {
|
|
903
|
+
mapKeySpy.mockRestore();
|
|
904
|
+
applyMapSpy.mockRestore();
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('query path success - writes id to cache (second call short-circuits)', async () => {
|
|
909
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
910
|
+
setupTypeUtilsSpies(dc, ['itemNumber', 'roles']);
|
|
911
|
+
|
|
912
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
913
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
914
|
+
|
|
915
|
+
mockGetFunction.mockClear();
|
|
916
|
+
mockGetFunction.mockImplementation(() => [{ id: 'cached-q', typePath: 'item:material' }] as any);
|
|
917
|
+
|
|
918
|
+
const nd = { itemNumber: 'MAT-100', roles: ['family'] };
|
|
919
|
+
const r1 = await dc.setObjectReferenceValue(refProp, nd);
|
|
920
|
+
const callsAfterFirst = mockGetFunction.mock.calls.length;
|
|
921
|
+
const r2 = await dc.setObjectReferenceValue(refProp, nd);
|
|
922
|
+
|
|
923
|
+
expect(r1).toEqual('cached-q');
|
|
924
|
+
expect(r2).toEqual('cached-q');
|
|
925
|
+
expect(mockGetFunction.mock.calls.length).toEqual(callsAfterFirst);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('query path with subtype filter - applies checkKeysAndValues when entityType !== entityTypePath', async () => {
|
|
929
|
+
const dc = new DataConverter(baseConfig(), new MapFileUtil(new Entities()));
|
|
930
|
+
setupTypeUtilsSpies(dc, ['itemNumber']);
|
|
931
|
+
|
|
932
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
933
|
+
.mockImplementation(async () => ['itemNumber', 'season']);
|
|
934
|
+
const checkSpy = jest.spyOn(dc, 'checkKeysAndValues')
|
|
935
|
+
.mockImplementation((_criteria, arr) => arr);
|
|
936
|
+
|
|
937
|
+
mockGetFunction.mockClear();
|
|
938
|
+
mockGetFunction.mockImplementation(() => [{ id: 'sub-1', typePath: 'item:material' }] as any);
|
|
939
|
+
|
|
940
|
+
try {
|
|
941
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', season: 'SS24' });
|
|
942
|
+
expect(checkSpy).toHaveBeenCalledTimes(1);
|
|
943
|
+
expect(checkSpy.mock.calls[0][2]).toEqual('item:material');
|
|
944
|
+
expect(result).toEqual('sub-1');
|
|
945
|
+
} finally {
|
|
946
|
+
checkSpy.mockRestore();
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('assertSingleObjectReference single result - returns id via callback', async () => {
|
|
951
|
+
const config = baseConfig();
|
|
952
|
+
config.search = { item: { useIdentityServiceForInboundData: true } };
|
|
953
|
+
const dc = new DataConverter(config, new MapFileUtil(new Entities()));
|
|
954
|
+
setupTypeUtilsSpies(dc);
|
|
955
|
+
|
|
956
|
+
identifierSpy = jest.spyOn(TypeConversionUtils, 'getIdentifierPropertiesFromObject')
|
|
957
|
+
.mockImplementation(async () => ['itemNumber', 'roles']);
|
|
958
|
+
poolKeySpy = jest.spyOn(TypeConversionUtils, 'getUniquenessPoolKeyFromObject')
|
|
959
|
+
.mockImplementation(async () => 'item:material');
|
|
960
|
+
|
|
961
|
+
mockGetFunction.mockClear();
|
|
962
|
+
mockGetFunction.mockImplementation(() => [{ entityReference: 'item:parsed-id-from-ref' }] as any);
|
|
963
|
+
|
|
964
|
+
const result = await dc.setObjectReferenceValue(refProp, { itemNumber: 'MAT-100', roles: ['family'] });
|
|
965
|
+
expect(result).toEqual('parsed-id-from-ref');
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
512
969
|
describe('getObjectReferenceValue - use mapping', () => {
|
|
513
970
|
|
|
514
971
|
const maps = require('./data-converter-spec-mockData');
|
|
@@ -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;
|
|
@@ -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
|
-
|
|
430
|
-
|
|
431
|
-
return objectReferenceId;
|
|
441
|
+
if (!nd) {
|
|
442
|
+
return "";
|
|
432
443
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
452
|
-
|
|
491
|
+
const rootTypeCriteria: Record<string, any> = {};
|
|
492
|
+
const typeCriteria: Record<string, any> = {};
|
|
453
493
|
identifierKeys.forEach(keyName => {
|
|
454
|
-
|
|
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,47 @@ 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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
518
|
+
const identityResults = await new Entities().get({
|
|
519
|
+
entityName: 'identity',
|
|
520
|
+
criteria: { poolKey, propertyName, propertyValue }
|
|
521
|
+
}) ?? [];
|
|
476
522
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
523
|
+
return this.assertSingleObjectReference(identityResults, ctx.combinedCriteria, (r) => r.entityReference.split(':')[1]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private async lookupObjectReferenceViaQuery(ctx: ObjectReferenceContext): Promise<string> {
|
|
527
|
+
let arrObjectReferences = await this.getAllObjectReferences(ctx.entityType, ctx.rootTypeCriteria);
|
|
528
|
+
if (ctx.entityType !== ctx.entityTypePath) {
|
|
529
|
+
arrObjectReferences = this.checkKeysAndValues(ctx.typeCriteria, arrObjectReferences, ctx.entityTypePath);
|
|
483
530
|
}
|
|
531
|
+
return this.assertSingleObjectReference(arrObjectReferences, ctx.combinedCriteria, (r) => r.id);
|
|
532
|
+
}
|
|
484
533
|
|
|
485
|
-
|
|
534
|
+
private assertSingleObjectReference(results: any[], combinedCriteria: any, getId: (r: any) => string): string {
|
|
535
|
+
if (!results.length) {
|
|
486
536
|
console.warn(`The passed in object reference criteria ${JSON.stringify(combinedCriteria)} didn't match any entities.`);
|
|
487
|
-
return
|
|
537
|
+
return "";
|
|
488
538
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
539
|
+
if (results.length > 1) {
|
|
540
|
+
console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
543
|
+
return getId(results[0]);
|
|
493
544
|
}
|
|
494
545
|
|
|
495
546
|
/**
|
|
@@ -15,9 +15,11 @@ export class TypeDefaults {
|
|
|
15
15
|
* through multiple layers of function calls.
|
|
16
16
|
*/
|
|
17
17
|
static applyConfig(config: any): void {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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');
|
|
21
23
|
}
|
|
22
24
|
/**Takes in full entity and returs the default FlexPLM
|
|
23
25
|
* object class.
|
|
@@ -189,6 +191,7 @@ export class TypeDefaults {
|
|
|
189
191
|
const customEntityClasses = TypeDefaults.processLCSMaterialAsItem
|
|
190
192
|
? ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast']
|
|
191
193
|
: ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', 'LCSMaterial'];
|
|
194
|
+
|
|
192
195
|
if(itemClasses.includes(objectClass)){
|
|
193
196
|
entityClass = 'item';
|
|
194
197
|
}else if(['LCSProductSeasonLink', 'LCSSKUSeasonLink'].includes(objectClass)){
|
|
@@ -300,6 +303,7 @@ export class TypeDefaults {
|
|
|
300
303
|
const itemClasses = TypeDefaults.processLCSMaterialAsItem
|
|
301
304
|
? ['LCSProduct', 'LCSMaterial']
|
|
302
305
|
: ['LCSProduct'];
|
|
306
|
+
|
|
303
307
|
if (itemClasses.includes(objectClass)) {
|
|
304
308
|
properties.push('name');
|
|
305
309
|
} else if ('LCSSKU' === objectClass) {
|