@balena/pinejs 16.0.0-build--batch-f2ffc3d6bcb9f3294fd4fc9de3c21bfe167e100d-1 → 16.0.0-build-fisehara-update-sbvr-types-b58e72aca3193964afac96c955fde178fe39d077-1
Sign up to get free protection for your applications and to get access to all the features.
- package/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +2168 -11
- package/CHANGELOG.md +815 -2
- package/Gruntfile.ts +9 -6
- package/README.md +10 -0
- package/build/browser.ts +2 -2
- package/build/config.ts +1 -1
- package/build/module.ts +2 -2
- package/build/server.ts +2 -2
- package/docker-compose.npm-test.yml +21 -3
- package/out/bin/abstract-sql-compiler.js +5 -5
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +10 -10
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +34 -11
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.js +25 -2
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +4 -2
- package/out/config-loader/config-loader.js +54 -13
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +2 -1
- package/out/config-loader/env.js +5 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +1 -1
- package/out/data-server/sbvr-server.js +3 -1
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.js +40 -14
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +5 -3
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +1 -1
- package/out/http-transactions/transactions.js +10 -5
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.js +32 -5
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/sync.d.ts +2 -1
- package/out/migrator/sync.js +29 -3
- package/out/migrator/sync.js.map +1 -1
- package/out/migrator/utils.d.ts +6 -3
- package/out/migrator/utils.js +30 -4
- package/out/migrator/utils.js.map +1 -1
- package/out/odata-metadata/odata-metadata-generator.js +4 -1
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/mount-login-router.d.ts +3 -0
- package/out/passport-pinejs/mount-login-router.js +65 -0
- package/out/passport-pinejs/mount-login-router.js.map +1 -0
- package/out/passport-pinejs/passport-pinejs.d.ts +2 -1
- package/out/passport-pinejs/passport-pinejs.js +28 -2
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +30 -7
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +2 -2
- package/out/sbvr-api/abstract-sql.js +35 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +9 -6
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +1 -1
- package/out/sbvr-api/control-flow.js +5 -2
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +10 -7
- package/out/sbvr-api/express-extension.js +1 -0
- package/out/sbvr-api/hooks.d.ts +5 -1
- package/out/sbvr-api/hooks.js +12 -10
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +5 -2
- package/out/sbvr-api/odata-response.js +36 -6
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +6 -7
- package/out/sbvr-api/permissions.js +69 -38
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +20 -9
- package/out/sbvr-api/sbvr-utils.js +134 -136
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +2 -2
- package/out/sbvr-api/translations.js +17 -10
- package/out/sbvr-api/translations.js.map +1 -1
- package/out/sbvr-api/uri-parser.d.ts +7 -10
- package/out/sbvr-api/uri-parser.js +46 -19
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/server-glue/global-ext.d.ts +2 -1
- package/out/server-glue/module.d.ts +3 -1
- package/out/server-glue/module.js +40 -13
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/sbvr-loader.js.map +1 -1
- package/out/server-glue/server.js +31 -39
- package/out/server-glue/server.js.map +1 -1
- package/out/webresource-handler/handlers/NoopHandler.d.ts +7 -0
- package/out/webresource-handler/handlers/NoopHandler.js +20 -0
- package/out/webresource-handler/handlers/NoopHandler.js.map +1 -0
- package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
- package/out/webresource-handler/handlers/S3Handler.js +97 -0
- package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
- package/out/webresource-handler/handlers/index.d.ts +2 -0
- package/out/webresource-handler/handlers/index.js +19 -0
- package/out/webresource-handler/handlers/index.js.map +1 -0
- package/out/webresource-handler/index.d.ts +34 -0
- package/out/webresource-handler/index.js +307 -0
- package/out/webresource-handler/index.js.map +1 -0
- package/package.json +68 -62
- package/src/bin/abstract-sql-compiler.ts +7 -9
- package/src/bin/odata-compiler.ts +12 -15
- package/src/bin/sbvr-compiler.ts +14 -18
- package/src/bin/utils.ts +1 -1
- package/src/config-loader/config-loader.ts +44 -10
- package/src/config-loader/env.ts +1 -1
- package/src/data-server/sbvr-server.js +3 -1
- package/src/database-layer/db.ts +23 -19
- package/src/express-emulator/express.js +5 -3
- package/src/extended-sbvr-parser/extended-sbvr-parser.ts +1 -1
- package/src/http-transactions/transactions.js +10 -5
- package/src/migrator/async.ts +7 -6
- package/src/migrator/sync.ts +10 -7
- package/src/migrator/utils.ts +11 -5
- package/src/odata-metadata/odata-metadata-generator.ts +2 -2
- package/src/passport-pinejs/mount-login-router.ts +46 -0
- package/src/passport-pinejs/passport-pinejs.ts +7 -3
- package/src/pinejs-session-store/pinejs-session-store.ts +6 -6
- package/src/sbvr-api/abstract-sql.ts +5 -5
- package/src/sbvr-api/cached-compile.ts +1 -2
- package/src/sbvr-api/common-types.ts +1 -1
- package/src/sbvr-api/control-flow.ts +1 -1
- package/src/sbvr-api/express-extension.ts +12 -8
- package/src/sbvr-api/hooks.ts +11 -11
- package/src/sbvr-api/odata-response.ts +56 -9
- package/src/sbvr-api/permissions.ts +44 -35
- package/src/sbvr-api/sbvr-utils.ts +118 -172
- package/src/sbvr-api/translations.ts +9 -6
- package/src/sbvr-api/uri-parser.ts +22 -28
- package/src/server-glue/global-ext.d.ts +2 -1
- package/src/server-glue/module.ts +8 -2
- package/src/server-glue/sbvr-loader.ts +1 -1
- package/src/server-glue/server.ts +11 -49
- package/src/webresource-handler/handlers/NoopHandler.ts +21 -0
- package/src/webresource-handler/handlers/S3Handler.ts +143 -0
- package/src/webresource-handler/handlers/index.ts +2 -0
- package/src/webresource-handler/index.ts +450 -0
- package/tsconfig.dev.json +2 -1
- package/tsconfig.json +1 -1
- package/typings/lf-to-abstract-sql.d.ts +1 -1
- package/typings/memoizee.d.ts +3 -4
@@ -12,11 +12,11 @@ import type { AnyObject } from './common-types';
|
|
12
12
|
import * as ODataParser from '@balena/odata-parser';
|
13
13
|
export const SyntaxError = ODataParser.SyntaxError;
|
14
14
|
import { OData2AbstractSQL } from '@balena/odata-to-abstract-sql';
|
15
|
-
import
|
15
|
+
import _ from 'lodash';
|
16
16
|
import memoizeWeak = require('memoizee/weak');
|
17
17
|
|
18
18
|
export { BadRequestError, ParsingError, TranslationError } from './errors';
|
19
|
-
import
|
19
|
+
import deepFreeze from 'deep-freeze';
|
20
20
|
import * as env from '../config-loader/env';
|
21
21
|
import {
|
22
22
|
BadRequestError,
|
@@ -25,22 +25,19 @@ import {
|
|
25
25
|
TranslationError,
|
26
26
|
} from './errors';
|
27
27
|
import * as sbvrUtils from './sbvr-utils';
|
28
|
-
import { IncomingHttpHeaders } from 'http';
|
29
28
|
|
30
29
|
export type OdataBinds = ODataBinds;
|
31
30
|
|
32
31
|
export interface UnparsedRequest {
|
33
|
-
id?: string;
|
34
32
|
method: string;
|
35
33
|
url: string;
|
36
|
-
|
37
|
-
headers?:
|
34
|
+
data?: any;
|
35
|
+
headers?: { [header: string]: string };
|
38
36
|
changeSet?: UnparsedRequest[];
|
39
37
|
_isChangeSet?: boolean;
|
40
38
|
}
|
41
39
|
|
42
40
|
export interface ParsedODataRequest {
|
43
|
-
headers?: IncomingHttpHeaders;
|
44
41
|
method: SupportedMethod;
|
45
42
|
url: string;
|
46
43
|
vocabulary: string;
|
@@ -51,7 +48,6 @@ export interface ParsedODataRequest {
|
|
51
48
|
odataBinds: OdataBinds;
|
52
49
|
custom: AnyObject;
|
53
50
|
id?: number | undefined;
|
54
|
-
batchRequestId?: string;
|
55
51
|
_defer?: boolean;
|
56
52
|
}
|
57
53
|
export interface ODataRequest extends ParsedODataRequest {
|
@@ -239,10 +235,14 @@ const memoizedOdata2AbstractSQL = (() => {
|
|
239
235
|
>,
|
240
236
|
) => {
|
241
237
|
const { method, odataBinds, values } = request;
|
242
|
-
let { odataQuery } = request;
|
243
|
-
const abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
|
244
238
|
// Sort the body keys to improve cache hits
|
245
239
|
const sortedBody = Object.keys(values).sort();
|
240
|
+
if (sortedBody.length === 0 && (method === 'PATCH' || method === 'MERGE')) {
|
241
|
+
throw new BadRequestError('No fields to update');
|
242
|
+
}
|
243
|
+
|
244
|
+
let { odataQuery } = request;
|
245
|
+
const abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
|
246
246
|
// Remove unused options for odata-to-abstract-sql to improve cache hits
|
247
247
|
if (odataQuery.options) {
|
248
248
|
odataQuery = {
|
@@ -265,22 +265,18 @@ const memoizedOdata2AbstractSQL = (() => {
|
|
265
265
|
|
266
266
|
export const metadataEndpoints = ['$metadata', '$serviceroot'];
|
267
267
|
|
268
|
-
export
|
268
|
+
export function parseOData(
|
269
269
|
b: UnparsedRequest & { _isChangeSet?: false },
|
270
|
-
|
271
|
-
|
272
|
-
export async function parseOData(
|
270
|
+
): ParsedODataRequest;
|
271
|
+
export function parseOData(
|
273
272
|
b: UnparsedRequest & { _isChangeSet: true },
|
274
|
-
|
275
|
-
|
276
|
-
export async function parseOData(
|
273
|
+
): ParsedODataRequest[];
|
274
|
+
export function parseOData(
|
277
275
|
b: UnparsedRequest,
|
278
|
-
|
279
|
-
|
280
|
-
export async function parseOData(
|
276
|
+
): ParsedODataRequest | ParsedODataRequest[];
|
277
|
+
export function parseOData(
|
281
278
|
b: UnparsedRequest,
|
282
|
-
|
283
|
-
): Promise<ParsedODataRequest | ParsedODataRequest[]> {
|
279
|
+
): ParsedODataRequest | ParsedODataRequest[] {
|
284
280
|
try {
|
285
281
|
if (b._isChangeSet && b.changeSet != null) {
|
286
282
|
// We sort the CS set once, we must assure that requests which reference
|
@@ -300,14 +296,12 @@ export async function parseOData(
|
|
300
296
|
const odata = memoizedParseOdata(url);
|
301
297
|
|
302
298
|
return {
|
303
|
-
batchRequestId: b.id,
|
304
|
-
headers: { ...batchHeaders, ...b.headers },
|
305
299
|
method: b.method as SupportedMethod,
|
306
300
|
url,
|
307
301
|
vocabulary: apiRoot,
|
308
302
|
resourceName: odata.tree.resource,
|
309
303
|
originalResourceName: odata.tree.resource,
|
310
|
-
values: b.
|
304
|
+
values: b.data ?? {},
|
311
305
|
odataQuery: odata.tree,
|
312
306
|
odataBinds: odata.binds,
|
313
307
|
custom: {},
|
@@ -372,7 +366,7 @@ const parseODataChangeset = (
|
|
372
366
|
originalResourceName: odata.tree.resource,
|
373
367
|
odataBinds: odata.binds,
|
374
368
|
odataQuery: odata.tree,
|
375
|
-
values: b.
|
369
|
+
values: b.data ?? {},
|
376
370
|
custom: {},
|
377
371
|
id: contentId,
|
378
372
|
_defer: defer,
|
@@ -389,7 +383,7 @@ const splitApiRoot = (url: string) => {
|
|
389
383
|
};
|
390
384
|
|
391
385
|
const mustExtractHeader = (
|
392
|
-
body: { headers?:
|
386
|
+
body: { headers?: { [header: string]: string } },
|
393
387
|
header: string,
|
394
388
|
) => {
|
395
389
|
const h: any = body.headers?.[header]?.[0];
|
@@ -426,7 +420,7 @@ export const translateUri = <
|
|
426
420
|
request = { ...request };
|
427
421
|
request.values = new Proxy(request.values, {
|
428
422
|
set: (obj: ODataRequest['values'], prop: string, value) => {
|
429
|
-
if (!
|
423
|
+
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
430
424
|
sbvrUtils.api[request.vocabulary].logger.warn(
|
431
425
|
`Assigning a new request.values property '${prop}' however it will be ignored`,
|
432
426
|
);
|
@@ -5,19 +5,21 @@ import './sbvr-loader';
|
|
5
5
|
import * as dbModule from '../database-layer/db';
|
6
6
|
import * as configLoader from '../config-loader/config-loader';
|
7
7
|
import * as migrator from '../migrator/sync';
|
8
|
-
import * as migratorUtils from '../migrator/utils';
|
8
|
+
import type * as migratorUtils from '../migrator/utils';
|
9
9
|
|
10
10
|
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
11
11
|
import { PINEJS_ADVISORY_LOCK } from '../config-loader/env';
|
12
12
|
|
13
13
|
export * as dbModule from '../database-layer/db';
|
14
14
|
export { PinejsSessionStore } from '../pinejs-session-store/pinejs-session-store';
|
15
|
+
export { mountLoginRouter } from '../passport-pinejs/mount-login-router';
|
15
16
|
export * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
16
17
|
export * as permissions from '../sbvr-api/permissions';
|
17
18
|
export * as errors from '../sbvr-api/errors';
|
18
19
|
export * as env from '../config-loader/env';
|
19
20
|
export * as types from '../sbvr-api/common-types';
|
20
21
|
export * as hooks from '../sbvr-api/hooks';
|
22
|
+
export * as webResourceHandler from '../webresource-handler';
|
21
23
|
export type { configLoader as ConfigLoader };
|
22
24
|
export type { migratorUtils as Migrator };
|
23
25
|
|
@@ -64,7 +66,8 @@ export const init = async <T extends string>(
|
|
64
66
|
|
65
67
|
const promises: Array<Promise<void>> = [];
|
66
68
|
if (process.env.SBVR_SERVER_ENABLED) {
|
67
|
-
const sbvrServer = await import('../data-server/sbvr-server');
|
69
|
+
const sbvrServer = await import('../data-server/sbvr-server.js');
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
68
71
|
const transactions = require('../http-transactions/transactions');
|
69
72
|
promises.push(cfgLoader.loadConfig(sbvrServer.config));
|
70
73
|
promises.push(
|
@@ -77,6 +80,9 @@ export const init = async <T extends string>(
|
|
77
80
|
promises.push(cfgLoader.loadApplicationConfig(config));
|
78
81
|
}
|
79
82
|
await Promise.all(promises);
|
83
|
+
// Execute it after all other promises have resolved. Execution of promises is not neccessarily
|
84
|
+
// guaranteed to be sequentially resolving them with Promise.all
|
85
|
+
await sbvrUtils.postSetup(app, db);
|
80
86
|
|
81
87
|
return cfgLoader;
|
82
88
|
} catch (err: any) {
|
@@ -15,7 +15,7 @@ if (!process.browser) {
|
|
15
15
|
global.nodeRequire = require;
|
16
16
|
}
|
17
17
|
// Register a .sbvr loader
|
18
|
-
//
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
19
19
|
const fs: typeof Fs = require('fs');
|
20
20
|
nodeRequire.extensions['.sbvr'] = (module: NodeModule, filename: string) =>
|
21
21
|
(module.exports = fs.readFileSync(filename, 'utf8'));
|
@@ -1,21 +1,20 @@
|
|
1
|
-
import type
|
2
|
-
import type
|
3
|
-
import type
|
4
|
-
import type
|
5
|
-
import type
|
6
|
-
import type * as Multer from 'multer';
|
1
|
+
import type BodyParser from 'body-parser';
|
2
|
+
import type Compression from 'compression';
|
3
|
+
import type CookieParser from 'cookie-parser';
|
4
|
+
import type ExpressSession from 'express-session';
|
5
|
+
import type MethodOverride from 'method-override';
|
7
6
|
import type * as Passport from 'passport';
|
8
7
|
import type * as Path from 'path';
|
9
|
-
import type
|
8
|
+
import type ServeStatic from 'serve-static';
|
10
9
|
|
11
10
|
import * as Pinejs from './module';
|
12
11
|
export { sbvrUtils, PinejsSessionStore } from './module';
|
13
12
|
|
14
13
|
export { ExtendedSBVRParser } from '../extended-sbvr-parser/extended-sbvr-parser';
|
15
14
|
|
16
|
-
import
|
15
|
+
import { mountLoginRouter } from '../passport-pinejs/mount-login-router';
|
17
16
|
|
18
|
-
import
|
17
|
+
import express from 'express';
|
19
18
|
|
20
19
|
const app = express();
|
21
20
|
|
@@ -28,17 +27,16 @@ switch (app.get('env')) {
|
|
28
27
|
}
|
29
28
|
|
30
29
|
if (!process.browser) {
|
31
|
-
|
30
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
32
31
|
const passport: typeof Passport = require('passport');
|
33
32
|
const path: typeof Path = require('path');
|
34
33
|
const compression: typeof Compression = require('compression');
|
35
34
|
const serveStatic: typeof ServeStatic = require('serve-static');
|
36
35
|
const cookieParser: typeof CookieParser = require('cookie-parser');
|
37
36
|
const bodyParser: typeof BodyParser = require('body-parser');
|
38
|
-
const multer: typeof Multer = require('multer');
|
39
37
|
const methodOverride: typeof MethodOverride = require('method-override');
|
40
38
|
const expressSession: typeof ExpressSession = require('express-session');
|
41
|
-
|
39
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
42
40
|
|
43
41
|
app.use(compression());
|
44
42
|
|
@@ -47,7 +45,6 @@ if (!process.browser) {
|
|
47
45
|
|
48
46
|
app.use(cookieParser());
|
49
47
|
app.use(bodyParser());
|
50
|
-
app.use(multer().any());
|
51
48
|
app.use(methodOverride());
|
52
49
|
app.use(
|
53
50
|
expressSession({
|
@@ -81,42 +78,7 @@ if (!process.browser) {
|
|
81
78
|
|
82
79
|
export const initialised = Pinejs.init(app)
|
83
80
|
.then(async (configLoader) => {
|
84
|
-
await
|
85
|
-
configLoader.loadConfig(passportPinejs.config),
|
86
|
-
configLoader.loadConfig(Pinejs.PinejsSessionStore.config),
|
87
|
-
]);
|
88
|
-
|
89
|
-
if (
|
90
|
-
typeof process === 'undefined' ||
|
91
|
-
process == null ||
|
92
|
-
!process.env.DISABLE_DEFAULT_AUTH
|
93
|
-
) {
|
94
|
-
app.post(
|
95
|
-
'/login',
|
96
|
-
passportPinejs.login((err, user, req, res) => {
|
97
|
-
if (err) {
|
98
|
-
console.error('Error logging in', err);
|
99
|
-
res.status(500).end();
|
100
|
-
} else if (user === false) {
|
101
|
-
if (req.xhr === true) {
|
102
|
-
res.status(401).end();
|
103
|
-
} else {
|
104
|
-
res.redirect('/login.html');
|
105
|
-
}
|
106
|
-
} else {
|
107
|
-
if (req.xhr === true) {
|
108
|
-
res.status(200).end();
|
109
|
-
} else {
|
110
|
-
res.redirect('/');
|
111
|
-
}
|
112
|
-
}
|
113
|
-
}),
|
114
|
-
);
|
115
|
-
|
116
|
-
app.get('/logout', passportPinejs.logout, (_req, res) => {
|
117
|
-
res.redirect('/');
|
118
|
-
});
|
119
|
-
}
|
81
|
+
await mountLoginRouter(configLoader, app);
|
120
82
|
|
121
83
|
app.listen(process.env.PORT || 1337, () => {
|
122
84
|
console.info('Server started');
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
2
|
+
import type { IncomingFile, UploadResponse, WebResourceHandler } from '..';
|
3
|
+
|
4
|
+
export class NoopHandler implements WebResourceHandler {
|
5
|
+
public async handleFile(resource: IncomingFile): Promise<UploadResponse> {
|
6
|
+
// handleFile must consume the file stream
|
7
|
+
resource.stream.resume();
|
8
|
+
return {
|
9
|
+
filename: 'noop',
|
10
|
+
size: 0,
|
11
|
+
};
|
12
|
+
}
|
13
|
+
|
14
|
+
public async removeFile(): Promise<void> {
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
|
18
|
+
public async onPreRespond(webResource: WebResource): Promise<WebResource> {
|
19
|
+
return webResource;
|
20
|
+
}
|
21
|
+
}
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import {
|
2
|
+
FileSizeExceededError,
|
3
|
+
type IncomingFile,
|
4
|
+
normalizeHref,
|
5
|
+
type UploadResponse,
|
6
|
+
WebResourceError,
|
7
|
+
type WebResourceHandler,
|
8
|
+
} from '..';
|
9
|
+
import {
|
10
|
+
S3Client,
|
11
|
+
type S3ClientConfig,
|
12
|
+
DeleteObjectCommand,
|
13
|
+
type PutObjectCommandInput,
|
14
|
+
GetObjectCommand,
|
15
|
+
} from '@aws-sdk/client-s3';
|
16
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
17
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
18
|
+
|
19
|
+
import { randomUUID } from 'crypto';
|
20
|
+
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
21
|
+
import memoize from 'memoizee';
|
22
|
+
|
23
|
+
export interface S3HandlerProps {
|
24
|
+
region: string;
|
25
|
+
accessKey: string;
|
26
|
+
secretKey: string;
|
27
|
+
endpoint: string;
|
28
|
+
bucket: string;
|
29
|
+
maxSize?: number;
|
30
|
+
signedUrlExpireTimeSeconds?: number;
|
31
|
+
signedUrlCacheExpireTimeSeconds?: number;
|
32
|
+
}
|
33
|
+
|
34
|
+
export class S3Handler implements WebResourceHandler {
|
35
|
+
private readonly config: S3ClientConfig;
|
36
|
+
private readonly bucket: string;
|
37
|
+
private readonly maxFileSize: number;
|
38
|
+
|
39
|
+
protected readonly signedUrlExpireTimeSeconds: number;
|
40
|
+
protected readonly signedUrlCacheExpireTimeSeconds: number;
|
41
|
+
protected cachedGetSignedUrl: (fileKey: string) => Promise<string>;
|
42
|
+
|
43
|
+
private client: S3Client;
|
44
|
+
|
45
|
+
constructor(config: S3HandlerProps) {
|
46
|
+
this.config = {
|
47
|
+
region: config.region,
|
48
|
+
credentials: {
|
49
|
+
accessKeyId: config.accessKey,
|
50
|
+
secretAccessKey: config.secretKey,
|
51
|
+
},
|
52
|
+
endpoint: config.endpoint,
|
53
|
+
forcePathStyle: true,
|
54
|
+
};
|
55
|
+
|
56
|
+
this.signedUrlExpireTimeSeconds =
|
57
|
+
config.signedUrlExpireTimeSeconds ?? 86400; // 24h
|
58
|
+
this.signedUrlCacheExpireTimeSeconds =
|
59
|
+
config.signedUrlCacheExpireTimeSeconds ?? 82800; // 22h
|
60
|
+
|
61
|
+
this.maxFileSize = config.maxSize ?? 52428800;
|
62
|
+
this.bucket = config.bucket;
|
63
|
+
this.client = new S3Client(this.config);
|
64
|
+
|
65
|
+
// Memoize expects maxAge in MS and s3 signing method in seconds.
|
66
|
+
// Normalization to use only seconds and therefore convert here from seconds to MS
|
67
|
+
this.cachedGetSignedUrl = memoize(this.s3SignUrl, {
|
68
|
+
maxAge: this.signedUrlCacheExpireTimeSeconds * 1000,
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
public async handleFile(resource: IncomingFile): Promise<UploadResponse> {
|
73
|
+
let size = 0;
|
74
|
+
const key = `${resource.fieldname}_${randomUUID()}_${
|
75
|
+
resource.originalname
|
76
|
+
}`;
|
77
|
+
const params: PutObjectCommandInput = {
|
78
|
+
Bucket: this.bucket,
|
79
|
+
Key: key,
|
80
|
+
Body: resource.stream,
|
81
|
+
ContentType: resource.mimetype,
|
82
|
+
};
|
83
|
+
const upload = new Upload({ client: this.client, params });
|
84
|
+
|
85
|
+
upload.on('httpUploadProgress', async (ev) => {
|
86
|
+
size = ev.total ?? ev.loaded!;
|
87
|
+
if (size > this.maxFileSize) {
|
88
|
+
await upload.abort();
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
try {
|
93
|
+
await upload.done();
|
94
|
+
} catch (err: any) {
|
95
|
+
resource.stream.resume();
|
96
|
+
if (size > this.maxFileSize) {
|
97
|
+
throw new FileSizeExceededError(this.maxFileSize);
|
98
|
+
}
|
99
|
+
throw new WebResourceError(err);
|
100
|
+
}
|
101
|
+
|
102
|
+
const filename = this.getS3URL(key);
|
103
|
+
return { size, filename };
|
104
|
+
}
|
105
|
+
|
106
|
+
public async removeFile(href: string): Promise<void> {
|
107
|
+
const fileKey = this.getKeyFromHref(href);
|
108
|
+
|
109
|
+
const command = new DeleteObjectCommand({
|
110
|
+
Bucket: this.bucket,
|
111
|
+
Key: fileKey,
|
112
|
+
});
|
113
|
+
|
114
|
+
await this.client.send(command);
|
115
|
+
}
|
116
|
+
|
117
|
+
public async onPreRespond(webResource: WebResource): Promise<WebResource> {
|
118
|
+
if (webResource.href != null) {
|
119
|
+
const fileKey = this.getKeyFromHref(webResource.href);
|
120
|
+
webResource.href = await this.cachedGetSignedUrl(fileKey);
|
121
|
+
}
|
122
|
+
return webResource;
|
123
|
+
}
|
124
|
+
|
125
|
+
private s3SignUrl(fileKey: string): Promise<string> {
|
126
|
+
const command = new GetObjectCommand({
|
127
|
+
Bucket: this.bucket,
|
128
|
+
Key: fileKey,
|
129
|
+
});
|
130
|
+
return getSignedUrl(this.client, command, {
|
131
|
+
expiresIn: this.signedUrlExpireTimeSeconds,
|
132
|
+
});
|
133
|
+
}
|
134
|
+
|
135
|
+
private getS3URL(key: string): string {
|
136
|
+
return `${this.config.endpoint}/${this.bucket}/${key}`;
|
137
|
+
}
|
138
|
+
|
139
|
+
private getKeyFromHref(href: string): string {
|
140
|
+
const hrefWithoutParams = normalizeHref(href);
|
141
|
+
return hrefWithoutParams.substring(hrefWithoutParams.lastIndexOf('/') + 1);
|
142
|
+
}
|
143
|
+
}
|