@squiz/component-cli-lib 1.21.1-alpha.7 → 1.22.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.
@@ -1,4 +1,4 @@
1
- import { ComponentSetWebModelForCreate } from '@squiz/component-lib';
1
+ import { ComponentSetWebModelForCreate, Manifest } from '@squiz/component-lib';
2
2
  import { ContentApi } from '@squiz/component-web-api-lib';
3
3
  interface Config {
4
4
  managementServiceUrl: string;
@@ -23,3 +23,5 @@ export declare function deleteComponentSet(webPath: string): Promise<void>;
23
23
  export declare function addComponentSet(componentSet: ComponentSetWebModelForCreate): Promise<void>;
24
24
  export declare function addContentItem(contentItem: ContentApi.ContentItemWebModel): Promise<void>;
25
25
  export declare function deleteContentItem(contentItemId: string): Promise<void>;
26
+ export declare function deleteComponent(manifest: Manifest): Promise<void>;
27
+ export declare function deleteComponents(manifests: Manifest[]): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/component-cli-lib",
3
- "version": "1.21.1-alpha.7",
3
+ "version": "1.22.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -13,16 +13,16 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "devDependencies": {
16
- "@squiz/component-lib": "1.21.1-alpha.7",
17
- "@squiz/component-web-api-lib": "1.21.1-alpha.7",
18
- "@squiz/dx-common-lib": "1.21.1-alpha.7",
19
- "@squiz/dx-json-schema-lib": "1.21.1-alpha.7",
20
- "@squiz/dx-logger-lib": "1.21.1-alpha.7",
21
- "@squiz/virus-scanner-lib": "1.21.1-alpha.7",
16
+ "@squiz/component-lib": "1.22.0",
17
+ "@squiz/component-web-api-lib": "1.22.0",
18
+ "@squiz/dx-common-lib": "1.22.0",
19
+ "@squiz/dx-json-schema-lib": "1.22.0",
20
+ "@squiz/dx-logger-lib": "1.22.0",
21
+ "@squiz/virus-scanner-lib": "1.22.0",
22
22
  "@types/cli-color": "2.0.2",
23
23
  "@types/express": "4.17.17",
24
24
  "@types/jest": "28.1.8",
25
- "@types/node": "17.0.27",
25
+ "@types/node": "18.15.2",
26
26
  "@types/supertest": "2.0.12",
27
27
  "dotenv": "16.0.3",
28
28
  "jest": "29.4.1",
@@ -32,12 +32,12 @@
32
32
  "typescript": "4.9.4"
33
33
  },
34
34
  "dependencies": {
35
- "@squiz/render-runtime-lib": "1.21.1-alpha.7",
35
+ "@squiz/render-runtime-lib": "1.22.0",
36
36
  "archiver": "5.3.1",
37
37
  "axios": "1.3.2",
38
38
  "cli-color": "^2.0.2",
39
39
  "open": "^8.4.0",
40
40
  "supertest": "^6.2.3"
41
41
  },
42
- "gitHead": "3473320147ec8528a9b52c791d28ef3ec0745c97"
42
+ "gitHead": "c683f01b30c8b35cc23c5bd7426778a94467baec"
43
43
  }
