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

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
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { logger } from '../logger.js';
3
3
  function rangeFormat(value, map) {
4
+ // @ts-expect-error 2532 Object is possibly 'undefined'.
4
5
  return Object.entries(map).find(([v]) => v === 'default' || value < Number(v))[1];
5
6
  }
6
7
  /**
package/odata.js CHANGED
@@ -20,7 +20,7 @@ export function processLiteral(token) {
20
20
  case Edm.Boolean:
21
21
  return token.raw === 'true';
22
22
  case Edm.String:
23
- return JSON.parse(`"${token.raw.slice(1, -1).replaceAll('"', '\\"')}"`);
23
+ return JSON.parse(`"${token.raw.slice(1, -1).replaceAll('"', '\\"').replaceAll("''", "'")}"`);
24
24
  case Edm.Byte:
25
25
  case Edm.Decimal:
26
26
  case Edm.Double:
@@ -37,6 +37,7 @@ export function processLiteral(token) {
37
37
  case Edm.Guid:
38
38
  return token.raw;
39
39
  case Edm.null:
40
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
40
41
  return null;
41
42
  default:
42
43
  throw new TypeError(`${token.position}: Unhandled OData literal type: ${token.value}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/node-utils",
3
- "version": "0.30.14-test.6",
3
+ "version": "0.32.1-test.15",
4
4
  "description": "NodeJS utilities used by Appsemble internally.",
5
5
  "keywords": [
6
6
  "app",
@@ -22,15 +22,17 @@
22
22
  "sideEffects": false,
23
23
  "type": "module",
24
24
  "exports": {
25
- ".": [
26
- "./index.js",
27
- "./index.ts"
28
- ]
25
+ ".": {
26
+ "ts-source": "./index.ts",
27
+ "default": "./index.js"
28
+ }
29
29
  },
30
30
  "files": [
31
31
  "**/*.d.ts",
32
32
  "**/*.js",
33
33
  "**/*.cjs",
34
+ "!**/*.test.js",
35
+ "!**/*.test.d.ts",
34
36
  "!*.config.js"
35
37
  ],
36
38
  "scripts": {
@@ -38,19 +40,19 @@
38
40
  "test": "vitest"
39
41
  },
40
42
  "dependencies": {
41
- "@appsemble/types": "0.30.14-test.6",
42
- "@appsemble/utils": "0.30.14-test.6",
43
+ "@appsemble/types": "0.32.1-test.15",
44
+ "@appsemble/utils": "0.32.1-test.15",
43
45
  "@formatjs/fast-memoize": "^2.0.0",
44
46
  "@fortawesome/fontawesome-free": "^6.0.0",
45
- "@kubernetes/client-node": "0.21.0",
46
- "@koa/cors": "^4.0.0",
47
+ "@kubernetes/client-node": "^0.22.2",
47
48
  "@odata/parser": "^0.2.0",
48
49
  "@types/koa": "^2.0.0",
49
50
  "axios": "^1.0.0",
50
- "bulma": "0.9.3",
51
- "bulma-checkradio": "2.1.1",
52
- "bulma-slider": "2.0.4",
51
+ "bulma": "=0.9.3",
52
+ "bulma-checkradio": "=2.1.1",
53
+ "bulma-slider": "=2.0.4",
53
54
  "bulma-switch": "^2.0.0",
55
+ "busboy": "^1.6.0",
54
56
  "chalk": "^5.0.0",
55
57
  "cli-highlight": "^2.0.0",
56
58
  "css-tree": "^2.0.0",
@@ -62,7 +64,7 @@
62
64
  "fs-extra": "^11.0.0",
63
65
  "inquirer": "^9.0.0",
64
66
  "intl-messageformat": "^10.0.0",
65
- "jsonschema": "^1.0.0",
67
+ "jsonschema": "~1.4.1",
66
68
  "keytar": "^7.0.0",
67
69
  "koa": "^2.0.0",
68
70
  "koa-compose": "^4.0.0",
@@ -82,8 +84,11 @@
82
84
  "lodash": "^4.0.0",
83
85
  "lodash-es": "^4.0.0",
84
86
  "logform": "^2.0.0",
87
+ "memfs": "^4.17.0",
85
88
  "mime-types": "^2.0.0",
89
+ "minio": "^8.0.3",
86
90
  "mustache": "^4.0.0",
91
+ "openapi-types": "^12.1.3",
87
92
  "parse-duration": "^1.0.0",
88
93
  "parse-json": "^8.0.0",
89
94
  "sass": "^1.0.0",
@@ -91,6 +96,7 @@
91
96
  "sort-keys": "^5.0.0",
92
97
  "strip-bom": "^5.0.0",
93
98
  "type-fest": "^4.0.0",
99
+ "type-is": "^1.6.18",
94
100
  "webpack": "^5.0.0",
95
101
  "webpack-dev-middleware": "^6.0.0",
96
102
  "winston": "^3.0.0",
@@ -98,12 +104,14 @@
98
104
  "yargs": "^17.0.0"
99
105
  },
