@appsemble/utils 0.29.8 → 0.29.10

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.29.8/config/assets/logo.svg) Appsemble Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.29.10/config/assets/logo.svg) Appsemble Utilities
2
2
 
3
3
  > Internal utility functions used across multiple Appsemble projects.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/utils)](https://www.npmjs.com/package/@appsemble/utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.29.8/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.29.8)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.29.10/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.29.10)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -26,5 +26,5 @@ not guaranteed.
26
26
 
27
27
  ## License
28
28
 
29
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.29.8/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.29.10/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
@@ -96,6 +96,7 @@ The most basic resource has a \`schema\` property and defines the minimal securi
96
96
  { $ref: '#/components/schemas/TabsPageDefinition' },
97
97
  { $ref: '#/components/schemas/FlowPageDefinition' },
98
98
  { $ref: '#/components/schemas/LoopPageDefinition' },
99
+ { $ref: '#/components/schemas/ContainerPageDefinition' },
99
100
  ],
100
101
  },
101
102
  },
@@ -0,0 +1 @@
1
+ export declare const ContainerPageDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -0,0 +1,55 @@
1
+ import { BasePageDefinition } from './BasePageDefinition.js';
2
+ import { extendJSONSchema } from './utils.js';
3
+ export const ContainerPageDefinition = extendJSONSchema(BasePageDefinition, {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ description: `Use this page type to group pages in the menu, this doesn't actually group pages for now. Following is an example of how this can be used
7
+ \`\`\`yaml
8
+ pages:
9
+ - name: Page 1
10
+ type: container
11
+ pages:
12
+ - name: Contained page 1
13
+ blocks:
14
+ - type: action-button
15
+ version: 0.29.8
16
+ parameters:
17
+ icon: git-alt
18
+ actions:
19
+ onClick:
20
+ type: link
21
+ to: Contained page 2
22
+ - name: Contained page 2
23
+ blocks:
24
+ - type: action-button
25
+ version: 0.29.8
26
+ parameters:
27
+ icon: git-alt
28
+ actions:
29
+ onClick:
30
+ type: link
31
+ to: Contained page 1
32
+ \`\`\`
33
+ `,
34
+ required: ['type', 'pages'],
35
+ properties: {
36
+ type: {
37
+ enum: ['container'],
38
+ },
39
+ pages: {
40
+ type: 'array',
41
+ minItems: 1,
42
+ description: 'The pages of the app.',
43
+ items: {
44
+ anyOf: [
45
+ { $ref: '#/components/schemas/PageDefinition' },
46
+ { $ref: '#/components/schemas/TabsPageDefinition' },
47
+ { $ref: '#/components/schemas/FlowPageDefinition' },
48
+ { $ref: '#/components/schemas/LoopPageDefinition' },
49
+ { $ref: '#/components/schemas/ContainerPageDefinition' },
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ });
55
+ //# sourceMappingURL=ContainerPageDefinition.js.map
@@ -20,6 +20,7 @@ export * from './BlockVersion.js';
20
20
  export * from './ConditionActionDefinition.js';
21
21
  export * from './CronDefinition.js';
22
22
  export * from './ContainerDefinition.js';
23
+ export * from './ContainerPageDefinition.js';
23
24
  export * from './CustomFontDefinition.js';
24
25
  export * from './DialogActionDefinition.js';
25
26
  export * from './DialogErrorActionDefinition.js';
@@ -20,6 +20,7 @@ export * from './BlockVersion.js';
20
20
  export * from './ConditionActionDefinition.js';
21
21
  export * from './CronDefinition.js';
22
22
  export * from './ContainerDefinition.js';
23
+ export * from './ContainerPageDefinition.js';
23
24
  export * from './CustomFontDefinition.js';
24
25
  export * from './DialogActionDefinition.js';
25
26
  export * from './DialogErrorActionDefinition.js';
@@ -187,6 +187,16 @@ export const paths = {
187
187
  },
188
188
  },
189
189
  },
190
+ head: {
191
+ tags: ['asset'],
192
+ description: 'Get the headers for a single asset',
193
+ operationId: 'getAssetHeadersById',
194
+ responses: {
195
+ 200: {
196
+ description: 'The asset that matches the given id.',
197
+ },
198
+ },
199
+ },
190
200
  delete: {
191
201
  tags: ['asset'],
192
202
  description: 'Remove an existing asset',
@@ -0,0 +1,2 @@
1
+ import { type PageDefinition } from '@appsemble/types';
2
+ export declare function findPageByName(pages: PageDefinition[], name: string): PageDefinition | undefined;
@@ -0,0 +1,15 @@
1
+ export function findPageByName(pages, name) {
2
+ for (const page of pages) {
3
+ if (page.name === name) {
4
+ return page;
5
+ }
6
+ if (page.type === 'container' && page.pages) {
7
+ const found = findPageByName(page.pages, name);
8
+ if (found) {
9
+ return found;
10
+ }
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+ //# sourceMappingURL=findPageByName.js.map
package/index.d.ts CHANGED
@@ -30,3 +30,5 @@ export * from './validation.js';
30
30
  export * from './serializeResource.js';
31
31
  export * from './convertToCsv.js';
32
32
  export * from './assets.js';
33
+ export * from './validateAppMessages.js';
34
+ export * from './findPageByName.js';
package/index.js CHANGED
@@ -30,4 +30,6 @@ export * from './validation.js';
30
30
  export * from './serializeResource.js';
31
31
  export * from './convertToCsv.js';
32
32
  export * from './assets.js';
33
+ export * from './validateAppMessages.js';
34
+ export * from './findPageByName.js';
33
35
  //# sourceMappingURL=index.js.map
package/iterApp.js CHANGED
@@ -119,6 +119,13 @@ export function iterPage(page, callbacks, prefix = []) {
119
119
  return (result ||
120
120
  ['steps.first', 'steps', 'steps.last'].some((suffix) => iterBlockList(page.foreach.blocks, callbacks, [...prefix, suffix, 'blocks'])));
121
121
  }
122
+ if (page.type === 'container') {
123
+ let result = false;
124
+ for (const containerPage of page.pages) {
125
+ result = iterPage(containerPage, callbacks, prefix);
126
+ }
127
+ return result;
128
+ }
122
129
  return iterBlockList(page.blocks, callbacks, [...prefix, 'blocks']);
123
130
  }
124
131
  /**
package/iterApp.test.js CHANGED
@@ -235,6 +235,33 @@ describe('iterPage', () => {
235
235
  expect(onPage).toHaveBeenCalledWith(page, []);
236
236
  expect(result).toBe(false);
237
237
  });
238
+ it('should iterate over a container page', () => {
239
+ const onBlockList = vi.fn();
240
+ const onPage = vi.fn();
241
+ const page = {
242
+ name: 'Container Page',
243
+ type: 'container',
244
+ pages: [
245
+ {
246
+ name: 'Page 1',
247
+ blocks: [],
248
+ },
249
+ {
250
+ name: 'Page 2',
251
+ blocks: [],
252
+ },
253
+ ],
254
+ };
255
+ const result = iterPage(page, { onBlockList, onPage });
256
+ expect(onBlockList).toHaveBeenCalledWith(page.pages[0].blocks, [
257
+ 'blocks',
258
+ ]);
259
+ expect(onBlockList).toHaveBeenCalledWith(page.pages[1].blocks, [
260
+ 'blocks',
261
+ ]);
262
+ expect(onPage).toHaveBeenCalledWith(page, []);
263
+ expect(result).toBe(false);
264
+ });
238
265
  it('should abort if onPage returns true', () => {
239
266
  const onBlockList = vi.fn();
240
267
  const onPage = vi.fn().mockReturnValue(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/utils",
3
- "version": "0.29.8",
3
+ "version": "0.29.10",
4
4
  "description": "Utility functions used in Appsemble internally",
5
5
  "keywords": [
6
6
  "app",
@@ -37,7 +37,7 @@
37
37
  "test": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@appsemble/types": "0.29.8",
40
+ "@appsemble/types": "0.29.10",
41
41
  "axios": "^1.0.0",
42
42
  "cron-parser": "^4.0.0",
43
43
  "date-fns": "^2.0.0",
@@ -0,0 +1,5 @@
1
+ import { type AppDefinition, type AppsembleMessages } from '@appsemble/types';
2
+ export declare class AppMessageValidationError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function validateMessages(messages: AppsembleMessages, app: AppDefinition): void;
@@ -0,0 +1,71 @@
1
+ import { extractAppMessages } from './appMessages.js';
2
+ import { normalizeBlockName } from './blockUtils.js';
3
+ import { has } from './has.js';
4
+ export class AppMessageValidationError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'AppMessageValidationError';
8
+ }
9
+ }
10
+ export function validateMessages(messages, app) {
11
+ var _a, _b;
12
+ const blockMessageKeys = {};
13
+ const extractedMessages = extractAppMessages(app, (block) => {
14
+ const type = normalizeBlockName(block.type);
15
+ if (blockMessageKeys[type]) {
16
+ blockMessageKeys[type][block.version] = {};
17
+ }
18
+ else {
19
+ blockMessageKeys[type] = {
20
+ [block.version]: {},
21
+ };
22
+ }
23
+ });
24
+ if (messages.messageIds) {
25
+ Object.keys(messages.messageIds).map((key) => {
26
+ if (typeof messages.messageIds[key] !== 'string') {
27
+ throw new AppMessageValidationError(`Not allowed to have non-string message ${messages.messageIds[key]}`);
28
+ }
29
+ });
30
+ }
31
+ if (messages.app) {
32
+ Object.keys(messages.app).map((key) => {
33
+ const match = /^(pages\.[\dA-Za-z-]+(\..+)?)\.blocks\.\d+.+/.exec(key);
34
+ if ((!has(extractedMessages === null || extractedMessages === void 0 ? void 0 : extractedMessages.app, key) || typeof messages.app[key] !== 'string') && !match) {
35
+ throw new AppMessageValidationError(`Invalid key ${key}`);
36
+ }
37
+ });
38
+ }
39
+ const blockMessages = {};
40
+ if (messages.blocks) {
41
+ for (const key of Object.keys(messages.blocks)) {
42
+ if (!has(blockMessageKeys, key)) {
43
+ throw new AppMessageValidationError(`Invalid translation key: blocks.${key}\nThis block is not used in the app`);
44
+ }
45
+ }
46
+ }
47
+ const coreMessages = (_a = messages.core) !== null && _a !== void 0 ? _a : {};
48
+ for (const [key, value] of Object.entries(coreMessages)) {
49
+ if (typeof value !== 'string') {
50
+ throw new AppMessageValidationError(`Invalid translation key: core.${key}`);
51
+ }
52
+ }
53
+ for (const [blockName] of Object.entries(blockMessageKeys)) {
54
+ if ((_b = messages.blocks) === null || _b === void 0 ? void 0 : _b[blockName]) {
55
+ blockMessages[blockName] = {};
56
+ for (const [version, oldValues] of Object.entries(messages.blocks[blockName])) {
57
+ if (!has(blockMessageKeys[blockName], version)) {
58
+ throw new AppMessageValidationError(`Invalid translation key: blocks.${blockName}.${version}
59
+ This block version is not used in the app`);
60
+ }
61
+ for (const [oldValueKey, oldValue] of Object.entries(messages.blocks[blockName][version])) {
62
+ if (typeof oldValue !== 'string') {
63
+ throw new AppMessageValidationError(`Invalid translation key: blocks.${blockName}.${version}.${oldValueKey}`);
64
+ }
65
+ blockMessages[blockName][version] = oldValues;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ //# sourceMappingURL=validateAppMessages.js.map
package/validation.js CHANGED
@@ -3,7 +3,7 @@ import { ValidationError, Validator } from 'jsonschema';
3
3
  import languageTags from 'language-tags';
4
4
  import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
5
5
  import { has } from './has.js';
6
- import { partialNormalized } from './index.js';
6
+ import { findPageByName, normalize, partialNormalized } from './index.js';
7
7
  import { iterApp } from './iterApp.js';
8
8
  import { serverActions } from './serverActions.js';
9
9
  /**
@@ -34,6 +34,37 @@ function validateJSONSchema(schema, prefix, report) {
34
34
  }
35
35
  }
36
36
  }
37
+ /**
38
+ * Validates the pages in the app definition to ensure there are no duplicate page names.
39
+ *
40
+ * @param definition The definition of the app
41
+ * @param report A function used to report a value.
42
+ */
43
+ function validateUniquePageNames(definition, report) {
44
+ if (!definition.pages) {
45
+ return;
46
+ }
47
+ const pageNames = new Map();
48
+ function checkPages(pages, parentPath = []) {
49
+ for (const page of pages) {
50
+ const pageName = page.name;
51
+ const normalizedPageName = normalize(page.name);
52
+ const pagePath = [...parentPath, pageName];
53
+ if (pageNames.has(normalizedPageName)) {
54
+ const paths = pageNames.get(normalizedPageName);
55
+ paths.push(pagePath);
56
+ report(pageName, 'is a duplicate page name', pagePath);
57
+ }
58
+ else {
59
+ pageNames.set(normalizedPageName, [pagePath]);
60
+ }
61
+ if (page.type === 'container') {
62
+ checkPages(page.pages, pagePath);
63
+ }
64
+ }
65
+ }
66
+ checkPages(definition.pages);
67
+ }
37
68
  function validateUsersSchema(definition, report) {
38
69
  var _a;
39
70
  if (!definition.users) {
@@ -462,7 +493,7 @@ function validateLanguage({ defaultLanguage }, report) {
462
493
  }
463
494
  }
464
495
  function validateDefaultPage({ defaultPage, pages }, report) {
465
- const page = pages === null || pages === void 0 ? void 0 : pages.find((p) => p.name === defaultPage);
496
+ const page = findPageByName(pages, defaultPage);
466
497
  if (!page) {
467
498
  report(defaultPage, 'does not refer to an existing page', ['defaultPage']);
468
499
  return;
@@ -606,7 +637,7 @@ function validateActions(definition, report) {
606
637
  return;
607
638
  }
608
639
  const [toBase, toSub] = [].concat(to);
609
- const toPage = definition.pages.find(({ name }) => name === toBase);
640
+ const toPage = findPageByName(definition.pages, toBase);
610
641
  if (!toPage) {
611
642
  report(to, 'refers to a page that doesn’t exist', [...path, 'to']);
612
643
  return;
@@ -833,6 +864,7 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
833
864
  validateBlocks(definition, blockVersionMap, report);
834
865
  validateActions(definition, report);
835
866
  validateEvents(definition, blockVersionMap, report);
867
+ validateUniquePageNames(definition, report);
836
868
  }
837
869
  catch (error) {
838
870
  report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);
@@ -43,6 +43,16 @@ function createTestApp() {
43
43
  { name: 'Step B', blocks: [] },
44
44
  ],
45
45
  },
46
+ {
47
+ name: 'Container Page 1',
48
+ type: 'container',
49
+ pages: [
50
+ {
51
+ name: 'Contained Page',
52
+ blocks: [],
53
+ },
54
+ ],
55
+ },
46
56
  ],
47
57
  };
48
58
  }
@@ -85,6 +95,24 @@ describe('validateAppDefinition', () => {
85
95
  ]),
86
96
  ]);
87
97
  });
