@appsemble/node-utils 0.30.14-test.5 → 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.
- package/README.md +3 -3
- package/app.js +8 -0
- package/assets.d.ts +16 -0
- package/assets.js +65 -0
- package/authentication.d.ts +1 -1
- package/container/helpers.d.ts +1 -1
- package/container/helpers.js +1 -0
- package/container/logs.js +8 -5
- package/container/operations.js +22 -14
- package/container/scale.js +3 -2
- package/container/specs.js +1 -1
- package/fs.js +4 -0
- package/getAppsembleMessages.js +2 -0
- package/getMessagesUtil.js +5 -4
- package/getValidTrainings.d.ts +8 -0
- package/getValidTrainings.js +47 -0
- package/icon.js +5 -0
- package/index.d.ts +5 -1
- package/index.js +5 -1
- package/interceptors.js +1 -1
- package/koa.d.ts +13 -1
- package/koa.js +17 -1
- package/logger.js +5 -3
- package/middleware/loggerMiddleware.js +1 -0
- package/odata.js +2 -1
- package/package.json +22 -14
- package/parsers.d.ts +1 -0
- package/parsers.js +130 -0
- package/render.js +1 -1
- package/resource.d.ts +18 -3
- package/resource.js +50 -10
- package/s3.d.ts +18 -0
- package/s3.js +131 -0
- package/server/controllers/common/apps/assets/createGetAppAssetByIdController.js +4 -3
- package/server/controllers/common/apps/groups/createGetAppGroupsController.js +2 -2
- package/server/controllers/common/apps/messages/createGetAppMessagesController.js +2 -0
- package/server/controllers/common/apps/resources/createCreateAppResourceController.js +16 -2
- package/server/controllers/common/apps/resources/createDeleteAppResourceController.js +2 -2
- package/server/controllers/common/apps/resources/createGetAppResourceByIdController.js +3 -3
- package/server/controllers/common/apps/resources/createQueryAppResourcesController.js +1 -1
- package/server/controllers/common/apps/resources/createUpdateAppResourceController.js +6 -3
- package/server/controllers/common/apps/variables/createGetAppVariablesController.js +2 -2
- package/server/controllers/main/apps/assets/createCreateAppAssetController.js +4 -4
- package/server/controllers/main/apps/resources/createDeleteAppSeedResourcesController.js +2 -2
- package/server/routes/appRouter/blockAssetHandler.js +5 -3
- package/server/routes/appRouter/blockCssHandler.js +5 -3
- package/server/routes/appRouter/bulmaHandler.js +1 -0
- package/server/routes/appRouter/cssHandler.js +2 -2
- package/server/routes/appRouter/iconHandler.js +4 -1
- package/server/routes/appRouter/indexHandler.js +3 -1
- package/server/routes/appRouter/manifestHandler.js +2 -2
- package/server/routes/appRouter/readmeHandler.js +4 -3
- package/server/routes/appRouter/robotsHandler.js +2 -2
- package/server/routes/appRouter/screenshotHandler.js +4 -3
- package/server/routes/appRouter/serviceWorkerHandler.js +2 -2
- package/server/types.d.ts +22 -7
- package/server/types.js +7 -1
- package/server/utils/actions.js +16 -10
- package/server/utils/resources.js +4 -1
- package/tinyRouter.js +3 -3
- package/uploads.d.ts +2 -0
- package/uploads.js +27 -0
- package/versions.d.ts +3 -0
- package/versions.js +7 -0
- package/AppsembleError.test.d.ts +0 -1
- package/AppsembleError.test.js +0 -7
- package/basicAuth.test.d.ts +0 -1
- package/basicAuth.test.js +0 -7
- package/formData.test.d.ts +0 -1
- package/formData.test.js +0 -93
- package/fs.test.d.ts +0 -1
- package/fs.test.js +0 -75
- package/handleError.test.d.ts +0 -1
- package/handleError.test.js +0 -33
- package/interceptors.test.d.ts +0 -1
- package/interceptors.test.js +0 -83
- package/logger.test.d.ts +0 -1
- package/logger.test.js +0 -37
- package/middleware/error.test.d.ts +0 -1
- package/middleware/error.test.js +0 -59
- package/middleware/loggerMiddleware.test.d.ts +0 -1
- package/middleware/loggerMiddleware.test.js +0 -120
- package/readFileOrString.test.d.ts +0 -1
- package/readFileOrString.test.js +0 -12
- package/server/controllers/common/apps/resources/resources.test.d.ts +0 -1
- package/server/controllers/common/apps/resources/resources.test.js +0 -580
- package/tinyRouter.test.d.ts +0 -1
- package/tinyRouter.test.js +0 -83
- package/versions.cjs +0 -4
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.
|
|
3
|
+
"version": "0.32.1-test.14",
|
|
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.
|
|
27
|
-
"./index.
|
|
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.
|
|
42
|
-
"@appsemble/utils": "0.
|
|
43
|
+
"@appsemble/types": "0.32.1-test.14",
|
|
44
|
+
"@appsemble/utils": "0.32.1-test.14",
|
|
43
45
|
"@formatjs/fast-memoize": "^2.0.0",
|
|
44
46
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
45
|
-
"@kubernetes/client-node": "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": "
|
|
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": "^
|
|
114
|
+
"vitest": "^2.0.0"
|
|
107
115
|
},
|
|
108
116
|
"peerDependencies": {
|
|
109
117
|
"prettier": "^3"
|
package/parsers.d.ts
CHANGED
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(
|
|
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
|
|
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>): [
|
|
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(({
|
|
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 {
|
|
159
|
+
return { path, filename, id, mime };
|
|
126
160
|
});
|
|
127
161
|
const usedPreparedRegularAssets = [];
|
|
128
|
-
const preparedThumbnailAssets = thumbnailAssets.map(({
|
|
162
|
+
const preparedThumbnailAssets = thumbnailAssets.map(({ filename, mime, path }, index) => {
|
|
129
163
|
const regularAsset = preparedRegularAssets.find((ra) => !usedPreparedRegularAssets.includes(ra.id) &&
|
|
130
|
-
ra.filename
|
|
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 {
|
|
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,
|
|
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)
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|