@appsemble/lang-sdk 0.40.9 → 0.40.11-test.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.
- package/constants/patterns.d.ts +6 -0
- package/constants/patterns.js +6 -0
- package/package.json +1 -1
- package/schemas/AppDefinition.js +25 -1
- package/schemas/BlockDefinition.js +14 -1
- package/schemas/Theme.js +33 -1
- package/schemas/actions/EmailActionDefinition.js +1 -1
- package/types/block.d.ts +8 -0
- package/types/core.d.ts +12 -2
- package/types/theme.d.ts +7 -0
- package/validation.js +111 -24
package/constants/patterns.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export declare const partialNormalized: RegExp;
|
|
|
18
18
|
* A pattern for exactly matching a lower case hyphen separated string.
|
|
19
19
|
*/
|
|
20
20
|
export declare const normalized: RegExp;
|
|
21
|
+
/**
|
|
22
|
+
* A pattern for validating a theme variable (custom CSS property) value.
|
|
23
|
+
*
|
|
24
|
+
* Any single-line CSS value that cannot escape its declaration block is allowed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const themeVariableValue: RegExp;
|
|
21
27
|
/**
|
|
22
28
|
* A pattern for matching the block name pattern of @organization/project.
|
|
23
29
|
*/
|
package/constants/patterns.js
CHANGED
|
@@ -19,6 +19,12 @@ export const partialNormalized = /([0-9a-z](?:(?!.*--)[-0-9a-z]*[0-9a-z])?)/;
|
|
|
19
19
|
* A pattern for exactly matching a lower case hyphen separated string.
|
|
20
20
|
*/
|
|
21
21
|
export const normalized = new RegExp(`^${partialNormalized.source}$`);
|
|
22
|
+
/**
|
|
23
|
+
* A pattern for validating a theme variable (custom CSS property) value.
|
|
24
|
+
*
|
|
25
|
+
* Any single-line CSS value that cannot escape its declaration block is allowed.
|
|
26
|
+
*/
|
|
27
|
+
export const themeVariableValue = /^[^\n\r;{}]+$/;
|
|
22
28
|
/**
|
|
23
29
|
* A pattern for matching the block name pattern of @organization/project.
|
|
24
30
|
*/
|
package/package.json
CHANGED
package/schemas/AppDefinition.js
CHANGED
|
@@ -27,7 +27,27 @@ This will be displayed on the app store.
|
|
|
27
27
|
description: 'Properties related to the layout of the app.',
|
|
28
28
|
},
|
|
29
29
|
notifications: {
|
|
30
|
-
|
|
30
|
+
oneOf: [
|
|
31
|
+
{ enum: ['login', 'opt-in', 'startup'] },
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
required: ['roles'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
properties: {
|
|
37
|
+
default: {
|
|
38
|
+
enum: ['login', 'none', 'opt-in', 'startup'],
|
|
39
|
+
description: `The strategy for roles that aren’t listed in \`roles\`, and for users without
|
|
40
|
+
a role. Defaults to \`none\`.`,
|
|
41
|
+
},
|
|
42
|
+
roles: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
minProperties: 1,
|
|
45
|
+
description: 'The strategy per role.',
|
|
46
|
+
additionalProperties: { enum: ['login', 'none', 'opt-in'] },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
31
51
|
description: `The strategy to use for apps to subscribe to push notifications.
|
|
32
52
|
|
|
33
53
|
If specified, push notifications can be sent to subscribed users via the _Notifications_ tab in the
|
|
@@ -37,6 +57,10 @@ to \`startup\` will cause Appsemble to immediately request for the permission up
|
|
|
37
57
|
If this is set to \`login\`, the app will request permissions for push notification once the user
|
|
38
58
|
logs in.
|
|
39
59
|
|
|
60
|
+
To use a different strategy per role, set \`roles\` to a map of role names to \`login\`, \`opt-in\`, or
|
|
61
|
+
\`none\`. Roles that aren’t listed use \`default\`. A member with several roles gets the most eager
|
|
62
|
+
strategy of those roles. Only roles assigned to the member count, not roles they inherit.
|
|
63
|
+
|
|
40
64
|
> **Note**: Setting \`notifications\` to \`startup\` is not recommended, due to its invasive nature.
|
|
41
65
|
`,
|
|
42
66
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { partialNormalized, semver } from '../constants/index.js';
|
|
1
|
+
import { normalized, partialNormalized, semver } from '../constants/index.js';
|
|
2
2
|
export const BlockDefinition = {
|
|
3
3
|
type: 'object',
|
|
4
4
|
description: 'A block that is displayed on a page.',
|
|
@@ -29,6 +29,19 @@ Some examples:
|
|
|
29
29
|
|
|
30
30
|
Since blocks may change over time, a version must be specified in order to ensure that the block
|
|
31
31
|
will remain the same until it is manually updated.
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
name: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
pattern: normalized.source,
|
|
37
|
+
maxLength: 40,
|
|
38
|
+
description: `A name which identifies this single block instance.
|
|
39
|
+
|
|
40
|
+
The name is rendered as a \`data-block-name\` attribute on the wrapper element of the block, so app
|
|
41
|
+
styles can target one instance instead of every block of the same type. Names must be lower case and
|
|
42
|
+
hyphen separated.
|
|
43
|
+
|
|
44
|
+
For example, a block named \`screen-title\` is targeted with \`[data-block-name='screen-title']\`.
|
|
32
45
|
`,
|
|
33
46
|
},
|
|
34
47
|
layout: {
|
package/schemas/Theme.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { baseTheme } from '../constants/baseTheme.js';
|
|
2
|
-
import { hexColor } from '../constants/patterns.js';
|
|
2
|
+
import { hexColor, normalized, themeVariableValue } from '../constants/patterns.js';
|
|
3
3
|
export const Theme = {
|
|
4
4
|
type: 'object',
|
|
5
5
|
description: 'A generic theme definition.',
|
|
@@ -90,6 +90,38 @@ the CSS of the app and its blocks. It falls back to the body font.
|
|
|
90
90
|
{ $ref: '#/components/schemas/GoogleFontDefinition' },
|
|
91
91
|
],
|
|
92
92
|
},
|
|
93
|
+
variables: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
description: `Custom CSS properties (design tokens) exposed on \`:root\` in the theme CSS.
|
|
96
|
+
|
|
97
|
+
Each entry is emitted as a \`--<name>\` custom property. Names must be lower case and hyphen
|
|
98
|
+
separated. Custom properties inherit into every block's shadow DOM, so tokens defined once are
|
|
99
|
+
usable in core, shared, and block styles.
|
|
100
|
+
|
|
101
|
+
For example:
|
|
102
|
+
|
|
103
|
+
\`\`\`yaml
|
|
104
|
+
theme:
|
|
105
|
+
variables:
|
|
106
|
+
night: '#0e1220'
|
|
107
|
+
font-display: "'Baloo 2', sans-serif"
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
These can be referenced in custom CSS as \`var(--night)\` and \`var(--font-display)\`.
|
|
111
|
+
`,
|
|
112
|
+
minProperties: 1,
|
|
113
|
+
maxProperties: 32,
|
|
114
|
+
// The validator supports propertyNames, but the OpenAPI schema object type does not know it.
|
|
115
|
+
propertyNames: {
|
|
116
|
+
pattern: normalized.source,
|
|
117
|
+
maxLength: 40,
|
|
118
|
+
},
|
|
119
|
+
additionalProperties: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
pattern: themeVariableValue.source,
|
|
122
|
+
maxLength: 128,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
93
125
|
},
|
|
94
126
|
};
|
|
95
127
|
//# sourceMappingURL=Theme.js.map
|
|
@@ -42,7 +42,7 @@ If \`to\`, \`cc\`, and \`bcc\` all end up being empty, no emails will be sent bu
|
|
|
42
42
|
|
|
43
43
|
The remapper must resolve to an object containing the following properties:
|
|
44
44
|
|
|
45
|
-
- \`target\`: The asset ID or link to download contents from to add as an attachment. This is mutually exclusive with \`content\`.
|
|
45
|
+
- \`target\`: The asset ID or link to download contents from to add as an attachment. This is mutually exclusive with \`content\`. The files downloaded from links may total at most 25 MiB per email.
|
|
46
46
|
- \`content\`: The raw content to include as the file content. This is mutually exclusive with \`target\`.
|
|
47
47
|
- \`filename\`: The filename to include the attachment as.
|
|
48
48
|
- \`accept\` If the target is a URL, this will be set as the HTTP \`Accept\` header when downloading the file.
|
package/types/block.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export interface BlockDefinition extends ControllerDefinition {
|
|
|
48
48
|
* ^\d+\.\d+\.\d+$
|
|
49
49
|
*/
|
|
50
50
|
version: string;
|
|
51
|
+
/**
|
|
52
|
+
* A name which identifies this single block instance.
|
|
53
|
+
*
|
|
54
|
+
* It is rendered as a `data-block-name` attribute on the wrapper element of the block, so app
|
|
55
|
+
* styles can target one instance instead of every block of the same type. Names must be lower
|
|
56
|
+
* case and hyphen separated.
|
|
57
|
+
*/
|
|
58
|
+
name?: string;
|
|
51
59
|
/**
|
|
52
60
|
* An optional header to render above the block.
|
|
53
61
|
*/
|
package/types/core.d.ts
CHANGED
|
@@ -184,9 +184,19 @@ export interface AppDefinition {
|
|
|
184
184
|
/**
|
|
185
185
|
* The strategy to use for apps to subscribe to push notifications.
|
|
186
186
|
*
|
|
187
|
-
* If this is omitted, push
|
|
187
|
+
* Either one strategy for all app members, or a strategy per role. If this is omitted, push
|
|
188
|
+
* notifications can not be sent.
|
|
188
189
|
*/
|
|
189
|
-
notifications?: 'login' | 'opt-in' | 'startup'
|
|
190
|
+
notifications?: 'login' | 'opt-in' | 'startup' | {
|
|
191
|
+
/**
|
|
192
|
+
* The strategy for roles that aren't listed in `roles`, and for users without a role.
|
|
193
|
+
*/
|
|
194
|
+
default?: 'login' | 'none' | 'opt-in' | 'startup';
|
|
195
|
+
/**
|
|
196
|
+
* The strategy per role.
|
|
197
|
+
*/
|
|
198
|
+
roles: Record<AppRole, 'login' | 'none' | 'opt-in'>;
|
|
199
|
+
};
|
|
190
200
|
/**
|
|
191
201
|
* The pages of the app.
|
|
192
202
|
*/
|
package/types/theme.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ export interface Theme {
|
|
|
51
51
|
* The name of a font available on Google fonts.
|
|
52
52
|
*/
|
|
53
53
|
font: FontDefinition;
|
|
54
|
+
/**
|
|
55
|
+
* Custom CSS properties (design tokens) exposed on `:root` in the compiled theme CSS.
|
|
56
|
+
*
|
|
57
|
+
* Each entry is emitted as a `--<name>` custom property. Custom properties inherit into every
|
|
58
|
+
* block's shadow DOM, so tokens defined once are usable in core, shared, and block styles.
|
|
59
|
+
*/
|
|
60
|
+
variables?: Record<string, string>;
|
|
54
61
|
/**
|
|
55
62
|
* A second font family the app loads, exposed as the `--font-family-display` custom property for
|
|
56
63
|
* the CSS of the app and its blocks.
|
package/validation.js
CHANGED
|
@@ -9,7 +9,7 @@ import { findPageByName } from './findPageByName.js';
|
|
|
9
9
|
import { isAppLink } from './pageUtils.js';
|
|
10
10
|
import { bottomNavigationGridArea, breadcrumbsGridArea, builtinPagesGridAreaOrder, getBuiltinPagesDefaultGridLayout, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, resendBannerGridArea, } from './gridUtils.js';
|
|
11
11
|
import { isValidIconName, parseIconReference } from './icons.js';
|
|
12
|
-
import { iterApp } from './iterApp.js';
|
|
12
|
+
import { iterApp, iterBlockList } from './iterApp.js';
|
|
13
13
|
import { has } from './miscellaneous.js';
|
|
14
14
|
import { normalize } from './normalize.js';
|
|
15
15
|
import { serverActions } from './serverActions.js';
|
|
@@ -97,7 +97,7 @@ function validateUniquePageNames(definition, report) {
|
|
|
97
97
|
pageNames.set(normalizedPageName, [pagePath]);
|
|
98
98
|
}
|
|
99
99
|
if (page.type === 'container') {
|
|
100
|
-
checkPages(page.pages, pagePath);
|
|
100
|
+
checkPages(page.pages ?? [], pagePath);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -122,7 +122,7 @@ function iterPageParents(pages, onParent, prefix = ['pages']) {
|
|
|
122
122
|
onParent(entry, Array.isArray(page.parent) ? [...pagePath, 'parent', entryIndex] : [...pagePath, 'parent'], page);
|
|
123
123
|
}
|
|
124
124
|
if (page.type === 'container') {
|
|
125
|
-
iterPageParents(page.pages, onParent, [...pagePath, 'pages']);
|
|
125
|
+
iterPageParents(page.pages ?? [], onParent, [...pagePath, 'pages']);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -167,6 +167,74 @@ function validatePageParents(definition, report) {
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Validates the blocks in the app definition to ensure there are no duplicate block names.
|
|
172
|
+
*
|
|
173
|
+
* A block name identifies a single block within the screen it is rendered in. A page renders one
|
|
174
|
+
* of its tabs or steps at a time, so uniqueness is scoped to the block list the block belongs to.
|
|
175
|
+
* The same name may be used on other pages, tabs and steps, which lets an app style the same role
|
|
176
|
+
* everywhere with one rule.
|
|
177
|
+
*
|
|
178
|
+
* @param definition The definition of the app
|
|
179
|
+
* @param report A function used to report a value.
|
|
180
|
+
*/
|
|
181
|
+
function validateUniqueBlockNames(definition, report) {
|
|
182
|
+
function checkBlockList(blocks, path) {
|
|
183
|
+
const names = new Set();
|
|
184
|
+
iterBlockList(blocks, {
|
|
185
|
+
onBlock(block, blockPath) {
|
|
186
|
+
if (block.name) {
|
|
187
|
+
if (names.has(block.name)) {
|
|
188
|
+
report(block.name, 'is a duplicate block name', [...blockPath, 'name']);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
names.add(block.name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}, path);
|
|
196
|
+
}
|
|
197
|
+
iterApp(definition, {
|
|
198
|
+
onPage(page, path) {
|
|
199
|
+
// Basic page
|
|
200
|
+
if (page.type === 'page' || page.type === undefined) {
|
|
201
|
+
checkBlockList(page.blocks, [...path, 'blocks']);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Tabs page
|
|
205
|
+
if (page.type === 'tabs') {
|
|
206
|
+
const tabsPage = page;
|
|
207
|
+
if (tabsPage.tabs) {
|
|
208
|
+
for (const [tabId, tab] of tabsPage.tabs.entries()) {
|
|
209
|
+
checkBlockList(tab.blocks, [...path, 'tabs', tabId, 'blocks']);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (tabsPage.definition?.foreach) {
|
|
213
|
+
checkBlockList(tabsPage.definition.foreach.blocks, [
|
|
214
|
+
...path,
|
|
215
|
+
'definition',
|
|
216
|
+
'foreach',
|
|
217
|
+
'blocks',
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Flow page
|
|
223
|
+
if (page.type === 'flow' && page.steps) {
|
|
224
|
+
const flowPage = page;
|
|
225
|
+
for (const [stepId, step] of flowPage.steps.entries()) {
|
|
226
|
+
checkBlockList(step.blocks, [...path, 'steps', stepId, 'blocks']);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Loop page
|
|
231
|
+
if (page.type === 'loop') {
|
|
232
|
+
const loopPage = page;
|
|
233
|
+
checkBlockList(loopPage.foreach.blocks, [...path, 'foreach', 'blocks']);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
170
238
|
function validateMembersSchema(definition, report) {
|
|
171
239
|
if (!definition.members?.properties) {
|
|
172
240
|
return;
|
|
@@ -703,7 +771,7 @@ function validateGridLayout(definition, report) {
|
|
|
703
771
|
// Flow page
|
|
704
772
|
if (page.type === 'flow') {
|
|
705
773
|
const flowPage = page;
|
|
706
|
-
const steps = flowPage.steps.map((step, stepId) => [
|
|
774
|
+
const steps = (flowPage.steps ?? []).map((step, stepId) => [
|
|
707
775
|
step,
|
|
708
776
|
[...path, 'steps', stepId],
|
|
709
777
|
]);
|
|
@@ -1122,8 +1190,9 @@ function validateSecurity(definition, report) {
|
|
|
1122
1190
|
checkRoleExists(role, [...path, 'roles', index], ['$guest', ...predefinedRoles]);
|
|
1123
1191
|
}
|
|
1124
1192
|
};
|
|
1193
|
+
const notificationRoles = notifications && typeof notifications === 'object' ? notifications.roles : undefined;
|
|
1125
1194
|
if (!security) {
|
|
1126
|
-
if (notifications === 'login') {
|
|
1195
|
+
if (notifications === 'login' || notificationRoles) {
|
|
1127
1196
|
report(notifications, 'only works if security is defined', ['notifications']);
|
|
1128
1197
|
}
|
|
1129
1198
|
return;
|
|
@@ -1224,6 +1293,11 @@ function validateSecurity(definition, report) {
|
|
|
1224
1293
|
}
|
|
1225
1294
|
});
|
|
1226
1295
|
}
|
|
1296
|
+
if (notificationRoles) {
|
|
1297
|
+
for (const role of Object.keys(notificationRoles)) {
|
|
1298
|
+
checkRoleExists(role, ['notifications', 'roles', role]);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1227
1301
|
const { hideGroupDropdown } = definition.layout ?? {};
|
|
1228
1302
|
if (Array.isArray(hideGroupDropdown)) {
|
|
1229
1303
|
for (const [index, role] of hideGroupDropdown.entries()) {
|
|
@@ -1819,6 +1893,9 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
|
|
|
1819
1893
|
const report = (instance, message, path) => {
|
|
1820
1894
|
result.errors.push(new ValidationError(message, instance, undefined, path));
|
|
1821
1895
|
};
|
|
1896
|
+
const reportUnexpectedError = (error) => {
|
|
1897
|
+
report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);
|
|
1898
|
+
};
|
|
1822
1899
|
try {
|
|
1823
1900
|
// IMPORTANT: Create a deep copy to prevent validation logic from mutating the original definition.
|
|
1824
1901
|
// The imperative/procedural-style algorithms below (e.g., in validateActions) build up arrays
|
|
@@ -1834,27 +1911,37 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
|
|
|
1834
1911
|
}
|
|
1835
1912
|
blockVersionMap.get(version.name).set(version.version, version);
|
|
1836
1913
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1914
|
+
// Run each check on its own, so a check that fails does not keep the others from reporting.
|
|
1915
|
+
const check = (validate) => {
|
|
1916
|
+
try {
|
|
1917
|
+
validate();
|
|
1918
|
+
}
|
|
1919
|
+
catch (error) {
|
|
1920
|
+
reportUnexpectedError(error);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
check(() => validateController(clonedDefinition, controllerImplementations, report));
|
|
1924
|
+
check(() => validateCronJobs(clonedDefinition, report));
|
|
1925
|
+
check(() => validateDefaultPage(clonedDefinition, report));
|
|
1926
|
+
check(() => validateHooks(clonedDefinition, report));
|
|
1927
|
+
check(() => validateIcons(clonedDefinition, report));
|
|
1928
|
+
check(() => validateIconReferences(clonedDefinition, blockVersionMap, report));
|
|
1929
|
+
check(() => validateLanguage(clonedDefinition, report));
|
|
1930
|
+
check(() => validateResourceReferences(clonedDefinition, report));
|
|
1931
|
+
check(() => validateMembersSchema(clonedDefinition, report));
|
|
1932
|
+
check(() => validatePageParents(clonedDefinition, report));
|
|
1933
|
+
check(() => validatePhoneNumberDefinition(clonedDefinition, report));
|
|
1934
|
+
check(() => validateResourceSchemas(clonedDefinition, report));
|
|
1935
|
+
check(() => validateSecurity(clonedDefinition, report));
|
|
1936
|
+
check(() => validateBlocks(clonedDefinition, blockVersionMap, report));
|
|
1937
|
+
check(() => validateActions(clonedDefinition, report));
|
|
1938
|
+
check(() => validateEvents(clonedDefinition, blockVersionMap, report));
|
|
1939
|
+
check(() => validateUniquePageNames(clonedDefinition, report));
|
|
1940
|
+
check(() => validateUniqueBlockNames(clonedDefinition, report));
|
|
1941
|
+
check(() => validateGridLayout(clonedDefinition, report));
|
|
1855
1942
|
}
|
|
1856
1943
|
catch (error) {
|
|
1857
|
-
|
|
1944
|
+
reportUnexpectedError(error);
|
|
1858
1945
|
}
|
|
1859
1946
|
return result;
|
|
1860
1947
|
}
|