@appsemble/node-utils 0.30.14-test.6 → 0.32.1-test.14

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.
Files changed (89) hide show
  1. package/README.md +3 -3
  2. package/app.js +8 -0
  3. package/assets.d.ts +16 -0
  4. package/assets.js +65 -0
  5. package/authentication.d.ts +1 -1
  6. package/container/helpers.d.ts +1 -1
  7. package/container/helpers.js +1 -0
  8. package/container/logs.js +8 -5
  9. package/container/operations.js +22 -14
  10. package/container/scale.js +3 -2
  11. package/container/specs.js +1 -1
  12. package/fs.js +4 -0
  13. package/getAppsembleMessages.js +2 -0
  14. package/getMessagesUtil.js +5 -4
  15. package/getValidTrainings.d.ts +8 -0
  16. package/getValidTrainings.js +47 -0
  17. package/icon.js +5 -0
  18. package/index.d.ts +5 -1
  19. package/index.js +5 -1
  20. package/interceptors.js +1 -1
  21. package/koa.d.ts +13 -1
  22. package/koa.js +17 -1
  23. package/logger.js +5 -3
  24. package/middleware/loggerMiddleware.js +1 -0
  25. package/odata.js +2 -1
  26. package/package.json +22 -14
  27. package/parsers.d.ts +1 -0
  28. package/parsers.js +130 -0
  29. package/render.js +1 -1
  30. package/resource.d.ts +18 -3
  31. package/resource.js +50 -10
  32. package/s3.d.ts +18 -0
  33. package/s3.js +131 -0
  34. package/server/controllers/common/apps/assets/createGetAppAssetByIdController.js +4 -3
  35. package/server/controllers/common/apps/groups/createGetAppGroupsController.js +2 -2
  36. package/server/controllers/common/apps/messages/createGetAppMessagesController.js +2 -0
  37. package/server/controllers/common/apps/resources/createCreateAppResourceController.js +16 -2
  38. package/server/controllers/common/apps/resources/createDeleteAppResourceController.js +2 -2
  39. package/server/controllers/common/apps/resources/createGetAppResourceByIdController.js +3 -3
  40. package/server/controllers/common/apps/resources/createQueryAppResourcesController.js +1 -1
  41. package/server/controllers/common/apps/resources/createUpdateAppResourceController.js +6 -3
  42. package/server/controllers/common/apps/variables/createGetAppVariablesController.js +2 -2
  43. package/server/controllers/main/apps/assets/createCreateAppAssetController.js +4 -4
  44. package/server/controllers/main/apps/resources/createDeleteAppSeedResourcesController.js +2 -2
  45. package/server/routes/appRouter/blockAssetHandler.js +5 -3
  46. package/server/routes/appRouter/blockCssHandler.js +5 -3
  47. package/server/routes/appRouter/bulmaHandler.js +1 -0
  48. package/server/routes/appRouter/cssHandler.js +2 -2
  49. package/server/routes/appRouter/iconHandler.js +4 -1
  50. package/server/routes/appRouter/indexHandler.js +3 -1
  51. package/server/routes/appRouter/manifestHandler.js +2 -2
  52. package/server/routes/appRouter/readmeHandler.js +4 -3
  53. package/server/routes/appRouter/robotsHandler.js +2 -2
  54. package/server/routes/appRouter/screenshotHandler.js +4 -3
  55. package/server/routes/appRouter/serviceWorkerHandler.js +2 -2
  56. package/server/types.d.ts +22 -7
  57. package/server/types.js +7 -1
  58. package/server/utils/actions.js +16 -10
  59. package/server/utils/resources.js +4 -1
  60. package/tinyRouter.js +3 -3
  61. package/uploads.d.ts +2 -0
  62. package/uploads.js +27 -0
  63. package/versions.d.ts +3 -0
  64. package/versions.js +7 -0
  65. package/AppsembleError.test.d.ts +0 -1
  66. package/AppsembleError.test.js +0 -7
  67. package/basicAuth.test.d.ts +0 -1
  68. package/basicAuth.test.js +0 -7
  69. package/formData.test.d.ts +0 -1
  70. package/formData.test.js +0 -93
  71. package/fs.test.d.ts +0 -1
  72. package/fs.test.js +0 -75
  73. package/handleError.test.d.ts +0 -1
  74. package/handleError.test.js +0 -33
  75. package/interceptors.test.d.ts +0 -1
  76. package/interceptors.test.js +0 -83
  77. package/logger.test.d.ts +0 -1
  78. package/logger.test.js +0 -37
  79. package/middleware/error.test.d.ts +0 -1
  80. package/middleware/error.test.js +0 -59
  81. package/middleware/loggerMiddleware.test.d.ts +0 -1
  82. package/middleware/loggerMiddleware.test.js +0 -120
  83. package/readFileOrString.test.d.ts +0 -1
  84. package/readFileOrString.test.js +0 -12
  85. package/server/controllers/common/apps/resources/resources.test.d.ts +0 -1
  86. package/server/controllers/common/apps/resources/resources.test.js +0 -580
  87. package/tinyRouter.test.d.ts +0 -1
  88. package/tinyRouter.test.js +0 -83
  89. package/versions.cjs +0 -4
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.30.14-test.6/config/assets/logo.svg) Appsemble Node Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.32.1-test.14/config/assets/logo.svg) Appsemble Node Utilities
2
2
 
