@contrail/flexplm 1.2.1 → 1.3.0-alpha.3

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.
Files changed (57) hide show
  1. package/.claude/settings.local.json +1 -2
  2. package/.github/pull_request_template.md +31 -31
  3. package/.github/workflows/flexplm-lib.yml +27 -27
  4. package/lib/entity-processor/base-entity-processor.js +16 -2
  5. package/lib/entity-processor/base-entity-processor.spec.js +124 -0
  6. package/lib/util/flexplm-connect.d.ts +5 -1
  7. package/lib/util/flexplm-connect.js +8 -3
  8. package/lib/util/flexplm-connect.spec.d.ts +1 -0
  9. package/lib/util/flexplm-connect.spec.js +88 -0
  10. package/lib/util/thumbnail-util.d.ts +9 -0
  11. package/lib/util/thumbnail-util.js +88 -0
  12. package/lib/util/thumbnail-util.spec.js +156 -0
  13. package/lib/util/type-conversion-utils-spec-mockData.js +18 -0
  14. package/lib/util/type-conversion-utils.d.ts +2 -0
  15. package/lib/util/type-conversion-utils.js +43 -0
  16. package/lib/util/type-conversion-utils.spec.js +160 -0
  17. package/package.json +1 -1
  18. package/publish.bat +4 -4
  19. package/publish.sh +4 -4
  20. package/src/entity-processor/base-entity-processor.spec.ts +157 -0
  21. package/src/entity-processor/base-entity-processor.ts +21 -2
  22. package/src/flexplm-request.ts +28 -28
  23. package/src/flexplm-utils.spec.ts +27 -27
  24. package/src/flexplm-utils.ts +29 -29
  25. package/src/index.ts +21 -21
  26. package/src/interfaces/item-family-changes.ts +66 -66
  27. package/src/interfaces/publish-change-data.ts +42 -42
  28. package/src/publish/base-process-publish-assortment-callback.ts +50 -50
  29. package/src/transform/identifier-conversion-spec-mockData.ts +495 -495
  30. package/src/transform/identifier-conversion.spec.ts +353 -353
  31. package/src/transform/identifier-conversion.ts +281 -281
  32. package/src/util/config-defaults.spec.ts +350 -350
  33. package/src/util/config-defaults.ts +92 -92
  34. package/src/util/data-converter-spec-mockData.ts +230 -230
  35. package/src/util/error-response-object.spec.ts +115 -115
  36. package/src/util/error-response-object.ts +49 -49
  37. package/src/util/federation.ts +172 -172
  38. package/src/util/flexplm-connect.spec.ts +132 -0
  39. package/src/util/flexplm-connect.ts +14 -5
  40. package/src/util/logger-config.ts +19 -19
  41. package/src/util/map-util-spec-mockData.ts +230 -230
  42. package/src/util/map-utils.spec.ts +102 -102
  43. package/src/util/map-utils.ts +40 -40
  44. package/src/util/mockData.ts +97 -97
  45. package/src/util/thumbnail-util.spec.ts +190 -0
  46. package/src/util/thumbnail-util.ts +109 -1
  47. package/src/util/type-conversion-utils-spec-mockData.ts +18 -0
  48. package/src/util/type-conversion-utils.spec.ts +184 -0
  49. package/src/util/type-conversion-utils.ts +75 -0
  50. package/src/util/type-defaults.spec.ts +668 -668
  51. package/src/util/type-defaults.ts +280 -280
  52. package/src/util/type-utils.spec.ts +227 -227
  53. package/src/util/type-utils.ts +144 -144
  54. package/tsconfig.json +28 -26
  55. package/tslint.json +57 -57
  56. package/scripts/output.png +0 -0
  57. package/scripts/test-get-request.ts +0 -35
@@ -2,6 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const thumbnail_util_1 = require("./thumbnail-util");
4
4
  const mockData_1 = require("./mockData");
