@contrail/flexplm 1.3.0-alpha.5 → 1.3.0
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.
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- '.github/**'
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [master]
|
|
10
|
+
paths-ignore:
|
|
11
|
+
- '.github/**'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
check-version:
|
|
15
|
+
name: Verify version is available on npm
|
|
16
|
+
if: github.event_name == 'pull_request'
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Check version not already published
|
|
22
|
+
run: |
|
|
23
|
+
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
24
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
25
|
+
echo "Checking if ${PACKAGE_NAME}@${VERSION} exists on npm..."
|
|
26
|
+
if npm view "${PACKAGE_NAME}@${VERSION}" version 2>/dev/null; then
|
|
27
|
+
echo "::error::Version ${VERSION} is already published on npm. Bump the version in package.json before merging."
|
|
28
|
+
exit 1
|
|
29
|
+
else
|
|
30
|
+
echo "Version ${VERSION} is available."
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
publish-alpha:
|
|
34
|
+
name: Publish alpha
|
|
35
|
+
if: github.event_name == 'pull_request'
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
outputs:
|
|
38
|
+
version: ${{ steps.publish.outputs.version }}
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-node@v4
|
|
43
|
+
with:
|
|
44
|
+
node-version: '20'
|
|
45
|
+
registry-url: 'https://registry.npmjs.org'
|
|
46
|
+
|
|
47
|
+
- name: Install and build
|
|
48
|
+
env:
|
|
49
|
+
NPM_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
50
|
+
NODE_AUTH_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
51
|
+
run: npm ci && npm run build
|
|
52
|
+
|
|
53
|
+
- name: Publish alpha
|
|
54
|
+
id: publish
|
|
55
|
+
run: |
|
|
56
|
+
SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)
|
|
57
|
+
VERSION=$(node -p "require('./package.json').version")-alpha.${SHORT_SHA}
|
|
58
|
+
npm version $VERSION --no-git-tag-version
|
|
59
|
+
OUTPUT=$(npm publish --tag alpha 2>&1) && {
|
|
60
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
61
|
+
} || {
|
|
62
|
+
if echo "$OUTPUT" | grep -q "You cannot publish over the previously published versions"; then
|
|
63
|
+
echo "::warning::Alpha ${VERSION} already published, skipping."
|
|
64
|
+
else
|
|
65
|
+
echo "::error::Failed to publish alpha ${VERSION}"
|
|
66
|
+
echo "$OUTPUT"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
env:
|
|
71
|
+
NPM_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
72
|
+
NODE_AUTH_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
73
|
+
|
|
74
|
+
publish-release:
|
|
75
|
+
name: Publish release
|
|
76
|
+
if: github.event_name == 'push'
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
outputs:
|
|
79
|
+
version: ${{ steps.publish.outputs.version }}
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v4
|
|
82
|
+
|
|
83
|
+
- uses: actions/setup-node@v4
|
|
84
|
+
with:
|
|
85
|
+
node-version: '20'
|
|
86
|
+
registry-url: 'https://registry.npmjs.org'
|
|
87
|
+
|
|
88
|
+
- name: Install and build
|
|
89
|
+
env:
|
|
90
|
+
NPM_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
91
|
+
NODE_AUTH_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
92
|
+
run: npm ci && npm run build
|
|
93
|
+
|
|
94
|
+
- name: Publish release
|
|
95
|
+
id: publish
|
|
96
|
+
run: |
|
|
97
|
+
npm publish
|
|
98
|
+
echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
99
|
+
env:
|
|
100
|
+
NPM_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
101
|
+
NODE_AUTH_TOKEN: ${{ secrets.ZACH_NPM_TOKEN_4 }}
|
|
102
|
+
|
|
103
|
+
verify-install:
|
|
104
|
+
name: Verify public install
|
|
105
|
+
needs: [publish-alpha, publish-release]
|
|
106
|
+
if: always() && (needs.publish-alpha.outputs.version || needs.publish-release.outputs.version)
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
steps:
|
|
109
|
+
- name: Install without auth
|
|
110
|
+
run: |
|
|
111
|
+
VERSION="${{ needs.publish-alpha.outputs.version || needs.publish-release.outputs.version }}"
|
|
112
|
+
echo "Verifying @contrail/flexplm@${VERSION} can be installed without auth..."
|
|
113
|
+
mkdir /tmp/install-test && cd /tmp/install-test
|
|
114
|
+
npm init -y > /dev/null
|
|
115
|
+
for attempt in 1 2 3; do
|
|
116
|
+
if npm install @contrail/flexplm@${VERSION} --no-save 2>/dev/null; then
|
|
117
|
+
echo "Install verified successfully."
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
echo "Attempt ${attempt} failed, waiting 10s for registry propagation..."
|
|
121
|
+
sleep 10
|
|
122
|
+
done
|
|
123
|
+
echo "::error::Failed to install @contrail/flexplm@${VERSION} after 3 attempts"
|
|
124
|
+
exit 1
|
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-
|
|
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:
|
|
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
|
@@ -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:
|
|
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,
|