@contrail/flexplm 1.3.0-alpha.4 → 1.3.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.0] - 2026-04-12
11
+ ### Added
12
+ - Added inbound thumbnail/primary content syncing from FlexPLM to VibeIQ via `ThumbnailUtil.syncThumbnailToVibeIQ`.
13
+ - Added `syncInboundImages` and `syncOutboundImages` methods to `TypeConversionUtils` for controlling image sync per map file configuration.
14
+ - Added `PRIMARY_CONTENT_UPDATED` status to `EventShortMessageStatus` for when only primary content changes are detected.
15
+
16
+ ### Changed
17
+ - `BaseEntityProcessor` update flow now distinguishes between primary-content-only changes and no changes, returning the updated entity when only the thumbnail was synced.
18
+ - Improved `FlexPLMConnect.getRequest` function and added unit tests.
19
+
10
20
  ## [1.2.1] - 2026-04-08
11
21
 
12
22
  ### Added
@@ -68,13 +68,10 @@ class BaseEntityProcessor {
68
68
  console.log(statusMsg);
69
69
  return createEntityResponse.earlyReturn;
70
70
  }
71
- const createdEntity = await this.createEntity(this.baseType, createEntityResponse.entity);
71
+ let createdEntity = await this.createEntity(this.baseType, createEntityResponse.entity);
72
72
  const shouldSyncThumbnail = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(this.transformMapFile, this.mapFileUtil, event.data);
73
73
  if (shouldSyncThumbnail) {
74
- const thumbnailUpdates = await new thumbnail_util_1.ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: createdEntity.id, primaryViewableId: createdEntity.primaryViewableId, event, entityName: this.baseType });
75
- if (thumbnailUpdates) {
76
- await this.updateEntity(this.baseType, createdEntity, thumbnailUpdates);
77
- }
74
+ createdEntity = await new thumbnail_util_1.ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: createdEntity.id, primaryViewableId: createdEntity.primaryViewableId, event, entityName: this.baseType }) || createdEntity;
78
75
  }
79
76
  const statusMsg = this.getInboundStatusMessage({
80
77
  status: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
@@ -88,12 +85,23 @@ class BaseEntityProcessor {
88
85
  }
89
86
  const diffs = await this.getUpdatesForEntity(entity, inboundData);
90
87
  const shouldSyncThumbnail = await type_conversion_utils_1.TypeConversionUtils.syncInboundImages(this.transformMapFile, this.mapFileUtil, event.data);
91
- let thumbnailUpdates;
88
+ let thumbnailEntity;
92
89
  if (shouldSyncThumbnail) {
93
- thumbnailUpdates = await new thumbnail_util_1.ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: entity.id, primaryViewableId: entity.primaryViewableId, event, entityName: this.baseType });
90
+ thumbnailEntity = await new thumbnail_util_1.ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: entity.id, primaryViewableId: entity.primaryViewableId, event, entityName: this.baseType });
94
91
  }
