@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.36.10-test.4",
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.3",
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 (path[0] === 'webhooks' && !serverActions.has(action.type)) {
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
- // TODO: repeats way too much, code not self-explanatory at all
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();