100
106
  "devDependencies": {
107
+ "@types/busboy": "^1.5.4",
101
108
  "@types/css-tree": "^2.0.0",
102
109
  "@types/koa-compress": "^4.0.0",
103
110
  "@types/koa-range": "^0.3.0",
111
+ "@types/type-is": "^1.6.6",
104
112
  "axios-mock-adapter": "^1.0.0",
105
113
  "axios-test-instance": "^7.0.0",
106
- "vitest": "^1.0.0"
114
+ "vitest": "^2.0.0"
107
115
  },
108
116
  "peerDependencies": {
109
117
  "prettier": "^3"
package/parsers.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { type Parser } from 'koas-body-parser';
2
2
  export declare const xWwwFormUrlencodedParser: Parser<unknown>;
3
3
  export declare const csvParser: Parser<unknown[]>;
4
+ export declare const streamParser: Parser<Record<string, unknown>>;
package/parsers.js CHANGED
@@ -1,6 +1,15 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
1
5
  import { parse } from 'node:querystring';
6
+ import busboy from 'busboy';
2
7
  import { parse as parseCSV } from 'csv-parse';
3
8
  import { bufferParser } from 'koas-body-parser';
9
+ import { SchemaValidationError } from 'koas-core';
10
+ import { is } from 'type-is';
11
+ import { logger } from './logger.js';
12
+ import { TempFile } from './server/types.js';
4
13
  export const xWwwFormUrlencodedParser = async (body, mediaTypeObject, options, ctx) => {
5
14
  const buffer = await bufferParser(body, mediaTypeObject, options, ctx);
6
15
  return parse(String(buffer));
@@ -15,4 +24,125 @@ export const csvParser = (body) => new Promise((resolve, reject) => {
15
24
  }
16
25
  }));
17
26
  });