5
+ const mockEntitiesGet = jest.fn();
6
+ const mockEntitiesUpdate = jest.fn();
7
+ const mockEntitiesDelete = jest.fn();
8
+ const mockContentCreate = jest.fn();
9
+ jest.mock('@contrail/sdk', () => {
10
+ return {
11
+ Entities: jest.fn().mockImplementation(() => ({
12
+ get: mockEntitiesGet,
13
+ update: mockEntitiesUpdate,
14
+ delete: mockEntitiesDelete,
15
+ })),
16
+ Content: jest.fn().mockImplementation(() => ({
17
+ create: mockContentCreate,
18
+ })),
19
+ };
20
+ });
21
+ const mockGetRequest = jest.fn();
22
+ jest.mock('./flexplm-connect', () => {
23
+ return {
24
+ FlexPLMConnect: jest.fn().mockImplementation(() => ({
25
+ getRequest: mockGetRequest,
26
+ })),
27
+ };
28
+ });
5
29
  describe('ThumbnailUtil Tests', () => {
6
30
  const config = {};
7
31
  describe('setOutboundThumbnail()', () => {
@@ -239,4 +263,136 @@ describe('ThumbnailUtil Tests', () => {
239
263
  expect(content.primaryFile.id).toEqual('file123');
240
264
  });
241
265
  });
266
+ describe('syncThumbnailToVibeIQ', () => {
267
+ let tu;
268
+ beforeEach(() => {
269
+ jest.clearAllMocks();
270
+ tu = new thumbnail_util_1.ThumbnailUtil(config);
271
+ mockEntitiesGet.mockImplementation((opts) => {
272
+ if (opts.entityName === 'content-custom-size')
273
+ return Promise.resolve([]);
274
+ return Promise.resolve({});
275
+ });
276
+ mockEntitiesUpdate.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
277
+ mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
278
+ });
279
+ it('returns undefined when no thumbnail IDs in event data', async () => {
280
+ const event = { data: {} };
281
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
282
+ expect(result).toBeUndefined();
283
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
284
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
285
+ expect(mockContentCreate).not.toHaveBeenCalled();
286
+ });
287
+ it('returns undefined when event.data is undefined', async () => {
288
+ const event = {};
289
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
290
+ expect(result).toBeUndefined();
291
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
292
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
293
+ });
294
+ it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and returns clear updates', async () => {
295
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: thumbnail_util_1.ThumbnailUtil.REMOVE_THUMBNAIL } };
296
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
297
+ expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'pv1' });
298
+ expect(result).toEqual(expect.objectContaining({ primaryViewableId: null, primaryFileUrl: null }));
299
+ });
300
+ it('REMOVE_THUMBNAIL with no primaryViewableId returns clear updates without deleting', async () => {
301
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: thumbnail_util_1.ThumbnailUtil.REMOVE_THUMBNAIL } };
302
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
303
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
304
+ expect(result).toEqual(expect.objectContaining({ primaryViewableId: null }));
305
+ });
306
+ it('creates new content when no primaryViewableId exists and returns primary viewable updates', async () => {
307
+ const mockResponse = {
308
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
309
+ headers: { get: jest.fn().mockReturnValue('image/png') },
310
+ };
311
+ mockGetRequest.mockResolvedValue(mockResponse);
312
+ const createdContent = {
313
+ id: 'newContent1',
314
+ contentType: 'image/png',
315
+ fileName: 'thumb.png',
316
+ primaryFileUrl: 'https://files/primary.png',
317
+ largeViewableUrl: 'https://files/large.png',
318
+ mediumLargeViewableUrl: null,
319
+ mediumViewableUrl: null,
320
+ smallViewableUrl: null,
321
+ tinyViewableUrl: null,
322
+ };
323
+ mockContentCreate.mockResolvedValue(createdContent);
324
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/thumb.png' } };
325
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
326
+ expect(mockGetRequest).toHaveBeenCalledWith({ urlPath: '/rest/thumbnail/thumb.png', includeUrlContext: false, returnFullResponse: true });
327
+ expect(mockContentCreate).toHaveBeenCalledWith(expect.objectContaining({
328
+ fileName: 'thumb.png',
329
+ contentType: 'image/png',
330
+ contentHolderReference: 'color:entity1',
331
+ }));
332
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({
333
+ entityName: 'content',
334
+ id: 'newContent1',
335
+ object: { flexplmThumbnailUrl: '/rest/thumbnail/thumb.png' },
336
+ }));
337
+ expect(result).toEqual(expect.objectContaining({
338
+ primaryViewableId: 'newContent1',
339
+ primaryFileUrl: 'https://files/primary.png',
340
+ largeViewableDownloadUrl: 'https://files/large.png',
341
+ }));
342
+ });
343
+ it('replaces content when primaryViewable.flexplmThumbnailUrl differs and returns updates', async () => {
344
+ mockEntitiesGet.mockImplementation((opts) => {
345
+ if (opts.entityName === 'content-custom-size')
346
+ return Promise.resolve([]);
347
+ if (opts.entityName === 'content' && opts.id === 'oldPv') {
348
+ return Promise.resolve({ id: 'oldPv', flexplmThumbnailUrl: '/rest/thumbnail/old.png' });
349
+ }
350
+ return Promise.resolve({});
351
+ });
352
+ const mockResponse = {
353
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
354
+ headers: { get: jest.fn().mockReturnValue('image/jpeg') },
355
+ };
356
+ mockGetRequest.mockResolvedValue(mockResponse);
357
+ const createdContent = {
358
+ id: 'newContent2',
359
+ contentType: 'image/jpeg',
360
+ fileName: 'new.jpg',
361
+ primaryFileUrl: 'https://files/new-primary.jpg',
362
+ largeViewableUrl: null,
363
+ mediumLargeViewableUrl: null,
364
+ mediumViewableUrl: null,
365
+ smallViewableUrl: null,
366
+ tinyViewableUrl: null,
367
+ };
368
+ mockContentCreate.mockResolvedValue(createdContent);
369
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/new.jpg' } };
370
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'oldPv', event, entityName: 'item' });
371
+ expect(mockContentCreate).toHaveBeenCalled();
372
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({
373
+ entityName: 'content',
374
+ id: 'newContent2',
375
+ object: { flexplmThumbnailUrl: '/rest/thumbnail/new.jpg' },
376
+ }));
377
+ expect(result).toEqual(expect.objectContaining({ primaryViewableId: 'newContent2' }));
378
+ expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'oldPv' });
379
+ });
380
+ it('returns undefined when primaryViewable.flexplmThumbnailUrl matches', async () => {
381
+ const thumbnailUrl = '/rest/thumbnail/same.png';
382
+ mockEntitiesGet.mockImplementation((opts) => {
383
+ if (opts.entityName === 'content-custom-size')
384
+ return Promise.resolve([]);
385
+ if (opts.entityName === 'content' && opts.id === 'pv1') {
386
+ return Promise.resolve({ id: 'pv1', flexplmThumbnailUrl: thumbnailUrl });
387
+ }
388
+ return Promise.resolve({});
389
+ });
390
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.EXISTING_THUMBNAIL_ID]: thumbnailUrl } };
391
+ const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
392
+ expect(result).toBeUndefined();
393
+ expect(mockContentCreate).not.toHaveBeenCalled();
394
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
395
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
396
+ });
397
+ });
242
398
  });
