@balena/pinejs 16.0.0-build--batch-09b8c466600d7df13e6df3eacabaf463d96f652f-1 → 16.0.0-build-fisehara-update-sbvr-types-b58e72aca3193964afac96c955fde178fe39d077-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 +2164 -15
- package/CHANGELOG.md +815 -3
- 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 +21 -10
- package/out/sbvr-api/sbvr-utils.js +128 -124
- 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 +10 -12
- 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 +117 -165
- package/src/sbvr-api/translations.ts +9 -6
- package/src/sbvr-api/uri-parser.ts +25 -30
- 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;
|
@@ -50,7 +47,7 @@ export interface ParsedODataRequest {
|
|
50
47
|
odataQuery: ODataQuery;
|
51
48
|
odataBinds: OdataBinds;
|
52
49
|
custom: AnyObject;
|
53
|
-
id?:
|
50
|
+
id?: number | undefined;
|
54
51
|
_defer?: boolean;
|
55
52
|
}
|
56
53
|
export interface ODataRequest extends ParsedODataRequest {
|
@@ -63,8 +60,8 @@ export interface ODataRequest extends ParsedODataRequest {
|
|
63
60
|
modifiedFields?: ReturnType<
|
64
61
|
AbstractSQLCompiler.EngineInstance['getModifiedFields']
|
65
62
|
>;
|
66
|
-
affectedIds?:
|
67
|
-
pendingAffectedIds?: Promise<
|
63
|
+
affectedIds?: number[];
|
64
|
+
pendingAffectedIds?: Promise<number[]>;
|
68
65
|
hooks?: Array<[string, InstantiatedHooks]>;
|
69
66
|
engine: AbstractSQLCompiler.Engines;
|
70
67
|
}
|
@@ -238,10 +235,14 @@ const memoizedOdata2AbstractSQL = (() => {
|
|
238
235
|
>,
|
239
236
|
) => {
|
240
237
|
const { method, odataBinds, values } = request;
|
241
|
-
let { odataQuery } = request;
|
242
|
-
const abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
|
243
238
|
// Sort the body keys to improve cache hits
|
244
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);
|
245
246
|
// Remove unused options for odata-to-abstract-sql to improve cache hits
|
246
247
|
if (odataQuery.options) {
|
247
248
|
odataQuery = {
|
@@ -264,22 +265,18 @@ const memoizedOdata2AbstractSQL = (() => {
|
|
264
265
|
|
265
266
|
export const metadataEndpoints = ['$metadata', '$serviceroot'];
|
266
267
|
|
267
|
-
export
|
268
|
+
export function parseOData(
|
268
269
|
b: UnparsedRequest & { _isChangeSet?: false },
|
269
|
-
|
270
|
-
|
271
|
-
export async function parseOData(
|
270
|
+
): ParsedODataRequest;
|
271
|
+
export function parseOData(
|
272
272
|
b: UnparsedRequest & { _isChangeSet: true },
|
273
|
-
|
274
|
-
|
275
|
-
export async function parseOData(
|
273
|
+
): ParsedODataRequest[];
|
274
|
+
export function parseOData(
|
276
275
|
b: UnparsedRequest,
|
277
|
-
|
278
|
-
|
279
|
-
export async function parseOData(
|
276
|
+
): ParsedODataRequest | ParsedODataRequest[];
|
277
|
+
export function parseOData(
|
280
278
|
b: UnparsedRequest,
|
281
|
-
|
282
|
-
): Promise<ParsedODataRequest | ParsedODataRequest[]> {
|
279
|
+
): ParsedODataRequest | ParsedODataRequest[] {
|
283
280
|
try {
|
284
281
|
if (b._isChangeSet && b.changeSet != null) {
|
285
282
|
// We sort the CS set once, we must assure that requests which reference
|
@@ -299,14 +296,12 @@ export async function parseOData(
|
|
299
296
|
const odata = memoizedParseOdata(url);
|
300
297
|
|
301
298
|
return {
|
302
|
-
id: b.id,
|
303
|
-
headers: { ...batchHeaders, ...b.headers },
|
304
299
|
method: b.method as SupportedMethod,
|
305
300
|
url,
|
306
301
|
vocabulary: apiRoot,
|
307
302
|
resourceName: odata.tree.resource,
|
308
303
|
originalResourceName: odata.tree.resource,
|
309
|
-
values: b.
|
304
|
+
values: b.data ?? {},
|
310
305
|
odataQuery: odata.tree,
|
311
306
|
odataBinds: odata.binds,
|
312
307
|
custom: {},
|
@@ -371,7 +366,7 @@ const parseODataChangeset = (
|
|
371
366
|
originalResourceName: odata.tree.resource,
|
372
367
|
odataBinds: odata.binds,
|
373
368
|
odataQuery: odata.tree,
|
374
|
-
values: b.
|
369
|
+
values: b.data ?? {},
|
375
370
|
custom: {},
|
376
371
|
id: contentId,
|
377
372
|
_defer: defer,
|
@@ -388,7 +383,7 @@ const splitApiRoot = (url: string) => {
|
|
388
383
|
};
|
389
384
|
|
390
385
|
const mustExtractHeader = (
|
391
|
-
body: { headers?:
|
386
|
+
body: { headers?: { [header: string]: string } },
|
392
387
|
header: string,
|
393
388
|
) => {
|
394
389
|
const h: any = body.headers?.[header]?.[0];
|
@@ -425,7 +420,7 @@ export const translateUri = <
|
|
425
420
|
request = { ...request };
|
426
421
|
request.values = new Proxy(request.values, {
|
427
422
|
set: (obj: ODataRequest['values'], prop: string, value) => {
|
428
|
-
if (!
|
423
|
+
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
429
424
|
sbvrUtils.api[request.vocabulary].logger.warn(
|
430
425
|
`Assigning a new request.values property '${prop}' however it will be ignored`,
|
431
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
|
+
}
|