98
+ it('should report duplicate page names', async () => {
99
+ const app = createTestApp();
100
+ app.pages.push({
101
+ name: 'Container Page',
102
+ type: 'container',
103
+ pages: [{ name: 'Test Page', blocks: [] }],
104
+ });
105
+ const result = await validateAppDefinition(app, () => [
106
+ { name: '@appsemble/test', version: '0.0.0', files: [], languages: [] },
107
+ ]);
108
+ expect(result.valid).toBe(false);
109
+ expect(result.errors).toStrictEqual([
110
+ new ValidationError('is a duplicate page name', 'Test Page', undefined, [
111
+ 'Container Page',
112
+ 'Test Page',
113
+ ]),
114
+ ]);
115
+ });
88
116
  it('should validate block parameters', async () => {
89
117
  const app = createTestApp();
90
118
  app.pages[0].blocks.push({
@@ -1445,6 +1473,10 @@ describe('validateAppDefinition', () => {
1445
1473
  ]),
1446
1474
  ]);
1447
1475
  });
1476
+ it('should check if the default page exists inside contained page', async () => {
1477
+ const result = await validateAppDefinition({ ...createTestApp(), defaultPage: 'Contained Page' }, () => []);
1478
+ expect(result.valid).toBe(true);
1479
+ });
1448
1480
  it('should validate the default page doesn’t specify parameters', async () => {
1449
1481
  const result = await validateAppDefinition({ ...createTestApp(), defaultPage: 'Page with parameters' }, () => []);
1450
1482
  expect(result.valid).toBe(false);