@@ -137,6 +137,12 @@ exports.mapping = {
137
137
  isOutboundCreatable: (entity, context) => {
138
138
  return false;
139
139
  },
140
+ syncInboundImages: (entity, context) => {
141
+ return true;
142
+ },
143
+ syncOutboundImages: (entity, context) => {
144
+ return false;
145
+ },
140
146
  vibe2flex: {
141
147
  transformOrder: [{ processor: 'REKEY', rekeyDelete: true, rekeyTransformersKey: 'rekey' }],
142
148
  rekey: {
@@ -169,6 +175,18 @@ exports.mapping = {
169
175
  }
170
176
  return true;
171
177
  },
178
+ syncInboundImages: (entity, context) => {
179
+ if (context && context.skipImages) {
180
+ return false;
181
+ }
182
+ return true;
183
+ },
184
+ syncOutboundImages: (entity, context) => {
185
+ if (context && context.skipImages) {
186
+ return false;
187
+ }
188
+ return true;
189
+ },
172
190
  vibe2flex: {
173
191
  transformOrder: [{ processor: 'REKEY', rekeyDelete: true, rekeyTransformersKey: 'rekey' }],
174
192
  rekey: {
@@ -17,5 +17,7 @@ export declare class TypeConversionUtils {
17
17
  static getMapKeyFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any, direction: string): Promise<string>;
18
18
  static isInboundCreatableFromObject(fileId: string, mapFileUtil: MapFileUtil, object: any, context?: any): Promise<boolean>;
19
19
  static isOutboundCreatableFromEntity(fileId: string, mapFileUtil: MapFileUtil, entity: any, context?: any): Promise<boolean>;
20
+ static syncInboundImages(fileId: string, mapFileUtil: MapFileUtil, object: any, context?: any): Promise<boolean>;
21
+ static syncOutboundImages(fileId: string, mapFileUtil: MapFileUtil, entity: any, context?: any): Promise<boolean>;
20
22
  static getObjectType(object: any): any;
21
23
  }
@@ -179,6 +179,7 @@ class TypeConversionUtils {
179
179
  }
180
180
  return type;
181
181
  }
182
+ return '';
182
183
  }
183
184
  static async isInboundCreatableFromObject(fileId, mapFileUtil, object, context) {
184
185
  let isInboundCreatable = false;
@@ -212,6 +213,48 @@ class TypeConversionUtils {
212
213
  }
213
214
  return isOutboundCreatable;
214
215
  }
216
+ static async syncInboundImages(fileId, mapFileUtil, object, context) {
217
+ let syncImages = false;
218
+ if (!fileId) {
219
+ return syncImages;
220
+ }
221
+ let mapKey;
222
+ try {
223
+ mapKey = await this.getMapKeyFromObject(fileId, mapFileUtil, object, TypeConversionUtils.FLEX2VIBE_DIRECTION);
224
+ }
225
+ catch {
226
+ return syncImages;
227
+ }
228
+ if (!mapKey) {
229
+ return syncImages;
230
+ }
231
+ const mapData = await map_utils_1.MapUtil.getFullMapSection(fileId, mapFileUtil, mapKey);
232
+ if (mapData && mapData['syncInboundImages']) {
233
+ syncImages = await mapData['syncInboundImages'](object, context);
234
+ }
235
+ return syncImages;
236
+ }
237
+ static async syncOutboundImages(fileId, mapFileUtil, entity, context) {
238
+ let syncImages = true;
239
+ if (!fileId) {
240
+ return syncImages;
241
+ }
242
+ let mapKey;
243
+ try {
244
+ mapKey = await this.getMapKey(fileId, mapFileUtil, entity, TypeConversionUtils.VIBE2FLEX_DIRECTION);
245
+ }
246
+ catch {
247
+ return syncImages;
248
+ }
249
+ if (!mapKey) {
250
+ return syncImages;
251
+ }
252
+ const mapData = await map_utils_1.MapUtil.getFullMapSection(fileId, mapFileUtil, mapKey);
253
+ if (mapData && mapData['syncOutboundImages']) {
254
+ syncImages = await mapData['syncOutboundImages'](entity, context);
255
+ }
256
+ return syncImages;
257
+ }
215
258
  static getObjectType(object) {
216
259
  let objectType = object['flexPLMObjectClass'];
217
260
  return objectType;
@@ -705,4 +705,164 @@ describe('conversion-utils', () => {
705
705
  }
706
706
  });
707
707
  });
