@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.
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';