95
- const allUpdates = { ...diffs, ...thumbnailUpdates };
96
- if (Object.getOwnPropertyNames(allUpdates).length == 0) {
92
+ const hasPropertyChanges = Object.getOwnPropertyNames(diffs).length > 0;
93
+ if (!hasPropertyChanges && thumbnailEntity) {
94
+ const statusMsg = this.getInboundStatusMessage({
95
+ status: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
96
+ statusMessage: event_short_message_status_1.EventShortMessageStatus.PRIMARY_CONTENT_UPDATED,
97
+ objectClass: event.objectClass,
98
+ entityId: entity.id,
99
+ federatedId: event.federatedId
100
+ });
101
+ console.log(statusMsg);
102
+ return thumbnailEntity;
103
+ }
104
+ if (!hasPropertyChanges) {
97
105
  const statusMsg = this.getInboundStatusMessage({
98
106
  status: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
99
107
  statusMessage: event_short_message_status_1.EventShortMessageStatus.NO_CHANGES,
@@ -108,7 +116,7 @@ class BaseEntityProcessor {
108
116
  data: { message }
109
117
  };
110
118
  }
111
- const updatedEntity = await this.updateEntity(this.baseType, entity, allUpdates);
119
+ const updatedEntity = await this.updateEntity(this.baseType, entity, diffs);
112
120
  const statusMsg = this.getInboundStatusMessage({
113
121
  status: event_short_message_status_1.EventShortMessageStatus.SUCCESS,
114
122
  statusMessage: event_short_message_status_1.EventShortMessageStatus.UPDATED,
@@ -320,30 +320,14 @@ describe('BaseEntityProcessor', () => {
320
320
  syncInboundImagesSpy = jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'syncInboundImages');
321
321
  syncThumbnailSpy = jest.spyOn(thumbnail_util_1.ThumbnailUtil.prototype, 'syncThumbnailToVibeIQ').mockResolvedValue(undefined);
322
322
  });
323
- it('should call updateEntity with thumbnail updates after create when syncInboundImages returns true', async () => {
323
+ it('should call syncThumbnailToVibeIQ with entity after create when syncInboundImages returns true', async () => {
324
324
  jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: null });
325
325
  jest.spyOn(btep, 'getCreateEntity').mockResolvedValue({ entity: { name: 'new' } });
326
326
  jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
327
- const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockCreatedEntity);
328
327
  syncInboundImagesSpy.mockResolvedValue(true);
329
- const thumbnailUpdates = { primaryViewableId: 'pv-new', primaryFileUrl: 'https://files/new.png' };
330
- syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
331
328
  const result = await btep.handleIncomingUpsert(mockEvent);
332
329
  expect(syncThumbnailSpy).toBeCalledTimes(1);
333
330
  expect(syncThumbnailSpy).toBeCalledWith({ entityId: 'created-1', primaryViewableId: 'pv-created', event: mockEvent, entityName: 'test' });
334
- expect(updateEntitySpy).toBeCalledWith('test', mockCreatedEntity, thumbnailUpdates);
335
- expect(result).toBe(mockCreatedEntity);
336
- });
337
- it('should not call updateEntity after create when syncThumbnailToVibeIQ returns undefined', async () => {
338
- jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: null });
339
- jest.spyOn(btep, 'getCreateEntity').mockResolvedValue({ entity: { name: 'new' } });
340
- jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
341
- const updateEntitySpy = jest.spyOn(btep, 'updateEntity');
342
- syncInboundImagesSpy.mockResolvedValue(true);
343
- syncThumbnailSpy.mockResolvedValue(undefined);
344
- const result = await btep.handleIncomingUpsert(mockEvent);
345
- expect(syncThumbnailSpy).toBeCalledTimes(1);
346
- expect(updateEntitySpy).not.toHaveBeenCalled();
347
331
  expect(result).toBe(mockCreatedEntity);
348
332
  });
349
333
  it('should not sync images after create when syncInboundImages returns false', async () => {
@@ -356,28 +340,15 @@ describe('BaseEntityProcessor', () => {
356
340
  expect(syncThumbnailSpy).toBeCalledTimes(0);
357
341
  expect(result).toBe(mockCreatedEntity);
358
342
  });
359
- it('should merge thumbnail updates with property diffs on update', async () => {
343
+ it('should call syncThumbnailToVibeIQ with entity and update with property diffs on update', async () => {
360
344
  jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: mockExistingEntity });
361
345
  jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({ name: 'changed' });
362
346
  const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
363
347
  syncInboundImagesSpy.mockResolvedValue(true);
364
- const thumbnailUpdates = { primaryViewableId: 'pv-new' };
365
- syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
366
348
  const result = await btep.handleIncomingUpsert(mockEvent);
367
349
  expect(syncThumbnailSpy).toBeCalledTimes(1);
368
350
  expect(syncThumbnailSpy).toBeCalledWith({ entityId: 'existing-1', primaryViewableId: undefined, event: mockEvent, entityName: 'test' });
369
- expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { name: 'changed', primaryViewableId: 'pv-new' });
370
- expect(result).toBe(mockUpdatedEntity);
371
- });
372
- it('should update entity with only thumbnail changes when no property diffs', async () => {
373
- jest.spyOn(btep, 'getIncomingEntity').mockResolvedValue({ entity: mockExistingEntity });
374
- jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({});
375
- const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
376
- syncInboundImagesSpy.mockResolvedValue(true);
377
- const thumbnailUpdates = { primaryViewableId: 'pv-new' };
378
- syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
379
- const result = await btep.handleIncomingUpsert(mockEvent);
380
- expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { primaryViewableId: 'pv-new' });
351
+ expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { name: 'changed' });
381
352
  expect(result).toBe(mockUpdatedEntity);
382
353
  });