3
3
  > NodeJS utilities used by Appsemble internally.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/node-utils)](https://www.npmjs.com/package/@appsemble/node-utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.30.14-test.6/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.30.14-test.6)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.32.1-test.14/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.32.1-test.14)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
26
26
 
27
27
  ## License
28
28
 
29
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.30.14-test.6/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.32.1-test.14/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
package/app.js CHANGED
@@ -36,23 +36,30 @@ export async function getRemapperContext(app, language, options, context) {
36
36
  const appMemberInfo = await getCurrentAppMember({ context });
37
37
  const cache = objectCache((message) => new IntlMessageFormat(message, language, undefined, {
38
38
  formatters: {
39
+ // XXX: idk
40
+ // @ts-expect-error 2322 undefined is not assignable to type ... (strictNullChecks)
39
41
  getNumberFormat,
40
42
  getPluralRules,
41
43
  getDateTimeFormat: memoize((locale, opts) => new Intl.DateTimeFormat(locale, { ...opts, timeZone: appMemberInfo?.zoneinfo })),
42
44
  },
43
45
  }));
44
46
  return {
47
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
45
48
  appId: app.id,
46
49
  appUrl,
47
50
  url: appUrl,
48
51
  getMessage({ defaultMessage, id }) {
52
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
53
+ // (strictNullChecks)
49
54
  const msg = appMessages.find(({ messages }) => has(messages.messageIds, id));
55
+ // @ts-expect-error 2538 type undefined cannot be used as an index type
50
56
  const message = msg ? msg.messages.messageIds[id] : defaultMessage;
51
57
  return cache(message || `'{${id}}'`);
52
58
  },
53
59
  getVariable(variableName) {
54
60
  return appVariables.find((appVariable) => appVariable.name === variableName)?.value;
55
61
  },
62
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
56
63
  appMemberInfo,
57
64
  context: {},
58
65
  locale: appMemberInfo?.locale ?? app.definition.defaultLanguage ?? defaultLocale,
@@ -150,6 +157,7 @@ async function patchStyle(replacementsPath, replacements) {
150
157
  parseValue: false,
151
158
  });
152
159
  for (const { property, selector, value } of replacements) {
160
+ // @ts-expect-error 2322 undefined is not assignable to type (strictNullChecks)
153
161
  doc = replaceStyle(doc, selector, property, String(value));
154
162
  }
155
163
  await writeFile(replacementsPath, generate(doc));
package/assets.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Context } from 'koa';
2
+ import { type BucketItemStat } from 'minio';
3
+ export declare function setAssetHeaders(ctx: Context, mime: string, filename: string | null, stats?: BucketItemStat): void;
4
+ interface FileMeta {
5
+ filename?: string;
6
+ mime: string;
7
+ }
8
+ export declare function getCompressedFileMeta({ filename, mime }: FileMeta): FileMeta;
9
+ export interface AssetToUpload {
10
+ id: string;
11
+ mime: string;
12
+ path: string;
13
+ }
14
+ export declare function uploadAsset(appId: number, asset: AssetToUpload): Promise<string[]>;
15
+ export declare function uploadAssets(appId: number, assets: AssetToUpload[]): Promise<void>;
16
+ export {};
package/assets.js ADDED
@@ -0,0 +1,65 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { stat } from 'node:fs/promises';
3
+ import sharp from 'sharp';
4
+ import { logger } from './logger.js';
5
+ import { uploadS3File } from './s3.js';
6
+ import { removeUploads } from './uploads.js';
7
+ export function setAssetHeaders(ctx, mime, filename, stats) {
8
+ ctx.set('content-type', mime || 'application/octet-stream');
9
+ if (filename) {
10
+ ctx.set('content-disposition', `${mime?.startsWith('image') ? 'inline' : 'attachment'}; filename=${JSON.stringify(filename)}`);
11
+ }
12
+ if (stats) {
13
+ ctx.set('Content-Length', String(stats.size));
14
+ ctx.set('ETag', stats.etag);
15
+ ctx.set('Last-Modified', stats.lastModified.toUTCString());
16
+ }
17
+ ctx.set('Access-Control-Expose-Headers', 'Content-Disposition');
18
+ ctx.set('Cache-Control', 'max-age=31536000,immutable');
19
+ }
20
+ const ignoredMimes = new Set(['image/avif']);
21
+ export function getCompressedFileMeta({ filename, mime }) {
22
+ if (mime?.startsWith('image') && !ignoredMimes.has(mime)) {
23
+ return {
24
+ filename: filename
25
+ ? filename.includes('.')
26
+ ? `${filename.slice(0, filename.lastIndexOf('.'))}.avif`
27
+ : `${filename}.avif`
28
+ : undefined,
29
+ mime: 'image/avif',
30
+ };
31
+ }
32
+ return { filename, mime };
33
+ }
34
+ export async function uploadAsset(appId, asset) {
35
+ const { id, mime, path } = asset;
36
+ const filesToUnlink = [path];
37
+ let uploadFrom = path;
38
+ if (mime?.startsWith('image') && !ignoredMimes.has(mime)) {
39
+ uploadFrom = `${path}_compressed`;
40
+ await sharp(path).rotate().toFormat('avif').toFile(uploadFrom);
41
+ filesToUnlink.push(uploadFrom);
42
+ }
43
+ try {
44
+ const stats = await stat(uploadFrom);
45
+ const stream = createReadStream(uploadFrom);
46
+ await uploadS3File(`app-${appId}`, id, stream, stats.size);
47
+ }
48
+ catch (error) {
49
+ logger.error(error);
50
+ }
51
+ return filesToUnlink;
52
+ }
53
+ export async function uploadAssets(appId, assets) {
54
+ const filesToUnlink = [];
55
+ for (const asset of assets) {
56
+ const toUnlink = await uploadAsset(appId, asset);
57
+ for (const path of toUnlink) {
58
+ if (!filesToUnlink.includes(path)) {
59
+ filesToUnlink.push(path);
60
+ }
61
+ }
62
+ }
63
+ await removeUploads(filesToUnlink);
64
+ }
65
+ //# sourceMappingURL=assets.js.map
@@ -9,4 +9,4 @@ export declare function getKeytar(): Promise<typeof import('keytar')>;
9
9
  * multiple scopes.
10
10
  * @param inputCredentials Client credentials passed from the command line.
11
11
  */
12
- export declare function authenticate(remote: string, scope: string, inputCredentials: string): Promise<void>;
12
+ export declare function authenticate(remote: string, scope: string, inputCredentials?: string): Promise<void>;
@@ -28,6 +28,6 @@ export declare function parseServiceUrl(url: string): {
28
28
  appName: string;
29
29
  namespace: string;
30
30
  };
31
- export declare function validateContainerResources(resources: ContainerResources): ContainerResources;
31
+ export declare function validateContainerResources(resources?: ContainerResources): ContainerResources;
32
32
  export declare function isWhitelisted(fullImageName: string): Boolean;
33
33
  export declare function formatEnv(input: ContainerEnvVar[], appName: string, appId: string): V1EnvVar[];
@@ -70,6 +70,7 @@ export function parseServiceUrl(url) {
70
70
  const namespace = elements[1];
71
71
  const appId = deploymentName.split('-').pop();
72
72
  const appName = deploymentName.replace(`-${appId}`, '');
73
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
73
74
  return { deploymentName, appId, appName, namespace };
74
75
  }
75
76
  export function validateContainerResources(resources) {
package/container/logs.js CHANGED
@@ -66,19 +66,22 @@ export async function getLogs(deploymentName, fromAppsemble) {
66
66
  for (const pod of pods.items) {
67
67
  if (pod.metadata?.ownerReferences?.find((r) => r.kind === 'ReplicaSet') &&
68
68
  pod.status?.phase === 'Running') {
69
- if (fromAppsemble && !pod.metadata?.name.includes(appsembleServiceName)) {
69
+ if (fromAppsemble && !pod.metadata?.name?.includes(appsembleServiceName)) {
70
70
  continue;
71
71
  }
72
- if (!fromAppsemble && !pod.metadata?.name.includes(deploymentName)) {
72
+ if (!fromAppsemble && !pod.metadata?.name?.includes(deploymentName)) {
73
73
  continue;
74
74
  }
75
75
  logger.silly(`Pod: ${pod.metadata?.name}, status: ${pod.status}, deployment: ${deploymentName}`);
76
76
  try {
77
- const res = await fetchLogs(fromAppsemble ? appsembleNamespace : containerNamespace, pod.metadata?.name, fromAppsemble ? 'appsemble' : deploymentName);
77
+ const res = await fetchLogs(fromAppsemble ? appsembleNamespace : containerNamespace,
78
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
79
+ // (strictNullChecks)
80
+ pod.metadata?.name, fromAppsemble ? 'appsemble' : deploymentName);
78
81
  let entries = res.split('\n');
79
82
  logger.silly(`Number of entries found for pod ${pod.metadata?.name}: ${entries.length}`);
80
- entries = filterLogEntries([...entries], deploymentName, fromAppsemble);
81
- podLogs.push({ fromAppsemble, entries });
83
+ entries = filterLogEntries([...entries], deploymentName, fromAppsemble ?? false);
84
+ podLogs.push({ fromAppsemble: fromAppsemble ?? false, entries });
82
85
  }
83
86
  catch (error) {
84
87
  handleKubernetesError(error);
@@ -41,7 +41,7 @@ export async function updateNamespacedSecret(secretName, value, appName, appId)
41
41
  existing.data[secretName] = Buffer.from(value).toString('base64');
42
42
  try {
43
43
  const { body: updatedSecret } = await coreApi.replaceNamespacedSecret(formattedName, namespace, existing);
44
- logger.info(`Secret ${updatedSecret.metadata.name} updated successfully`);
44
+ logger.info(`Secret ${updatedSecret.metadata?.name} updated successfully`);
45
45
  }
46
46
  catch (error) {
47
47
  handleKubernetesError(error);
@@ -68,7 +68,7 @@ export async function deleteSecret(appName, appId, key) {
68
68
  try {
69
69
  const { body: secret } = await coreApi.readNamespacedSecret(secretName, namespace);
70
70
  logger.verbose(`Deleteing key ${key} of secret ${secretName} from namespace ${namespace} ...`);
71
- if (!(key in secret.data)) {
71
+ if (!(secret.data && key in secret.data)) {
72
72
  logger.warn(`Key ${key} does not exist on secret ${secret.metadata?.name}`);
73
73
  return;
74
74
  }
@@ -113,17 +113,19 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
113
113
  // Update or create deployment and service
114
114
  for (const def of definitions) {
115
115
  const serviceName = formatServiceName(def.name, appName, appId);
116
- const existing = services?.items?.find((s) => s.metadata.name === serviceName);
116
+ const existing = services?.items?.find((s) => s.metadata?.name === serviceName);
117
117
  // Update
118
118
  if (existing) {
119
- logger.verbose(`Updating resources for ${existing.metadata.name} in namespace ${namespace}`);
119
+ logger.verbose(`Updating resources for ${existing.metadata?.name} in namespace ${namespace}`);
120
120
  const patchOptions = [
121
- existing.metadata?.name,
121
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
122
+ existing.metadata.name,
122
123
  namespace,
123
124
  [
124
125
  {
125
126
  op: 'replace',
126
127
  path: '/metadata',
128
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
127
129
  value: { ...def.metadata, name: existing.metadata.name },
128
130
  },
129
131
  {
@@ -159,6 +161,7 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
159
161
  {
160
162
  op: 'replace',
161
163
  path: '/metadata',
164
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
162
165
  value: { ...def.metadata, name: existing.metadata.name },
163
166
  },
164
167
  {
@@ -169,7 +172,7 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
169
172
  {
170
173
  op: 'replace',
171
174
  path: '/spec/template/spec/containers/0/env',
172
- value: [...formatEnv(def.env, appName, appId)],
175
+ value: [...formatEnv(def.env ?? [], appName, appId)],
173
176
  },
174
177
  {
175
178
  op: 'add',
@@ -197,18 +200,19 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
197
200
  logger.silly(`Deployment ${serviceName} has 0 pods, skipping ...`);
198
201
  continue;
199
202
  }
200
- const pod = body?.items?.find((i) => i.metadata.labels.app === existing.metadata.name);
203
+ const pod = body?.items?.find((i) => i.metadata?.labels?.app === existing.metadata?.name);
201
204
  // Set base labels, such as selector, pod name, pod hash
202
205
  const props = {
203
206
  ...def.metadata,
204
207
  labels: {
205
208
  ...(def.metadata ? def.metadata.labels : null),
206
- app: pod.metadata.labels.app,
207
- 'pod-template-hash': pod.metadata.labels['pod-template-hash'],
209
+ app: pod?.metadata?.labels?.app,
210
+ 'pod-template-hash': pod?.metadata?.labels?.['pod-template-hash'],
208
211
  appId,
209
212
  },
210
- name: pod.metadata.name,
213
+ name: pod?.metadata?.name,
211
214
  };
215
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
212
216
  patchOptions[0] = pod?.metadata?.name;
213
217
  patchOptions[2] = [
214
218
  {
@@ -227,7 +231,9 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
227
231
  // Create
228
232
  else {
229
233
  const { deployment, service } = generateDeploymentAndServiceSpecs(def, serviceName, appName, appId, registry);
230
- if (isWhitelisted(deployment.spec.template.spec.containers[0].image)) {
234
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
235
+ // (strictNullChecks)
236
+ if (isWhitelisted(deployment.spec?.template.spec?.containers[0].image)) {
231
237
  logger.verbose(`Creating resources in namespace ${namespace} ...`);
232
238
  try {
233
239
  const deploymentObj = await appsApi.createNamespacedDeployment(namespace, deployment);
@@ -246,20 +252,22 @@ export async function updateCompanionContainers(definitions, appName, appId, reg
246
252
  }
247
253
  }
248
254
  else {
249
- logger.error(`Image ${deployment.spec.template.spec.containers[0].image} is not in the repository whitelist!`);
255
+ logger.error(`Image ${deployment.spec?.template.spec?.containers[0].image} is not in the repository whitelist!`);
250
256
  }
251
257
  }
252
258
  }
253
259
  // Delete companions removed from the definition
254
260
  for (const service of services.items) {
255
- if (service.spec.type === 'ExternalName') {
261
+ if (service.spec?.type === 'ExternalName') {
256
262
  continue;
257
263
  }
258
264
  if (service.metadata?.labels &&
259
265
  appIdLabel in service.metadata.labels &&
260
266
  service.metadata.labels[appIdLabel] === appId) {
261
- const existing = definitions.find((d) => formatServiceName(d.name, appName, appId) === service.metadata.name);
267
+ const existing = definitions.find((d) => formatServiceName(d.name, appName, appId) === service.metadata?.name);
262
268
  if (!existing) {
269
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
270
+ // (strictNullChecks)
263
271
  await deleteCompanionContainers(service.metadata?.name);
264
272
  }
265
273
  }
@@ -30,9 +30,10 @@ export async function stopIdleContainers(interval = 10) {
30
30
  return;
31
31
  }
32
32
  for (const deployment of deployments.items) {
33
+ // @ts-expect-error 2339 Property does not exist on type undefined
33
34
  const { annotations, name, namespace } = deployment.metadata;
34
35
  if (deployment.metadata?.namespace !== getContainerNamespace() ||
35
- deployment.spec.replicas === 0) {
36
+ deployment.spec?.replicas === 0) {
36
37
  continue;
37
38
  }
38
39
  const milliseconds = interval * 60 * 1000;
@@ -67,7 +68,7 @@ export async function stopIdleContainers(interval = 10) {
67
68
  }
68
69
  }
69
70
  export async function waitForPodReadiness(namespace, appSelector, maxWait = 10000) {
70
- let isReady;
71
+ let isReady = false;
71
72
  const interval = 500;
72
73
  let elapsed = 0;
73
74
  const { coreApi } = getKubeConfig();
@@ -18,7 +18,7 @@ export function generateDeploymentAndServiceSpecs(definition, name, appName, app
18
18
  image = `${registry.endsWith('/') ? registry.slice(0, -1) : registry}/${image}`;
19
19
  }
20
20
  const validatedResources = validateContainerResources(resources);
21
- const env = formatEnv(definition.env, appName, appId);
21
+ const env = formatEnv(definition.env ?? [], appName, appId);
22
22
  // Env should have string keys!
23
23
  const podTemplateSpec = {
24
24
  metadata: {
package/fs.js CHANGED
@@ -61,15 +61,19 @@ export async function readData(path) {
61
61
  * @returns The formatted content.
62
62
  */
63
63
  export async function writeData(path, data, { compare = compareStrings, sort = true } = {}) {
64
+ // @ts-expect-error 2769 No overload matches this call (strictNullChecks)
64
65
  const sorted = sort ? sortKeys(data, { deep: true, compare: compare || undefined }) : data;
65
66
  let buffer;
66
67
  try {
67
68
  const { default: prettier } = await import('prettier');
68
69
  const { inferredParser } = await prettier.getFileInfo(path, { resolveConfig: true });
69
70
  const prettierOptions = await prettier.resolveConfig(path, { editorconfig: true });
71
+ // @ts-expect-error 18048 variable is possibly null (strictNullChecks)
70
72
  prettierOptions.parser = inferredParser;
71
73
  buffer =
72
74
  inferredParser === 'yaml' ? stringify(sorted) : `${JSON.stringify(sorted, undefined, 2)}\n`;
75
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
76
+ // (strictNullChecks)
73
77
  buffer = await prettier.format(buffer, prettierOptions);
74
78
  }
75
79
  catch {
@@ -17,6 +17,8 @@ export async function getAppsembleMessages(language, baseLanguage) {
17
17
  const baseLang = baseLanguage?.toLowerCase();
18
18
  const languages = await getSupportedLanguages();
19
19
  const messages = {};
20
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
21
+ // (strictNullChecks)
20
22
  if (baseLang && languages.has(baseLanguage)) {
21
23
  Object.assign(messages, JSON.parse(await readFile(new URL(`${baseLang}.json`, translationsDir), 'utf8')));
22
24
  }
@@ -1,7 +1,7 @@
1
1
  import { getAppsembleMessages, mergeMessages, } from '@appsemble/node-utils';
2
2
  import { defaultLocale, extractAppMessages, normalizeBlockName, } from '@appsemble/utils';
3
3
  import tags from 'language-tags';
4
- import { assertKoaError, throwKoaError } from './koa.js';
4
+ import { assertKoaCondition, throwKoaError } from './koa.js';
5
5
  export async function getMessagesUtil(ctx, language, appId, merge, { getApp, getAppMessages, getBlockMessages }, override = 'true') {
6
6
  if (!tags.check(language)) {
7
7
  throwKoaError(ctx, 400, '`Language “${language}” is invalid`');
@@ -12,7 +12,7 @@ export async function getMessagesUtil(ctx, language, appId, merge, { getApp, get
12
12
  .find((sub) => sub.type() === 'language');
13
13
  const baseLang = baseLanguage && String(baseLanguage).toLowerCase();
14
14
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
15
- assertKoaError(!app, ctx, 404, 'App not found');
15
+ assertKoaCondition(app != null, ctx, 404, 'App not found');
16
16
  const blockPrefixes = [];
17
17
  const blockQuery = [];
18
18
  const appMessages = await getAppMessages({ context: ctx, app, language });
@@ -27,13 +27,14 @@ export async function getMessagesUtil(ctx, language, appId, merge, { getApp, get
27
27
  blockPrefixes.push([blockName, prefix]);
28
28
  }),
29
29
  };
30
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
30
31
  const blockMessages = await getBlockMessages({ context: ctx, blockQuery, baseLang, lang });
31
32
  if ((!appMessages.length || (merge && !appMessages.some((m) => m.language === lang))) &&
32
33
  lang !== (app.definition.defaultLanguage || defaultLocale)) {
33
34
  throwKoaError(ctx, 404, `Language “${language}” could not be found`);
34
35
  }
35
- const baseLanguageMessages = override === 'true' && appMessages.find((m) => m.language === baseLang);
36
- const languageMessages = override === 'true' && appMessages.find((m) => m.language === lang);
36
+ const baseLanguageMessages = override === 'true' ? appMessages.find((m) => m.language === baseLang) : undefined;
37
+ const languageMessages = override === 'true' ? appMessages.find((m) => m.language === lang) : undefined;
37
38
  for (const block of blockMessages) {
38
39
  const { name } = block;
39
40
  const defaultMessages = block.messages?.[defaultLocale];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Returns all the directory names that are specified in the
3
+ * 'trainingOrder' of the chapters' `properties.json`
4
+ *
5
+ * @param path The absolute path to read the trainings from
6
+ * @returns List of the directory names that contain valid trainings
7
+ */
8
+ export declare function getValidTrainings(path: string): Promise<string[]>;
@@ -0,0 +1,47 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { logger } from './logger.js';
3
+ /**
4
+ * Returns all the directory names that are specified in the
5
+ * 'trainingOrder' of the chapters' `properties.json`
6
+ *
7
+ * @param path The absolute path to read the trainings from
8
+ * @returns List of the directory names that contain valid trainings
9
+ */
10
+ export async function getValidTrainings(path) {
11
+ const chapters = await readdir(path);
12
+ const trainingIds = [];
13
+ for (const chapterDirectory of chapters) {
14
+ let properties;
15
+ try {
16
+ properties = JSON.parse(await readFile(`${path}/${chapterDirectory}/properties.json`, 'utf8'));
17
+ }
18
+ catch {
19
+ logger.error(`Chapter directory "${chapterDirectory}" is missing a 'properties.json' file`);
20
+ continue;
21
+ }
22
+ const missingProperties = ['blockedBy', 'title', 'trainingOrder'].filter((prop) => !Object.prototype.hasOwnProperty.call(properties, prop));
23
+ if (missingProperties.length > 0) {
24
+ logger.error(`Missing properties [${missingProperties}] in "properties.json"`);
25
+ continue;
26
+ }
27
+ for (const trainingDirectory of properties.trainingOrder) {
28
+ try {
29
+ await readdir(`${path}/${chapterDirectory}/${trainingDirectory}`);
30
+ }
31
+ catch {
32
+ logger.error(`Could not find "${trainingDirectory}"`);
33
+ continue;
34
+ }
35
+ try {
36
+ await readFile(`${path}/${chapterDirectory}/${trainingDirectory}/index.md`);
37
+ }
38
+ catch {
39
+ logger.error(`Training "${trainingDirectory}" is missing an index.md`);
40
+ continue;
41
+ }
42
+ trainingIds.push(trainingDirectory);
43
+ }
44
+ }
45
+ return trainingIds;
46
+ }
47
+ //# sourceMappingURL=getValidTrainings.js.map
package/icon.js CHANGED
@@ -16,6 +16,7 @@ export async function serveIcon(ctx, { background, cache, fallback, height, icon
16
16
  if (raw) {
17
17
  const { format } = await img.metadata();
18
18
  ctx.body = buffer;
19
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
19
20
  ctx.type = format;
20
21
  return;
21
22
  }
@@ -41,8 +42,10 @@ export async function serveIcon(ctx, { background, cache, fallback, height, icon
41
42
  // Make the regular icon maskable
42
43
  const actual = img;
43
44
  const metadata = await actual.metadata();
45
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
44
46
  const angle = Math.atan(metadata.height / metadata.width);
45
47
  actual.resize({
48
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
46
49
  width: Math.ceil(Math.cos(angle) * safeAreaDiameter * width),
47
50
  // By leaving out height, libvips will determine this for us. This has better precision than
48
51
  // calculating this using JavasScript and passing it manually.
@@ -50,7 +53,9 @@ export async function serveIcon(ctx, { background, cache, fallback, height, icon
50
53
  fit: 'contain',
51
54
  background,
52
55
  });
56
+ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks)
53
57
  img = sharp(Buffer.alloc(width * height * 4, 0), {
58
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
54
59
  raw: { width, height, channels: 4 },
55
60
  });
56
61
  img.resize({ width, height });
package/index.d.ts CHANGED
@@ -30,10 +30,14 @@ export * from './server/routes/appRouter/iconHandler.js';
30
30
  export * from './server/controllers/index.js';
31
31
  export * from './server/types.js';
32
32
  export * from './koa.js';
33
- export { bulmaVersion, faVersion, version } from './versions.cjs';
33
+ export { bulmaVersion, faVersion, version } from './versions.js';
34
34
  export * from './EmailQuotaExceededError.js';
35
35
  export * from './EmailError.js';
36
36
  export * from './AppMemberPropertiesError.js';
37
37
  export * from './authentication.js';
38
38
  export * from './getMessagesUtil.js';
39
39
  export * from './container/index.js';
40
+ export * from './s3.js';
41
+ export * from './uploads.js';
42
+ export * from './assets.js';
43
+ export * from './getValidTrainings.js';
package/index.js CHANGED
@@ -30,11 +30,15 @@ export * from './server/routes/appRouter/iconHandler.js';
30
30
  export * from './server/controllers/index.js';
31
31
  export * from './server/types.js';
32
32
  export * from './koa.js';
33
- export { bulmaVersion, faVersion, version } from './versions.cjs';
33
+ export { bulmaVersion, faVersion, version } from './versions.js';
34
34
  export * from './EmailQuotaExceededError.js';
35
35
  export * from './EmailError.js';
36
36
  export * from './AppMemberPropertiesError.js';
37
37
  export * from './authentication.js';
38
38
  export * from './getMessagesUtil.js';
39
39
  export * from './container/index.js';
40
+ export * from './s3.js';
41
+ export * from './uploads.js';
42
+ export * from './assets.js';
43
+ export * from './getValidTrainings.js';
40
44
  //# sourceMappingURL=index.js.map
package/interceptors.js CHANGED
@@ -27,7 +27,7 @@ export function formData(config) {
27
27
  */
28
28
  export function requestLogger(config) {
29
29
  const time = Date.now();
30
- logger.verbose(`> ${time} ${highlight(`${config.method.toUpperCase()} ${axios.getUri(config)} HTTP/1.1`, {
30
+ logger.verbose(`> ${time} ${highlight(`${config.method?.toUpperCase()} ${axios.getUri(config)} HTTP/1.1`, {
31
31
  language: 'http',
32
32
  })}`);
33
33
  // eslint-disable-next-line no-param-reassign
package/koa.d.ts CHANGED
@@ -56,7 +56,7 @@ type HttpErrorCodes = keyof typeof errorStatusMap;
56
56
  * @param message The message for in the response body.
57
57
  * @param data Optional data to include.
58
58
  */
59
- export declare function throwKoaError(ctx: Context, status: HttpErrorCodes, message?: string, data?: Record<string, any>): void;
59
+ export declare function throwKoaError(ctx: Context, status: HttpErrorCodes, message?: string, data?: Record<string, any>): never;
60
60
  /**
61
61
  * Throw standard Koa error response when condition is met.
62
62
  *
@@ -67,3 +67,15 @@ export declare function throwKoaError(ctx: Context, status: HttpErrorCodes, mess
67
67
  * @param data Optional data to include.
68
68
  */
69
69
  export declare function assertKoaError(condition: boolean, ctx: Context, status: HttpErrorCodes, message?: string, data?: Record<string, any>): void;
70
+ /**
71
+ * Throws standard Koa error response when condition is _not_ met.
72
+ * Does the opposite of `assertKoaError`.
73
+ *
74
+ * @param condition The condition to check.
75
+ * @param ctx The Koa context used to throw the error response.
76
+ * @param status The HTTP status code to use.
77
+ * @param message The message for in the response body.
78
+ * @param data Optional data to include.
79
+ * @see throwKoaError
80
+ */
81
+ export declare function assertKoaCondition(condition: boolean, ctx: Context, status: HttpErrorCodes, message?: string, data?: Record<string, any>): asserts condition;
package/koa.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { convertToCsv } from '@appsemble/utils';
2
2
  import { bufferParser, jsonParser, bodyParser as koaBodyParser } from 'koas-body-parser';
3
3
  import { serializer as koaSerializer } from 'koas-serializer';
4
- import { csvParser, xWwwFormUrlencodedParser } from './parsers.js';
4
+ import { csvParser, streamParser, xWwwFormUrlencodedParser } from './parsers.js';
5
5
  function serializer() {
6
6
  return koaSerializer({
7
7
  serializers: {
8
8
  'application/scim+json': (body) => JSON.stringify(body),
9
+ // @ts-expect-error 2322 unknown is not assignable to type string (strictNullChecks)
9
10
  'application/xml': (body) => body,
10
11
  'text/csv': convertToCsv,
11
12
  },
@@ -17,6 +18,7 @@ function bodyParser() {
17
18
  'application/scim+json': jsonParser,
18
19
  'application/x-www-form-urlencoded': xWwwFormUrlencodedParser,
19
20
  'text/csv': csvParser,
21
+ 'multipart/form-data': streamParser,
20
22
  '*/*': bufferParser,
21
23
  },
22
24
  });
@@ -98,4 +100,18 @@ export function assertKoaError(condition, ctx, status, message, data) {
98
100
  throwKoaError(ctx, status, message, data);
99
101
  }
100
102
  }
103
+ /**
104
+ * Throws standard Koa error response when condition is _not_ met.
105
+ * Does the opposite of `assertKoaError`.
106
+ *
107
+ * @param condition The condition to check.
108
+ * @param ctx The Koa context used to throw the error response.
109
+ * @param status The HTTP status code to use.
110
+ * @param message The message for in the response body.
111
+ * @param data Optional data to include.
112
+ * @see throwKoaError
113
+ */
114
+ export function assertKoaCondition(condition, ctx, status, message, data) {
115
+ assertKoaError(!condition, ctx, status, message, data);
116
+ }
101
117
  //# sourceMappingURL=koa.js.map
package/logger.js CHANGED
@@ -15,8 +15,8 @@ function httpErrorToString(error) {
15
15
  return [
16
16
  chalk.blue.bold('Request:'),
17
17
  highlight([
18
- `${config.method.toUpperCase()} ${axios.getUri(config)} HTTP/${request?.res?.httpVersion ?? '1.1'}`,
19
- ...Object.entries(config.headers)
18
+ `${config?.method?.toUpperCase()} ${axios.getUri(config)} HTTP/${request?.res?.httpVersion ?? '1.1'}`,
19
+ ...Object.entries(config?.headers ?? {})
20
20
  .map(([key, value]) => [headerCase(key), value])
21
21
  .map(([key, value]) => `${key}: ${key === 'Authorization' ? 'xxxxxxxxxx' : value}`)
22
22
  .sort(),
@@ -40,11 +40,13 @@ function httpErrorToString(error) {
40
40
  function toString(info) {
41
41
  if (info instanceof Error) {
42
42
  if ('sql' in info) {
43
- return info.stack.replace(/^Error/, `${info.sql}\n${info.name}: ${info.message}`);
43
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
44
+ return info.stack?.replace(/^Error/, `${info.sql}\n${info.name}: ${info.message}`);
44
45
  }
45
46
  if (axios.isAxiosError(info)) {
46
47
  return httpErrorToString(info);
47
48
  }
49
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
48
50
  return info.stack;
49
51
  }
50
52
  if (typeof info.message === 'string') {