708
+ describe('syncInboundImages', () => {
709
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
710
+ it('should return true for Revisable Entity\\packaging (mapping entry true)', async () => {
711
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
712
+ .mockImplementation(async () => {
713
+ return mapping;
714
+ });
715
+ const obj = {
716
+ flexPLMObjectClass: 'LCSRevisableEntity',
717
+ flexPLMTypePath: 'Revisable Entity\\packaging'
718
+ };
719
+ try {
720
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(TRANSFORM_MAP_FILE, mapFileUtil, obj);
721
+ expect(results).toBeTruthy();
722
+ }
723
+ finally {
724
+ spy.mockRestore();
725
+ }
726
+ });
727
+ it('should return true for Revisable Entity\\prefix (mapping entry true)', async () => {
728
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
729
+ .mockImplementation(async () => {
730
+ return mapping;
731
+ });
732
+ const obj = {
733
+ flexPLMObjectClass: 'LCSRevisableEntity',
734
+ flexPLMTypePath: 'Revisable Entity\\prefix'
735
+ };
736
+ try {
737
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(TRANSFORM_MAP_FILE, mapFileUtil, obj);
738
+ expect(results).toBeTruthy();
739
+ }
740
+ finally {
741
+ spy.mockRestore();
742
+ }
743
+ });
744
+ it('should pass context to syncInboundImages function', async () => {
745
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
746
+ .mockImplementation(async () => {
747
+ return mapping;
748
+ });
749
+ const obj = {
750
+ flexPLMObjectClass: 'LCSRevisableEntity',
751
+ flexPLMTypePath: 'Revisable Entity\\prefix'
752
+ };
753
+ const context = { skipImages: true };
754
+ try {
755
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(TRANSFORM_MAP_FILE, mapFileUtil, obj, context);
756
+ expect(results).toBeFalsy();
757
+ }
758
+ finally {
759
+ spy.mockRestore();
760
+ }
761
+ });
762
+ it('should default to false when no mapping exists', async () => {
763
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
764
+ .mockImplementation(async () => {
765
+ return mapping;
766
+ });
767
+ const obj = {
768
+ flexPLMObjectClass: 'LCSRevisableEntity',
769
+ flexPLMTypePath: 'Revisable Entity\\catName'
770
+ };
771
+ try {
772
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(TRANSFORM_MAP_FILE, mapFileUtil, obj);
773
+ expect(results).toBeFalsy();
774
+ }
775
+ finally {
776
+ spy.mockRestore();
777
+ }
778
+ });
779
+ it('should default to false when no fileId', async () => {
780
+ const obj = {
781
+ flexPLMObjectClass: 'LCSRevisableEntity',
782
+ flexPLMTypePath: 'Revisable Entity\\pack'
783
+ };
784
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages('', mapFileUtil, obj);
785
+ expect(results).toBeFalsy();
786
+ });
787
+ });
788
+ describe('syncOutboundImages', () => {
789
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
790
+ it('should return false for custom-entity:pack (mapping entry false)', async () => {
791
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
792
+ .mockImplementation(async () => {
793
+ return mapping;
794
+ });
795
+ const entity = {
796
+ entityType: 'custom-entity',
797
+ typePath: 'custom-entity:pack'
798
+ };
799
+ try {
800
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncOutboundImages(TRANSFORM_MAP_FILE, mapFileUtil, entity);
801
+ expect(results).toBeFalsy();
802
+ }
803
+ finally {
804
+ spy.mockRestore();
805
+ }
806
+ });
807
+ it('should return true for custom-entity:prefix (mapping entry true)', async () => {
808
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
809
+ .mockImplementation(async () => {
810
+ return mapping;
811
+ });
812
+ const entity = {
813
+ entityType: 'custom-entity',
814
+ typePath: 'custom-entity:prefix'
815
+ };
816
+ try {
817
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncOutboundImages(TRANSFORM_MAP_FILE, mapFileUtil, entity);
818
+ expect(results).toBeTruthy();
819
+ }
820
+ finally {
821
+ spy.mockRestore();
822
+ }
823
+ });
824
+ it('should pass context to syncOutboundImages function', async () => {
825
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
826
+ .mockImplementation(async () => {
827
+ return mapping;
828
+ });
829
+ const entity = {
830
+ entityType: 'custom-entity',
831
+ typePath: 'custom-entity:prefix'
832
+ };
833
+ const context = { skipImages: true };
834
+ try {
835
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncOutboundImages(TRANSFORM_MAP_FILE, mapFileUtil, entity, context);
836
+ expect(results).toBeFalsy();
837
+ }
838
+ finally {
839
+ spy.mockRestore();
840
+ }
841
+ });
842
+ it('should default to true when no mapping exists', async () => {
843
+ const spy = jest.spyOn(mapFileUtil, 'getMapFile')
844
+ .mockImplementation(async () => {
845
+ return mapping;
846
+ });
847
+ const entity = {
848
+ entityType: 'custom-entity',
849
+ typePath: 'custom-entity:catName'
850
+ };
851
+ try {
852
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncOutboundImages(TRANSFORM_MAP_FILE, mapFileUtil, entity);
853
+ expect(results).toBeTruthy();
854
+ }
855
+ finally {
856
+ spy.mockRestore();
857
+ }
858
+ });
859
+ it('should default to true when no fileId', async () => {
860
+ const entity = {
861
+ entityType: 'custom-entity',
862
+ typePath: 'custom-entity:pack'
863
+ };
864
+ const results = await type_conversion_utils_1.TypeConversionUtils.syncOutboundImages('', mapFileUtil, entity);
865
+ expect(results).toBeTruthy();
866
+ });
867
+ });
708
868
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.2.1",
3
+ "version": "1.3.0-alpha.3",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/publish.bat CHANGED
@@ -1,5 +1,5 @@
1
- rd /s /q lib
2
- call npm install
3
- call npm run build
4
- call npm version patch
1
+ rd /s /q lib
2
+ call npm install
3
+ call npm run build
4
+ call npm version patch
5
5
  call npm publish
