@balena/pinejs 16.0.0-build--batch-09b8c466600d7df13e6df3eacabaf463d96f652f-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.
Files changed (141) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +2164 -15
  3. package/CHANGELOG.md +815 -3
  4. package/Gruntfile.ts +9 -6
  5. package/README.md +10 -0
  6. package/build/browser.ts +2 -2
  7. package/build/config.ts +1 -1
  8. package/build/module.ts +2 -2
  9. package/build/server.ts +2 -2
  10. package/docker-compose.npm-test.yml +21 -3
  11. package/out/bin/abstract-sql-compiler.js +5 -5
  12. package/out/bin/abstract-sql-compiler.js.map +1 -1
  13. package/out/bin/odata-compiler.js +10 -10
  14. package/out/bin/odata-compiler.js.map +1 -1
  15. package/out/bin/sbvr-compiler.js +34 -11
  16. package/out/bin/sbvr-compiler.js.map +1 -1
  17. package/out/bin/utils.js +25 -2
  18. package/out/bin/utils.js.map +1 -1
  19. package/out/config-loader/config-loader.d.ts +4 -2
  20. package/out/config-loader/config-loader.js +54 -13
  21. package/out/config-loader/config-loader.js.map +1 -1
  22. package/out/config-loader/env.d.ts +2 -1
  23. package/out/config-loader/env.js +5 -2
  24. package/out/config-loader/env.js.map +1 -1
  25. package/out/data-server/sbvr-server.d.ts +1 -1
  26. package/out/data-server/sbvr-server.js +3 -1
  27. package/out/data-server/sbvr-server.js.map +1 -1
  28. package/out/database-layer/db.js +40 -14
  29. package/out/database-layer/db.js.map +1 -1
  30. package/out/express-emulator/express.js +5 -3
  31. package/out/express-emulator/express.js.map +1 -1
  32. package/out/http-transactions/transactions.d.ts +1 -1
  33. package/out/http-transactions/transactions.js +10 -5
  34. package/out/http-transactions/transactions.js.map +1 -1
  35. package/out/migrator/async.js +32 -5
  36. package/out/migrator/async.js.map +1 -1
  37. package/out/migrator/sync.d.ts +2 -1
  38. package/out/migrator/sync.js +29 -3
  39. package/out/migrator/sync.js.map +1 -1
  40. package/out/migrator/utils.d.ts +6 -3
  41. package/out/migrator/utils.js +30 -4
  42. package/out/migrator/utils.js.map +1 -1
  43. package/out/odata-metadata/odata-metadata-generator.js +4 -1
  44. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  45. package/out/passport-pinejs/mount-login-router.d.ts +3 -0
  46. package/out/passport-pinejs/mount-login-router.js +65 -0
  47. package/out/passport-pinejs/mount-login-router.js.map +1 -0
  48. package/out/passport-pinejs/passport-pinejs.d.ts +2 -1
  49. package/out/passport-pinejs/passport-pinejs.js +28 -2
  50. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  51. package/out/pinejs-session-store/pinejs-session-store.js +30 -7
  52. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  53. package/out/sbvr-api/abstract-sql.d.ts +2 -2
  54. package/out/sbvr-api/abstract-sql.js +35 -9
  55. package/out/sbvr-api/abstract-sql.js.map +1 -1
  56. package/out/sbvr-api/cached-compile.js +9 -6
  57. package/out/sbvr-api/cached-compile.js.map +1 -1
  58. package/out/sbvr-api/common-types.d.ts +1 -1
  59. package/out/sbvr-api/control-flow.js +5 -2
  60. package/out/sbvr-api/control-flow.js.map +1 -1
  61. package/out/sbvr-api/express-extension.d.ts +10 -7
  62. package/out/sbvr-api/express-extension.js +1 -0
  63. package/out/sbvr-api/hooks.d.ts +5 -1
  64. package/out/sbvr-api/hooks.js +12 -10
  65. package/out/sbvr-api/hooks.js.map +1 -1
  66. package/out/sbvr-api/odata-response.d.ts +5 -2
  67. package/out/sbvr-api/odata-response.js +36 -6
  68. package/out/sbvr-api/odata-response.js.map +1 -1
  69. package/out/sbvr-api/permissions.d.ts +6 -7
  70. package/out/sbvr-api/permissions.js +69 -38
  71. package/out/sbvr-api/permissions.js.map +1 -1
  72. package/out/sbvr-api/sbvr-utils.d.ts +21 -10
  73. package/out/sbvr-api/sbvr-utils.js +128 -124
  74. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  75. package/out/sbvr-api/translations.d.ts +2 -2
  76. package/out/sbvr-api/translations.js +17 -10
  77. package/out/sbvr-api/translations.js.map +1 -1
  78. package/out/sbvr-api/uri-parser.d.ts +10 -12
  79. package/out/sbvr-api/uri-parser.js +46 -19
  80. package/out/sbvr-api/uri-parser.js.map +1 -1
  81. package/out/server-glue/global-ext.d.ts +2 -1
  82. package/out/server-glue/module.d.ts +3 -1
  83. package/out/server-glue/module.js +40 -13
  84. package/out/server-glue/module.js.map +1 -1
  85. package/out/server-glue/sbvr-loader.js.map +1 -1
  86. package/out/server-glue/server.js +31 -39
  87. package/out/server-glue/server.js.map +1 -1
  88. package/out/webresource-handler/handlers/NoopHandler.d.ts +7 -0
  89. package/out/webresource-handler/handlers/NoopHandler.js +20 -0
  90. package/out/webresource-handler/handlers/NoopHandler.js.map +1 -0
  91. package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
  92. package/out/webresource-handler/handlers/S3Handler.js +97 -0
  93. package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
  94. package/out/webresource-handler/handlers/index.d.ts +2 -0
  95. package/out/webresource-handler/handlers/index.js +19 -0
  96. package/out/webresource-handler/handlers/index.js.map +1 -0
  97. package/out/webresource-handler/index.d.ts +34 -0
  98. package/out/webresource-handler/index.js +307 -0
  99. package/out/webresource-handler/index.js.map +1 -0
  100. package/package.json +68 -62
  101. package/src/bin/abstract-sql-compiler.ts +7 -9
  102. package/src/bin/odata-compiler.ts +12 -15
  103. package/src/bin/sbvr-compiler.ts +14 -18
  104. package/src/bin/utils.ts +1 -1
  105. package/src/config-loader/config-loader.ts +44 -10
  106. package/src/config-loader/env.ts +1 -1
  107. package/src/data-server/sbvr-server.js +3 -1
  108. package/src/database-layer/db.ts +23 -19
  109. package/src/express-emulator/express.js +5 -3
  110. package/src/extended-sbvr-parser/extended-sbvr-parser.ts +1 -1
  111. package/src/http-transactions/transactions.js +10 -5
  112. package/src/migrator/async.ts +7 -6
  113. package/src/migrator/sync.ts +10 -7
  114. package/src/migrator/utils.ts +11 -5
  115. package/src/odata-metadata/odata-metadata-generator.ts +2 -2
  116. package/src/passport-pinejs/mount-login-router.ts +46 -0
  117. package/src/passport-pinejs/passport-pinejs.ts +7 -3
  118. package/src/pinejs-session-store/pinejs-session-store.ts +6 -6
  119. package/src/sbvr-api/abstract-sql.ts +5 -5
  120. package/src/sbvr-api/cached-compile.ts +1 -2
  121. package/src/sbvr-api/common-types.ts +1 -1
  122. package/src/sbvr-api/control-flow.ts +1 -1
  123. package/src/sbvr-api/express-extension.ts +12 -8
  124. package/src/sbvr-api/hooks.ts +11 -11
  125. package/src/sbvr-api/odata-response.ts +56 -9
  126. package/src/sbvr-api/permissions.ts +44 -35
  127. package/src/sbvr-api/sbvr-utils.ts +117 -165
  128. package/src/sbvr-api/translations.ts +9 -6
  129. package/src/sbvr-api/uri-parser.ts +25 -30
  130. package/src/server-glue/global-ext.d.ts +2 -1
  131. package/src/server-glue/module.ts +8 -2
  132. package/src/server-glue/sbvr-loader.ts +1 -1
  133. package/src/server-glue/server.ts +11 -49
  134. package/src/webresource-handler/handlers/NoopHandler.ts +21 -0
  135. package/src/webresource-handler/handlers/S3Handler.ts +143 -0
  136. package/src/webresource-handler/handlers/index.ts +2 -0
  137. package/src/webresource-handler/index.ts +450 -0
  138. package/tsconfig.dev.json +2 -1
  139. package/tsconfig.json +1 -1
  140. package/typings/lf-to-abstract-sql.d.ts +1 -1
  141. 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 * as _ from 'lodash';