27
+ /**
28
+ * Parse a raw string value based on a JSON schema.
29
+ *
30
+ * @param schema The JSON schema whose type to use for parsing the value.
31
+ * @param content The value to parse.
32
+ * @returns The parsed value.
33
+ */
34
+ function fromString(schema, content) {
35
+ switch (schema?.type) {
36
+ case 'integer':
37
+ case 'number':
38
+ return Number(content);
39
+ case 'boolean':
40
+ if (content === 'true') {
41
+ return true;
42
+ }
43
+ if (content === 'false') {
44
+ return false;
45
+ }
46
+ return content;
47
+ case 'object':
48
+ try {
49
+ return JSON.parse(content);
50
+ }
51
+ catch {
52
+ return content;
53
+ }
54
+ default:
55
+ return content;
56
+ }
57
+ }
58
+ // Taken from https://gitlab.com/remcohaszing/koas/-/blob/main/packages/koas-body-parser/src/parsers/formdata.ts
59
+ // Files are passed as streams rather than buffers
60
+ export const streamParser = async (body, mediaTypeObject, { resolveRef }, ctx) => {
61
+ const bb = busboy({ headers: ctx.req.headers });
62
+ const { encoding = {}, schema } = mediaTypeObject || {};
63
+ const { properties = {} } = resolveRef(schema) || {};
64
+ const response = {};
65
+ const tempFiles = {};
66
+ await new Promise((resolve, reject) => {
67
+ function onError(error) {
68
+ bb.removeAllListeners();
69
+ logger.error(error);
70
+ reject(error);
71
+ }
72
+ bb.on('file', (fieldName, stream, { filename, mimeType: mime }) => {
73
+ const propertySchema = resolveRef(properties[fieldName]);
74
+ const path = join(tmpdir(), `${Date.now()}-${randomUUID()}`);
75
+ try {
76
+ const fileWriteStream = createWriteStream(path);
77
+ stream.pipe(fileWriteStream);
78
+ stream.on('error', onError);
79
+ fileWriteStream.on('error', onError);
80
+ }
81
+ catch (error) {
82
+ onError(error);
83
+ }
84
+ if (propertySchema && propertySchema.type === 'array') {
85
+ if (!Object.hasOwnProperty.call(response, fieldName)) {
86
+ response[fieldName] = [];
87
+ }
88
+ if (!Object.hasOwnProperty.call(tempFiles, fieldName)) {
89
+ tempFiles[fieldName] = [];
90
+ }
91
+ response[fieldName].push('');
92
+ tempFiles[fieldName].push(new TempFile({ path, mime, filename }));
93
+ }
94
+ else {
95
+ response[fieldName] = '';
96
+ tempFiles[fieldName] = new TempFile({ path, mime, filename });
97
+ }
98
+ })
99
+ .on('field', (fieldName, content) => {
100
+ const propertySchema = resolveRef(properties[fieldName]);
101
+ if (propertySchema && propertySchema.type === 'array') {
102
+ if (!Object.hasOwnProperty.call(response, fieldName)) {
103
+ response[fieldName] = [];
104
+ }
105
+ response[fieldName].push(fromString(resolveRef(propertySchema.items), content));
106
+ }
107
+ else {
108
+ response[fieldName] = fromString(propertySchema, content);
109
+ }
110
+ })
111
+ .on('close', () => {
112
+ bb.removeAllListeners();
113
+ resolve(response);
114
+ })
115
+ .on('error', onError)
116
+ .on('partsLimit', onError)
117
+ .on('filesLimit', onError)
118
+ .on('fieldsLimit', onError);
119
+ ctx.req.pipe(bb);
120
+ });
121
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
122
+ // (strictNullChecks) (schema may be undefined for some reason)
123
+ const result = ctx.openApi.validate(response, schema, { throw: false });
124
+ for (const [key, values] of Object.entries(tempFiles)) {
125
+ const vals = Array.isArray(values) ? values : [values];
126
+ for (const [index, value] of vals.entries()) {
127
+ if (!encoding?.[key]?.contentType) {
128
+ continue;
129
+ }
130
+ if (!is(value.mime, encoding[key].contentType.split(',').map((contentType) => contentType.trim()))) {
131
+ const error = result.addError({
132
+ name: 'contentType',
133
+ argument: encoding[key].contentType,
134
+ message: 'has an invalid content type',
135
+ });
136
+ error.schema = {};
137
+ error.path = values === value ? [key] : [key, index];
138
+ error.property = values === value ? `instance.${key}` : `instance.${key}[${index}]`;
139
+ }
140
+ }
141
+ }
142
+ if (!result.valid) {
143
+ throw new SchemaValidationError('Invalid content types found', { result });
144
+ }
145
+ return Object.assign(response, tempFiles);
146
+ };
147
+ streamParser.skipValidation = true;
18
148
  //# sourceMappingURL=parsers.js.map