383
354
  it('should not sync images on update when syncInboundImages returns false', async () => {
@@ -9,6 +9,7 @@ export declare enum EventShortMessageStatus {
9
9
  MISSING_INPUT = "Missing_input",
10
10
  NOT_CREATABLE = "Not_creatable",
11
11
  NO_CHANGES = "No_Changes",
12
+ PRIMARY_CONTENT_UPDATED = "Primary_Content_Updated",
12
13
  TOO_MANY_ENTITIES_FOUND = "Too_Many_Entities_Found",
13
14
  UPDATED = "Updated",
14
15
  NOT_PUBLISHABLE = "Not_Publishable",
@@ -13,6 +13,7 @@ var EventShortMessageStatus;
13
13
  EventShortMessageStatus["MISSING_INPUT"] = "Missing_input";
14
14
  EventShortMessageStatus["NOT_CREATABLE"] = "Not_creatable";
15
15
  EventShortMessageStatus["NO_CHANGES"] = "No_Changes";
16
+ EventShortMessageStatus["PRIMARY_CONTENT_UPDATED"] = "Primary_Content_Updated";
16
17
  EventShortMessageStatus["TOO_MANY_ENTITIES_FOUND"] = "Too_Many_Entities_Found";
17
18
  EventShortMessageStatus["UPDATED"] = "Updated";
18
19
  EventShortMessageStatus["NOT_PUBLISHABLE"] = "Not_Publishable";
@@ -119,8 +119,9 @@ class ThumbnailUtil {
119
119
  await this.entities.delete({ entityName: 'content', id: primaryViewableId });
120
120
  }
121
121
  const clearUpdates = await this.getClearPrimaryViewableUpdates();
122
- console.debug(`syncThumbnailToVibeIQ: returning clear updates for entityId=${entityId}`);
123
- return clearUpdates;
122
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: clearUpdates });
123
+ console.debug(`syncThumbnailToVibeIQ: applied clear updates for entityId=${entityId}`);
124
+ return updatedEntity;
124
125
  }
125
126
  if (!thumbnailUrl) {
126
127
  console.debug(`syncThumbnailToVibeIQ: no thumbnail URL for entityId=${entityId}`);
@@ -130,8 +131,9 @@ class ThumbnailUtil {
130
131
  const content = await this.createContentFromFlexPLM(thumbnailUrl, entityId, entityName);
131
132
  await this.entities.update({ entityName: 'content', id: content.id, object: { flexplmThumbnailUrl: thumbnailUrl } });
132
133
  const primaryUpdates = await this.getPrimaryViewableUpdates(content);
134
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: primaryUpdates });
133
135
  console.debug(`syncThumbnailToVibeIQ: created new content ${content.id} for entityId=${entityId}`);
134
- return primaryUpdates;
136
+ return updatedEntity;
135
137
  }
136
138
  const primaryViewable = await this.entities.get({ entityName: 'content', id: primaryViewableId });
137
139
  if (primaryViewable?.flexplmThumbnailUrl === thumbnailUrl) {
@@ -141,9 +143,10 @@ class ThumbnailUtil {
141
143
  const content = await this.createContentFromFlexPLM(thumbnailUrl, entityId, entityName);
142
144
  await this.entities.update({ entityName: 'content', id: content.id, object: { flexplmThumbnailUrl: thumbnailUrl } });
143
145
  const primaryUpdates = await this.getPrimaryViewableUpdates(content);
146
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: primaryUpdates });
144
147
  await this.entities.delete({ entityName: 'content', id: primaryViewableId });
145
148
  console.debug(`syncThumbnailToVibeIQ: replaced content ${primaryViewableId} with ${content.id} for entityId=${entityId}`);
146
- return primaryUpdates;
149
+ return updatedEntity;
147
150
  }
148
151
  async createContentFromFlexPLM(thumbnailUrl, entityId, entityName) {
149
152
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
@@ -276,34 +276,32 @@ describe('ThumbnailUtil Tests', () => {
276
276
  mockEntitiesUpdate.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
277
277
  mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
278
278
  });
279
- it('returns undefined when no thumbnail IDs in event data', async () => {
279
+ it('does not update when no thumbnail IDs in event data', async () => {
280
280
  const event = { data: {} };
281
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
282
- expect(result).toBeUndefined();
281
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
283
282
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
284
283
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
285
284
  expect(mockContentCreate).not.toHaveBeenCalled();
286
285
  });
287
- it('returns undefined when event.data is undefined', async () => {
286
+ it('does not update when event.data is undefined', async () => {
288
287
  const event = {};
289
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
290
- expect(result).toBeUndefined();
288
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
291
289
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
292
290
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
293
291
  });
294
- it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and returns clear updates', async () => {
292
+ it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and updates entity', async () => {
295
293
  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' });
294
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
297
295
  expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'pv1' });
298
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: null, primaryFileUrl: null }));
296
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'color', id: 'entity1' }));
299
297
  });