15
+ import _ from 'lodash';
16
16
  import memoizeWeak = require('memoizee/weak');
17
17
 
18
18
  export { BadRequestError, ParsingError, TranslationError } from './errors';
19
- import * as deepFreeze from 'deep-freeze';
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
- body?: any;
37
- headers?: IncomingHttpHeaders;
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?: string | undefined;
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?: string[];
67
- pendingAffectedIds?: Promise<string[]>;
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 async function parseOData(
268
+ export function parseOData(
268
269
  b: UnparsedRequest & { _isChangeSet?: false },
269
- headers?: IncomingHttpHeaders,
270
- ): Promise<ParsedODataRequest>;
271
- export async function parseOData(
270
+ ): ParsedODataRequest;
271
+ export function parseOData(
272
272
  b: UnparsedRequest & { _isChangeSet: true },
273
- headers?: IncomingHttpHeaders,
274
- ): Promise<ParsedODataRequest[]>;
275
- export async function parseOData(
273
+ ): ParsedODataRequest[];
274
+ export function parseOData(
276
275
  b: UnparsedRequest,
277
- headers?: IncomingHttpHeaders,
278
- ): Promise<ParsedODataRequest | ParsedODataRequest[]>;
279
- export async function parseOData(
276
+ ): ParsedODataRequest | ParsedODataRequest[];
277
+ export function parseOData(
280
278
  b: UnparsedRequest,
281
- batchHeaders?: IncomingHttpHeaders,
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.body ?? {},
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.body ?? {},
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?: IncomingHttpHeaders },
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 (!obj.hasOwnProperty(prop)) {
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
  );
@@ -1,5 +1,6 @@
1
+ // We have to use var when extending the global namespace
2
+ // eslint-disable-next-line no-var
1
3
  declare var nodeRequire: NodeRequire;
2
- /* tslint:disable-next-line:no-namespace */
3
4
  declare namespace NodeJS {
4
5
  export interface Process {
5
6
  browser: boolean;
@@ -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
- // tslint:disable-next-line:no-var-requires
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 * as BodyParser from 'body-parser';
2
- import type * as Compression from 'compression';
3
- import type * as CookieParser from 'cookie-parser';
4
- import type * as ExpressSession from 'express-session';
5
- import type * as MethodOverride from 'method-override';
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 * as ServeStatic from 'serve-static';
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 * as passportPinejs from '../passport-pinejs/passport-pinejs';
15
+ import { mountLoginRouter } from '../passport-pinejs/mount-login-router';
17
16
 
18
- import * as express from 'express';
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
- // tslint:disable:no-var-requires
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
- // tslint:enable:no-var-requires
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 Promise.all([
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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './NoopHandler';
2
+ export * from './S3Handler';