@appsemble/utils 0.23.4 → 0.23.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/README.md +3 -3
- package/api/components/schemas/ActionDefinition.js +1 -0
- package/api/components/schemas/App.js +8 -0
- package/api/components/schemas/AppDefinition.js +3 -0
- package/api/components/schemas/ControllerActionDefinition.d.ts +1 -0
- package/api/components/schemas/ControllerActionDefinition.js +18 -0
- package/api/components/schemas/ControllerDefinition.d.ts +2 -0
- package/api/components/schemas/ControllerDefinition.js +17 -0
- package/api/components/schemas/index.d.ts +2 -0
- package/api/components/schemas/index.js +2 -0
- package/api/paths/apps.js +16 -0
- package/constants/patterns.d.ts +1 -1
- package/constants/patterns.js +1 -1
- package/iterApp.d.ts +3 -1
- package/iterApp.js +13 -0
- package/package.json +2 -2
- package/validation.d.ts +3 -2
- package/validation.js +142 -13
- package/validation.test.js +205 -9
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#  Appsemble Utilities
|
|
2
2
|
|
|
3
3
|
> Internal utility functions used across multiple Appsemble projects.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@appsemble/utils)
|
|
6
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.23.6)
|
|
7
7
|
[](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.23.
|
|
29
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.23.6/LICENSE.md) ©
|
|
30
30
|
[Appsemble](https://appsemble.com)
|
|
@@ -26,6 +26,7 @@ export const ActionDefinition = {
|
|
|
26
26
|
{
|
|
27
27
|
anyOf: [
|
|
28
28
|
{ $ref: '#/components/schemas/AnalyticsActionDefinition' },
|
|
29
|
+
{ $ref: '#/components/schemas/ControllerActionDefinition' },
|
|
29
30
|
{ $ref: '#/components/schemas/ConditionActionDefinition' },
|
|
30
31
|
{ $ref: '#/components/schemas/DialogActionDefinition' },
|
|
31
32
|
{ $ref: '#/components/schemas/DialogErrorActionDefinition' },
|
|
@@ -104,6 +104,14 @@ domain fall back to use the Appsemble server Sentry DSN.
|
|
|
104
104
|
|
|
105
105
|
This is only applied when \`sentryDsn\` is specified.`,
|
|
106
106
|
},
|
|
107
|
+
controllerCode: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Custom app logic as a JavaScript string',
|
|
110
|
+
},
|
|
111
|
+
controllerImplementations: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Appsemble SDK interfaces implementations',
|
|
114
|
+
},
|
|
107
115
|
},
|
|
108
116
|
};
|
|
109
117
|
//# sourceMappingURL=App.js.map
|
|
@@ -65,6 +65,9 @@ This **must** match the name of a page defined for the app.
|
|
|
65
65
|
minLength: 2,
|
|
66
66
|
description: 'The default language for the app.',
|
|
67
67
|
},
|
|
68
|
+
controller: {
|
|
69
|
+
$ref: '#/components/schemas/ControllerDefinition',
|
|
70
|
+
},
|
|
68
71
|
resources: {
|
|
69
72
|
type: 'object',
|
|
70
73
|
description: `Resources define how Appsemble can store data for an app.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ControllerActionDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseActionDefinition } from './BaseActionDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from './utils.js';
|
|
3
|
+
export const ControllerActionDefinition = extendJSONSchema(BaseActionDefinition, {
|
|
4
|
+
type: 'object',
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
required: ['type', 'handler'],
|
|
7
|
+
properties: {
|
|
8
|
+
type: {
|
|
9
|
+
enum: ['controller'],
|
|
10
|
+
description: 'Use the controller to handle the action.',
|
|
11
|
+
},
|
|
12
|
+
handler: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'The name of the function in the controller that will handle the action',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=ControllerActionDefinition.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const ControllerDefinition = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
description: 'A controller for application logic.',
|
|
4
|
+
required: [],
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
properties: {
|
|
7
|
+
actions: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
description: 'A mapping of actions that can be fired by the controller to action handlers.',
|
|
10
|
+
additionalProperties: {
|
|
11
|
+
$ref: '#/components/schemas/ActionDefinition',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
events: { $ref: '#/components/schemas/EventsDefinition' },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=ControllerDefinition.js.map
|
|
@@ -2,6 +2,8 @@ export * from './ActionDefinition.js';
|
|
|
2
2
|
export * from './AnalyticsActionDefinition.js';
|
|
3
3
|
export * from './App.js';
|
|
4
4
|
export * from './AppCollection.js';
|
|
5
|
+
export * from './ControllerDefinition.js';
|
|
6
|
+
export * from './ControllerActionDefinition.js';
|
|
5
7
|
export * from './AppCollectionDefinition.js';
|
|
6
8
|
export * from './AppAccount.js';
|
|
7
9
|
export * from './AppDefinition.js';
|
|
@@ -2,6 +2,8 @@ export * from './ActionDefinition.js';
|
|
|
2
2
|
export * from './AnalyticsActionDefinition.js';
|
|
3
3
|
export * from './App.js';
|
|
4
4
|
export * from './AppCollection.js';
|
|
5
|
+
export * from './ControllerDefinition.js';
|
|
6
|
+
export * from './ControllerActionDefinition.js';
|
|
5
7
|
export * from './AppCollectionDefinition.js';
|
|
6
8
|
export * from './AppAccount.js';
|
|
7
9
|
export * from './AppDefinition.js';
|
package/api/paths/apps.js
CHANGED
|
@@ -76,6 +76,14 @@ export const paths = {
|
|
|
76
76
|
format: 'binary',
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
|
+
controllerCode: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Custom app logic as a JavaScript string',
|
|
82
|
+
},
|
|
83
|
+
controllerImplementations: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Appsemble SDK interfaces implementations',
|
|
86
|
+
},
|
|
79
87
|
},
|
|
80
88
|
},
|
|
81
89
|
encoding: {
|
|
@@ -217,6 +225,14 @@ export const paths = {
|
|
|
217
225
|
format: 'binary',
|
|
218
226
|
},
|
|
219
227
|
},
|
|
228
|
+
controllerCode: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Custom app logic as a JavaScript string',
|
|
231
|
+
},
|
|
232
|
+
controllerImplementations: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'Appsemble SDK interfaces implementations',
|
|
235
|
+
},
|
|
220
236
|
showAppsembleLogin: {
|
|
221
237
|
type: 'boolean',
|
|
222
238
|
description: 'Whether the Appsemble login method should be shown.',
|
package/constants/patterns.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const partialNormalized: RegExp;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare const normalized: RegExp;
|
|
21
21
|
/**
|
|
22
|
-
* A pattern for matching the block name pattern of @organization/
|
|
22
|
+
* A pattern for matching the block name pattern of @organization/project.
|
|
23
23
|
*/
|
|
24
24
|
export declare const blockNamePattern: RegExp;
|
|
25
25
|
export declare const domainPattern: RegExp;
|
package/constants/patterns.js
CHANGED
|
@@ -19,7 +19,7 @@ export const partialNormalized = /([\da-z](?:(?!.*--)[\da-z-]*[\da-z])?)/;
|
|
|
19
19
|
*/
|
|
20
20
|
export const normalized = new RegExp(`^${partialNormalized.source}$`);
|
|
21
21
|
/**
|
|
22
|
-
* A pattern for matching the block name pattern of @organization/
|
|
22
|
+
* A pattern for matching the block name pattern of @organization/project.
|
|
23
23
|
*/
|
|
24
24
|
export const blockNamePattern = new RegExp(`^@${partialNormalized.source}/${partialNormalized.source}$`);
|
|
25
25
|
export const domainPattern = new RegExp(`^(${partialNormalized.source}+\\.)+[a-z]{2,}$`);
|
package/iterApp.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActionDefinition, type AppDefinition, type BlockDefinition, type PageDefinition } from '@appsemble/types';
|
|
1
|
+
import { type ActionDefinition, type AppDefinition, type BlockDefinition, type ControllerDefinition, type PageDefinition } from '@appsemble/types';
|
|
2
2
|
export type Prefix = (number | string)[];
|
|
3
3
|
type IterCallback<T> = (item: T, path: Prefix) => boolean | void;
|
|
4
4
|
interface IterCallbacks {
|
|
@@ -6,6 +6,7 @@ interface IterCallbacks {
|
|
|
6
6
|
onBlockList?: IterCallback<BlockDefinition[]>;
|
|
7
7
|
onBlock?: IterCallback<BlockDefinition>;
|
|
8
8
|
onAction?: IterCallback<ActionDefinition>;
|
|
9
|
+
onController?: IterCallback<ControllerDefinition>;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* Iterate over an action definition and call each callback if relevant.
|
|
@@ -18,6 +19,7 @@ interface IterCallbacks {
|
|
|
18
19
|
* @returns True if any callback returns true, false otherwise.
|
|
19
20
|
*/
|
|
20
21
|
export declare function iterAction(action: ActionDefinition, callbacks: IterCallbacks, prefix?: Prefix): boolean;
|
|
22
|
+
export declare function iterController(controller: ControllerDefinition, callbacks: IterCallbacks, prefix?: Prefix): boolean;
|
|
21
23
|
/**
|
|
22
24
|
* Iterate over a block definition and call each callback if relevant.
|
|
23
25
|
*
|
package/iterApp.js
CHANGED
|
@@ -32,6 +32,16 @@ export function iterAction(action, callbacks, prefix = []) {
|
|
|
32
32
|
}
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
|
+
export function iterController(controller, callbacks, prefix = []) {
|
|
36
|
+
var _a;
|
|
37
|
+
if ((_a = callbacks.onController) === null || _a === void 0 ? void 0 : _a.call(callbacks, controller, prefix)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (controller.actions) {
|
|
41
|
+
return Object.entries(controller.actions).some(([key, action]) => iterAction(action, callbacks, [...prefix, 'actions', key]));
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Iterate over a block definition and call each callback if relevant.
|
|
37
47
|
*
|
|
@@ -116,6 +126,9 @@ export function iterApp(app, callbacks) {
|
|
|
116
126
|
app.pages.some((page, index) => iterPage(page, callbacks, ['pages', index]))) {
|
|
117
127
|
return true;
|
|
118
128
|
}
|
|
129
|
+
if (app.controller && iterController(app.controller, callbacks, ['controller'])) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
119
132
|
if (app.cron) {
|
|
120
133
|
for (const [name, job] of Object.entries(app.cron)) {
|
|
121
134
|
if ((job === null || job === void 0 ? void 0 : job.action) && iterAction(job.action, callbacks, ['cron', name, 'action'])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/utils",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.6",
|
|
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.23.
|
|
40
|
+
"@appsemble/types": "0.23.6",
|
|
41
41
|
"axios": "^1.0.0",
|
|
42
42
|
"cron-parser": "^4.0.0",
|
|
43
43
|
"date-fns": "^2.0.0",
|
package/validation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AppDefinition, type BlockManifest } from '@appsemble/types';
|
|
1
|
+
import { type AppDefinition, type BlockManifest, type ProjectImplementations } from '@appsemble/types';
|
|
2
2
|
import { type ValidatorResult } from 'jsonschema';
|
|
3
3
|
import { type Promisable } from 'type-fest';
|
|
4
4
|
import { type IdentifiableBlock } from './blockUtils.js';
|
|
@@ -17,8 +17,9 @@ export type BlockVersionsGetter = (blockMap: IdentifiableBlock[]) => Promisable<
|
|
|
17
17
|
*
|
|
18
18
|
* @param definition The app validation to check.
|
|
19
19
|
* @param getBlockVersions A function for getting block manifests from block versions.
|
|
20
|
+
* @param controllerImplementations App controller implementations of interfaces.
|
|
20
21
|
* @param validatorResult If specified, error messages will be applied to this existing validator
|
|
21
22
|
* result.
|
|
22
23
|
* @returns A validator result which contains all app validation violations.
|
|
23
24
|
*/
|
|
24
|
-
export declare function validateAppDefinition(definition: AppDefinition, getBlockVersions: BlockVersionsGetter, validatorResult?: ValidatorResult): Promise<ValidatorResult>;
|
|
25
|
+
export declare function validateAppDefinition(definition: AppDefinition, getBlockVersions: BlockVersionsGetter, controllerImplementations?: ProjectImplementations, validatorResult?: ValidatorResult): Promise<ValidatorResult>;
|
package/validation.js
CHANGED
|
@@ -120,6 +120,72 @@ function validateResourceSchemas(definition, report) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
function validateController(definition, controllerImplementations, report) {
|
|
124
|
+
if (!definition.controller || !controllerImplementations) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
iterApp(definition, {
|
|
128
|
+
onController(controller, path) {
|
|
129
|
+
var _a, _b, _c, _d, _e, _f;
|
|
130
|
+
const actionParameters = new Set();
|
|
131
|
+
if (controller.actions) {
|
|
132
|
+
if (controllerImplementations.actions) {
|
|
133
|
+
for (const [key, action] of Object.entries(controller.actions)) {
|
|
134
|
+
if (action.type in
|
|
135
|
+
[
|
|
136
|
+
'link',
|
|
137
|
+
'link.back',
|
|
138
|
+
'link.next',
|
|
139
|
+
'dialog',
|
|
140
|
+
'dialog.ok',
|
|
141
|
+
'dialog.error',
|
|
142
|
+
'flow.back',
|
|
143
|
+
'flow.cancel',
|
|
144
|
+
'flow.finish',
|
|
145
|
+
'flow.next',
|
|
146
|
+
'flow.to',
|
|
147
|
+
]) {
|
|
148
|
+
report(action, 'cannot be used in controllers', [...path, 'actions', key]);
|
|
149
|
+
}
|
|
150
|
+
if (controllerImplementations.actions.$any) {
|
|
151
|
+
if (actionParameters.has(key)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!has(controllerImplementations.actions, key)) {
|
|
155
|
+
report(action, 'is unused', [...path, 'actions', key]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (!has(controllerImplementations.actions, key)) {
|
|
159
|
+
report(action, 'is an unknown action for this controller', [...path, 'actions', key]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
report(controller.actions, 'is not allowed on this controller', [...path, 'actions']);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!controller.events) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (controller.events.emit) {
|
|
171
|
+
for (const [key, value] of Object.entries(controller.events.emit)) {
|
|
172
|
+
if (!((_b = (_a = controllerImplementations.events) === null || _a === void 0 ? void 0 : _a.emit) === null || _b === void 0 ? void 0 : _b.$any) &&
|
|
173
|
+
!has((_c = controllerImplementations.events) === null || _c === void 0 ? void 0 : _c.emit, key)) {
|
|
174
|
+
report(value, 'is an unknown event emitter', [...path, 'events', 'emit', key]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (controller.events.listen) {
|
|
179
|
+
for (const [key, value] of Object.entries(controller.events.listen)) {
|
|
180
|
+
if (!((_e = (_d = controllerImplementations.events) === null || _d === void 0 ? void 0 : _d.listen) === null || _e === void 0 ? void 0 : _e.$any) &&
|
|
181
|
+
!has((_f = controllerImplementations.events) === null || _f === void 0 ? void 0 : _f.listen, key)) {
|
|
182
|
+
report(value, 'is an unknown event listener', [...path, 'events', 'listen', key]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
123
189
|
function validateBlocks(definition, blockVersions, report) {
|
|
124
190
|
iterApp(definition, {
|
|
125
191
|
onBlock(block, path) {
|
|
@@ -506,17 +572,29 @@ function validateEvents(definition, blockVersions, report) {
|
|
|
506
572
|
const indexMap = new Map();
|
|
507
573
|
function collect(prefix, name, isEmitter) {
|
|
508
574
|
const [firstKey, pageIndex] = prefix;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
575
|
+
let mapAtKey;
|
|
576
|
+
// Ignore anything not part of controller or a page.
|
|
577
|
+
// For example cron actions never support events.
|
|
578
|
+
switch (firstKey) {
|
|
579
|
+
case 'controller':
|
|
580
|
+
if (!indexMap.has('controller')) {
|
|
581
|
+
indexMap.set('controller', { emitters: new Map(), listeners: new Map() });
|
|
582
|
+
}
|
|
583
|
+
mapAtKey = indexMap.get('controller');
|
|
584
|
+
break;
|
|
585
|
+
case 'pages':
|
|
586
|
+
if (typeof pageIndex !== 'number') {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (!indexMap.has(pageIndex)) {
|
|
590
|
+
indexMap.set(pageIndex, { emitters: new Map(), listeners: new Map() });
|
|
591
|
+
}
|
|
592
|
+
mapAtKey = indexMap.get(pageIndex);
|
|
593
|
+
break;
|
|
594
|
+
default:
|
|
595
|
+
return;
|
|
518
596
|
}
|
|
519
|
-
const { emitters, listeners } =
|
|
597
|
+
const { emitters, listeners } = mapAtKey;
|
|
520
598
|
const context = isEmitter ? emitters : listeners;
|
|
521
599
|
if (!context.has(name)) {
|
|
522
600
|
context.set(name, []);
|
|
@@ -525,6 +603,21 @@ function validateEvents(definition, blockVersions, report) {
|
|
|
525
603
|
prefixes.push(prefix);
|
|
526
604
|
}
|
|
527
605
|
iterApp(definition, {
|
|
606
|
+
onController(controller, path) {
|
|
607
|
+
if (!controller.events) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (controller.events.emit) {
|
|
611
|
+
for (const [prefix, name] of Object.entries(controller.events.emit)) {
|
|
612
|
+
collect([...path, 'events', 'emit', prefix], name, true);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (controller.events.listen) {
|
|
616
|
+
for (const [prefix, name] of Object.entries(controller.events.listen)) {
|
|
617
|
+
collect([...path, 'events', 'listen', prefix], name, false);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
},
|
|
528
621
|
onAction(action, path) {
|
|
529
622
|
if (action.type === 'dialog') {
|
|
530
623
|
for (const block of action.blocks) {
|
|
@@ -567,16 +660,50 @@ function validateEvents(definition, blockVersions, report) {
|
|
|
567
660
|
}
|
|
568
661
|
},
|
|
569
662
|
});
|
|
663
|
+
let controllerEvents = {
|
|
664
|
+
emitters: new Map(),
|
|
665
|
+
listeners: new Map(),
|
|
666
|
+
};
|
|
667
|
+
if (indexMap.has('controller')) {
|
|
668
|
+
controllerEvents = { ...indexMap.get('controller') };
|
|
669
|
+
}
|
|
670
|
+
indexMap.delete('controller');
|
|
671
|
+
for (const [name, prefixes] of controllerEvents.emitters.entries()) {
|
|
672
|
+
let found = false;
|
|
673
|
+
for (const { listeners } of indexMap.values()) {
|
|
674
|
+
if (listeners.has(name)) {
|
|
675
|
+
found = true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (!found) {
|
|
679
|
+
for (const prefix of prefixes) {
|
|
680
|
+
report(name, 'does not match any listeners', prefix);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const [name, prefixes] of controllerEvents.listeners.entries()) {
|
|
685
|
+
let found = false;
|
|
686
|
+
for (const { emitters } of indexMap.values()) {
|
|
687
|
+
if (emitters.has(name)) {
|
|
688
|
+
found = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!found) {
|
|
692
|
+
for (const prefix of prefixes) {
|
|
693
|
+
report(name, 'does not match any emitters', prefix);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
570
697
|
for (const { emitters, listeners } of indexMap.values()) {
|
|
571
698
|
for (const [name, prefixes] of listeners.entries()) {
|
|
572
|
-
if (!emitters.has(name)) {
|
|
699
|
+
if (!emitters.has(name) && !controllerEvents.emitters.has(name)) {
|
|
573
700
|
for (const prefix of prefixes) {
|
|
574
701
|
report(name, 'does not match any event emitters', prefix);
|
|
575
702
|
}
|
|
576
703
|
}
|
|
577
704
|
}
|
|
578
705
|
for (const [name, prefixes] of emitters.entries()) {
|
|
579
|
-
if (!listeners.has(name)) {
|
|
706
|
+
if (!listeners.has(name) && !controllerEvents.listeners.has(name)) {
|
|
580
707
|
for (const prefix of prefixes) {
|
|
581
708
|
report(name, 'does not match any event listeners', prefix);
|
|
582
709
|
}
|
|
@@ -591,11 +718,12 @@ function validateEvents(definition, blockVersions, report) {
|
|
|
591
718
|
*
|
|
592
719
|
* @param definition The app validation to check.
|
|
593
720
|
* @param getBlockVersions A function for getting block manifests from block versions.
|
|
721
|
+
* @param controllerImplementations App controller implementations of interfaces.
|
|
594
722
|
* @param validatorResult If specified, error messages will be applied to this existing validator
|
|
595
723
|
* result.
|
|
596
724
|
* @returns A validator result which contains all app validation violations.
|
|
597
725
|
*/
|
|
598
|
-
export async function validateAppDefinition(definition, getBlockVersions, validatorResult) {
|
|
726
|
+
export async function validateAppDefinition(definition, getBlockVersions, controllerImplementations, validatorResult) {
|
|
599
727
|
let result = validatorResult;
|
|
600
728
|
if (!result) {
|
|
601
729
|
const validator = new Validator();
|
|
@@ -617,6 +745,7 @@ export async function validateAppDefinition(definition, getBlockVersions, valida
|
|
|
617
745
|
result.errors.push(new ValidationError(message, instance, undefined, path));
|
|
618
746
|
};
|
|
619
747
|
try {
|
|
748
|
+
validateController(definition, controllerImplementations, report);
|
|
620
749
|
validateCronJobs(definition, report);
|
|
621
750
|
validateDefaultPage(definition, report);
|
|
622
751
|
validateHooks(definition, report);
|
package/validation.test.js
CHANGED
|
@@ -332,6 +332,28 @@ describe('validateAppDefinition', () => {
|
|
|
332
332
|
]),
|
|
333
333
|
]);
|
|
334
334
|
});
|
|
335
|
+
it('should validate controller actions', async () => {
|
|
336
|
+
const app = createTestApp();
|
|
337
|
+
app.controller = {
|
|
338
|
+
actions: {
|
|
339
|
+
onClick: { type: 'noop' },
|
|
340
|
+
onSubmit: { type: 'noop' },
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const result = await validateAppDefinition(app, () => [], {
|
|
344
|
+
actions: {
|
|
345
|
+
onClick: {},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
expect(result.valid).toBe(false);
|
|
349
|
+
expect(result.errors).toStrictEqual([
|
|
350
|
+
new ValidationError('is an unknown action for this controller', { type: 'noop' }, undefined, [
|
|
351
|
+
'controller',
|
|
352
|
+
'actions',
|
|
353
|
+
'onSubmit',
|
|
354
|
+
]),
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
335
357
|
it('should report if a block doesn’t support actions', async () => {
|
|
336
358
|
const app = createTestApp();
|
|
337
359
|
app.pages[0].blocks.push({
|
|
@@ -358,6 +380,20 @@ describe('validateAppDefinition', () => {
|
|
|
358
380
|
]),
|
|
359
381
|
]);
|
|
360
382
|
});
|
|
383
|
+
it('should report if a controller doesn’t support actions', async () => {
|
|
384
|
+
const app = createTestApp();
|
|
385
|
+
app.controller = {
|
|
386
|
+
actions: {},
|
|
387
|
+
};
|
|
388
|
+
const result = await validateAppDefinition(app, () => [], {});
|
|
389
|
+
expect(result.valid).toBe(false);
|
|
390
|
+
expect(result.errors).toStrictEqual([
|
|
391
|
+
new ValidationError('is not allowed on this controller', {}, undefined, [
|
|
392
|
+
'controller',
|
|
393
|
+
'actions',
|
|
394
|
+
]),
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
361
397
|
it('should report unused block actions based on parameters', async () => {
|
|
362
398
|
const app = createTestApp();
|
|
363
399
|
app.pages[0].blocks.push({
|
|
@@ -439,7 +475,7 @@ describe('validateAppDefinition', () => {
|
|
|
439
475
|
]);
|
|
440
476
|
expect(result.valid).toBe(true);
|
|
441
477
|
});
|
|
442
|
-
it('should report unknown event emitters', async () => {
|
|
478
|
+
it('should report unknown event emitters on blocks', async () => {
|
|
443
479
|
const app = createTestApp();
|
|
444
480
|
app.pages[0].blocks.push({
|
|
445
481
|
type: 'test',
|
|
@@ -479,7 +515,51 @@ describe('validateAppDefinition', () => {
|
|
|
479
515
|
]),
|
|
480
516
|
]);
|
|
481
517
|
});
|
|
482
|
-
it('should
|
|
518
|
+
it('should report unknown event emitters on controller', async () => {
|
|
519
|
+
const app = createTestApp();
|
|
520
|
+
app.pages[0].blocks.push({
|
|
521
|
+
type: 'test',
|
|
522
|
+
version: '1.2.3',
|
|
523
|
+
events: {
|
|
524
|
+
listen: {
|
|
525
|
+
foo: 'bar',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
app.controller = {
|
|
530
|
+
events: {
|
|
531
|
+
emit: {
|
|
532
|
+
foo: 'bar',
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const result = await validateAppDefinition(app, () => [
|
|
537
|
+
{
|
|
538
|
+
name: '@appsemble/test',
|
|
539
|
+
version: '1.2.3',
|
|
540
|
+
files: [],
|
|
541
|
+
languages: [],
|
|
542
|
+
wildcardActions: true,
|
|
543
|
+
events: {
|
|
544
|
+
listen: { foo: {} },
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
], {
|
|
548
|
+
events: {
|
|
549
|
+
emit: {},
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
expect(result.valid).toBe(false);
|
|
553
|
+
expect(result.errors).toStrictEqual([
|
|
554
|
+
new ValidationError('is an unknown event emitter', 'bar', undefined, [
|
|
555
|
+
'controller',
|
|
556
|
+
'events',
|
|
557
|
+
'emit',
|
|
558
|
+
'foo',
|
|
559
|
+
]),
|
|
560
|
+
]);
|
|
561
|
+
});
|
|
562
|
+
it('should allow $any matching unknown event emitters on blocks', async () => {
|
|
483
563
|
const app = createTestApp();
|
|
484
564
|
app.pages[0].blocks.push({
|
|
485
565
|
type: 'test',
|
|
@@ -508,7 +588,7 @@ describe('validateAppDefinition', () => {
|
|
|
508
588
|
]);
|
|
509
589
|
expect(result.valid).toBe(true);
|
|
510
590
|
});
|
|
511
|
-
it('should report unknown event listeners', async () => {
|
|
591
|
+
it('should report unknown event listeners on blocks', async () => {
|
|
512
592
|
const app = createTestApp();
|
|
513
593
|
app.pages[0].blocks.push({
|
|
514
594
|
type: 'test',
|
|
@@ -548,6 +628,50 @@ describe('validateAppDefinition', () => {
|
|
|
548
628
|
]),
|
|
549
629
|
]);
|
|
550
630
|
});
|
|
631
|
+
it('should report unknown event listeners on controller', async () => {
|
|
632
|
+
const app = createTestApp();
|
|
633
|
+
app.pages[0].blocks.push({
|
|
634
|
+
type: 'test',
|
|
635
|
+
version: '1.2.3',
|
|
636
|
+
events: {
|
|
637
|
+
emit: {
|
|
638
|
+
foo: 'bar',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
app.controller = {
|
|
643
|
+
events: {
|
|
644
|
+
listen: {
|
|
645
|
+
foo: 'bar',
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
const result = await validateAppDefinition(app, () => [
|
|
650
|
+
{
|
|
651
|
+
name: '@appsemble/test',
|
|
652
|
+
version: '1.2.3',
|
|
653
|
+
files: [],
|
|
654
|
+
languages: [],
|
|
655
|
+
wildcardActions: true,
|
|
656
|
+
events: {
|
|
657
|
+
emit: { foo: {} },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
], {
|
|
661
|
+
events: {
|
|
662
|
+
listen: {},
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
expect(result.valid).toBe(false);
|
|
666
|
+
expect(result.errors).toStrictEqual([
|
|
667
|
+
new ValidationError('is an unknown event listener', 'bar', undefined, [
|
|
668
|
+
'controller',
|
|
669
|
+
'events',
|
|
670
|
+
'listen',
|
|
671
|
+
'foo',
|
|
672
|
+
]),
|
|
673
|
+
]);
|
|
674
|
+
});
|
|
551
675
|
it('should allow $any matching unknown event listener', async () => {
|
|
552
676
|
const app = createTestApp();
|
|
553
677
|
app.pages[0].blocks.push({
|
|
@@ -577,7 +701,7 @@ describe('validateAppDefinition', () => {
|
|
|
577
701
|
]);
|
|
578
702
|
expect(result.valid).toBe(true);
|
|
579
703
|
});
|
|
580
|
-
it('should report unmatched event listeners', async () => {
|
|
704
|
+
it('should report unmatched event listeners when there is no controller present', async () => {
|
|
581
705
|
const app = createTestApp();
|
|
582
706
|
app.pages[0].blocks.push({
|
|
583
707
|
type: 'test',
|
|
@@ -613,7 +737,7 @@ describe('validateAppDefinition', () => {
|
|
|
613
737
|
]),
|
|
614
738
|
]);
|
|
615
739
|
});
|
|
616
|
-
it('should report unmatched event emitters', async () => {
|
|
740
|
+
it('should report unmatched event emitters when there is no controller present', async () => {
|
|
617
741
|
const app = createTestApp();
|
|
618
742
|
app.pages[0].blocks.push({
|
|
619
743
|
type: 'test',
|
|
@@ -649,7 +773,7 @@ describe('validateAppDefinition', () => {
|
|
|
649
773
|
]),
|
|
650
774
|
]);
|
|
651
775
|
});
|
|
652
|
-
it('should report unmatched event from event actions', async () => {
|
|
776
|
+
it('should report unmatched event from event actions when there is no controller present', async () => {
|
|
653
777
|
const app = createTestApp();
|
|
654
778
|
app.pages[0].blocks.push({
|
|
655
779
|
type: 'test',
|
|
@@ -702,6 +826,12 @@ describe('validateAppDefinition', () => {
|
|
|
702
826
|
const result = await validateAppDefinition(app, () => []);
|
|
703
827
|
expect(result.valid).toBe(true);
|
|
704
828
|
});
|
|
829
|
+
it('should not crash if controller is undefined', async () => {
|
|
830
|
+
const app = createTestApp();
|
|
831
|
+
app.controller = undefined;
|
|
832
|
+
const result = await validateAppDefinition(app, () => [], {});
|
|
833
|
+
expect(result.valid).toBe(true);
|
|
834
|
+
});
|
|
705
835
|
it('should report if notifications is "login" without a security definition', async () => {
|
|
706
836
|
const app = createTestApp();
|
|
707
837
|
delete app.security;
|
|
@@ -1559,7 +1689,7 @@ describe('validateAppDefinition', () => {
|
|
|
1559
1689
|
new ValidationError('was defined but ‘onFlowCancel’ page action wasn’t defined', 'flow.cancel', undefined, ['pages', 3, 'steps', 1, 'blocks', 0, 'actions', 'onWhatever', 'type']),
|
|
1560
1690
|
]);
|
|
1561
1691
|
});
|
|
1562
|
-
it('should report an error if a resource action refers to a non-existent resource', async () => {
|
|
1692
|
+
it('should report an error if a resource action on a block refers to a non-existent resource', async () => {
|
|
1563
1693
|
const app = createTestApp();
|
|
1564
1694
|
app.pages[0].blocks.push({
|
|
1565
1695
|
type: 'test',
|
|
@@ -1595,7 +1725,32 @@ describe('validateAppDefinition', () => {
|
|
|
1595
1725
|
]),
|
|
1596
1726
|
]);
|
|
1597
1727
|
});
|
|
1598
|
-
it('should report an error if a resource action refers to a
|
|
1728
|
+
it('should report an error if a resource action on the controller refers to a non-existent resource', async () => {
|
|
1729
|
+
const app = createTestApp();
|
|
1730
|
+
app.controller = {
|
|
1731
|
+
actions: {
|
|
1732
|
+
onWhatever: {
|
|
1733
|
+
type: 'resource.get',
|
|
1734
|
+
resource: 'Nonexistent',
|
|
1735
|
+
},
|
|
1736
|
+
},
|
|
1737
|
+
};
|
|
1738
|
+
const result = await validateAppDefinition(app, () => [], {
|
|
1739
|
+
actions: {
|
|
1740
|
+
onWhatever: {},
|
|
1741
|
+
},
|
|
1742
|
+
});
|
|
1743
|
+
expect(result.valid).toBe(false);
|
|
1744
|
+
expect(result.errors).toStrictEqual([
|
|
1745
|
+
new ValidationError('refers to a resource that doesn’t exist', 'resource.get', undefined, [
|
|
1746
|
+
'controller',
|
|
1747
|
+
'actions',
|
|
1748
|
+
'onWhatever',
|
|
1749
|
+
'resource',
|
|
1750
|
+
]),
|
|
1751
|
+
]);
|
|
1752
|
+
});
|
|
1753
|
+
it('should report an error if a resource action on a block refers to a private resource action', async () => {
|
|
1599
1754
|
const app = createTestApp();
|
|
1600
1755
|
app.pages[0].blocks.push({
|
|
1601
1756
|
type: 'test',
|
|
@@ -1623,7 +1778,27 @@ describe('validateAppDefinition', () => {
|
|
|
1623
1778
|
new ValidationError('refers to a resource action that is currently set to private', 'resource.get', undefined, ['pages', 0, 'blocks', 0, 'actions', 'onWhatever', 'resource']),
|
|
1624
1779
|
]);
|
|
1625
1780
|
});
|
|
1626
|
-
it('should report an error if a resource action
|
|
1781
|
+
it('should report an error if a resource action on the controller refers to a private resource action', async () => {
|
|
1782
|
+
const app = createTestApp();
|
|
1783
|
+
app.controller = {
|
|
1784
|
+
actions: {
|
|
1785
|
+
onWhatever: {
|
|
1786
|
+
type: 'resource.get',
|
|
1787
|
+
resource: 'person',
|
|
1788
|
+
},
|
|
1789
|
+
},
|
|
1790
|
+
};
|
|
1791
|
+
const result = await validateAppDefinition(app, () => [], {
|
|
1792
|
+
actions: {
|
|
1793
|
+
onWhatever: {},
|
|
1794
|
+
},
|
|
1795
|
+
});
|
|
1796
|
+
expect(result.valid).toBe(false);
|
|
1797
|
+
expect(result.errors).toStrictEqual([
|
|
1798
|
+
new ValidationError('refers to a resource action that is currently set to private', 'resource.get', undefined, ['controller', 'actions', 'onWhatever', 'resource']),
|
|
1799
|
+
]);
|
|
1800
|
+
});
|
|
1801
|
+
it('should report an error if a resource action on a block refers is private action without a security definition', async () => {
|
|
1627
1802
|
const { security, ...app } = createTestApp();
|
|
1628
1803
|
app.resources.person.roles = [];
|
|
1629
1804
|
app.pages[0].blocks.push({
|
|
@@ -1652,6 +1827,27 @@ describe('validateAppDefinition', () => {
|
|
|
1652
1827
|
new ValidationError('refers to a resource action that is accessible when logged in, but the app has no security definitions', 'resource.get', undefined, ['pages', 0, 'blocks', 0, 'actions', 'onWhatever', 'resource']),
|
|
1653
1828
|
]);
|
|
1654
1829
|
});
|
|
1830
|
+
it('should report an error if a resource action on the controller refers is private action without a security definition', async () => {
|
|
1831
|
+
const { security, ...app } = createTestApp();
|
|
1832
|
+
app.resources.person.roles = [];
|
|
1833
|
+
app.controller = {
|
|
1834
|
+
actions: {
|
|
1835
|
+
onWhatever: {
|
|
1836
|
+
type: 'resource.get',
|
|
1837
|
+
resource: 'person',
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
};
|
|
1841
|
+
const result = await validateAppDefinition(app, () => [], {
|
|
1842
|
+
actions: {
|
|
1843
|
+
onWhatever: {},
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
expect(result.valid).toBe(false);
|
|
1847
|
+
expect(result.errors).toStrictEqual([
|
|
1848
|
+
new ValidationError('refers to a resource action that is accessible when logged in, but the app has no security definitions', 'resource.get', undefined, ['controller', 'actions', 'onWhatever', 'resource']),
|
|
1849
|
+
]);
|
|
1850
|
+
});
|
|
1655
1851
|
it('should ignore if an app is null', async () => {
|
|
1656
1852
|
const result = await validateAppDefinition(null, () => []);
|
|
1657
1853
|
expect(result.valid).toBe(true);
|