300
- it('REMOVE_THUMBNAIL with no primaryViewableId returns clear updates without deleting', async () => {
298
+ it('REMOVE_THUMBNAIL with no primaryViewableId updates entity without deleting', async () => {
301
299
  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' });
300
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
303
301
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
304
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: null }));
302
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'item', id: 'entity1' }));
305
303
  });
306
- it('creates new content when no primaryViewableId exists and returns primary viewable updates', async () => {
304
+ it('creates new content when no primaryViewableId exists and updates entity', async () => {
307
305
  const mockResponse = {
308
306
  arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
309
307
  headers: { get: jest.fn().mockReturnValue('image/png') },
@@ -322,7 +320,7 @@ describe('ThumbnailUtil Tests', () => {
322
320
  };
323
321
  mockContentCreate.mockResolvedValue(createdContent);
324
322
  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' });
323
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
326
324
  expect(mockGetRequest).toHaveBeenCalledWith({ urlPath: '/rest/thumbnail/thumb.png', includeUrlContext: false, returnFullResponse: true });
327
325
  expect(mockContentCreate).toHaveBeenCalledWith(expect.objectContaining({
328
326
  fileName: 'thumb.png',
@@ -334,13 +332,12 @@ describe('ThumbnailUtil Tests', () => {
334
332
  id: 'newContent1',
335
333
  object: { flexplmThumbnailUrl: '/rest/thumbnail/thumb.png' },
336
334
  }));
337
- expect(result).toEqual(expect.objectContaining({
338
- primaryViewableId: 'newContent1',
339
- primaryFileUrl: 'https://files/primary.png',
340
- largeViewableDownloadUrl: 'https://files/large.png',
335
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({
336
+ entityName: 'color',
337
+ id: 'entity1',
341
338
  }));
342
339
  });
343
- it('replaces content when primaryViewable.flexplmThumbnailUrl differs and returns updates', async () => {
340
+ it('replaces content when primaryViewable.flexplmThumbnailUrl differs and updates entity', async () => {
344
341
  mockEntitiesGet.mockImplementation((opts) => {
345
342
  if (opts.entityName === 'content-custom-size')
346
343
  return Promise.resolve([]);
@@ -367,17 +364,17 @@ describe('ThumbnailUtil Tests', () => {
367
364
  };
368
365
  mockContentCreate.mockResolvedValue(createdContent);
369
366
  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' });
367
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'oldPv', event, entityName: 'item' });
371
368
  expect(mockContentCreate).toHaveBeenCalled();
372
369
  expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({
373
370
  entityName: 'content',
374
371
  id: 'newContent2',
375
372
  object: { flexplmThumbnailUrl: '/rest/thumbnail/new.jpg' },
376
373
  }));
377
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: 'newContent2' }));
374
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'item', id: 'entity1' }));
378
375
  expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'oldPv' });
379
376
  });
380
- it('returns undefined when primaryViewable.flexplmThumbnailUrl matches', async () => {
377
+ it('does not update when primaryViewable.flexplmThumbnailUrl matches', async () => {
381
378
  const thumbnailUrl = '/rest/thumbnail/same.png';
382
379
  mockEntitiesGet.mockImplementation((opts) => {
383
380
  if (opts.entityName === 'content-custom-size')
@@ -388,8 +385,7 @@ describe('ThumbnailUtil Tests', () => {
388
385
  return Promise.resolve({});
389
386
  });
390
387
  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();
388
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
393
389
  expect(mockContentCreate).not.toHaveBeenCalled();
394
390
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
395
391
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
@@ -14,7 +14,7 @@ export declare class TypeConversionUtils {
14
14
  static getEntityTypePathFromOjbect(fileId: any, mapFileUtil: any, object: any): Promise<string>;
15
15
  static getIdentifierPropertiesFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any): Promise<string[]>;
16
16
  static getInformationalPropertiesFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any): Promise<string[]>;
17
- static getMapKeyFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any, direction: string): Promise<string>;
17
+ static getMapKeyFromObject(fileId: any, mapFileUtil: MapFileUtil, object: any, direction: string): Promise<string | undefined>;
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
20
  static syncInboundImages(fileId: string, mapFileUtil: MapFileUtil, object: any, context?: any): Promise<boolean>;
@@ -179,7 +179,6 @@ class TypeConversionUtils {
179
179
  }
180
180
  return type;
181
181
  }
182
- return '';
183
182
  }
184
183
  static async isInboundCreatableFromObject(fileId, mapFileUtil, object, context) {
185
184
  let isInboundCreatable = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.3.0-alpha.4",
3
+ "version": "1.3.0-alpha.5",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -359,35 +359,16 @@ describe('BaseEntityProcessor', () =>{
359
359
  syncThumbnailSpy = jest.spyOn(ThumbnailUtil.prototype, 'syncThumbnailToVibeIQ').mockResolvedValue(undefined);
360
360
  });
361
361
 
362
- it('should call updateEntity with thumbnail updates after create when syncInboundImages returns true', async () =>{
362
+ it('should call syncThumbnailToVibeIQ with entity after create when syncInboundImages returns true', async () =>{
363
363
  jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: null });
364
364
  jest.spyOn(btep, 'getCreateEntity' as any).mockResolvedValue({ entity: { name: 'new' } });
365
365
  jest.spyOn(btep, 'createEntity').mockResolvedValue(mockCreatedEntity);
366
- const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockCreatedEntity);
367
366
  syncInboundImagesSpy.mockResolvedValue(true);
368
- const thumbnailUpdates = { primaryViewableId: 'pv-new', primaryFileUrl: 'https://files/new.png' };
369
- syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
370
367
 
371
368
  const result = await btep.handleIncomingUpsert(mockEvent);
372
369
 
373
370
  expect(syncThumbnailSpy).toBeCalledTimes(1);
374
371
  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
372
  expect(result).toBe(mockCreatedEntity);
392
373
  });
393
374
 
@@ -404,33 +385,17 @@ describe('BaseEntityProcessor', () =>{
404
385
  expect(result).toBe(mockCreatedEntity);
405
386
  });
406
387
 
407
- it('should merge thumbnail updates with property diffs on update', async () =>{
388
+ it('should call syncThumbnailToVibeIQ with entity and update with property diffs on update', async () =>{
408
389
  jest.spyOn(btep, 'getIncomingEntity' as any).mockResolvedValue({ entity: mockExistingEntity });
409
390
  jest.spyOn(btep, 'getUpdatesForEntity').mockResolvedValue({ name: 'changed' });
410
391
  const updateEntitySpy = jest.spyOn(btep, 'updateEntity').mockResolvedValue(mockUpdatedEntity);
411
392
  syncInboundImagesSpy.mockResolvedValue(true);
412
- const thumbnailUpdates = { primaryViewableId: 'pv-new' };
413
- syncThumbnailSpy.mockResolvedValue(thumbnailUpdates);
414
393
 
415
394
  const result = await btep.handleIncomingUpsert(mockEvent);
416
395
 
417
396
  expect(syncThumbnailSpy).toBeCalledTimes(1);
418
397
  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' });
398
+ expect(updateEntitySpy).toBeCalledWith('test', mockExistingEntity, { name: 'changed' });
434
399
  expect(result).toBe(mockUpdatedEntity);
435
400
  });
436
401
 
@@ -84,15 +84,12 @@ export abstract class BaseEntityProcessor {
84
84
  console.log(statusMsg);
85
85
  return createEntityResponse.earlyReturn;
86
86
  }
87
- const createdEntity = await this.createEntity(this.baseType, createEntityResponse.entity);
87
+ let createdEntity = await this.createEntity(this.baseType, createEntityResponse.entity);
88
88
  const shouldSyncThumbnail = await TypeConversionUtils.syncInboundImages(
89
89
  this.transformMapFile, this.mapFileUtil, event.data
90
90
  );
91
91
  if (shouldSyncThumbnail) {
92
- const thumbnailUpdates = await new ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: createdEntity.id, primaryViewableId: createdEntity.primaryViewableId, event, entityName: this.baseType });
93
- if (thumbnailUpdates) {
94
- await this.updateEntity(this.baseType, createdEntity, thumbnailUpdates);
95
- }
92
+ createdEntity = await new ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: createdEntity.id, primaryViewableId: createdEntity.primaryViewableId, event, entityName: this.baseType }) || createdEntity;
96
93
  }
97
94
  const statusMsg = this.getInboundStatusMessage({
98
95
  status: EventShortMessageStatus.SUCCESS,
@@ -109,13 +106,26 @@ export abstract class BaseEntityProcessor {
109
106
  const shouldSyncThumbnail = await TypeConversionUtils.syncInboundImages(
110
107
  this.transformMapFile, this.mapFileUtil, event.data
111
108
  );
112
- let thumbnailUpdates;
109
+ let thumbnailEntity;
113
110
  if (shouldSyncThumbnail) {
114
- thumbnailUpdates = await new ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: entity.id, primaryViewableId: entity.primaryViewableId, event, entityName: this.baseType });
111
+ thumbnailEntity = await new ThumbnailUtil(this.config).syncThumbnailToVibeIQ({ entityId: entity.id, primaryViewableId: entity.primaryViewableId, event, entityName: this.baseType });
115
112
  }
116
113
 
117
- const allUpdates = { ...diffs, ...thumbnailUpdates };
118
- if(Object.getOwnPropertyNames(allUpdates).length == 0){
114
+ const hasPropertyChanges = Object.getOwnPropertyNames(diffs).length > 0;
115
+
116
+ if (!hasPropertyChanges && thumbnailEntity) {
117
+ const statusMsg = this.getInboundStatusMessage({
118
+ status: EventShortMessageStatus.SUCCESS,
119
+ statusMessage: EventShortMessageStatus.PRIMARY_CONTENT_UPDATED,
120
+ objectClass: event.objectClass,
121
+ entityId: entity.id,
122
+ federatedId: event.federatedId
123
+ });
124
+ console.log(statusMsg);
125
+ return thumbnailEntity;
126
+ }
127
+
128
+ if (!hasPropertyChanges) {
119
129
  const statusMsg = this.getInboundStatusMessage({
120
130
  status: EventShortMessageStatus.SUCCESS,
121
131
  statusMessage: EventShortMessageStatus.NO_CHANGES,
@@ -131,7 +141,7 @@ export abstract class BaseEntityProcessor {
131
141
  };
132
142
  }
133
143
 
134
- const updatedEntity = await this.updateEntity(this.baseType, entity, allUpdates);
144
+ const updatedEntity = await this.updateEntity(this.baseType, entity, diffs);
135
145
  const statusMsg = this.getInboundStatusMessage({
136
146
  status: EventShortMessageStatus.SUCCESS,
137
147
  statusMessage: EventShortMessageStatus.UPDATED,
@@ -9,6 +9,7 @@ export enum EventShortMessageStatus {
9
9
  MISSING_INPUT = 'Missing_input',
10
10
  NOT_CREATABLE = 'Not_creatable',
11
11
  NO_CHANGES = 'No_Changes',
12
+ PRIMARY_CONTENT_UPDATED = 'Primary_Content_Updated',
12
13
  TOO_MANY_ENTITIES_FOUND = 'Too_Many_Entities_Found',
13
14
  UPDATED = 'Updated',
14
15
 
@@ -308,42 +308,40 @@ describe('ThumbnailUtil Tests', () =>{
308
308
  mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
309
309
  });
310
310
 
311
- it('returns undefined when no thumbnail IDs in event data', async () => {
311
+ it('does not update when no thumbnail IDs in event data', async () => {
312
312
  const event = { data: {} };
313
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
313
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
314
314
 
315
- expect(result).toBeUndefined();
316
315
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
317
316
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
318
317
  expect(mockContentCreate).not.toHaveBeenCalled();
319
318
  });
320
319
 
321
- it('returns undefined when event.data is undefined', async () => {
320
+ it('does not update when event.data is undefined', async () => {
322
321
  const event = {};
323
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
322
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
324
323
 
325
- expect(result).toBeUndefined();
326
324
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
327
325
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
328
326
  });
329
327
 
330
- it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and returns clear updates', async () => {
328
+ it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and updates entity', async () => {
331
329
  const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: ThumbnailUtil.REMOVE_THUMBNAIL } };
332
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
330
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
333
331
 
334
332
  expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'pv1' });
335
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: null, primaryFileUrl: null }));
333
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'color', id: 'entity1' }));
336
334
  });
337
335
 
338
- it('REMOVE_THUMBNAIL with no primaryViewableId returns clear updates without deleting', async () => {
336
+ it('REMOVE_THUMBNAIL with no primaryViewableId updates entity without deleting', async () => {
339
337
  const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: ThumbnailUtil.REMOVE_THUMBNAIL } };
340
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
338
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
341
339
 
342
340
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
343
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: null }));
341
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'item', id: 'entity1' }));
344
342
  });