package/publish.sh CHANGED
@@ -1,5 +1,5 @@
1
- rm -rf lib;
2
- npm install;
3
- npm run build;
4
- npm version patch;
1
+ rm -rf lib;
2
+ npm install;
3
+ npm run build;
4
+ npm version patch;
5
5
  npm publish;
@@ -3,6 +3,8 @@ import { MapFileUtil } from "@contrail/transform-data";
3
3
  import { EntityPayloadType, FCConfig } from "../interfaces/interfaces";
4
4
  import { DataConverter } from "../util/data-converter";
5
5
  import { BaseEntityProcessor, IncomingEntityResponse } from "./base-entity-processor";
6
+ import { TypeConversionUtils } from "../util/type-conversion-utils";
7
+ import { ThumbnailUtil } from "../util/thumbnail-util";
6
8
 
7
9
  const mockRootType = {
8
10
  typeProperties: [
@@ -335,4 +337,159 @@ describe('BaseEntityProcessor', () =>{
335
337
 
336
338
  });
337
339
 
340
+ describe('handleIncomingUpsert - inbound image sync', () =>{
341
+ const config = {} as FCConfig;
342
+ const mapFileUtil = new MapFileUtil(new Entities());
343
+ const dc = new DataConverter(config, mapFileUtil);
344
+ const mockEvent = { objectClass: 'TestClass', federatedId: 'fed-123', data: { name: 'test' }, entityReference: 'ref-1', eventType: 'PERSIST' } as EntityPayloadType;
345
+ const mockInboundData = { name: 'transformed-test' };
346
+ const mockCreatedEntity = { id: 'created-1', name: 'created', primaryViewableId: 'pv-created' };
347
+ const mockUpdatedEntity = { id: 'updated-1', name: 'updated', primaryViewableId: 'pv-updated' };
348
+ const mockExistingEntity = { id: 'existing-1', name: 'existing', typeId: 'type-1', roles: [] };
349
+
350
+ let btep: TestBaseEntityProcessor;
351
+ let syncInboundImagesSpy: jest.SpyInstance;
352
+ let syncThumbnailSpy: jest.SpyInstance;
353
+
354
+ beforeEach(() =>{
355
+ jest.clearAllMocks();
356
+ btep = new TestBaseEntityProcessor(config, dc, mapFileUtil, 'test');
357
+ jest.spyOn(btep, 'getTransformedData').mockResolvedValue(mockInboundData);
358
+ syncInboundImagesSpy = jest.spyOn(TypeConversionUtils, 'syncInboundImages');
359
+ syncThumbnailSpy = jest.spyOn(ThumbnailUtil.prototype, 'syncThumbnailToVibeIQ').mockResolvedValue(undefined);
360
+ });
361
+
362
+ it('should call updateEntity with thumbnail updates after create when syncInboundImages returns true', async () =>{
363
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: null });
364
+ jest.spyOn(btep, 'getCreateEntity' as any).mockResolvedValue({ entity: { name: 'new' } });
365
+ jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
366
+ const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockCreatedEntity);
367
+ syncInboundImagesSpy.mockResolvedValue(true);
368
+ const thumbnailUpdates = { primaryViewableId: 'pv-new', primaryFileUrl: 'https://files/new.png' };
369
+ syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
370
+
371
+ const result = await btep.handleIncomingUpsert(mockEvent);
372
+
373
+ expect(syncThumbnailSpy).toBeCalledTimes(1);
374
+ expect(syncThumbnailSpy).toBeCalledWith({ entityId: 'created-1', primaryViewableId: 'pv-created', event: mockEvent, entityName: 'test' });
375
+ expect(updateEntitySpy).toBeCalledWith('test', mockCreatedEntity, thumbnailUpdates);
376
+ expect(result).toBe(mockCreatedEntity);
377
+ });
378
+
379
+ it('should not call updateEntity after create when syncThumbnailToVibeIQ returns undefined', async () =>{
380
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: null });
381
+ jest.spyOn(btep, 'getCreateEntity' as any).mockResolvedValue({ entity: { name: 'new' } });
382
+ jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
383
+ const updateEntitySpy = jest.spyOn(btep, 'updateEntity');
384
+ syncInboundImagesSpy.mockResolvedValue(true);
385
+ syncThumbnailSpy.mockResolvedValue(undefined);
386
+
387
+ const result = await btep.handleIncomingUpsert(mockEvent);
388
+
389
+ expect(syncThumbnailSpy).toBeCalledTimes(1);
390
+ expect(updateEntitySpy).not.toHaveBeenCalled();
391
+ expect(result).toBe(mockCreatedEntity);
392
+ });
393
+
394
+ it('should not sync images after create when syncInboundImages returns false', async () =>{
395
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: null });
396
+ jest.spyOn(btep, 'getCreateEntity' as any).mockResolvedValue({ entity: { name: 'new' } });
397
+ jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
398
+ syncInboundImagesSpy.mockResolvedValue(false);
399
+
400
+ const result = await btep.handleIncomingUpsert(mockEvent);
401
+
402
+ expect(syncInboundImagesSpy).toBeCalledTimes(1);
403
+ expect(syncThumbnailSpy).toBeCalledTimes(0);
404
+ expect(result).toBe(mockCreatedEntity);
405
+ });
406
+
407
+ it('should merge thumbnail updates with property diffs on update', async () =>{
408
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
409
+ jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({ name: 'changed' });
410
+ const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
411
+ syncInboundImagesSpy.mockResolvedValue(true);
412
+ const thumbnailUpdates = { primaryViewableId: 'pv-new' };
413
+ syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
414
+
415
+ const result = await btep.handleIncomingUpsert(mockEvent);
416
+
417
+ expect(syncThumbnailSpy).toBeCalledTimes(1);
418
+ expect(syncThumbnailSpy).toBeCalledWith({ entityId: 'existing-1', primaryViewableId: undefined, event: mockEvent, entityName: 'test' });
419
+ expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { name: 'changed', primaryViewableId: 'pv-new' });
420
+ expect(result).toBe(mockUpdatedEntity);
421
+ });
422
+
423
+ it('should update entity with only thumbnail changes when no property diffs', async () =>{
424
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
425
+ jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({});
426
+ const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
427
+ syncInboundImagesSpy.mockResolvedValue(true);
428
+ const thumbnailUpdates = { primaryViewableId: 'pv-new' };
429
+ syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
430
+
431
+ const result = await btep.handleIncomingUpsert(mockEvent);
432
+
433
+ expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { primaryViewableId: 'pv-new' });
434
+ expect(result).toBe(mockUpdatedEntity);
435
+ });
436
+
437
+ it('should not sync images on update when syncInboundImages returns false', async () =>{
438
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
439
+ jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({ name: 'changed' });
440
+ jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
441
+ syncInboundImagesSpy.mockResolvedValue(false);
442
+
443
+ const result = await btep.handleIncomingUpsert(mockEvent);
444
+
445
+ expect(syncInboundImagesSpy).toBeCalledTimes(1);
446
+ expect(syncThumbnailSpy).toBeCalledTimes(0);
447
+ expect(result).toBe(mockUpdatedEntity);
448
+ });
449
+
450
+ it('should not sync images on early return from getIncomingEntity', async () =>{
451
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({
452
+ earlyReturn: { status: 400, data: { message: 'error' }, shortStatusMessage: 'FAIL' }
453
+ });
454
+
455
+ await btep.handleIncomingUpsert(mockEvent);
456
+
457
+ expect(syncInboundImagesSpy).toBeCalledTimes(0);
458
+ expect(syncThumbnailSpy).toBeCalledTimes(0);
459
+ });
460
+
461
+ it('should not sync images on early return from getCreateEntity', async () =>{
462
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: null });
463
+ jest.spyOn(btep, 'getCreateEntity' as any).mockResolvedValue({
464
+ earlyReturn: { status: 400, data: { message: 'not creatable' }, shortStatusMessage: 'NOT_CREATABLE' }
465
+ });
466
+
467
+ await btep.handleIncomingUpsert(mockEvent);
468
+
469
+ expect(syncInboundImagesSpy).toBeCalledTimes(0);
470
+ expect(syncThumbnailSpy).toBeCalledTimes(0);
471
+ });
472
+
473
+ it('should return no changes when no property diffs and no thumbnail updates', async () =>{
474
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
475
+ jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({});
476
+ syncInboundImagesSpy.mockResolvedValue(true);
477
+ syncThumbnailSpy.mockResolvedValue(undefined);
478
+
479
+ const result = await btep.handleIncomingUpsert(mockEvent);
480
+
481
+ expect(result).toEqual({ status: 200, data: { message: 'No Changes to persist for entity: existing-1' } });
482
+ });
483
+
484
+ it('should return no changes when syncInboundImages is false and no property diffs', async () =>{
485
+ jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
486
+ jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({});
487
+ syncInboundImagesSpy.mockResolvedValue(false);
488
+
489
+ const result = await btep.handleIncomingUpsert(mockEvent);
490
+
491
+ expect(result).toEqual({ status: 200, data: { message: 'No Changes to persist for entity: existing-1' } });
492
+ });
493
+ });
494
+
338
495
  });