@contrail/flexplm 1.7.8 → 1.8.0-alpha.562c8dd

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/README.md CHANGED
@@ -1 +1,19 @@
1
- Core model for contrail, which establishes the base peristence services (without dependency on a persistence implementaiton) and core NestJS elements such as the InvocationContext.
1
+ Core model for contrail, which establishes the base peristence services (without dependency on a persistence implementaiton) and core NestJS elements such as the InvocationContext.
2
+
3
+ ## LCSCountry defaults
4
+ `LCSCountry` <-> VibeIQ `custom-entity:country` works with built-in defaults for the type mapping. The built-in conventions are:
5
+
6
+ | Item | Default |
7
+ |---|---|
8
+ | VibeIQ type path | `custom-entity:country` (case-insensitive) |
9
+ | FlexPLM class / type path | `LCSCountry` / `Country` |
10
+ | Identifier property | `vibeIQIdentifier` (same key in both systems; no rekey runs without a mapping) |
11
+ | Informational property | `name` |
12
+
13
+ A mapping section for `LCSCountry` overrides any of these. The mapping section must also declare which system creates Countries, with the existing creation gates (there is no default; if it is not declared, the app treats Country as read-only):
14
+
15
+ ```ts
16
+ LCSCountry: { isInboundCreatable: () => true, isOutboundCreatable: () => false }, // FlexPLM creates
17
+ LCSCountry: { isInboundCreatable: () => false, isOutboundCreatable: () => true }, // VibeIQ creates
18
+ ```
19
+ Read the declared origin with `TypeConversionUtils.getOriginSystem(...)`.
@@ -340,6 +340,13 @@ export interface MappingSection {
340
340
  * update existing entities.
341
341
  * - Conditional — inspect the row and allow / disallow per record.
342
342
  *
343
+ * On the **`LCSCountry`** section this pair declares which system creates
344
+ * Countries: `isInboundCreatable: () => true` together with
345
+ * `isOutboundCreatable: () => false` means FlexPLM creates them (the
346
+ * reverse pair means VibeIQ creates them). Both functions must be present
347
+ * and opposite; otherwise the origin is undeclared and the connector treats
348
+ * Country as read-only. See `TypeConversionUtils.getOriginSystem`.
349
+ *
343
350
  * On the **season-link** sections (`LCSProductSeasonLink` /
344
351
  * `LCSSKUSeasonLink`), `() => true` enables the add-to-project
345
352
  * flow: FlexPLM-driven inbound events can add an item to a Vibe
@@ -707,6 +714,7 @@ interface MappingFileBase {
707
714
  * | `item` | `LCSProduct` / `LCSSKU` | Syncs once `lifecycleStage` is out of `concept`. Sends Primary Viewable as the thumbnail (no other images). Creates `LCSProduct` / `LCSSKU` if missing; auto-retries `LCSSKU` creation when the parent `LCSProduct` isn't ready yet. |
708
715
  * | `color` | `LCSColor` | Syncs on create / update. Sends Primary Viewable as the thumbnail. Creates `LCSColor` if missing. |
709
716
  * | `custom-entity` | `LCSRevisableEntity` | Syncs on create / update. Creates `LCSRevisableEntity` if missing. Workflows should gate on the custom-entity subtype so only the intended subtypes publish. |
717
+ * | `custom-entity` (`custom-entity:country`) | `LCSCountry` | Built-in defaults for type path (`Country`), identifier (`vibeIQIdentifier`, same key in both systems) and name; the mapping section overrides each default and **must** declare the origin. Which system creates Countries is declared in the `LCSCountry` mapping section with the `isInboundCreatable` / `isOutboundCreatable` pair: `(true, false)` = FlexPLM creates, `(false, true)` = VibeIQ creates; anything else is undeclared. See `TypeConversionUtils.getOriginSystem`. |
710
718
  * | `project-item` | `LCSProductSeasonLink` / `LCSSKUSeasonLink` | Syncs on plan publish. Requires the assortment to have `publishToFlexPLM=true` and `flexPLMSeasonName` set. Adds the Product / SKU to the `LCSSeason` if not already linked. Only project-items changed since the last publish are sent (configurable to reach further back). Processing on the FlexPLM side is async via a Windchill Queue. |
711
719
  * | `item:material` | `LCSMaterial` | Supported via custom mapping section + `typeConversion`. Route `item:material` typePath to a mapping section that declares `getClass: () => 'LCSMaterial'` and `getSoftType: () => 'Material'`. See {@link TypeConversion} for the `LCSMaterial.processAsItem` config flag that pairs with this. |
712
720
  * | `custom-entity` | `LCSLast` / `LCSLifecycleManaged` | **Not yet supported.** |
@@ -724,6 +732,7 @@ interface MappingFileBase {
724
732
  * | `LCSLifecycleManaged` | `custom-entity` | Configurable to create new custom-entities; updates existing. |
725
733
  * | `LCSMaterial` | `custom-entity` (default) or `item:material` | Default: custom-entity. To route to `item:material` instead, set `{ "LCSMaterial": { "processAsItem": true } }` in the connector app config and write a mapping section with `getClass: () => 'item'` + `getSoftType: () => 'item:material'`. Wire it via `flex2vibe.LCSMaterial.getMapKey` returning that section's name. |
726
734
  * | `LCSRevisableEntity` | `custom-entity` | Configurable to create new custom-entities; updates existing. |
735
+ * | `LCSCountry` | `custom-entity` (`custom-entity:country`) | Built-in defaults for type path and identifier; the mapping section must declare the origin. Identifier `vibeIQIdentifier` (same key in both systems). Creation is allowed only when the `LCSCountry` section declares FlexPLM as the origin: `isInboundCreatable: () => true` and `isOutboundCreatable: () => false`. |
727
736
  * | (any) | `item` (direct, no link) | **Not yet supported.** |
728
737
  *
729
738
  * @example
@@ -1,4 +1,6 @@
1
1
  import { MapFileUtil } from '@contrail/transform-data';
2
+ /** The system that originates (creates) an entity type: 'flexplm' or 'vibeiq'. */
3
+ export type OriginSystem = 'flexplm' | 'vibeiq';
2
4
  /** This class is helper functions to get the data for converting
3
5
  * VibeIQ entities to / from FlexPLM Objects
4
6
  */
@@ -147,6 +149,23 @@ export declare class TypeConversionUtils {
147
149
  static getMapKeyFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any, direction: string): Promise<string | undefined>;
148
150
  static isInboundCreatableFromObject(fileId: string, mapFileUtil: MapFileUtil, object: any, context?: any): Promise<boolean>;
149
151
  static isOutboundCreatableFromEntity(fileId: string, mapFileUtil: MapFileUtil, entity: any, context?: any): Promise<boolean>;
152
+ /** Returns which system originates (creates) an entity type, declared in its mapping section
153
+ * with the isInboundCreatable / isOutboundCreatable pair:
154
+ * - isInboundCreatable true and isOutboundCreatable false -> 'flexplm' (FlexPLM creates)
155
+ * - isInboundCreatable false and isOutboundCreatable true -> 'vibeiq' (VibeIQ creates)
156
+ * Anything else is undeclared and returns undefined: no mapping file, no section, either function
157
+ * missing, both true, both false, or an error while resolving. Unlike isInboundCreatableFromObject /
158
+ * isOutboundCreatableFromEntity, there are NO defaults: the origin must be stated explicitly,
159
+ * so callers can fail safe (treat the entity as read-only) when it is not.
160
+ *
161
+ * @param fileId id for mapFile
162
+ * @param mapFileUtil class to get mapfile
163
+ * @param entityOrObject VibeIQ entity (vibe2flex) or FlexPLM object (flex2vibe)
164
+ * @param direction VIBE2FLEX_DIRECTION or FLEX2VIBE_DIRECTION; selects how the map section key is resolved
165
+ * @param context optional context object passed to the mapping functions
166
+ * @returns Promise<'flexplm' | 'vibeiq' | undefined>
167
+ */
168
+ static getOriginSystem(fileId: string, mapFileUtil: MapFileUtil, entityOrObject: any, direction: string, context?: any): Promise<OriginSystem | undefined>;
150
169
  /** Takes in a FlexPLM object and determines whether inbound
151
170
  * images should be synced. In most cases, the creation owning system
152
171
  * will also control image syncing. Defaults to false if no mapping exists.
@@ -143,6 +143,11 @@ class TypeConversionUtils {
143
143
  const mappingData = await mapFileUtil.getMappingSection(transformMapFile, 'typeConversion', direction);
144
144
  if (mappingData && mappingData[type] && mappingData[type]['getMapKey']) {
145
145
  const mapKey = await mappingData[type]['getMapKey'](entity);
146
+ // An org's custom-entity routing usually returns '' for the subtypes it does not list.
147
+ // Country has a built-in section key, so its section is found without a typeConversion entry.
148
+ if (!mapKey && type_defaults_1.TypeDefaults.isCountryEntity(entity)) {
149
+ return type_defaults_1.TypeDefaults.COUNTRY_OBJECT_CLASS;
150
+ }
146
151
  return mapKey;
147
152
  }
148
153
  }
@@ -360,6 +365,51 @@ class TypeConversionUtils {
360
365
  }
361
366
  return isOutboundCreatable;
362
367
  }
368
+ /** Returns which system originates (creates) an entity type, declared in its mapping section
369
+ * with the isInboundCreatable / isOutboundCreatable pair:
370
+ * - isInboundCreatable true and isOutboundCreatable false -> 'flexplm' (FlexPLM creates)
371
+ * - isInboundCreatable false and isOutboundCreatable true -> 'vibeiq' (VibeIQ creates)
372
+ * Anything else is undeclared and returns undefined: no mapping file, no section, either function
373
+ * missing, both true, both false, or an error while resolving. Unlike isInboundCreatableFromObject /
374
+ * isOutboundCreatableFromEntity, there are NO defaults: the origin must be stated explicitly,
375
+ * so callers can fail safe (treat the entity as read-only) when it is not.
376
+ *
377
+ * @param fileId id for mapFile
378
+ * @param mapFileUtil class to get mapfile
379
+ * @param entityOrObject VibeIQ entity (vibe2flex) or FlexPLM object (flex2vibe)
380
+ * @param direction VIBE2FLEX_DIRECTION or FLEX2VIBE_DIRECTION; selects how the map section key is resolved
381
+ * @param context optional context object passed to the mapping functions
382
+ * @returns Promise<'flexplm' | 'vibeiq' | undefined>
383
+ */
384
+ static async getOriginSystem(fileId, mapFileUtil, entityOrObject, direction, context) {
385
+ if (!fileId || !entityOrObject) {
386
+ return undefined;
387
+ }
388
+ try {
389
+ const mapKey = (TypeConversionUtils.VIBE2FLEX_DIRECTION === direction)
390
+ ? await this.getMapKey(fileId, mapFileUtil, entityOrObject, direction)
391
+ : await this.getMapKeyFromObject(fileId, mapFileUtil, entityOrObject, direction);
392
+ if (!mapKey) {
393
+ return undefined;
394
+ }
395
+ const mapData = await map_utils_1.MapUtil.getFullMapSection(fileId, mapFileUtil, mapKey);
396
+ if (!mapData || typeof mapData['isInboundCreatable'] !== 'function' || typeof mapData['isOutboundCreatable'] !== 'function') {
397
+ return undefined;
398
+ }
399
+ const inbound = !!(await mapData['isInboundCreatable'](entityOrObject, context));
400
+ const outbound = !!(await mapData['isOutboundCreatable'](entityOrObject, context));
401
+ if (inbound && !outbound) {
402
+ return 'flexplm';
403
+ }
404
+ if (!inbound && outbound) {
405
+ return 'vibeiq';
406
+ }
407
+ }
408
+ catch (e) {
409
+ console.error('TypeConversionUtils.getOriginSystem: unable to read the origin from the mapping file: ' + e?.message);
410
+ }
411
+ return undefined;
412
+ }
363
413
  /** Takes in a FlexPLM object and determines whether inbound
364
414
  * images should be synced. In most cases, the creation owning system
365
415
  * will also control image syncing. Defaults to false if no mapping exists.
@@ -924,4 +924,126 @@ describe('conversion-utils', () => {
924
924
  expect(results).toBeTruthy();
925
925
  });
926
926
  });
927
+ describe('Country with no mapping section (SDK defaults)', () => {
928
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
929
+ const mapWithoutCountry = {
930
+ typeConversion: {
931
+ vibe2flex: { 'custom-entity': { getMapKey: () => '' } },
932
+ flex2vibe: {}
933
+ }
934
+ };
935
+ let spy;
936
+ beforeEach(() => {
937
+ spy = jest.spyOn(mapFileUtil, 'getMapFile').mockImplementation(async () => mapWithoutCountry);
938
+ });
939
+ afterEach(() => {
940
+ spy.mockRestore();
941
+ });
942
+ it('should use the defaults when sending a Country from VibeIQ to FlexPLM', async () => {
943
+ const entity = { entityType: 'custom-entity', typePath: 'custom-entity:country' };
944
+ expect(await type_conversion_utils_1.TypeConversionUtils.getObjectClass(TRANSFORM_MAP_FILE, mapFileUtil, entity)).toEqual('LCSCountry');
945
+ expect(await type_conversion_utils_1.TypeConversionUtils.getObjectTypePath(TRANSFORM_MAP_FILE, mapFileUtil, entity)).toEqual('Country');
946
+ expect(await type_conversion_utils_1.TypeConversionUtils.getIdentifierProperties(TRANSFORM_MAP_FILE, mapFileUtil, entity)).toEqual(['vibeIQIdentifier']);
947
+ expect(await type_conversion_utils_1.TypeConversionUtils.getInformationalProperties(TRANSFORM_MAP_FILE, mapFileUtil, entity)).toEqual(['name']);
948
+ });
949
+ it('should use the defaults when receiving a Country from FlexPLM into VibeIQ', async () => {
950
+ const object = { flexPLMObjectClass: 'LCSCountry', flexPLMTypePath: 'Country' };
951
+ expect(await type_conversion_utils_1.TypeConversionUtils.getMapKeyFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toEqual('LCSCountry');
952
+ expect(await type_conversion_utils_1.TypeConversionUtils.getEntityClassFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object)).toEqual('custom-entity');
953
+ expect(await type_conversion_utils_1.TypeConversionUtils.getEntityTypePathFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object)).toEqual('custom-entity:country');
954
+ expect(await type_conversion_utils_1.TypeConversionUtils.getIdentifierPropertiesFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object)).toEqual(['vibeIQIdentifier']);
955
+ expect(await type_conversion_utils_1.TypeConversionUtils.getInformationalPropertiesFromObject(TRANSFORM_MAP_FILE, mapFileUtil, object)).toEqual(['name']);
956
+ });
957
+ it('should let a mapping section override the Country defaults', async () => {
958
+ const overriding = {
959
+ typeConversion: { vibe2flex: { 'custom-entity': { getMapKey: () => 'LCSCountry' } }, flex2vibe: {} },
960
+ LCSCountry: { getIdentifierProperties: () => ['isoCode'] }
961
+ };
962
+ spy.mockImplementation(async () => overriding);
963
+ const entity = { entityType: 'custom-entity', typePath: 'custom-entity:country' };
964
+ expect(await type_conversion_utils_1.TypeConversionUtils.getIdentifierProperties(TRANSFORM_MAP_FILE, mapFileUtil, entity)).toEqual(['isoCode']);
965
+ });
966
+ });
967
+ describe('getOriginSystem (isInboundCreatable / isOutboundCreatable pair)', () => {
968
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
969
+ const countryEntity = { entityType: 'custom-entity', typePath: 'custom-entity:country' };
970
+ const countryObject = { flexPLMObjectClass: 'LCSCountry', flexPLMTypePath: 'Country' };
971
+ let spy;
972
+ // Org-style routing: custom-entity subtypes that are not listed return ''
973
+ const typeConversion = {
974
+ vibe2flex: { 'custom-entity': { getMapKey: () => '' } },
975
+ flex2vibe: {},
976
+ };
977
+ function useMap(countrySection) {
978
+ const map = { typeConversion };
979
+ if (countrySection) {
980
+ map.LCSCountry = countrySection;
981
+ }
982
+ spy.mockImplementation(async () => map);
983
+ }
984
+ beforeEach(() => {
985
+ spy = jest.spyOn(mapFileUtil, 'getMapFile');
986
+ });
987
+ afterEach(() => {
988
+ spy.mockRestore();
989
+ });
990
+ it('should return flexplm when inbound is creatable and outbound is not, in both directions', async () => {
991
+ useMap({ isInboundCreatable: () => true, isOutboundCreatable: () => false });
992
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toBe('flexplm');
993
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBe('flexplm');
994
+ });
995
+ it('should return vibeiq when outbound is creatable and inbound is not, in both directions', async () => {
996
+ useMap({ isInboundCreatable: () => false, isOutboundCreatable: () => true });
997
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toBe('vibeiq');
998
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBe('vibeiq');
999
+ });
1000
+ it('should support async mapping functions', async () => {
1001
+ useMap({ isInboundCreatable: async () => true, isOutboundCreatable: async () => false });
1002
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBe('flexplm');
1003
+ });
1004
+ it('should return undefined when both gates are true', async () => {
1005
+ useMap({ isInboundCreatable: () => true, isOutboundCreatable: () => true });
1006
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1007
+ });
1008
+ it('should return undefined when both gates are false', async () => {
1009
+ useMap({ isInboundCreatable: () => false, isOutboundCreatable: () => false });
1010
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toBeUndefined();
1011
+ });
1012
+ it('should return undefined when either gate function is missing', async () => {
1013
+ useMap({ isInboundCreatable: () => true });
1014
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1015
+ useMap({ isOutboundCreatable: () => true });
1016
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toBeUndefined();
1017
+ });
1018
+ it('should return undefined when there is no LCSCountry section', async () => {
1019
+ useMap();
1020
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toBeUndefined();
1021
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1022
+ });
1023
+ it('should return undefined when there is no mapping file id or no entity', async () => {
1024
+ useMap({ isInboundCreatable: () => true, isOutboundCreatable: () => false });
1025
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem('', mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1026
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, undefined, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1027
+ });
1028
+ it('should return undefined and log when a mapping function throws', async () => {
1029
+ useMap({ isInboundCreatable: () => { throw new Error('boom'); }, isOutboundCreatable: () => false });
1030
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
1031
+ try {
1032
+ expect(await type_conversion_utils_1.TypeConversionUtils.getOriginSystem(TRANSFORM_MAP_FILE, mapFileUtil, countryObject, type_conversion_utils_1.TypeConversionUtils.FLEX2VIBE_DIRECTION)).toBeUndefined();
1033
+ expect(errorSpy).toHaveBeenCalled();
1034
+ }
1035
+ finally {
1036
+ errorSpy.mockRestore();
1037
+ }
1038
+ });
1039
+ it('should not change the section key of other custom entity subtypes', async () => {
1040
+ useMap({ isInboundCreatable: () => true, isOutboundCreatable: () => false });
1041
+ const other = { entityType: 'custom-entity', typePath: 'custom-entity:exchangeRate' };
1042
+ expect(await type_conversion_utils_1.TypeConversionUtils.getMapKey(TRANSFORM_MAP_FILE, mapFileUtil, other, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toEqual('');
1043
+ });
1044
+ it('should return the LCSCountry section key when the org custom entity routing returns an empty key', async () => {
1045
+ useMap({ isInboundCreatable: () => true, isOutboundCreatable: () => false });
1046
+ expect(await type_conversion_utils_1.TypeConversionUtils.getMapKey(TRANSFORM_MAP_FILE, mapFileUtil, countryEntity, type_conversion_utils_1.TypeConversionUtils.VIBE2FLEX_DIRECTION)).toEqual('LCSCountry');
1047
+ });
1048
+ });
927
1049
  });
@@ -3,6 +3,11 @@ export declare class TypeDefaults {
3
3
  static NO_OBJECT_CLASS: string;
4
4
  static NO_TYPE_PATH: string;
5
5
  static processLCSMaterialAsItem: boolean;
6
+ static COUNTRY_OBJECT_CLASS: string;
7
+ static COUNTRY_VIBE_TYPE_PATH: string;
8
+ static COUNTRY_FLEX_TYPE_PATH: string;
9
+ static COUNTRY_IDENTIFIER_PROPERTY: string;
10
+ static COUNTRY_INFORMATIONAL_PROPERTY: string;
6
11
  constructor();
7
12
  /** Applies values from the resolved config to TypeDefaults static state.
8
13
  * Currently toggles whether LCSMaterial is treated as an item (new) or a
@@ -14,6 +19,13 @@ export declare class TypeDefaults {
14
19
  */
15
20
  static applyConfig(config: any): void;
16
21
  static isPropertyTrue(value: any): boolean;
22
+ /** True when the VibeIQ entity is a Country: a custom-entity whose typePath is
23
+ * 'custom-entity:country' (compared case-insensitively).
24
+ *
25
+ * @param entity VibeIQ entity
26
+ * @returns boolean
27
+ */
28
+ static isCountryEntity(entity: any): boolean;
17
29
  /**Takes in full entity and returs the default FlexPLM
18
30
  * object class.
19
31
  *
@@ -18,6 +18,16 @@ class TypeDefaults {
18
18
  static isPropertyTrue(value) {
19
19
  return value === true || (typeof value === 'string' && value.toLowerCase() === 'true');
20
20
  }
21
+ /** True when the VibeIQ entity is a Country: a custom-entity whose typePath is
22
+ * 'custom-entity:country' (compared case-insensitively).
23
+ *
24
+ * @param entity VibeIQ entity
25
+ * @returns boolean
26
+ */
27
+ static isCountryEntity(entity) {
28
+ const typePath = entity?.['typePath'];
29
+ return typeof typePath === 'string' && typePath.toLowerCase() === TypeDefaults.COUNTRY_VIBE_TYPE_PATH;
30
+ }
21
31
  /**Takes in full entity and returs the default FlexPLM
22
32
  * object class.
23
33
  *
@@ -47,7 +57,7 @@ class TypeDefaults {
47
57
  objectClass = 'LCSColor';
48
58
  }
49
59
  else if ('custom-entity' === entityType) {
50
- objectClass = 'LCSRevisableEntity';
60
+ objectClass = TypeDefaults.isCountryEntity(entity) ? TypeDefaults.COUNTRY_OBJECT_CLASS : 'LCSRevisableEntity';
51
61
  }
52
62
  else if ('project' === entityType) {
53
63
  objectClass = 'LCSSeason';
@@ -80,7 +90,7 @@ class TypeDefaults {
80
90
  typePath = 'Color';
81
91
  break;
82
92
  case 'custom-entity':
83
- typePath = 'Revisable Entity';
93
+ typePath = TypeDefaults.isCountryEntity(entity) ? TypeDefaults.COUNTRY_FLEX_TYPE_PATH : 'Revisable Entity';
84
94
  break;
85
95
  case 'project':
86
96
  typePath = 'Season';
@@ -122,9 +132,11 @@ class TypeDefaults {
122
132
  }
123
133
  break;
124
134
  case 'color':
125
- case 'custom-entity':
126
135
  identifierProps.push('name');
127
136
  break;
137
+ case 'custom-entity':
138
+ identifierProps.push(TypeDefaults.isCountryEntity(entity) ? TypeDefaults.COUNTRY_IDENTIFIER_PROPERTY : 'name');
139
+ break;
128
140
  }
129
141
  return identifierProps;
130
142
  }
@@ -149,6 +161,9 @@ class TypeDefaults {
149
161
  else if (['project', 'assortment'].includes(entityType)) {
150
162
  properties.push('name');
151
163
  }
164
+ else if (TypeDefaults.isCountryEntity(entity)) {
165
+ properties.push(TypeDefaults.COUNTRY_INFORMATIONAL_PROPERTY);
166
+ }
152
167
  return properties;
153
168
  }
154
169
  /** Returns the VibeIQ entity type from an entity.
@@ -186,8 +201,8 @@ class TypeDefaults {
186
201
  ? ['LCSProduct', 'LCSSKU', 'LCSMaterial']
187
202
  : ['LCSProduct', 'LCSSKU'];
188
203
  const customEntityClasses = TypeDefaults.processLCSMaterialAsItem
189
- ? ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast']
190
- : ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', 'LCSMaterial'];
204
+ ? ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', TypeDefaults.COUNTRY_OBJECT_CLASS]
205
+ : ['LCSRevisableEntity', 'LCSLifecycleManaged', 'LCSLast', 'LCSMaterial', TypeDefaults.COUNTRY_OBJECT_CLASS];
191
206
  if (itemClasses.includes(objectClass)) {
192
207
  entityClass = 'item';
193
208
  }
@@ -237,6 +252,9 @@ class TypeDefaults {
237
252
  case 'SeasonGroup':
238
253
  typePath = 'assortment';
239
254
  break;
255
+ case 'LCSCountry':
256
+ typePath = TypeDefaults.COUNTRY_VIBE_TYPE_PATH;
257
+ break;
240
258
  }
241
259
  if (typePath === '')
242
260
  throw Error(TypeDefaults.NO_TYPE_PATH);
@@ -271,6 +289,9 @@ class TypeDefaults {
271
289
  case 'SeasonGroup':
272
290
  identifierProps.push('seasonGroupName');
273
291
  break;
292
+ case 'LCSCountry':
293
+ identifierProps.push(TypeDefaults.COUNTRY_IDENTIFIER_PROPERTY);
294
+ break;
274
295
  case 'LCSColor':
275
296
  case 'LCSRevisableEntity':
276
297
  case 'LCSLifecycleManaged':
@@ -302,6 +323,9 @@ class TypeDefaults {
302
323
  else if (['LCSSeason', 'SeasonGroup'].includes(objectClass)) {
303
324
  properties.push('name');
304
325
  }
326
+ else if (TypeDefaults.COUNTRY_OBJECT_CLASS === objectClass) {
327
+ properties.push(TypeDefaults.COUNTRY_INFORMATIONAL_PROPERTY);
328
+ }
305
329
  return properties;
306
330
  }
307
331
  static getObjectClass(object) {
@@ -313,3 +337,8 @@ TypeDefaults.NO_ENTITY_TYPE = 'Not able to determine the entity type of the enti
313
337
  TypeDefaults.NO_OBJECT_CLASS = 'Please ensure that the flexPLMObjectClass property is provided.';
314
338
  TypeDefaults.NO_TYPE_PATH = 'Please ensure that the flexPLMTypePath property is provided.';
315
339
  TypeDefaults.processLCSMaterialAsItem = false;
340
+ TypeDefaults.COUNTRY_OBJECT_CLASS = 'LCSCountry';
341
+ TypeDefaults.COUNTRY_VIBE_TYPE_PATH = 'custom-entity:country';
342
+ TypeDefaults.COUNTRY_FLEX_TYPE_PATH = 'Country';
343
+ TypeDefaults.COUNTRY_IDENTIFIER_PROPERTY = 'vibeIQIdentifier';
344
+ TypeDefaults.COUNTRY_INFORMATIONAL_PROPERTY = 'name';
@@ -42,6 +42,28 @@ describe('Type Defaults', () => {
42
42
  const objectClass = type_defaults_1.TypeDefaults.getDefaultObjectClass(newData);
43
43
  expect(objectClass).toEqual('LCSColor');
44
44
  });
45
+ it('should return LCSCountry for a Country custom entity', () => {
46
+ const newData = {
47
+ entityType: 'custom-entity',
48
+ typePath: 'custom-entity:country'
49
+ };
50
+ const objectClass = type_defaults_1.TypeDefaults.getDefaultObjectClass(newData);
51
+ expect(objectClass).toEqual('LCSCountry');
52
+ });
53
+ it('should return LCSCountry when the Country typePath uses different casing', () => {
54
+ const newData = {
55
+ entityType: 'custom-entity',
56
+ typePath: 'custom-entity:Country'
57
+ };
58
+ expect(type_defaults_1.TypeDefaults.getDefaultObjectClass(newData)).toEqual('LCSCountry');
59
+ });
60
+ it('should return LCSRevisableEntity for a custom entity that is not a Country', () => {
61
+ const newData = {
62
+ entityType: 'custom-entity',
63
+ typePath: 'custom-entity:exchangeRate'
64
+ };
65
+ expect(type_defaults_1.TypeDefaults.getDefaultObjectClass(newData)).toEqual('LCSRevisableEntity');
66
+ });
45
67
  it('LCSRevisableEntity', () => {
46
68
  const newData = {
47
69
  entityType: 'custom-entity'
@@ -120,6 +142,20 @@ describe('Type Defaults', () => {
120
142
  const typePath = type_defaults_1.TypeDefaults.getDefaultObjectTypePath(newData);
121
143
  expect(typePath).toEqual('Color');
122
144
  });
145
+ it('should return Country as the FlexPLM type path for a Country custom entity', () => {
146
+ const newData = {
147
+ entityType: 'custom-entity',
148
+ typePath: 'custom-entity:country'
149
+ };
150
+ expect(type_defaults_1.TypeDefaults.getDefaultObjectTypePath(newData)).toEqual('Country');
151
+ });
152
+ it('should return Revisable Entity as the type path for a custom entity that is not a Country', () => {
153
+ const newData = {
154
+ entityType: 'custom-entity',
155
+ typePath: 'custom-entity:exchangeRate'
156
+ };
157
+ expect(type_defaults_1.TypeDefaults.getDefaultObjectTypePath(newData)).toEqual('Revisable Entity');
158
+ });
123
159
  it('custom-entity', () => {
124
160
  const newData = {
125
161
  entityType: 'custom-entity'
@@ -219,6 +255,21 @@ describe('Type Defaults', () => {
219
255
  expect(defaultIdentifiers).toContain('name');
220
256
  expect(defaultIdentifiers).toHaveLength(1);
221
257
  });
258
+ it('should return vibeIQIdentifier as the identifier for a Country custom entity', () => {
259
+ const newData = {
260
+ entityType: 'custom-entity',
261
+ typePath: 'custom-entity:country'
262
+ };
263
+ const defaultIdentifiers = type_defaults_1.TypeDefaults.getDefaultIdentifierProperties(newData);
264
+ expect(defaultIdentifiers).toEqual(['vibeIQIdentifier']);
265
+ });
266
+ it('should return name as the identifier for a custom entity that is not a Country', () => {
267
+ const newData = {
268
+ entityType: 'custom-entity',
269
+ typePath: 'custom-entity:exchangeRate'
270
+ };
271
+ expect(type_defaults_1.TypeDefaults.getDefaultIdentifierProperties(newData)).toEqual(['name']);
272
+ });
222
273
  it('custom-entity', () => {
223
274
  const newData = {
224
275
  entityType: 'custom-entity'
@@ -270,6 +321,20 @@ describe('Type Defaults', () => {
270
321
  const defaultIdentifiers = type_defaults_1.TypeDefaults.getDefaultInformationalProperties(newData);
271
322
  expect(defaultIdentifiers).toHaveLength(0);
272
323
  });
324
+ it('should return name as the informational property for a Country custom entity', () => {
325
+ const newData = {
326
+ entityType: 'custom-entity',
327
+ typePath: 'custom-entity:country'
328
+ };
329
+ expect(type_defaults_1.TypeDefaults.getDefaultInformationalProperties(newData)).toEqual(['name']);
330
+ });
331
+ it('should return no informational properties for a custom entity that is not a Country', () => {
332
+ const newData = {
333
+ entityType: 'custom-entity',
334
+ typePath: 'custom-entity:exchangeRate'
335
+ };
336
+ expect(type_defaults_1.TypeDefaults.getDefaultInformationalProperties(newData)).toEqual([]);
337
+ });
273
338
  it('custom-entity', () => {
274
339
  const newData = {
275
340
  entityType: 'custom-entity'
@@ -278,6 +343,16 @@ describe('Type Defaults', () => {
278
343
  expect(defaultIdentifiers).toHaveLength(0);
279
344
  });
280
345
  });
346
+ describe('isCountryEntity', () => {
347
+ it('should return true for the custom-entity:country type path', () => {
348
+ expect(type_defaults_1.TypeDefaults.isCountryEntity({ typePath: 'custom-entity:country' })).toBe(true);
349
+ });
350
+ it('should return false for another subtype, a missing typePath, or an undefined entity', () => {
351
+ expect(type_defaults_1.TypeDefaults.isCountryEntity({ typePath: 'custom-entity:other' })).toBe(false);
352
+ expect(type_defaults_1.TypeDefaults.isCountryEntity({})).toBe(false);
353
+ expect(type_defaults_1.TypeDefaults.isCountryEntity(undefined)).toBe(false);
354
+ });
355
+ });
281
356
  describe('getEntityType', () => {
282
357
  it('entityType', () => {
283
358
  const entityType = 'item';
@@ -355,6 +430,24 @@ describe('Type Defaults', () => {
355
430
  const entityClass = type_defaults_1.TypeDefaults.getDefaultEntityClass(object);
356
431
  expect(entityClass).toBe('assortment');
357
432
  });
433
+ it('should return custom-entity as the entity class for LCSCountry', () => {
434
+ const object = {
435
+ flexPLMObjectClass: 'LCSCountry'
436
+ };
437
+ expect(type_defaults_1.TypeDefaults.getDefaultEntityClass(object)).toBe('custom-entity');
438
+ });
439
+ it('should return custom-entity for LCSCountry when LCSMaterial processAsItem is true', () => {
440
+ const object = {
441
+ flexPLMObjectClass: 'LCSCountry'
442
+ };
443
+ type_defaults_1.TypeDefaults.applyConfig({ LCSMaterial: { processAsItem: true } });
444
+ try {
445
+ expect(type_defaults_1.TypeDefaults.getDefaultEntityClass(object)).toBe('custom-entity');
446
+ }
447
+ finally {
448
+ type_defaults_1.TypeDefaults.applyConfig({});
449
+ }
450
+ });
358
451
  it('custom-entity - LCSRevisableEntity', () => {
359
452
  const object = {
360
453
  flexPLMObjectClass: 'LCSRevisableEntity'
@@ -419,6 +512,12 @@ describe('Type Defaults', () => {
419
512
  const typePath = type_defaults_1.TypeDefaults.getDefaultEntityTypePath(object);
420
513
  expect(typePath).toBe('project-item');
421
514
  });
515
+ it('should return custom-entity:country as the VibeIQ type path for LCSCountry', () => {
516
+ const object = {
517
+ flexPLMObjectClass: 'LCSCountry'
518
+ };
519
+ expect(type_defaults_1.TypeDefaults.getDefaultEntityTypePath(object)).toBe('custom-entity:country');
520
+ });
422
521
  it('LCSColor', () => {
423
522
  const object = {
424
523
  flexPLMObjectClass: 'LCSColor'
@@ -501,6 +600,12 @@ describe('Type Defaults', () => {
501
600
  expect(defaultIdentifiers).toContain('name');
502
601
  expect(defaultIdentifiers).toHaveLength(1);
503
602
  });
603
+ it('should return vibeIQIdentifier as the identifier for an LCSCountry object', () => {
604
+ const object = {
605
+ flexPLMObjectClass: 'LCSCountry'
606
+ };
607
+ expect(type_defaults_1.TypeDefaults.getDefaultIdentifierPropertiesFromObject(object)).toEqual(['vibeIQIdentifier']);
608
+ });
504
609
  it('LCSRevisableEntity', () => {
505
610
  const object = {
506
611
  flexPLMObjectClass: 'LCSRevisableEntity'
@@ -541,6 +646,12 @@ describe('Type Defaults', () => {
541
646
  });
542
647
  }); //getDefaultIdentifierPropertiesFromObject
543
648
  describe('getDefaultInformationalPropertiesFromObject', () => {
649
+ it('should return name as the informational property for an LCSCountry object', () => {
650
+ const object = {
651
+ flexPLMObjectClass: 'LCSCountry'
652
+ };
653
+ expect(type_defaults_1.TypeDefaults.getDefaultInformationalPropertiesFromObject(object)).toEqual(['name']);
654
+ });
544
655
  it('LCSProduct', () => {
545
656
  const object = {
546
657
  flexPLMObjectClass: 'LCSProduct'
@@ -4,6 +4,7 @@ exports.TypeUtils = void 0;
4
4
  const app_framework_1 = require("@contrail/app-framework");
5
5
  const sdk_1 = require("@contrail/sdk");
6
6
  const type_conversion_utils_1 = require("./type-conversion-utils");
7
+ const type_defaults_1 = require("./type-defaults");
7
8
  class TypeUtils {
8
9
  constructor() {
9
10
  this.typesObj = new sdk_1.Types();
@@ -52,7 +53,7 @@ class TypeUtils {
52
53
  objectClass = 'LCSColor';
53
54
  }
54
55
  else if ('custom-entity' === entityType) {
55
- objectClass = 'LCSRevisableEntity';
56
+ objectClass = type_defaults_1.TypeDefaults.isCountryEntity(newData) ? type_defaults_1.TypeDefaults.COUNTRY_OBJECT_CLASS : 'LCSRevisableEntity';
56
57
  }
57
58
  return objectClass;
58
59
  }
@@ -89,7 +90,7 @@ class TypeUtils {
89
90
  root: 'color'
90
91
  };
91
92
  }
92
- else if (['LCSBusinessObject', 'LCSRevisableEntity', 'LCSLast'].includes(objectClass)) {
93
+ else if (['LCSBusinessObject', 'LCSRevisableEntity', 'LCSLast', type_defaults_1.TypeDefaults.COUNTRY_OBJECT_CLASS].includes(objectClass)) {
93
94
  //TODO need to write logic using mapping file
94
95
  tco = {
95
96
  root: 'custom-entity'
@@ -47,6 +47,31 @@ describe('TypeUtils.getEventObjectClass() tests', () => {
47
47
  const objectClass = tu.getEventObjectClass(entityType, newData);
48
48
  expect(objectClass).toEqual('LCSRevisableEntity');
49
49
  });
50
+ it('should return LCSCountry for a custom entity with the custom-entity:country typePath', () => {
51
+ const entityType = 'custom-entity';
52
+ const newData = { typePath: 'custom-entity:country' };
53
+ const objectClass = tu.getEventObjectClass(entityType, newData);
54
+ expect(objectClass).toEqual('LCSCountry');
55
+ });
56
+ it('should return LCSCountry when the Country typePath uses different casing', () => {
57
+ const objectClass = tu.getEventObjectClass('custom-entity', { typePath: 'custom-entity:Country' });
58
+ expect(objectClass).toEqual('LCSCountry');
59
+ });
60
+ it('should return LCSRevisableEntity for a custom entity that is not a Country', () => {
61
+ const objectClass = tu.getEventObjectClass('custom-entity', { typePath: 'custom-entity:exchangeRate' });
62
+ expect(objectClass).toEqual('LCSRevisableEntity');
63
+ });
64
+ });
65
+ describe('TypeUtils.getEntityTypeClientOptions() tests', () => {
66
+ const tu = new type_utils_1.TypeUtils();
67
+ it('should resolve LCSCountry to the custom-entity root type', () => {
68
+ const options = tu.getEntityTypeClientOptions('LCSCountry', {});
69
+ expect(options).toEqual({ root: 'custom-entity' });
70
+ });
71
+ it('should keep resolving LCSRevisableEntity to the custom-entity root type', () => {
72
+ const options = tu.getEntityTypeClientOptions('LCSRevisableEntity', {});
73
+ expect(options).toEqual({ root: 'custom-entity' });
74
+ });
50
75
  });
51
76
  describe('filterProperties-item', () => {
52
77
  const itemType = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.8",
3
+ "version": "1.8.0-alpha.562c8dd",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",