345
343
 
346
- it('creates new content when no primaryViewableId exists and returns primary viewable updates', async () => {
344
+ it('creates new content when no primaryViewableId exists and updates entity', async () => {
347
345
  const mockResponse = {
348
346
  arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
349
347
  headers: { get: jest.fn().mockReturnValue('image/png') },
@@ -364,7 +362,7 @@ describe('ThumbnailUtil Tests', () =>{
364
362
  mockContentCreate.mockResolvedValue(createdContent);
365
363
 
366
364
  const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/thumb.png' } };
367
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
365
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
368
366
 
369
367
  expect(mockGetRequest).toHaveBeenCalledWith({ urlPath: '/rest/thumbnail/thumb.png', includeUrlContext: false, returnFullResponse: true });
370
368
  expect(mockContentCreate).toHaveBeenCalledWith(
@@ -382,15 +380,16 @@ describe('ThumbnailUtil Tests', () =>{
382
380
  object: { flexplmThumbnailUrl: '/rest/thumbnail/thumb.png' },
383
381
  }),
384
382
  );
385
- // Returns primary viewable updates for the entity
386
- expect(result).toEqual(expect.objectContaining({
387
- primaryViewableId: 'newContent1',
388
- primaryFileUrl: 'https://files/primary.png',
389
- largeViewableDownloadUrl: 'https://files/large.png',
390
- }));
383
+ // Updates the main entity
384
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
385
+ expect.objectContaining({
386
+ entityName: 'color',
387
+ id: 'entity1',
388
+ }),
389
+ );
391
390
  });
392
391
 
393
- it('replaces content when primaryViewable.flexplmThumbnailUrl differs and returns updates', async () => {
392
+ it('replaces content when primaryViewable.flexplmThumbnailUrl differs and updates entity', async () => {
394
393
  mockEntitiesGet.mockImplementation((opts) => {
395
394
  if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
396
395
  if (opts.entityName === 'content' && opts.id === 'oldPv') {
@@ -419,7 +418,7 @@ describe('ThumbnailUtil Tests', () =>{
419
418
  mockContentCreate.mockResolvedValue(createdContent);
420
419
 
421
420
  const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/new.jpg' } };
422
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'oldPv', event, entityName: 'item' });
421
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'oldPv', event, entityName: 'item' });
423
422
 
424
423
  // Creates new content
425
424
  expect(mockContentCreate).toHaveBeenCalled();
@@ -431,13 +430,15 @@ describe('ThumbnailUtil Tests', () =>{
431
430
  object: { flexplmThumbnailUrl: '/rest/thumbnail/new.jpg' },
432
431
  }),
433
432
  );
434
- // Returns primary viewable updates for the entity
435
- expect(result).toEqual(expect.objectContaining({ primaryViewableId: 'newContent2' }));
433
+ // Updates the main entity
434
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
435
+ expect.objectContaining({ entityName: 'item', id: 'entity1' }),
436
+ );
436
437
  // Deletes old content
437
438
  expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'oldPv' });
438
439
  });
439
440
 
440
- it('returns undefined when primaryViewable.flexplmThumbnailUrl matches', async () => {
441
+ it('does not update when primaryViewable.flexplmThumbnailUrl matches', async () => {
441
442
  const thumbnailUrl = '/rest/thumbnail/same.png';
442
443
  mockEntitiesGet.mockImplementation((opts) => {
443
444
  if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
@@ -448,9 +449,7 @@ describe('ThumbnailUtil Tests', () =>{
448
449
  });
449
450
 
450
451
  const event = { data: { [ThumbnailUtil.EXISTING_THUMBNAIL_ID]: thumbnailUrl } };
451
- const result = await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
452
-
453
- expect(result).toBeUndefined();
452
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
454
453
  expect(mockContentCreate).not.toHaveBeenCalled();
455
454
  expect(mockEntitiesUpdate).not.toHaveBeenCalled();
456
455
  expect(mockEntitiesDelete).not.toHaveBeenCalled();
@@ -9,6 +9,7 @@ interface ContentCustomSize {
9
9
  name: string;
10
10
  };
11
11
  export class ThumbnailUtil {
12
+ /** The max_thumbnail_size is for limiting the size of the thumbnail being sent to FlexPLM. It is used when checking the size of the auto generated thumbnails (smallViewable, tinyViewable, etc.). */
12
13
  private max_thumbnail_size = 5 * 1_024 * 1_024;
13
14
  private entities: Entities;
14
15
  static NEW_THUMBNAIL_ID = 'NEW_THUMBNAIL_ID';
@@ -140,6 +141,15 @@ export class ThumbnailUtil {
140
141
  }
141
142
  }
142
143
 
144
+ /** Syncs the thumbnail from FlexPLM to VibeIQ. Handles creating, replacing, or removing
145
+ * the primary viewable content and persists the updates directly to the entity.
146
+ *
147
+ * @param entityId - The ID of the entity to update with thumbnail properties.
148
+ * @param primaryViewableId - The existing primary viewable content ID, if any.
149
+ * @param event - The inbound event containing thumbnail data (NEW_THUMBNAIL_ID / EXISTING_THUMBNAIL_ID).
150
+ * @param entityName - The entity type name (e.g. 'item', 'color') used for API calls.
151
+ * @returns The updated entity, or undefined if no thumbnail changes were needed.
152
+ */
143
153
  async syncThumbnailToVibeIQ({ entityId, primaryViewableId, event, entityName }: { entityId: string; primaryViewableId?: string; event: any; entityName: string }): Promise<any> {
144
154
  console.debug(`syncThumbnailToVibeIQ: entityId=${entityId}, primaryViewableId=${primaryViewableId}, entityName=${entityName}`);
145
155
  const eventData = event.data || {};
@@ -153,8 +163,9 @@ export class ThumbnailUtil {
153
163
  await this.entities.delete({ entityName: 'content', id: primaryViewableId });
154
164
  }
155
165
  const clearUpdates = await this.getClearPrimaryViewableUpdates();
156
- console.debug(`syncThumbnailToVibeIQ: returning clear updates for entityId=${entityId}`);
157
- return clearUpdates;
166
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: clearUpdates });
167
+ console.debug(`syncThumbnailToVibeIQ: applied clear updates for entityId=${entityId}`);
168
+ return updatedEntity;
158
169
  }
159
170
 
160
171
  // Early return if no thumbnail URL
@@ -168,8 +179,9 @@ export class ThumbnailUtil {
168
179
  const content = await this.createContentFromFlexPLM(thumbnailUrl, entityId, entityName);
169
180
  await this.entities.update({ entityName: 'content', id: content.id, object: { flexplmThumbnailUrl: thumbnailUrl } });
170
181
  const primaryUpdates = await this.getPrimaryViewableUpdates(content);
182
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: primaryUpdates });
171
183
  console.debug(`syncThumbnailToVibeIQ: created new content ${content.id} for entityId=${entityId}`);
172
- return primaryUpdates;
184
+ return updatedEntity;
173
185
  }
174
186
 
175
187
  // Case 3: Has primaryViewableId — check if thumbnail changed
@@ -182,9 +194,10 @@ export class ThumbnailUtil {
182
194
  const content = await this.createContentFromFlexPLM(thumbnailUrl, entityId, entityName);
183
195
  await this.entities.update({ entityName: 'content', id: content.id, object: { flexplmThumbnailUrl: thumbnailUrl } });
184
196
  const primaryUpdates = await this.getPrimaryViewableUpdates(content);
197
+ const updatedEntity = await this.entities.update({ entityName, id: entityId, object: primaryUpdates });
185
198
  await this.entities.delete({ entityName: 'content', id: primaryViewableId });
186
199
  console.debug(`syncThumbnailToVibeIQ: replaced content ${primaryViewableId} with ${content.id} for entityId=${entityId}`);
187
- return primaryUpdates;
200
+ return updatedEntity;
188
201
  }
189
202
 
190
203
  private async createContentFromFlexPLM(thumbnailUrl: string, entityId: string, entityName: string): Promise<any> {
@@ -323,7 +323,7 @@ export class TypeConversionUtils {
323
323
  return TypeDefaults.getDefaultInformationalPropertiesFromObject(object);
324
324
  }
325
325
 
326
- static async getMapKeyFromObject(fileId, mapFileUtil: MapFileUtil, object: any, direction:string): Promise<string> {
326
+ static async getMapKeyFromObject(fileId, mapFileUtil: MapFileUtil, object: any, direction:string): Promise<string | undefined> {
327
327
  const type = this.getObjectType(object);
328
328
  if(fileId){
329
329
  const mappingData = await mapFileUtil.getMappingSection(fileId, 'typeConversion', direction);
@@ -334,7 +334,6 @@ export class TypeConversionUtils {
334
334
  return type;
335
335
  }
336
336
  //TODO use TypeDefaults?
337
- return '';
338
337
  }
339
338
 
340
339
  static async isInboundCreatableFromObject(fileId: string, mapFileUtil: MapFileUtil, object: any, context?: any): Promise<boolean> {