@appsemble/lang-sdk 0.40.9 → 0.40.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.40.9",
3
+ "version": "0.40.10",
4
4
  "description": "Language SDK for Appsemble",
5
5
  "keywords": [
6
6
  "app",
@@ -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
- enum: ['login', 'opt-in', 'startup'],
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
  },
@@ -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/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 notifications can not be sent.
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/validation.js CHANGED
@@ -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
  }
@@ -703,7 +703,7 @@ function validateGridLayout(definition, report) {
703
703
  // Flow page
704
704
  if (page.type === 'flow') {
705
705
  const flowPage = page;
706
- const steps = flowPage.steps.map((step, stepId) => [
706
+ const steps = (flowPage.steps ?? []).map((step, stepId) => [
707
707
  step,
708
708
  [...path, 'steps', stepId],
709
709
  ]);
@@ -1122,8 +1122,9 @@ function validateSecurity(definition, report) {
1122
1122
  checkRoleExists(role, [...path, 'roles', index], ['$guest', ...predefinedRoles]);
1123
1123
  }
1124
1124
  };
1125
+ const notificationRoles = notifications && typeof notifications === 'object' ? notifications.roles : undefined;
1125
1126
  if (!security) {
1126
- if (notifications === 'login') {
1127
+ if (notifications === 'login' || notificationRoles) {
1127
1128
  report(notifications, 'only works if security is defined', ['notifications']);
1128
1129
  }
1129
1130
  return;
@@ -1224,6 +1225,11 @@ function validateSecurity(definition, report) {
1224
1225
  }
1225
1226
  });
1226
1227
  }
1228
+ if (notificationRoles) {
1229
+ for (const role of Object.keys(notificationRoles)) {
1230
+ checkRoleExists(role, ['notifications', 'roles', role]);
1231
+ }
1232
+ }
1227
1233
  const { hideGroupDropdown } = definition.layout ?? {};
1228
1234
  if (Array.isArray(hideGroupDropdown)) {
1229
1235
  for (const [index, role] of hideGroupDropdown.entries()) {
@@ -1819,6 +1825,9 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
1819
1825
  const report = (instance, message, path) => {
1820
1826
  result.errors.push(new ValidationError(message, instance, undefined, path));
1821
1827
  };
1828
+ const reportUnexpectedError = (error) => {
1829
+ report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);
1830
+ };
1822
1831
  try {
1823
1832
  // IMPORTANT: Create a deep copy to prevent validation logic from mutating the original definition.
1824
1833
  // The imperative/procedural-style algorithms below (e.g., in validateActions) build up arrays
@@ -1834,27 +1843,36 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
1834
1843
  }
1835
1844
  blockVersionMap.get(version.name).set(version.version, version);
1836
1845
  }
1837
- validateController(clonedDefinition, controllerImplementations, report);
1838
- validateCronJobs(clonedDefinition, report);
1839
- validateDefaultPage(clonedDefinition, report);
1840
- validateHooks(clonedDefinition, report);
1841
- validateIcons(clonedDefinition, report);
1842
- validateIconReferences(clonedDefinition, blockVersionMap, report);
1843
- validateLanguage(clonedDefinition, report);
1844
- validateResourceReferences(clonedDefinition, report);
1845
- validateMembersSchema(clonedDefinition, report);
1846
- validatePageParents(clonedDefinition, report);
1847
- validatePhoneNumberDefinition(clonedDefinition, report);
1848
- validateResourceSchemas(clonedDefinition, report);
1849
- validateSecurity(clonedDefinition, report);
1850
- validateBlocks(clonedDefinition, blockVersionMap, report);
1851
- validateActions(clonedDefinition, report);
1852
- validateEvents(clonedDefinition, blockVersionMap, report);
1853
- validateUniquePageNames(clonedDefinition, report);
1854
- validateGridLayout(clonedDefinition, report);
1846
+ // Run each check on its own, so a check that fails does not keep the others from reporting.
1847
+ const check = (validate) => {
1848
+ try {
1849
+ validate();
1850
+ }
1851
+ catch (error) {
1852
+ reportUnexpectedError(error);
1853
+ }
1854
+ };
1855
+ check(() => validateController(clonedDefinition, controllerImplementations, report));
1856
+ check(() => validateCronJobs(clonedDefinition, report));
1857
+ check(() => validateDefaultPage(clonedDefinition, report));
1858
+ check(() => validateHooks(clonedDefinition, report));
1859
+ check(() => validateIcons(clonedDefinition, report));
1860
+ check(() => validateIconReferences(clonedDefinition, blockVersionMap, report));
1861
+ check(() => validateLanguage(clonedDefinition, report));
1862
+ check(() => validateResourceReferences(clonedDefinition, report));
1863
+ check(() => validateMembersSchema(clonedDefinition, report));
1864
+ check(() => validatePageParents(clonedDefinition, report));
1865
+ check(() => validatePhoneNumberDefinition(clonedDefinition, report));
1866
+ check(() => validateResourceSchemas(clonedDefinition, report));
1867
+ check(() => validateSecurity(clonedDefinition, report));
1868
+ check(() => validateBlocks(clonedDefinition, blockVersionMap, report));
1869
+ check(() => validateActions(clonedDefinition, report));
1870
+ check(() => validateEvents(clonedDefinition, blockVersionMap, report));
1871
+ check(() => validateUniquePageNames(clonedDefinition, report));
1872
+ check(() => validateGridLayout(clonedDefinition, report));
1855
1873
  }
1856
1874
  catch (error) {
1857
- report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);
1875
+ reportUnexpectedError(error);
1858
1876
  }
1859
1877
  return result;
1860
1878
  }