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

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,7 +7,7 @@ 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
10
+ ## [1.3.0] - 2026-04-15
11
11
  ### Added
12
12
  - Added inbound thumbnail/primary content syncing from FlexPLM to VibeIQ via `ThumbnailUtil.syncThumbnailToVibeIQ`.
13
13
  - Added `syncInboundImages` and `syncOutboundImages` methods to `TypeConversionUtils` for controlling image sync per map file configuration.
@@ -149,9 +149,12 @@ class ThumbnailUtil {
149
149
  return updatedEntity;
150
150
  }
151
151
  async createContentFromFlexPLM(thumbnailUrl, entityId, entityName) {
152
+ const urlParts = thumbnailUrl.split('/');
153
+ const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
154
+ const encodedUrl = urlParts.map(part => encodeURIComponent(part)).join('/');
152
155
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
153
156
  const response = await flexPLMConnect.getRequest({
154
- urlPath: thumbnailUrl,
157
+ urlPath: encodedUrl,
155
158
  includeUrlContext: false,
156
159
  returnFullResponse: true,
157
160
  });
@@ -159,8 +162,6 @@ class ThumbnailUtil {
159
162
  const buffer = Buffer.from(fileBuffer);
160
163
  const contentTypeHeader = response.headers.get('content-type');
161
164
  const contentType = contentTypeHeader ? contentTypeHeader.split(';')[0] : 'application/octet-stream';
162
- const urlParts = thumbnailUrl.split('/');
163
- const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
164
165
  const contentHolderReference = `${entityName}:${entityId}`;
165
166
  const content = await new sdk_1.Content().create({
166
167
  fileBuffer: buffer,
@@ -337,6 +337,46 @@ describe('ThumbnailUtil Tests', () => {
337
337
  id: 'entity1',
338
338
  }));
339
339
  });
340
+ it('encodes URL path segments with special characters', async () => {
341
+ const mockResponse = {
342
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
343
+ headers: { get: jest.fn().mockReturnValue('image/png') },
344
+ };
345
+ mockGetRequest.mockResolvedValue(mockResponse);
346
+ mockContentCreate.mockResolvedValue({
347
+ id: 'c1', contentType: 'image/png', fileName: 'my image.png',
348
+ primaryFileUrl: 'https://files/primary.png', largeViewableUrl: null,
349
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
350
+ });
351
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/my image.png' } };
352
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
353
+ expect(mockGetRequest).toHaveBeenCalledWith({
354
+ urlPath: '/rest/thumbnail/my%20image.png',
355
+ includeUrlContext: false,
356
+ returnFullResponse: true,
357
+ });
358
+ expect(mockContentCreate).toHaveBeenCalledWith(expect.objectContaining({ fileName: 'my image.png' }));
359
+ });
360
+ it('encodes URL path segments with unicode characters', async () => {
361
+ const mockResponse = {
362
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
363
+ headers: { get: jest.fn().mockReturnValue('image/jpeg') },
364
+ };
365
+ mockGetRequest.mockResolvedValue(mockResponse);
366
+ mockContentCreate.mockResolvedValue({
367
+ id: 'c2', contentType: 'image/jpeg', fileName: 'café-logo.jpg',
368
+ primaryFileUrl: 'https://files/primary.jpg', largeViewableUrl: null,
369
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
370
+ });
371
+ const event = { data: { [thumbnail_util_1.ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/café-logo.jpg' } };
372
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
373
+ expect(mockGetRequest).toHaveBeenCalledWith({
374
+ urlPath: '/rest/thumbnail/caf%C3%A9-logo.jpg',
375
+ includeUrlContext: false,
376
+ returnFullResponse: true,
377
+ });
378
+ expect(mockContentCreate).toHaveBeenCalledWith(expect.objectContaining({ fileName: 'café-logo.jpg' }));
379
+ });
340
380
  it('replaces content when primaryViewable.flexplmThumbnailUrl differs and updates entity', async () => {
341
381
  mockEntitiesGet.mockImplementation((opts) => {
342
382
  if (opts.entityName === 'content-custom-size')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.3.0-alpha.5",
3
+ "version": "1.3.0-alpha.6",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -389,6 +389,56 @@ describe('ThumbnailUtil Tests', () =>{
389
389
  );
390
390
  });
391
391
 
392
+ it('encodes URL path segments with special characters', async () => {
393
+ const mockResponse = {
394
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
395
+ headers: { get: jest.fn().mockReturnValue('image/png') },
396
+ };
397
+ mockGetRequest.mockResolvedValue(mockResponse);
398
+ mockContentCreate.mockResolvedValue({
399
+ id: 'c1', contentType: 'image/png', fileName: 'my image.png',
400
+ primaryFileUrl: 'https://files/primary.png', largeViewableUrl: null,
401
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
402
+ });
403
+
404
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/my image.png' } };
405
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
406
+
407
+ expect(mockGetRequest).toHaveBeenCalledWith({
408
+ urlPath: '/rest/thumbnail/my%20image.png',
409
+ includeUrlContext: false,
410
+ returnFullResponse: true,
411
+ });
412
+ expect(mockContentCreate).toHaveBeenCalledWith(
413
+ expect.objectContaining({ fileName: 'my image.png' }),
414
+ );
415
+ });
416
+
417
+ it('encodes URL path segments with unicode characters', async () => {
418
+ const mockResponse = {
419
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
420
+ headers: { get: jest.fn().mockReturnValue('image/jpeg') },
421
+ };
422
+ mockGetRequest.mockResolvedValue(mockResponse);
423
+ mockContentCreate.mockResolvedValue({
424
+ id: 'c2', contentType: 'image/jpeg', fileName: 'café-logo.jpg',
425
+ primaryFileUrl: 'https://files/primary.jpg', largeViewableUrl: null,
426
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
427
+ });
428
+
429
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/café-logo.jpg' } };
430
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
431
+
432
+ expect(mockGetRequest).toHaveBeenCalledWith({
433
+ urlPath: '/rest/thumbnail/caf%C3%A9-logo.jpg',
434
+ includeUrlContext: false,
435
+ returnFullResponse: true,
436
+ });
437
+ expect(mockContentCreate).toHaveBeenCalledWith(
438
+ expect.objectContaining({ fileName: 'café-logo.jpg' }),
439
+ );
440
+ });
441
+
392
442
  it('replaces content when primaryViewable.flexplmThumbnailUrl differs and updates entity', async () => {
393
443
  mockEntitiesGet.mockImplementation((opts) => {
394
444
  if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
@@ -201,9 +201,13 @@ export class ThumbnailUtil {
201
201
  }
202
202
 
203
203
  private async createContentFromFlexPLM(thumbnailUrl: string, entityId: string, entityName: string): Promise<any> {
204
+ const urlParts = thumbnailUrl.split('/');
205
+ const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
206
+
207
+ const encodedUrl = urlParts.map(part => encodeURIComponent(part)).join('/');
204
208
  const flexPLMConnect = new FlexPLMConnect(this.config);
205
209
  const response = await flexPLMConnect.getRequest({
206
- urlPath: thumbnailUrl,
210
+ urlPath: encodedUrl,
207
211
  includeUrlContext: false,
208
212
  returnFullResponse: true,
209
213
  }) as Response;
@@ -213,9 +217,6 @@ export class ThumbnailUtil {
213
217
  const contentTypeHeader = response.headers.get('content-type');
214
218
  const contentType = contentTypeHeader ? contentTypeHeader.split(';')[0] : 'application/octet-stream';
215
219
 
216
- const urlParts = thumbnailUrl.split('/');
217
- const fileName = urlParts[urlParts.length - 1] || 'thumbnail';
218
-
219
220
  const contentHolderReference = `${entityName}:${entityId}`;
220
221
  const content = await new Content().create({
221
222
  fileBuffer: buffer,