@balena/pinejs 16.0.0-build--batch-f2ffc3d6bcb9f3294fd4fc9de3c21bfe167e100d-1 → 16.0.0-build-fisehara-update-sbvr-types-cfbbecbb0387e87e17e14be8991be73d1a4efdd0-1
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/.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
|
+
}
|