@appsemble/lang-sdk 0.36.10-test.4 → 0.37.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/package.json +2 -2
- package/schemas/AppLayoutDefinition.js +7 -0
- package/schemas/actions/ResourceCountActionDefinition.js +4 -0
- package/schemas/actions/ResourceCreateActionDefinition.js +4 -0
- package/schemas/actions/ResourceDeleteActionDefinition.js +4 -0
- package/schemas/actions/ResourceDeleteAllActionDefinition.js +4 -0
- package/schemas/actions/ResourceDeleteBulkActionDefinition.js +4 -0
- package/schemas/actions/ResourceGetActionDefinition.js +4 -0
- package/schemas/actions/ResourceHistoryGetActionDefinition.js +4 -0
- package/schemas/actions/ResourcePatchActionDefinition.js +17 -0
- package/schemas/actions/ResourceQueryActionDefinition.js +4 -0
- package/schemas/actions/ResourceUpdateActionDefinition.js +17 -0
- package/schemas/actions/ResourceUpdateGroupActionDefinition.js +4 -0
- package/schemas/actions/ResourceUpdatePositionsActionDefinition.js +9 -0
- package/types/actionDefinition.d.ts +29 -3
- package/types/core.d.ts +8 -1
- package/validation.js +67 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/lang-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Language SDK for Appsemble",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"intl-messageformat": "^11.0.0",
|
|
50
50
|
"jsonschema": "~1.4.1",
|
|
51
51
|
"language-tags": "^1.0.0",
|
|
52
|
-
"lcm": "^0.0.
|
|
52
|
+
"lcm": "^0.0.4",
|
|
53
53
|
"openapi-types": "^12.0.0",
|
|
54
54
|
"parse-duration": "^1.0.0",
|
|
55
55
|
"type-fest": "^4.0.0",
|
|
@@ -94,6 +94,13 @@ If this is omitted, a collapsable side navigation menu will be rendered on the l
|
|
|
94
94
|
type: 'boolean',
|
|
95
95
|
default: false,
|
|
96
96
|
},
|
|
97
|
+
hideGroupDropdown: {
|
|
98
|
+
description: `Whether to hide the group dropdown.
|
|
99
|
+
|
|
100
|
+
Set to \`true\` to hide it for all users, or provide a list of role names to hide it only
|
|
101
|
+
for members holding any of those roles.`,
|
|
102
|
+
oneOf: [{ type: 'boolean' }, { type: 'array', items: { type: 'string' } }],
|
|
103
|
+
},
|
|
97
104
|
},
|
|
98
105
|
};
|
|
99
106
|
//# sourceMappingURL=AppLayoutDefinition.js.map
|
|
@@ -13,6 +13,10 @@ export const ResourceCountActionDefinition = extendJSONSchema(RequestActionDefin
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to count.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
own: {
|
|
17
21
|
type: 'boolean',
|
|
18
22
|
description: 'If only the resources created by the authenticated app member should be included',
|
|
@@ -13,6 +13,10 @@ export const ResourceCreateActionDefinition = extendJSONSchema(RequestActionDefi
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to create.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
},
|
|
17
21
|
}, ['url', 'method']);
|
|
18
22
|
//# sourceMappingURL=ResourceCreateActionDefinition.js.map
|
|
@@ -13,6 +13,10 @@ export const ResourceDeleteActionDefinition = extendJSONSchema(RequestActionDefi
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to delete.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
},
|
|
17
21
|
}, ['url', 'method']);
|
|
18
22
|
//# sourceMappingURL=ResourceDeleteActionDefinition.js.map
|
|
@@ -16,6 +16,10 @@ export const ResourceDeleteAllActionDefinition = extendJSONSchema(RequestActionD
|
|
|
16
16
|
type: 'string',
|
|
17
17
|
description: 'The type of the resource to delete.',
|
|
18
18
|
},
|
|
19
|
+
selectedGroupId: {
|
|
20
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
21
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
22
|
+
},
|
|
19
23
|
},
|
|
20
24
|
}, ['url', 'method']);
|
|
21
25
|
//# sourceMappingURL=ResourceDeleteAllActionDefinition.js.map
|
|
@@ -13,6 +13,10 @@ export const ResourceDeleteBulkActionDefinition = extendJSONSchema(RequestAction
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to delete.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
},
|
|
17
21
|
}, ['url', 'method']);
|
|
18
22
|
//# sourceMappingURL=ResourceDeleteBulkActionDefinition.js.map
|
|
@@ -13,6 +13,10 @@ export const ResourceGetActionDefinition = extendJSONSchema(RequestActionDefinit
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to get.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
view: {
|
|
17
21
|
type: 'string',
|
|
18
22
|
description: 'The view to use for the resource.',
|
|
@@ -13,6 +13,10 @@ export const ResourceHistoryGetActionDefinition = extendJSONSchema(RequestAction
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to get.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
},
|
|
17
21
|
}, ['url']);
|
|
18
22
|
//# sourceMappingURL=ResourceHistoryGetActionDefinition.js.map
|
|
@@ -13,6 +13,23 @@ export const ResourcePatchActionDefinition = extendJSONSchema(RequestActionDefin
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to patch.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
20
|
+
optimistic: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
description: 'Fetch the latest resource before writing, merge its `$etag` into the request data so the implicit `If-Match` precondition holds, and retry on precondition conflicts. The fetched resource is also exposed to remappers as `{ context: resource }`.',
|
|
24
|
+
properties: {
|
|
25
|
+
retries: {
|
|
26
|
+
type: 'integer',
|
|
27
|
+
minimum: 0,
|
|
28
|
+
default: 0,
|
|
29
|
+
description: 'The number of precondition conflicts to retry after fetching the latest resource again.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
16
33
|
id: {
|
|
17
34
|
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
35
|
},
|
|
@@ -13,6 +13,10 @@ export const ResourceQueryActionDefinition = extendJSONSchema(RequestActionDefin
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to query.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
view: {
|
|
17
21
|
type: 'string',
|
|
18
22
|
description: 'The view to use for the resource.',
|
|
@@ -13,6 +13,23 @@ export const ResourceUpdateActionDefinition = extendJSONSchema(RequestActionDefi
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to update.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
20
|
+
optimistic: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
description: 'Fetch the latest resource before writing, merge its `$etag` into the request data so the implicit `If-Match` precondition holds, and retry on precondition conflicts. The fetched resource is also exposed to remappers as `{ context: resource }`.',
|
|
24
|
+
properties: {
|
|
25
|
+
retries: {
|
|
26
|
+
type: 'integer',
|
|
27
|
+
minimum: 0,
|
|
28
|
+
default: 0,
|
|
29
|
+
description: 'The number of precondition conflicts to retry after fetching the latest resource again.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
16
33
|
},
|
|
17
34
|
}, ['url', 'method']);
|
|
18
35
|
//# sourceMappingURL=ResourceUpdateActionDefinition.js.map
|
|
@@ -13,6 +13,10 @@ export const ResourceUpdateGroupActionDefinition = extendJSONSchema(RequestActio
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to move.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
id: {
|
|
17
21
|
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
22
|
description: 'The ID of the resource to move.',
|
|
@@ -13,9 +13,18 @@ export const ResourceUpdatePositionsActionDefinition = extendJSONSchema(RequestA
|
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'The type of the resource to update.',
|
|
15
15
|
},
|
|
16
|
+
selectedGroupId: {
|
|
17
|
+
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
|
+
description: 'The ID of the group to scope the request to. Defaults to the currently selected group.',
|
|
19
|
+
},
|
|
16
20
|
id: {
|
|
17
21
|
$ref: '#/components/schemas/RemapperDefinition',
|
|
18
22
|
},
|
|
23
|
+
order: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
enum: ['asc', 'desc'],
|
|
26
|
+
default: 'asc',
|
|
27
|
+
},
|
|
19
28
|
},
|
|
20
29
|
}, ['url', 'method']);
|
|
21
30
|
//# sourceMappingURL=ResourceUpdatePositionsActionDefinition.js.map
|
|
@@ -532,6 +532,13 @@ export interface ResourceActionDefinition<T extends ActionName> extends RequestL
|
|
|
532
532
|
* The name of the resource.
|
|
533
533
|
*/
|
|
534
534
|
resource: string;
|
|
535
|
+
/**
|
|
536
|
+
* The ID of the group to scope the request to.
|
|
537
|
+
*
|
|
538
|
+
* Defaults to the `selectedGroupId` input property, or the app member's currently selected
|
|
539
|
+
* group when the property is not present.
|
|
540
|
+
*/
|
|
541
|
+
selectedGroupId?: Remapper;
|
|
535
542
|
}
|
|
536
543
|
interface ViewResourceDefinition {
|
|
537
544
|
/**
|
|
@@ -551,6 +558,25 @@ interface ResourceActionWithIdDefinition {
|
|
|
551
558
|
*/
|
|
552
559
|
id?: Remapper;
|
|
553
560
|
}
|
|
561
|
+
interface ResourceOrderQueryDefinition {
|
|
562
|
+
/**
|
|
563
|
+
* Whether the resources loaded are in descending order.
|
|
564
|
+
*/
|
|
565
|
+
order?: 'asc' | 'desc';
|
|
566
|
+
}
|
|
567
|
+
interface OptimisticResourceWriteActionDefinition {
|
|
568
|
+
/**
|
|
569
|
+
* Fetch the latest resource before writing, merge its `$etag` into the request data so the
|
|
570
|
+
* implicit `If-Match` precondition holds, and retry on precondition conflicts. The fetched
|
|
571
|
+
* resource is also exposed to remappers as `{ context: resource }`.
|
|
572
|
+
*/
|
|
573
|
+
optimistic?: {
|
|
574
|
+
/**
|
|
575
|
+
* The number of precondition conflicts to retry after fetching the latest resource again.
|
|
576
|
+
*/
|
|
577
|
+
retries?: number;
|
|
578
|
+
};
|
|
579
|
+
}
|
|
554
580
|
export interface ControllerActionDefinition extends BaseActionDefinition<'controller'> {
|
|
555
581
|
handler: string;
|
|
556
582
|
}
|
|
@@ -563,12 +589,12 @@ export type ResourceHistoryGetActionDefinition = ResourceActionDefinition<'resou
|
|
|
563
589
|
export type ResourceGetActionDefinition = ResourceActionDefinition<'resource.get'> & ResourceActionWithIdDefinition & ViewResourceDefinition;
|
|
564
590
|
export type ResourceQueryActionDefinition = OwnResourceDefinition & ResourceActionDefinition<'resource.query'> & ViewResourceDefinition;
|
|
565
591
|
export type ResourceCountActionDefinition = OwnResourceDefinition & ResourceActionDefinition<'resource.count'>;
|
|
566
|
-
export type ResourceUpdateActionDefinition = ResourceActionDefinition<'resource.update'
|
|
592
|
+
export type ResourceUpdateActionDefinition = ResourceActionDefinition<'resource.update'> & OptimisticResourceWriteActionDefinition;
|
|
567
593
|
export type ResourceUpdateGroupActionDefinition = ResourceActionDefinition<'resource.update.group'> & ResourceActionWithIdDefinition & {
|
|
568
594
|
groupId?: Remapper;
|
|
569
595
|
};
|
|
570
|
-
export type ResourceUpdatePositionsActionDefinition = ResourceActionDefinition<'resource.update.positions'> & ResourceActionWithIdDefinition;
|
|
571
|
-
export type ResourcePatchActionDefinition = ResourceActionDefinition<'resource.patch'> & ResourceActionWithIdDefinition;
|
|
596
|
+
export type ResourceUpdatePositionsActionDefinition = ResourceActionDefinition<'resource.update.positions'> & ResourceActionWithIdDefinition & ResourceOrderQueryDefinition;
|
|
597
|
+
export type ResourcePatchActionDefinition = ResourceActionDefinition<'resource.patch'> & ResourceActionWithIdDefinition & OptimisticResourceWriteActionDefinition;
|
|
572
598
|
export type AppMemberLogoutAction = BaseActionDefinition<'app.member.logout'>;
|
|
573
599
|
export interface BaseResourceSubscribeActionDefinition<T extends ActionName> extends BaseActionDefinition<T> {
|
|
574
600
|
/**
|
package/types/core.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type CompanionContainerDefinition } from './container.js';
|
|
|
6
6
|
import { type LayoutPosition, type Navigation } from './layout.js';
|
|
7
7
|
import { type Remapper } from './remapper.js';
|
|
8
8
|
import { type ResourceDefinition } from './resource.js';
|
|
9
|
-
import { type ViewRole } from './roles.js';
|
|
9
|
+
import { type AppRole, type ViewRole } from './roles.js';
|
|
10
10
|
import { type Security } from './security.js';
|
|
11
11
|
import { type Theme } from './theme.js';
|
|
12
12
|
import { type PageLayoutDefinition } from './gridLayout.js';
|
|
@@ -110,6 +110,13 @@ export interface AppDefinition {
|
|
|
110
110
|
* Whether to hide the title bar from all pages in the app.
|
|
111
111
|
*/
|
|
112
112
|
hideTitleBar?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Whether to hide the group dropdown.
|
|
115
|
+
*
|
|
116
|
+
* Set to `true` to hide it for all users, or provide a list of role names to hide it
|
|
117
|
+
* only for members holding any of those roles.
|
|
118
|
+
*/
|
|
119
|
+
hideGroupDropdown?: boolean | AppRole[];
|
|
113
120
|
};
|
|
114
121
|
/**
|
|
115
122
|
* The strategy to use for apps to subscribe to push notifications.
|
package/validation.js
CHANGED
|
@@ -6,7 +6,7 @@ import { BlockParamInstanceValidator } from './BasicValidator.js';
|
|
|
6
6
|
import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
|
|
7
7
|
import { partialNormalized } from './constants/index.js';
|
|
8
8
|
import { findPageByName } from './findPageByName.js';
|
|
9
|
-
import { iterApp } from './iterApp.js';
|
|
9
|
+
import { iterAction, iterApp } from './iterApp.js';
|
|
10
10
|
import { has } from './miscellaneous.js';
|
|
11
11
|
import { normalize } from './normalize.js';
|
|
12
12
|
import { serverActions } from './serverActions.js';
|
|
@@ -19,6 +19,29 @@ const allResourceViewPermissionPattern = /^\$resource:all:(get|query):[^:]+$/;
|
|
|
19
19
|
const resourceViewPermissionPattern = /^\$resource:[^:]+:(get|query):[^:]+$/;
|
|
20
20
|
const allWebhookPermissionPattern = /^\$webhook:all:invoke$/;
|
|
21
21
|
const webhookPermissionPattern = /^\$webhook:[^:]+:invoke$/;
|
|
22
|
+
function hasResourcePermission(permissions, resourceName, resourceAction, view) {
|
|
23
|
+
return permissions.some((permission) => {
|
|
24
|
+
if (resourcePermissionPattern.test(permission)) {
|
|
25
|
+
const [, permissionResourceName, permissionResourceAction] = permission.split(':');
|
|
26
|
+
return (['all', resourceName].includes(permissionResourceName) &&
|
|
27
|
+
(permissionResourceAction === resourceAction ||
|
|
28
|
+
(resourceAction === 'count' && permissionResourceAction === 'query')));
|
|
29
|
+
}
|
|
30
|
+
if (ownResourcePermissionPattern.test(permission)) {
|
|
31
|
+
const [, permissionResourceName, , permissionResourceAction] = permission.split(':');
|
|
32
|
+
return (['all', resourceName].includes(permissionResourceName) &&
|
|
33
|
+
(permissionResourceAction === resourceAction ||
|
|
34
|
+
(resourceAction === 'count' && permissionResourceAction === 'query')));
|
|
35
|
+
}
|
|
36
|
+
if (view && resourceViewPermissionPattern.test(permission)) {
|
|
37
|
+
const [, permissionResourceName, permissionResourceAction, permissionResourceView] = permission.split(':');
|
|
38
|
+
return (['all', resourceName].includes(permissionResourceName) &&
|
|
39
|
+
permissionResourceAction === resourceAction &&
|
|
40
|
+
(!permissionResourceView || permissionResourceView === view));
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
22
45
|
/**
|
|
23
46
|
* Check whether or not the given link represents a link related to the Appsemble core.
|
|
24
47
|
*
|
|
@@ -968,6 +991,12 @@ function validateSecurity(definition, report) {
|
|
|
968
991
|
}
|
|
969
992
|
}
|
|
970
993
|
iterApp(definition, { onBlock: checkRoles, onPage: checkRoles });
|
|
994
|
+
const { hideGroupDropdown } = definition.layout ?? {};
|
|
995
|
+
if (Array.isArray(hideGroupDropdown)) {
|
|
996
|
+
for (const [index, role] of hideGroupDropdown.entries()) {
|
|
997
|
+
checkRoleExists(role, ['layout', 'hideGroupDropdown', index]);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
971
1000
|
}
|
|
972
1001
|
/**
|
|
973
1002
|
* Validates the hooks in resource definition to ensure its properties are valid.
|
|
@@ -1068,6 +1097,18 @@ function validateCronJobs({ cron }, report) {
|
|
|
1068
1097
|
// TODO: horrible nesting
|
|
1069
1098
|
function validateActions(definition, report) {
|
|
1070
1099
|
const urlRegex = new RegExp(`^${partialNormalized.source}:`);
|
|
1100
|
+
const validateUnsupportedServerOptimisticResourceWrite = (action, path) => {
|
|
1101
|
+
if ((path[0] === 'cron' || path[0] === 'webhooks') &&
|
|
1102
|
+
(action.type === 'resource.update' || action.type === 'resource.patch') &&
|
|
1103
|
+
'optimistic' in action) {
|
|
1104
|
+
report(action.type, 'optimistic resource writes are not supported for server-side actions', [
|
|
1105
|
+
...path,
|
|
1106
|
+
'optimistic',
|
|
1107
|
+
]);
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
return false;
|
|
1111
|
+
};
|
|
1071
1112
|
iterApp(definition, {
|
|
1072
1113
|
onAction(action, path) {
|
|
1073
1114
|
// XXX: could we validate server-side actions differently
|
|
@@ -1075,8 +1116,7 @@ function validateActions(definition, report) {
|
|
|
1075
1116
|
report(action.type, 'action type is not supported for cron jobs', [...path, 'type']);
|
|
1076
1117
|
return;
|
|
1077
1118
|
}
|
|
1078
|
-
if (
|
|
1079
|
-
report(action.type, 'action type is not supported for webhooks', [...path, 'type']);
|
|
1119
|
+
if (validateUnsupportedServerOptimisticResourceWrite(action, path)) {
|
|
1080
1120
|
return;
|
|
1081
1121
|
}
|
|
1082
1122
|
if (action.type.startsWith('app.member.') && !definition.security) {
|
|
@@ -1100,6 +1140,13 @@ function validateActions(definition, report) {
|
|
|
1100
1140
|
const { resource: resourceName, view } = action;
|
|
1101
1141
|
const resource = definition.resources?.[resourceName];
|
|
1102
1142
|
const [, resourceAction] = action.type.split('.');
|
|
1143
|
+
// Optimistic concurrency is implicit via `data.$etag`; the explicit
|
|
1144
|
+
// `ifMatch` field used to live on the action schema and has been removed.
|
|
1145
|
+
if (['update', 'patch', 'delete'].includes(resourceAction) &&
|
|
1146
|
+
'ifMatch' in action) {
|
|
1147
|
+
report(action.type, 'ifMatch is no longer accepted; resource writes use the implicit $etag carried in data', [...path, 'ifMatch']);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1103
1150
|
if (!resource) {
|
|
1104
1151
|
report(action.type, 'refers to a resource that doesn’t exist', [...path, 'resource']);
|
|
1105
1152
|
return;
|
|
@@ -1118,31 +1165,16 @@ function validateActions(definition, report) {
|
|
|
1118
1165
|
const allRolePermissions = getAppRolePermissions(definition.security, Object.keys(definition.security.roles));
|
|
1119
1166
|
allPermissions.push(...allRolePermissions);
|
|
1120
1167
|
}
|
|
1121
|
-
|
|
1122
|
-
if (!allPermissions.some((permission) => {
|
|
1123
|
-
if (resourcePermissionPattern.test(permission)) {
|
|
1124
|
-
const [, permissionResourceName, permissionResourceAction] = permission.split(':');
|
|
1125
|
-
return (['all', resourceName].includes(permissionResourceName) &&
|
|
1126
|
-
(permissionResourceAction === resourceAction ||
|
|
1127
|
-
(resourceAction === 'count' && permissionResourceAction === 'query')));
|
|
1128
|
-
}
|
|
1129
|
-
if (ownResourcePermissionPattern.test(permission)) {
|
|
1130
|
-
const [, permissionResourceName, , permissionResourceAction] = permission.split(':');
|
|
1131
|
-
return (['all', resourceName].includes(permissionResourceName) &&
|
|
1132
|
-
(permissionResourceAction === resourceAction ||
|
|
1133
|
-
(resourceAction === 'count' && permissionResourceAction === 'query')));
|
|
1134
|
-
}
|
|
1135
|
-
if (view && resourceViewPermissionPattern.test(permission)) {
|
|
1136
|
-
const [, permissionResourceName, permissionResourceAction, permissionResourceView] = permission.split(':');
|
|
1137
|
-
return (['all', resourceName].includes(permissionResourceName) &&
|
|
1138
|
-
permissionResourceAction === resourceAction &&
|
|
1139
|
-
(!permissionResourceView || permissionResourceView === view));
|
|
1140
|
-
}
|
|
1141
|
-
return false;
|
|
1142
|
-
})) {
|
|
1168
|
+
if (!hasResourcePermission(allPermissions, resourceName, resourceAction, view)) {
|
|
1143
1169
|
report(action.type, 'there is no one in the app who has permissions to use this action', [...path, 'resource']);
|
|
1144
1170
|
return;
|
|
1145
1171
|
}
|
|
1172
|
+
if (['update', 'patch'].includes(resourceAction) &&
|
|
1173
|
+
'optimistic' in action &&
|
|
1174
|
+
!hasResourcePermission(allPermissions, resourceName, 'get', view)) {
|
|
1175
|
+
report(action.type, 'there is no one in the app who has permissions to fetch this resource for optimistic writes', [...path, 'optimistic']);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1146
1178
|
}
|
|
1147
1179
|
}
|
|
1148
1180
|
if (action.type === 'webhook') {
|
|
@@ -1254,6 +1286,16 @@ function validateActions(definition, report) {
|
|
|
1254
1286
|
}
|
|
1255
1287
|
},
|
|
1256
1288
|
});
|
|
1289
|
+
for (const [name, webhook] of Object.entries(definition.webhooks ?? {})) {
|
|
1290
|
+
if (webhook.action &&
|
|
1291
|
+
iterAction(webhook.action, {
|
|
1292
|
+
onAction(action, path) {
|
|
1293
|
+
return validateUnsupportedServerOptimisticResourceWrite(action, path);
|
|
1294
|
+
},
|
|
1295
|
+
}, ['webhooks', name, 'action'])) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1257
1299
|
}
|
|
1258
1300
|
function validateEvents(definition, blockVersions, report) {
|
|
1259
1301
|
const indexMap = new Map();
|