@@ -21,7 +21,7 @@ describe('component-dev', () => {
21
21
 
22
22
  it('should fail validation when requesting a function with a missing entry file', async () => {
23
23
  const response = await request.get(
24
- '/r/unit-test-components/test-component/1.0.3/non-existent-entry-file?_componentSet=set&something=not-used',
24
+ '/r/unit-test-components/test-component/1.0.3/main?_componentSet=set&something=not-used',
25
25
  );
26
26
 
27
27
  expect(response.body).toEqual({
@@ -9,7 +9,6 @@ import path from 'path';
9
9
  import { ComponentFunctionService, ComponentSetServiceForLocalDev, ManifestServiceForDev } from '@squiz/component-lib';
10
10
  import open from 'open';
11
11
  import { DevelopmentApiKeyService } from '@squiz/dx-common-lib';
12
- import { JsonValidationService } from '@squiz/dx-json-schema-lib';
13
12
 
14
13
  /**
15
14
  * startDevelopmentRender starts a dev-mode render stack for any
@@ -37,14 +36,14 @@ export function startDevelopmentRender(
37
36
  dataMountPoint,
38
37
  shouldCacheResponses: false,
39
38
  workerTimeout: 5_000,
40
- numOfWorkers: 2,
39
+ minWorkers: 2,
40
+ maxWorkers: 2,
41
41
  },
42
42
  logger,
43
43
  );
44
- const jsonValidationService = new JsonValidationService();
45
- const componentFunctionService = new ComponentFunctionService(rootUrl, jsonValidationService);
44
+ const componentFunctionService = new ComponentFunctionService(rootUrl);
46
45
  const componentSetService = new ComponentSetServiceForLocalDev(logger);
47
- const manifestService = new ManifestServiceForDev(dataMountPoint, logger, jsonValidationService);
46
+ const manifestService = new ManifestServiceForDev(dataMountPoint, logger);
48
47
  const contentItemService = undefined;
49
48
  const renderInputService = new RenderInputService(
50
49
  componentSetService,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @param {object} input
3
+ * @param {ComponentInfo} info
4
+ */
5
+ module.exports = async function (input, info) {
6
+ function hasDxpApiKey(obj) {
7
+ if (typeof obj === 'object' && obj !== null) {
8
+ if (obj.hasOwnProperty('dxpApiKey')) {
9
+ return true;
10
+ }
11
+ for (const prop in obj) {
12
+ if (hasDxpApiKey(obj[prop])) {
13
+ return true;
14
+ }
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+
20
+ if (hasDxpApiKey(input) || hasDxpApiKey(info)) {
21
+ return 'true';
22
+ }
23
+
24
+ return 'false';
25
+ };
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "http://localhost:3000/schemas/v1.json#",
3
+
4
+ "name": "cmp-no-api-key",
5
+ "version": "1.0.0",
6
+ "mainFunction": "main",
7
+ "displayName": "some-display-name",
8
+ "namespace": "smoke-test-components",
9
+ "description": "some-description",
10
+ "functions": [
11
+ {
12
+ "name": "main",
13
+ "entry": "main.js",
14
+ "input": {
15
+ "type": "object",
16
+ "properties": {
17
+ "text": {
18
+ "type": "string",
19
+ "format": "multi-line"
20
+ }
21
+ },
22
+ "required": ["text"]
23
+ },
24
+ "output": { "responseType": "html" }
25
+ }
26
+ ],
27
+ "previews": {
28
+ "test-preview": {
29
+ "functionData": {
30
+ "main": {
31
+ "inputData": {
32
+ "type": "inline",
33
+ "value": {
34
+ "text": "this is a test"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @param {object} input
3
+ */
4
+ module.exports = async function (input) {
5
+ return `<div>Input: ${JSON.stringify(input)}</div>`;
6
+ };
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "http://localhost:3000/schemas/v1.json#",
3
+
4
+ "name": "cmp-property-order",
5
+ "version": "1.0.0",
6
+ "mainFunction": "main",
7
+ "displayName": "some-display-name",
8
+ "namespace": "smoke-test-components",
9
+ "description": "some-description",
10
+ "functions": [
11
+ {
12
+ "name": "main",
13
+ "entry": "main.js",
14
+ "input": {
15
+ "type": "object",
16
+ "properties": {
17
+ "input1": {
18
+ "type": "string"
19
+ },
20
+ "333": {
21
+ "type": "string"
22
+ },
23
+ "input333": {
24
+ "type": "string"
25
+ },
26
+ "\"": {
27
+ "type": "string"
28
+ },
29
+ "~": {
30
+ "type": "string"
31
+ },
32
+ "": {
33
+ "type": "string"
34
+ },
35
+ "$": {
36
+ "type": "string"
37
+ },
38
+ "111": {
39
+ "type": "string"
40
+ },
41
+ "input22": {
42
+ "type": "string"
43
+ },
44
+ "222": {
45
+ "type": "string"
46
+ },
47
+ "*": {
48
+ "type": "string"
49
+ },
50
+ "null": {
51
+ "type": "null",
52
+ "default": null
53
+ }
54
+ },
55
+ "required": []
56
+ },
57
+ "output": { "responseType": "html" }
58
+ }
59
+ ]
60
+ }
@@ -32,7 +32,7 @@
32
32
  "inputData": {
33
33
  "type": "inline",
34
34
  "value": {
35
- "text": "matrix://canary.uat.matrix.squiz.cloud/abc123"
35
+ "text": "matrix-asset://canary.uat.matrix.squiz.cloud/abc123"
36
36
  }
37
37
  }
38
38
  }
@@ -1,15 +1,14 @@
1
- import axios from 'axios';
1
+ import axios, { AxiosError } from 'axios';
2
2
  import path from 'path';
3
3
 
4
4
  import fsp from 'fs/promises';
5
5
  import { randomBytes } from 'crypto';
6
- import { ComponentSetWebModelForCreate, ManifestServiceForDev } from '@squiz/component-lib';
6
+ import { ComponentSetWebModelForCreate, Manifest, ManifestServiceForDev } from '@squiz/component-lib';
7
7
  import { parseEnvVarForVar } from '@squiz/dx-common-lib';
8
8
  import { ContentApi } from '@squiz/component-web-api-lib';
9
9
  import { config } from 'dotenv';
10
10
  import { execSync } from 'child_process';
11
11
  import { getLogger } from '@squiz/dx-logger-lib';
12
- import { JsonValidationService } from '@squiz/dx-json-schema-lib';
13
12
 
14
13
  config();
15
14
 
@@ -112,11 +111,7 @@ export const ci_buildBranch = configObj.ci_buildBranch;
112
111
 
113
112
  export async function getTestComponents() {
114
113
  const componentsDir = path.join(__dirname, '/__components__/');
115
- const manifestService = new ManifestServiceForDev(
116
- componentsDir,
117
- getLogger({ name: 'getTestComponents' }),
118
- new JsonValidationService(),
119
- );
114
+ const manifestService = new ManifestServiceForDev(componentsDir, getLogger({ name: 'getTestComponents' }));
120
115
  return await manifestService.listAllComponentManifests();
121
116
  }
122
117
  export async function createFile(filePath: string, sizeInMB: number) {
@@ -159,3 +154,20 @@ export async function deleteContentItem(contentItemId: string) {
159
154
  //no ops
160
155
  }
161
156
  }
157
+
158
+ export async function deleteComponent(manifest: Manifest) {
159
+ try {
160
+ await managementService.delete(`/component/${manifest.getName()}`);
161
+ await contentService.delete(
162
+ `/content-schema/${manifest.getName()}/${manifest.getVersion()}/${manifest.getComponentFunctionByName().name}`,
163
+ );
164
+ } catch (error) {
165
+ if ((error as AxiosError).response?.status !== 404) {
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+
171
+ export async function deleteComponents(manifests: Manifest[]) {
172
+ await Promise.all(manifests.map((manifest) => deleteComponent(manifest)));
173
+ }
@@ -1,16 +1,16 @@
1
1
  import { uploadComponentFolder } from '../index';
2
2
  import configObj, {
3
- renderService,
4
3
  managementService,
4
+ renderService,
5
5
  getTestComponents,
6
6
  createFile,
7
+ deleteComponents,
7
8
  removeFile,
8
9
  addComponentSet,
9
10
  deleteComponentSet,
10
11
  addContentItem,
11
12
  deleteContentItem,
12
13
  managementServiceRoot,
13
- contentService,
14
14
  } from './helper';
15
15
 
16
16
  import path from 'path';
@@ -19,7 +19,6 @@ import { logger } from '../upload-component-folder';
19
19
  import { ComponentSetWebModelForCreate } from '@squiz/component-lib';
20
20
  import fsp from 'fs/promises';
21
21
  import { randomUUID } from 'crypto';
22
- import { AxiosError } from 'axios';
23
22
 
24
23
  const webPath = 'set-' + randomUUID();
25
24
  const contentItemId = randomUUID();
@@ -36,18 +35,8 @@ describe('uploading a component', () => {
36
35
  // clean up the component added by the test
37
36
  await deleteComponentSet(webPath);
38
37
  await deleteContentItem(contentItemId);
39
-
40
- for (const manifest of await getTestComponents()) {
41
- await managementService.delete(`/component/${manifest.getName()}`).catch(() => null);
42
- await contentService
43
- .delete(
44
- `/content-schema/${manifest.getName()}/${manifest.getVersion()}/${
45
- manifest.getComponentFunctionByName().name
46
- }`,
47
- )
48
- .catch(() => null);
49
- }
50
- // clean up the test componnet files
38
+ await deleteComponents(await getTestComponents());
39
+ // clean up the test component files
51
40
  await fsp.rm(testFilesDir, { force: true, recursive: true });
52
41
  logger.silent = false;
53
42
  });
@@ -102,22 +91,9 @@ describe('uploading a component', () => {
102
91
 
103
92
  describe('Deploy a basic component having a input with multiline format', () => {
104
93
  beforeAll(async () => {
105
- try {
106
- await deleteComponentSet(webPath);
107
- await deleteContentItem(contentItemId);
108
- for (const manifest of await getTestComponents()) {
109
- await managementService.delete(`/component/${manifest.getName()}`);
110
- await contentService.delete(
111
- `/content-schema/${manifest.getName()}/${manifest.getVersion()}/${
112
- manifest.getComponentFunctionByName().name
113
- }`,
114
- );
115
- }
116
- } catch (error: unknown) {
117
- if ((error as AxiosError).response?.status !== 404) {
118
- throw error;
119
- }
120
- }
94
+ await deleteComponentSet(webPath);
95
+ await deleteContentItem(contentItemId);
96
+ await deleteComponents(await getTestComponents());
121
97
  });
122
98
 
123
99
  it('Should upload the component and return a valid url to preview', async () => {
@@ -160,26 +136,27 @@ describe('uploading a component', () => {
160
136
  expect(response.status).toEqual(200);
161
137
  expect(response.data).toEqual(`<div>Input: from-content-item-service</div>`);
162
138
  });
139
+
140
+ it('Should return relevant error if upload fails at the server', async () => {
141
+ // delete the previously uploaded component
142
+ await managementService.delete(`/component/smoke-test-components/cmp-format-string`);
143
+
144
+ // trying uploading the component again should fail at the server
145
+ // due to content item schema already exist matching the component name/version
146
+ const componentPath = path.join(__dirname, '/__components__/cmp-format-string');
147
+ await expect(
148
+ uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir),
149
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
150
+ `"Unexpected response code 500. Error occurred when creating content schema: Content schema smoke-test-components/cmp-format-string/1.0.0/main already exists"`,
151
+ );
152
+ });
163
153
  });
164
154
 
165
155
  describe('Deploy a basic component with input format matrix-asset-uri', () => {
166
156
  beforeAll(async () => {
167
- try {
168
- await deleteComponentSet(webPath);
169
- await deleteContentItem(contentItemId);
170
- for (const manifest of await getTestComponents()) {
171
- await managementService.delete(`/component/${manifest.getName()}`);
172
- await contentService.delete(
173
- `/content-schema/${manifest.getName()}/${manifest.getVersion()}/${
174
- manifest.getComponentFunctionByName().name
175
- }`,
176
- );
177
- }
178
- } catch (error) {
179
- if ((error as AxiosError).response?.status !== 404) {
180
- throw error;
181
- }
182
- }
157
+ await deleteComponentSet(webPath);
158
+ await deleteContentItem(contentItemId);
159
+ await deleteComponents(await getTestComponents());
183
160
  });
184
161
  it('Should upload the component and return a valid url to preview', async () => {
185
162
  const componentPath = path.join(__dirname, '/__components__/matrix-asset-uri');
@@ -231,22 +208,9 @@ describe('uploading a component', () => {
231
208
  const componentPath = path.join(__dirname, '/__components__/cmp-static-file-test');
232
209
 
233
210
  beforeAll(async () => {
234
- try {
235
- await deleteComponentSet(webPath);
236
- await deleteContentItem(contentItemId);
237
- for (const manifest of await getTestComponents()) {
238
- await managementService.delete(`/component/${manifest.getName()}`);
239
- await contentService.delete(
240
- `/content-schema/${manifest.getName()}/${manifest.getVersion()}/${
241
- manifest.getComponentFunctionByName().name
242
- }`,
243
- );
244
- }
245
- } catch (error) {
246
- if ((error as AxiosError).response?.status !== 404) {
247
- throw error;
248
- }
249
- }
211
+ await deleteComponentSet(webPath);
212
+ await deleteContentItem(contentItemId);
213
+ await deleteComponents(await getTestComponents());
250
214
  });
251
215
 
252
216
  it('Should upload the component and return a valid url to preview', async () => {
@@ -351,4 +315,109 @@ describe('uploading a component', () => {
351
315
  );
352
316
  });
353
317
  });
318
+
319
+ describe('Deploy a basic component using and not expose the x-api-key', () => {
320
+ // component to deploy for this test
321
+ const componentPath = path.join(__dirname, '/__components__/cmp-no-api-key');
322
+
323
+ beforeAll(async () => {
324
+ await deleteComponentSet(webPath);
325
+ await deleteContentItem(contentItemId);
326
+ await deleteComponents(await getTestComponents());
327
+ });
328
+
329
+ it('Should upload the component and return a valid url', async () => {
330
+ await uploadComponentFolder(
331
+ managementServiceRoot,
332
+ configObj.managementServiceUrl,
333
+ componentPath,
334
+
335
+ testFilesDir,
336
+ );
337
+
338
+ const uploadedComponent =
339
+ '<a href="/r/smoke-test-components/cmp-no-api-key/1.0.0?_previewKey=test-preview">1.0.0</a>';
340
+ const get = await supertest(configObj.renderServiceUrl).get('/');
341
+ expect(get.status).toEqual(200);
342
+ expect((get as any)?.res?.text).toContain(uploadedComponent);
343
+ });
344
+
345
+ it('Should render component', async () => {
346
+ const componentSet: ComponentSetWebModelForCreate = {
347
+ webPath,
348
+ displayName: 'some-display-name',
349
+ description: 'Set description',
350
+ headers: {},
351
+ environmentVariables: {},
352
+ components: {},
353
+ componentVersionRules: {
354
+ 'smoke-test-components/cmp-no-api-key': {
355
+ renderableVersionPattern: '1.0.0',
356
+ editableVersions: [],
357
+ excludedVersions: [],
358
+ },
359
+ },
360
+ };
361
+
362
+ await addComponentSet(componentSet);
363
+
364
+ const response = await renderService.get(
365
+ `/r/smoke-test-components/cmp-no-api-key/1.0.0/?_componentSet=${webPath}&text=hello`,
366
+ { headers: { 'x-api-key': 'this is key' } },
367
+ );
368
+ expect(response.status).toEqual(200);
369
+ expect(response.data).toEqual(false);
370
+ });
371
+ });
372
+
373
+ describe('Deploy component with properties defined in a specific order', () => {
374
+ // component to deploy for this test
375
+ const componentPath = path.join(__dirname, '/__components__/cmp-property-order');
376
+
377
+ beforeAll(async () => {
378
+ await deleteComponentSet(webPath);
379
+ await deleteContentItem(contentItemId);
380
+ await deleteComponents(await getTestComponents());
381
+
382
+ await uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir);
383
+ });
384
+
385
+ it('Should preserve input field order when uploading schema to content store', async () => {
386
+ const response = await supertest(configObj.contentServiceUrl).get(
387
+ '/content-schema/smoke-test-components/cmp-property-order/1.0.0/main',
388
+ );
389
+
390
+ // Relates to DEVX-891, property order is preserved so that we can reliably draw a UI from it.
391
+ //
392
+ // The sequence here is important and should follow the rules defined here:
393
+ // * https://tc39.es/ecma262/#sec-ordinaryownpropertykeys
394
+ // * https://stackoverflow.com/a/38218582
395
+ //
396
+ // Per EMCA-262 numeric keys appear first in ascending order but all other keys should appear
397
+ // in the order they were originally defined, they should not be affected by them being stored in
398
+ // a JSONB DB column or anything else.
399
+ //
400
+ // Asserting on the encoded contents of the response to avoid any decoding in the test having
401
+ // a potential influence on the outcome.
402
+ const expectedProperties =
403
+ '"111":{"type":"string"},' +
404
+ '"222":{"type":"string"},' +
405
+ '"333":{"type":"string"},' +
406
+ '"input1":{"type":"string"},' +
407
+ '"input333":{"type":"string"},' +
408
+ '"\\"":{"type":"string"},' +
409
+ '"~":{"type":"string"},' +
410
+ '"":{"type":"string"},' +
411
+ '"$":{"type":"string"},' +
412
+ '"input22":{"type":"string"},' +
413
+ '"*":{"type":"string"},' +
414
+ '"null":{"type":"null","default":null}';
415
+
416
+ expect(response.status).toEqual(200);
417
+ expect(response.text).toEqual(
418
+ `{"name":"smoke-test-components/cmp-property-order/1.0.0/main",` +
419
+ `"schema":{"type":"object","properties":{${expectedProperties}},"required":[]},"contentSchemaType":"component"}`,
420
+ );
421
+ });
422
+ });
354
423
  });
@@ -7,7 +7,6 @@ import path from 'path';
7
7
  import { AxiosResponse, AxiosError, AxiosInstance } from 'axios';
8
8
  import color from 'cli-color';
9
9
  import { getLogger, Logger } from '@squiz/dx-logger-lib';
10
- import { JsonValidationService } from '@squiz/dx-json-schema-lib';
11
10
 
12
11
  export const logger: Logger = getLogger({ name: 'upload-component', format: 'human' });
13
12
 
@@ -44,7 +43,8 @@ export async function uploadComponentFolder(
44
43
  logger.info(`deployment id: ${initialUpload.id} status: ${color.green('success')}`);
45
44
  logger.info(`uploaded location: ${result.accessLink}`);
46
45
  } else {
47
- logger.error('failed for an unknown reason', color.red(result));
46
+ const message = result?.message ?? 'unknown';
47
+ logger.error(`failed due an unexpected reason: ${message}`);
48
48
  }
49
49
  } catch (e) {
50
50
  await fsp.rm(tmpDir, { force: true, recursive: true });
@@ -54,7 +54,7 @@ export async function uploadComponentFolder(
54
54
  }
55
55
 
56
56
  async function preUploadChecks(apiClient: AxiosInstance, managementURL: string, folderPath: string) {
57
- const service = new ManifestServiceForDev(folderPath, logger, new JsonValidationService());
57
+ const service = new ManifestServiceForDev(folderPath, logger);
58
58
  const manifestPath = path.join(folderPath, `manifest.json`);
59
59
 
60
60
  const result = await service.readManifest(manifestPath);
@@ -136,10 +136,11 @@ function handleError(error: any): Error {
136
136
  const newError = new Error(errorMessage || 'An error has occurred');
137
137
 
138
138
  if (isAxiosError(error)) {
139
+ const errorCode = response.status ?? 'unknown';
139
140
  if (response?.data?.message) {
140
- newError.message = `${errorMessage} (code: ${response.status})`;
141
+ newError.message = `Unexpected response code ${errorCode}. ${response.data.message}`.trim();
141
142
  } else {
142
- newError.message = `Unexpected response code ${response.status}. ${errorMessage}`.trim();
143
+ newError.message = `${errorMessage} (code: ${errorCode})`;
143
144
  }
144
145
  }
145
146