@balena/pinejs 16.0.0-build--batch-f2ffc3d6bcb9f3294fd4fc9de3c21bfe167e100d-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 +2168 -11
  3. package/CHANGELOG.md +815 -2
  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 +20 -9
  73. package/out/sbvr-api/sbvr-utils.js +134 -136
  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 +7 -10
  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 +118 -172
  128. package/src/sbvr-api/translations.ts +9 -6
  129. package/src/sbvr-api/uri-parser.ts +22 -28
  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;
@@ -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 async function parseOData(
268
+ export function parseOData(
269
269
  b: UnparsedRequest & { _isChangeSet?: false },
270
- headers?: IncomingHttpHeaders,
271
- ): Promise<ParsedODataRequest>;
272
- export async function parseOData(
270
+ ): ParsedODataRequest;
271
+ export function parseOData(
273
272
  b: UnparsedRequest & { _isChangeSet: true },
274
- headers?: IncomingHttpHeaders,
275
- ): Promise<ParsedODataRequest[]>;
276
- export async function parseOData(
273
+ ): ParsedODataRequest[];
274
+ export function parseOData(
277
275
  b: UnparsedRequest,
278
- headers?: IncomingHttpHeaders,
279
- ): Promise<ParsedODataRequest | ParsedODataRequest[]>;
280
- export async function parseOData(
276
+ ): ParsedODataRequest | ParsedODataRequest[];
277
+ export function parseOData(
281
278
  b: UnparsedRequest,
282
- batchHeaders?: IncomingHttpHeaders,
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.body ?? {},
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.body ?? {},
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?: IncomingHttpHeaders },
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 (!obj.hasOwnProperty(prop)) {
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
  );
@@ -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';