package/render.js CHANGED
@@ -37,7 +37,7 @@ export async function render(ctx, filename, data) {
37
37
  */
38
38
  export function makeCSP(csp) {
39
39
  return Object.entries(csp)
40
- .map(([key, values]) => [key, values.filter(Boolean)])
40
+ .map(([key, values]) => [key, values.filter((val) => val !== false && val != null)])
41
41
  .filter(([, values]) => values?.length)
42
42
  .sort(([a], [b]) => a.localeCompare(b))
43
43
  .map(([key, values]) => `${key} ${[...new Set(values)].sort(compareStrings).join(' ')}`)
package/resource.d.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import { type AppDefinition, type ResourceDefinition, type Resource as ResourceType } from '@appsemble/types';
2
2
  import { type PreValidatePropertyFunction } from 'jsonschema';
3
3
  import { type Context, type DefaultContext, type DefaultState, type ParameterizedContext } from 'koa';
4
- import { type File } from 'koas-body-parser';
5
- import { type PreparedAsset } from './index.js';
4
+ import { type JsonValue } from 'type-fest';
5
+ import { type PreparedAsset, TempFile } from './index.js';
6
6
  export declare function stripResource({ $author, $created, $editor, $ephemeral, $group, $seed, $updated, ...data }: ResourceType): Record<string, unknown>;
7
+ /**
8
+ * Works on a resource, which has been processed on the server by the streamParser
9
+ *
10
+ * @param data The resource to be serialized, optionally containing parsed TempFile instance assets
11
+ * @returns An object containing the resource and an array of the assets referenced
12
+ * from the resource
13
+ */
14
+ export declare function serializeServerResource(data: any): JsonValue | {
15
+ resource: JsonValue;
16
+ assets: TempFile[];
17
+ };
7
18
  /**
8
19
  * Get the resource definition of an app by name.
9
20
  *
@@ -26,7 +37,11 @@ export declare function getResourceDefinition(appDefinition: AppDefinition, reso
26
37
  * 2. A list of newly uploaded assets which should be linked to the resources.
27
38
  * 3. preValidateProperty function used for reconstructing resources from a CSV file.
28
39
  */
29
- export declare function extractResourceBody(ctx: Context | ParameterizedContext<DefaultState, DefaultContext, any>): [Record<string, unknown> | Record<string, unknown>[], File[], PreValidatePropertyFunction];
40
+ export declare function extractResourceBody(ctx: Context | ParameterizedContext<DefaultState, DefaultContext, any>): [
41
+ Record<string, unknown> | Record<string, unknown>[],
42
+ TempFile[],
43
+ PreValidatePropertyFunction | undefined
44
+ ];
30
45
  /**
31
46
  * Process an incoming resource request body.
32
47
  *
package/resource.js CHANGED
@@ -1,13 +1,47 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { mapValues } from '@appsemble/utils';
2
3
  import { addMilliseconds, isPast, parseISO } from 'date-fns';
3
4
  import { ValidationError, Validator } from 'jsonschema';
4
5
  import parseDuration from 'parse-duration';
5
6
  import { preProcessCSV } from './csv.js';
6
- import { handleValidatorResult } from './index.js';
7
+ import { handleValidatorResult, TempFile } from './index.js';
7
8
  import { throwKoaError } from './koa.js';
8
9
  export function stripResource({ $author, $created, $editor, $ephemeral, $group, $seed, $updated, ...data }) {
9
10
  return data;
10
11
  }
12
+ /**
13
+ * Works on a resource, which has been processed on the server by the streamParser
14
+ *
15
+ * @param data The resource to be serialized, optionally containing parsed TempFile instance assets
16
+ * @returns An object containing the resource and an array of the assets referenced
17
+ * from the resource
18
+ */
19
+ export function serializeServerResource(data) {
20
+ const assets = [];
21
+ const extractAssets = (value) => {
22
+ if (Array.isArray(value)) {
23
+ return value.map(extractAssets);
24
+ }
25
+ if (value instanceof TempFile) {
26
+ return String(assets.push(value) - 1);
27
+ }
28
+ if (value instanceof Date) {
29
+ return value.toJSON();
30
+ }
31
+ if (value && typeof value === 'object') {
32
+ return mapValues(value, extractAssets);
33
+ }
34
+ return value;
35
+ };
36
+ const resource = extractAssets(data);
37
+ if (!assets.length) {
38
+ return resource;
39
+ }
40
+ return {
41
+ resource,
42
+ assets,
43
+ };
44
+ }
11
45
  /**
12
46
  * Get the resource definition of an app by name.
13
47
  *
@@ -37,7 +71,7 @@ export function getResourceDefinition(appDefinition, resourceType, ctx, view) {
37
71
  throwKoaError(ctx, 404, `App does not have resources called ${resourceType}`);
38
72
  }
39
73
  }
40
- if (view && !definition.views[view]) {
74
+ if (view && !definition.views?.[view]) {
41
75
  if (ctx === undefined) {
42
76
  throw new Error(`View ${view} does not exist for resource type ${resourceType}`);
43
77
  }
@@ -118,21 +152,22 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
118
152
  regularAssets.push(asset);
119
153
  }
120
154
  }
121
- const preparedRegularAssets = regularAssets.map(({ contents, filename, mime }, index) => {
155
+ const preparedRegularAssets = regularAssets.map(({ filename, mime, path }, index) => {
122
156
  const id = randomUUID();
123
157
  assetIdMap.set(index, id);
124
158
  assetUsedMap.set(index, false);
125
- return { data: contents, filename, id, mime };
159
+ return { path, filename, id, mime };
126
160
  });
127
161
  const usedPreparedRegularAssets = [];
128
- const preparedThumbnailAssets = thumbnailAssets.map(({ contents, filename, mime }, index) => {
162
+ const preparedThumbnailAssets = thumbnailAssets.map(({ filename, mime, path }, index) => {
129
163
  const regularAsset = preparedRegularAssets.find((ra) => !usedPreparedRegularAssets.includes(ra.id) &&
130
- ra.filename.startsWith(filename.replace(thumbnailAssetSuffix, '')));
164
+ ra.filename?.startsWith(filename.replace(thumbnailAssetSuffix, '')));
131
165
  const id = regularAsset ? `${regularAsset.id}-thumbnail` : randomUUID();
166
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
132
167
  usedPreparedRegularAssets.push(regularAsset?.id);
133
168
  assetIdMap.set(index + preparedRegularAssets.length, id);
134
169
  assetUsedMap.set(index, false);
135
- return { data: contents, filename, id, mime };
170
+ return { path, filename, id, mime };
136
171
  });
137
172
  const preparedAssets = [...preparedRegularAssets, ...preparedThumbnailAssets];
138
173
  validator.customFormats.binary = (input) => {
@@ -202,11 +237,13 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
202
237
  if (value !== undefined) {
203
238
  const date = parseISO(value);
204
239
  if (Number.isNaN(date.getTime())) {
240
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
241
+ // (strictNullChecks) - Severe
205
242
  return addMilliseconds(new Date(), parseDuration(value));
206
243
  }
207
244
  if (isPast(date) &&
208
245
  !customErrors.some((error) => error.message === 'has already passed' && error.path === path)) {
209
- customErrors.push(new ValidationError('has already passed', value, null, path));
246
+ customErrors.push(new ValidationError('has already passed', value, undefined, path));
210
247
  }
211
248
  return date;
212
249
  }
@@ -235,7 +272,10 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
235
272
  }
236
273
  const uuid = assetIdMap.get(num);
237
274
  const currentResource = Array.isArray(resource) ? resource[path[0]] : resource;
238
- preparedAssets.find((asset) => asset.id === uuid).resource = currentResource;
275
+ const preparedAsset = preparedAssets.find((asset) => asset.id === uuid);
276
+ if (preparedAsset) {
277
+ preparedAsset.resource = currentResource;
278
+ }
239
279
  assetUsedMap.set(num, true);
240
280
  return uuid;
241
281
  },
@@ -243,7 +283,7 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
243
283
  result.errors.push(...customErrors);
244
284
  for (const [assetId, used] of assetUsedMap.entries()) {
245
285
  if (!used) {
246
- result.errors.push(new ValidationError('is not referenced from the resource', assetId, null, ['assets', assetId], 'binary', 'format'));
286
+ result.errors.push(new ValidationError('is not referenced from the resource', assetId, undefined, ['assets', assetId], 'binary', 'format'));
247
287
  }
248
288
  }
249
289
  handleValidatorResult(ctx, result, 'Resource validation failed');
package/s3.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type Readable } from 'node:stream';
2
+ import { type BucketItemStat } from 'minio';
3
+ export interface InitS3ClientParams {
4
+ endPoint: string;
5
+ port?: number;
6
+ useSSL?: boolean;
7
+ accessKey: string;
8
+ secretKey: string;
9
+ }
10
+ export declare function initS3Client({ accessKey, endPoint, port, secretKey, useSSL, }: InitS3ClientParams): void;
11
+ export declare function uploadS3File(bucket: string, key: string, content: Buffer | Readable | string, size?: number): Promise<void>;
12
+ export declare function uploadS3FileFromPath(bucket: string, key: string, path: string): Promise<void>;
13
+ export declare function getS3File(bucket: string, key: string): Promise<Readable>;
14
+ export declare function getS3FileBuffer(bucket: string, key: string): Promise<Buffer>;
15
+ export declare function getS3FileStats(bucket: string, key: string): Promise<BucketItemStat>;
16
+ export declare function deleteS3File(bucket: string, key: string): Promise<void>;
17
+ export declare function deleteS3Files(bucket: string, keys: string[]): Promise<void>;
18
+ export declare function clearAllS3Buckets(): Promise<void>;
package/s3.js ADDED
@@ -0,0 +1,131 @@
1
+ import { streamToBuffer } from 'memfs/lib/node/util.js';
2
+ import { Client, S3Error } from 'minio';
3
+ import { logger } from './logger.js';
4
+ let s3Client;
5
+ export function initS3Client({ accessKey, endPoint, port = 9000, secretKey, useSSL = true, }) {
6
+ try {
7
+ s3Client = new Client({
8
+ endPoint,
9
+ port,
10
+ useSSL,
11
+ accessKey,
12
+ secretKey,
13
+ });
14
+ }
15
+ catch (error) {
16
+ logger.error(error);
17
+ throw error;
18
+ }
19
+ }
20
+ async function ensureBucket(name) {
21
+ try {
22
+ const bucketExists = await s3Client.bucketExists(name);
23
+ if (!bucketExists) {
24
+ try {
25
+ await s3Client.makeBucket(name);
26
+ }
27
+ catch (makeBucketError) {
28
+ if (makeBucketError instanceof S3Error &&
29
+ makeBucketError.code === 'BucketAlreadyOwnedByYou') {
30
+ logger.warn(makeBucketError);
31
+ logger.info('This was probably called in an asynchronous batch upload.');
32
+ }
33
+ else {
34
+ throw makeBucketError;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ catch (error) {
40
+ logger.error(error);
41
+ throw error;
42
+ }
43
+ }
44
+ export async function uploadS3File(bucket, key, content, size) {
45
+ try {
46
+ await ensureBucket(bucket);
47
+ await s3Client.putObject(bucket, key, content, size);
48
+ }
49
+ catch (error) {
50
+ logger.error(error);
51
+ throw error;
52
+ }
53
+ }
54
+ export async function uploadS3FileFromPath(bucket, key, path) {
55
+ try {
56
+ await ensureBucket(bucket);
57
+ await s3Client.fPutObject(bucket, key, path);
58
+ }
59
+ catch (error) {
60
+ logger.error(error);
61
+ throw error;
62
+ }
63
+ }
64
+ export async function getS3File(bucket, key) {
65
+ try {
66
+ return await s3Client.getObject(bucket, key);
67
+ }
68
+ catch (error) {
69
+ logger.error(error);
70
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
71
+ return null;
72
+ }
73
+ }
74
+ export async function getS3FileBuffer(bucket, key) {
75
+ try {
76
+ const stream = await getS3File(bucket, key);
77
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
78
+ return stream ? streamToBuffer(stream) : null;
79
+ }
80
+ catch (error) {
81
+ logger.error(error);
82
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) - Severe
83
+ return null;
84
+ }
85
+ }
86
+ export async function getS3FileStats(bucket, key) {
87
+ try {
88
+ return await s3Client.statObject(bucket, key);
89
+ }
90
+ catch (error) {
91
+ logger.error(error);
92
+ throw error;
93
+ }
94
+ }
95
+ export async function deleteS3File(bucket, key) {
96
+ try {
97
+ await s3Client.removeObject(bucket, key);
98
+ }
99
+ catch (error) {
100
+ logger.error(error);
101
+ throw error;
102
+ }
103
+ }
104
+ export async function deleteS3Files(bucket, keys) {
105
+ try {
106
+ await s3Client.removeObjects(bucket, keys);
107
+ }
108
+ catch (error) {
109
+ logger.error(error);
110
+ throw error;
111
+ }
112
+ }
113
+ export async function clearAllS3Buckets() {
114
+ try {
115
+ const buckets = await s3Client.listBuckets();
116
+ for (const bucket of buckets) {
117
+ const objectsStream = s3Client.listObjectsV2(bucket.name, '', true);
118
+ const objects = [];
119
+ for await (const o of objectsStream) {
120
+ objects.push(o.name);
121
+ }
122
+ await s3Client.removeObjects(bucket.name, objects);
123
+ await s3Client.removeBucket(bucket.name);
124
+ }
125
+ }
126
+ catch (error) {
127
+ logger.error(error);
128
+ throw error;
129
+ }
130
+ }
131
+ //# sourceMappingURL=s3.js.map
@@ -1,15 +1,16 @@
1
1
  import { extension } from 'mime-types';
2
- import { assertKoaError } from '../../../../../koa.js';
2
+ import { assertKoaCondition } from '../../../../../koa.js';
3
3
  export function createGetAppAssetByIdController({ getApp, getAppAsset }) {
4
4
  return async (ctx) => {
5
5
  const { pathParams: { appId, assetId }, } = ctx;
6
6
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
7
- assertKoaError(!app, ctx, 404, 'App not found');
7
+ assertKoaCondition(app != null, ctx, 404, 'App not found');
8
8
  const asset = await getAppAsset({ app, context: ctx, id: assetId });
9
9
  if (assetId !== asset.id) {
10
10
  // Redirect to asset using current asset ID
11
11
  ctx.status = 302;
12
12
  ctx.set('location', `/api/apps/${app.id}/assets/${asset.id}`);
13
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
13
14
  ctx.type = null;
14
15
  return;
15
16
  }
@@ -28,7 +29,7 @@ export function createGetAppAssetByIdController({ getApp, getAppAsset }) {
28
29
  ctx.set('content-disposition', `attachment; filename=${JSON.stringify(filename)}`);
29
30
  }
30
31
  ctx.set('Cache-Control', 'max-age=31536000,immutable');
31
- ctx.body = asset.data;
32
+ ctx.body = asset.stream;
32
33
  };
33
34
  }
34
35
  //# sourceMappingURL=createGetAppAssetByIdController.js.map
@@ -1,10 +1,10 @@
1
1
  import { AppPermission } from '@appsemble/types';
2
- import { assertKoaError } from '../../../../../koa.js';
2
+ import { assertKoaCondition } from '../../../../../koa.js';
3
3
  export function createGetAppGroupsController({ checkAuthSubjectAppPermissions, getApp, getAppGroups, }) {
4
4
  return async (ctx) => {
5
5
  const { pathParams: { appId }, } = ctx;
6
6
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
7
- assertKoaError(!app, ctx, 404, 'App not found');
7
+ assertKoaCondition(app != null, ctx, 404, 'App not found');
8
8
  await checkAuthSubjectAppPermissions({
9
9
  app,
10
10
  context: ctx,
@@ -2,6 +2,8 @@ import { getMessagesUtil } from '../../../../../index.js';
2
2
  export function createGetAppMessagesController(options) {
3
3
  return async (ctx) => {
4
4
  const { pathParams: { appId, language }, query: { merge, override = 'true' }, } = ctx;
5
+ // @ts-expect-error 2345 argument of type is not assignable to parameter of type
6
+ // (strictNullChecks)
5
7
  ctx.body = await getMessagesUtil(ctx, language, appId, merge, options, override);
6
8
  };
7
9
  }