@balena/pinejs 17.0.0-build-large-file-uploads-293e65ee371a69130834c50fef2f7f42cc18133f-1 → 17.0.0-build-17-x-74b0e8403edbd7922c684e3ce19bdb42ac41846b-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 (66) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +2295 -17
  3. package/CHANGELOG.md +860 -5
  4. package/out/bin/abstract-sql-compiler.js +1 -1
  5. package/out/bin/abstract-sql-compiler.js.map +1 -1
  6. package/out/bin/sbvr-compiler.js +1 -1
  7. package/out/bin/sbvr-compiler.js.map +1 -1
  8. package/out/database-layer/db.js +30 -19
  9. package/out/database-layer/db.js.map +1 -1
  10. package/out/http-transactions/transactions.js +2 -2
  11. package/out/http-transactions/transactions.js.map +1 -1
  12. package/out/migrator/async.js +8 -9
  13. package/out/migrator/async.js.map +1 -1
  14. package/out/migrator/sync.js +6 -6
  15. package/out/migrator/sync.js.map +1 -1
  16. package/out/pinejs-session-store/pinejs-session-store.js +103 -108
  17. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  18. package/out/sbvr-api/abstract-sql.js +1 -1
  19. package/out/sbvr-api/abstract-sql.js.map +1 -1
  20. package/out/sbvr-api/errors.js +3 -0
  21. package/out/sbvr-api/errors.js.map +1 -1
  22. package/out/sbvr-api/hooks.js +4 -5
  23. package/out/sbvr-api/hooks.js.map +1 -1
  24. package/out/sbvr-api/permissions.js +3 -3
  25. package/out/sbvr-api/permissions.js.map +1 -1
  26. package/out/sbvr-api/sbvr-utils.d.ts +5 -12
  27. package/out/sbvr-api/sbvr-utils.js +16 -15
  28. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  29. package/out/sbvr-api/uri-parser.js +1 -1
  30. package/out/sbvr-api/uri-parser.js.map +1 -1
  31. package/out/server-glue/module.js +0 -2
  32. package/out/server-glue/module.js.map +1 -1
  33. package/out/webresource-handler/handlers/NoopHandler.d.ts +1 -3
  34. package/out/webresource-handler/handlers/NoopHandler.js +0 -6
  35. package/out/webresource-handler/handlers/NoopHandler.js.map +1 -1
  36. package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
  37. package/out/webresource-handler/handlers/S3Handler.js +104 -0
  38. package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
  39. package/out/webresource-handler/handlers/index.d.ts +1 -0
  40. package/out/webresource-handler/handlers/index.js +1 -0
  41. package/out/webresource-handler/handlers/index.js.map +1 -1
  42. package/out/webresource-handler/index.d.ts +7 -31
  43. package/out/webresource-handler/index.js +22 -23
  44. package/out/webresource-handler/index.js.map +1 -1
  45. package/package.json +40 -39
  46. package/src/bin/abstract-sql-compiler.ts +1 -1
  47. package/src/bin/sbvr-compiler.ts +1 -1
  48. package/src/http-transactions/transactions.js +2 -2
  49. package/src/migrator/async.ts +10 -11
  50. package/src/migrator/sync.ts +6 -6
  51. package/src/sbvr-api/abstract-sql.ts +1 -1
  52. package/src/sbvr-api/permissions.ts +3 -3
  53. package/src/sbvr-api/sbvr-utils.ts +21 -30
  54. package/src/sbvr-api/uri-parser.ts +1 -1
  55. package/src/server-glue/module.ts +0 -2
  56. package/src/webresource-handler/handlers/NoopHandler.ts +1 -14
  57. package/src/webresource-handler/handlers/S3Handler.ts +143 -0
  58. package/src/webresource-handler/handlers/index.ts +1 -0
  59. package/src/webresource-handler/index.ts +22 -64
  60. package/tsconfig.json +1 -1
  61. package/out/webresource-handler/multipartUpload.d.ts +0 -16
  62. package/out/webresource-handler/multipartUpload.js +0 -182
  63. package/out/webresource-handler/multipartUpload.js.map +0 -1
  64. package/out/webresource-handler/webresource.sbvr +0 -63
  65. package/src/webresource-handler/multipartUpload.ts +0 -275
  66. package/src/webresource-handler/webresource.sbvr +0 -63
@@ -671,20 +671,20 @@ export const executeModels = async (
671
671
  await validateModel(tx, apiRoot);
672
672
  }
673
673
 
674
- // TODO: Can we do this without the cast?
675
- api[apiRoot] = new PinejsClient('/' + apiRoot + '/') as LoggingClient;
676
- api[apiRoot].logger = { ...console };
674
+ api[apiRoot] = new PinejsClient('/' + apiRoot + '/');
675
+
676
+ logger[apiRoot] = { ...console };
677
677
  if (model.logging != null) {
678
678
  const defaultSetting = model.logging?.default ?? true;
679
- const { logger } = api[apiRoot];
679
+ const log = logger[apiRoot];
680
680
  for (const k of Object.keys(model.logging)) {
681
681
  const key = k as keyof Console;
682
682
  if (
683
683
  key !== 'Console' &&
684
- typeof logger[key] === 'function' &&
684
+ typeof log[key] === 'function' &&
685
685
  !(model.logging?.[key] ?? defaultSetting)
686
686
  ) {
687
- logger[key] = _.noop;
687
+ log[key] = _.noop;
688
688
  }
689
689
  }
690
690
  }
@@ -856,7 +856,7 @@ export const runRule = (() => {
856
856
  translator.addTypes(sbvrTypes);
857
857
  return async (vocab: string, rule: string) => {
858
858
  const seModel = models[vocab].se;
859
- const { logger } = api[vocab];
859
+ const log = logger[vocab];
860
860
  let lfModel: LFModel;
861
861
  let slfModel: LFModel;
862
862
  let abstractSqlModel: AbstractSQLCompiler.AbstractSqlModel;
@@ -867,7 +867,7 @@ export const runRule = (() => {
867
867
  'Process',
868
868
  );
869
869
  } catch (e) {
870
- logger.error('Error parsing rule', rule, e);
870
+ log.error('Error parsing rule', rule, e);
871
871
  throw new Error(`Error parsing rule'${rule}': ${e}`);
872
872
  }
873
873
 
@@ -881,7 +881,7 @@ export const runRule = (() => {
881
881
  translator.reset();
882
882
  abstractSqlModel = translator.match(slfModel, 'Process');
883
883
  } catch (e) {
884
- logger.error('Error compiling rule', rule, e);
884
+ log.error('Error compiling rule', rule, e);
885
885
  throw new Error(`Error compiling rule '${rule}': ${e}`);
886
886
  }
887
887
 
@@ -992,16 +992,7 @@ export const runRule = (() => {
992
992
  };
993
993
  })();
994
994
 
995
- export type Passthrough = AnyObject & {
996
- req?: {
997
- user?: User;
998
- };
999
- tx?: Db.Tx;
1000
- };
1001
-
1002
- export class PinejsClient extends PinejsClientCore<PinejsClient> {
1003
- // @ts-expect-error This is actually assigned by `super` so it is always declared but that isn't detected here
1004
- public passthrough: Passthrough;
995
+ export class PinejsClient extends PinejsClientCore {
1005
996
  public async _request({
1006
997
  method,
1007
998
  url,
@@ -1021,11 +1012,11 @@ export class PinejsClient extends PinejsClientCore<PinejsClient> {
1021
1012
  }
1022
1013
  }
1023
1014
 
1024
- export type LoggingClient = PinejsClient & {
1025
- logger: Console;
1026
- };
1027
1015
  export const api: {
1028
- [vocab: string]: LoggingClient;
1016
+ [vocab: string]: PinejsClient;
1017
+ } = {};
1018
+ export const logger: {
1019
+ [vocab: string]: Console;
1029
1020
  } = {};
1030
1021
 
1031
1022
  // We default to guest only permissions if no req object is passed in
@@ -1224,7 +1215,7 @@ export const getModel = (vocabulary: string) => {
1224
1215
 
1225
1216
  const runODataRequest = (req: Express.Request, vocabulary: string) => {
1226
1217
  if (env.DEBUG) {
1227
- api[vocabulary].logger.log('Parsing', req.method, req.url);
1218
+ logger[vocabulary].log('Parsing', req.method, req.url);
1228
1219
  }
1229
1220
 
1230
1221
  // Get the hooks for the current method/vocabulary as we know it,
@@ -1535,10 +1526,10 @@ const runRequest = async (
1535
1526
  tx: Db.Tx,
1536
1527
  request: uriParser.ODataRequest,
1537
1528
  ): Promise<Response> => {
1538
- const { logger } = api[request.vocabulary];
1529
+ const log = logger[request.vocabulary];
1539
1530
 
1540
1531
  if (env.DEBUG) {
1541
- logger.log('Running', req.method, req.url);
1532
+ log.log('Running', req.method, req.url);
1542
1533
  }
1543
1534
  let result: Db.Result | number | undefined;
1544
1535
 
@@ -1566,7 +1557,7 @@ const runRequest = async (
1566
1557
  } catch (err: any) {
1567
1558
  if (err instanceof db.DatabaseError) {
1568
1559
  prettifyConstraintError(err, request);
1569
- logger.error(err);
1560
+ log.error(err);
1570
1561
  // Override the error message so we don't leak any internal db info
1571
1562
  err.message = 'Database error';
1572
1563
  throw err;
@@ -1580,7 +1571,7 @@ const runRequest = async (
1580
1571
  err instanceof TypeError ||
1581
1572
  err instanceof URIError
1582
1573
  ) {
1583
- logger.error(err);
1574
+ log.error(err);
1584
1575
  throw new InternalRequestError();
1585
1576
  }
1586
1577
  throw err;
@@ -1740,7 +1731,7 @@ const runQuery = async (
1740
1731
  );
1741
1732
 
1742
1733
  if (env.DEBUG) {
1743
- api[vocabulary].logger.log(query, values);
1734
+ logger[vocabulary].log(query, values);
1744
1735
  }
1745
1736
 
1746
1737
  // We only add the returning clause if it's been requested and `affectedIds` hasn't been populated yet
@@ -1848,7 +1839,7 @@ const respondPost = async (
1848
1839
  id,
1849
1840
  );
1850
1841
  if (env.DEBUG) {
1851
- api[vocab].logger.log('Insert ID: ', request.resourceName, id);
1842
+ logger[vocab].log('Insert ID: ', request.resourceName, id);
1852
1843
  }
1853
1844
 
1854
1845
  let result: AnyObject = { d: [{ id }] };
@@ -421,7 +421,7 @@ export const translateUri = <
421
421
  request.values = new Proxy(request.values, {
422
422
  set: (obj: ODataRequest['values'], prop: string, value) => {
423
423
  if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
424
- sbvrUtils.api[request.vocabulary].logger.warn(
424
+ sbvrUtils.logger[request.vocabulary].warn(
425
425
  `Assigning a new request.values property '${prop}' however it will be ignored`,
426
426
  );
427
427
  }
@@ -5,7 +5,6 @@ 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 webResourceHandler from '../webresource-handler';
9
8
  import type * as migratorUtils from '../migrator/utils';
10
9
 
11
10
  import * as sbvrUtils from '../sbvr-api/sbvr-utils';
@@ -64,7 +63,6 @@ export const init = async <T extends string>(
64
63
  await sbvrUtils.setup(app, db);
65
64
  const cfgLoader = await configLoader.setup(app);
66
65
  await cfgLoader.loadConfig(migrator.config);
67
- await cfgLoader.loadConfig(webResourceHandler.config);
68
66
 
69
67
  const promises: Array<Promise<void>> = [];
70
68
  if (process.env.SBVR_SERVER_ENABLED) {
@@ -1,10 +1,5 @@
1
1
  import type { WebResourceType as WebResource } from '@balena/sbvr-types';
2
- import type {
3
- BeginMultipartUploadHandlerResponse,
4
- IncomingFile,
5
- UploadResponse,
6
- WebResourceHandler,
7
- } from '..';
2
+ import type { IncomingFile, UploadResponse, WebResourceHandler } from '..';
8
3
 
9
4
  export class NoopHandler implements WebResourceHandler {
10
5
  public async handleFile(resource: IncomingFile): Promise<UploadResponse> {
@@ -23,12 +18,4 @@ export class NoopHandler implements WebResourceHandler {
23
18
  public async onPreRespond(webResource: WebResource): Promise<WebResource> {
24
19
  return webResource;
25
20
  }
26
-
27
- public async beginMultipartUpload(): Promise<BeginMultipartUploadHandlerResponse> {
28
- return { fileKey: 'noop', uploadId: 'noop', uploadParts: [] };
29
- }
30
-
31
- public async commitMultipartUpload(): Promise<WebResource> {
32
- return { filename: 'noop', href: 'noop' };
33
- }
34
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
+ }
@@ -1 +1,2 @@
1
1
  export * from './NoopHandler';
2
+ export * from './S3Handler';
@@ -13,8 +13,7 @@ import {
13
13
  } from '@balena/odata-to-abstract-sql';
14
14
  import { errors, permissions } from '../server-glue/module';
15
15
  import type { WebResourceType as WebResource } from '@balena/sbvr-types';
16
- import type { AnyObject } from 'pinejs-client-core';
17
- import { multipartUploadHooks } from './multipartUpload';
16
+ import { TypedError } from 'typed-error';
18
17
 
19
18
  export * from './handlers';
20
19
 
@@ -31,44 +30,19 @@ export interface UploadResponse {
31
30
  filename: string;
32
31
  }
33
32
 
34
- export interface BeginMultipartUploadPayload {
35
- filename: string;
36
- content_type: string;
37
- size: number;
38
- chunk_size: number;
39
- }
40
-
41
- export interface UploadPart {
42
- url: string;
43
- chunkSize: number;
44
- partNumber: number;
45
- }
46
-
47
- export interface BeginMultipartUploadHandlerResponse {
48
- uploadParts: UploadPart[];
49
- fileKey: string;
50
- uploadId: string;
51
- }
52
-
53
- export interface CommitMultipartUploadPayload {
54
- fileKey: string;
55
- uploadId: string;
56
- filename: string;
57
- providerCommitData?: AnyObject;
58
- }
59
-
60
33
  export interface WebResourceHandler {
61
34
  handleFile: (resource: IncomingFile) => Promise<UploadResponse>;
62
35
  removeFile: (fileReference: string) => Promise<void>;
63
36
  onPreRespond: (webResource: WebResource) => Promise<WebResource>;
37
+ }
38
+
39
+ export class WebResourceError extends TypedError {}
64
40
 
65
- beginMultipartUpload: (
66
- fieldName: string,
67
- payload: BeginMultipartUploadPayload,
68
- ) => Promise<BeginMultipartUploadHandlerResponse>;
69
- commitMultipartUpload: (
70
- commitInfo: CommitMultipartUploadPayload,
71
- ) => Promise<WebResource>;
41
+ export class FileSizeExceededError extends WebResourceError {
42
+ name = 'FileSizeExceededError';
43
+ constructor(maxSize: number) {
44
+ super(`File size exceeded the limit of ${maxSize} bytes.`);
45
+ }
72
46
  }
73
47
 
74
48
  type WebResourcesDbResponse = {
@@ -77,7 +51,7 @@ type WebResourcesDbResponse = {
77
51
 
78
52
  const getLogger = (vocab?: string): Console => {
79
53
  if (vocab) {
80
- return sbvrUtils.api[vocab]?.logger ?? console;
54
+ return sbvrUtils.logger[vocab] ?? console;
81
55
  }
82
56
  return console;
83
57
  };
@@ -219,12 +193,17 @@ export const getUploaderMiddlware = (
219
193
  next();
220
194
  } catch (err: any) {
221
195
  await clearFiles();
222
- getLogger(getApiRoot(req)).warn('Error uploading file', err);
223
- return sbvrUtils.handleHttpErrors(
224
- req,
225
- res,
226
- new errors.BadRequestError(err),
227
- );
196
+
197
+ if (err instanceof FileSizeExceededError) {
198
+ return sbvrUtils.handleHttpErrors(
199
+ req,
200
+ res,
201
+ new errors.BadRequestError(err.message),
202
+ );
203
+ }
204
+
205
+ getLogger(getApiRoot(req)).error('Error uploading file', err);
206
+ next(err);
228
207
  }
229
208
  });
230
209
 
@@ -237,7 +216,7 @@ export const getUploaderMiddlware = (
237
216
  };
238
217
  };
239
218
 
240
- export const getWebResourceFields = (
219
+ const getWebResourceFields = (
241
220
  request: uriParser.ODataRequest,
242
221
  useTranslations = true,
243
222
  ): string[] => {
@@ -270,8 +249,6 @@ const throwIfWebresourceNotInMultipart = (
270
249
  { req, request }: HookArgs,
271
250
  ) => {
272
251
  if (
273
- request.custom.isAction !== 'beginUpload' &&
274
- request.custom.isAction !== 'commitUpload' &&
275
252
  !req.is?.('multipart') &&
276
253
  webResourceFields.some((field) => request.values[field] != null)
277
254
  ) {
@@ -470,23 +447,4 @@ export const setupUploadHooks = (
470
447
  resourceName,
471
448
  getCreateWebResourceHooks(handler),
472
449
  );
473
-
474
- sbvrUtils.addPureHook(
475
- 'POST',
476
- apiRoot,
477
- resourceName,
478
- multipartUploadHooks(handler),
479
- );
480
- };
481
-
482
- // eslint-disable-next-line @typescript-eslint/no-var-requires
483
- const webresourceModel: string = require('./webresource.sbvr');
484
- export const config = {
485
- models: [
486
- {
487
- apiRoot: 'webresource',
488
- modelText: webresourceModel,
489
- modelName: 'webresource',
490
- },
491
- ] as sbvrUtils.ExecutableModel[],
492
450
  };
package/tsconfig.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "removeComments": true,
12
12
  "rootDir": "src",
13
13
  "sourceMap": true,
14
- "target": "es2021",
14
+ "target": "es2022",
15
15
  "declaration": true,
16
16
  "skipLibCheck": true,
17
17
  "resolveJsonModule": true,
@@ -1,16 +0,0 @@
1
- import type { BeginMultipartUploadPayload, UploadPart, WebResourceHandler } from '.';
2
- import type { ODataRequest } from '../sbvr-api/uri-parser';
3
- import { sbvrUtils } from '../server-glue/module';
4
- export interface PendingUpload extends BeginMultipartUploadPayload {
5
- fieldName: string;
6
- fileKey: string;
7
- uploadId: string;
8
- }
9
- export interface BeginUploadResponse {
10
- [fieldName: string]: {
11
- uuid: string;
12
- uploadParts: UploadPart[];
13
- };
14
- }
15
- export declare const multipartUploadHooks: (webResourceHandler: WebResourceHandler) => sbvrUtils.Hooks;
16
- export declare const beginUpload: (webResourceHandler: WebResourceHandler, odataRequest: ODataRequest, actorId?: number) => Promise<BeginUploadResponse>;
@@ -1,182 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.beginUpload = exports.multipartUploadHooks = void 0;
4
- const node_crypto_1 = require("node:crypto");
5
- const _1 = require(".");
6
- const sbvr_utils_1 = require("../sbvr-api/sbvr-utils");
7
- const module_1 = require("../server-glue/module");
8
- const MB = 1024 * 1024;
9
- const multipartUploadHooks = (webResourceHandler) => {
10
- return {
11
- POSTPARSE: async ({ req, request, tx, api: applicationApi }) => {
12
- if (request.odataQuery.property?.resource === 'beginUpload') {
13
- const uploadParams = await validateBeginUpload(request, applicationApi);
14
- tx = await module_1.sbvrUtils.db.transaction();
15
- req.tx = tx;
16
- request.tx = tx;
17
- request.method = 'PATCH';
18
- request.values = uploadParams;
19
- request.odataQuery.resource = request.resourceName;
20
- delete request.odataQuery.property;
21
- request.custom.isAction = 'beginUpload';
22
- }
23
- else if (request.odataQuery.property?.resource === 'commitUpload') {
24
- const commitPayload = await validateCommitUpload(request, applicationApi);
25
- const webresource = await webResourceHandler.commitMultipartUpload({
26
- fileKey: commitPayload.metadata.fileKey,
27
- uploadId: commitPayload.metadata.uploadId,
28
- filename: commitPayload.metadata.filename,
29
- providerCommitData: commitPayload.providerCommitData,
30
- });
31
- await sbvr_utils_1.api.webresource.patch({
32
- resource: 'multipart_upload',
33
- body: {
34
- status: 'completed',
35
- },
36
- options: {
37
- $filter: {
38
- uuid: commitPayload.uuid,
39
- },
40
- },
41
- passthrough: {
42
- tx: tx,
43
- },
44
- });
45
- request.method = 'PATCH';
46
- request.values = {
47
- [commitPayload.metadata.fieldName]: webresource,
48
- };
49
- request.odataQuery.resource = request.resourceName;
50
- delete request.odataQuery.property;
51
- request.custom.isAction = 'commitUpload';
52
- request.custom.commitUploadPayload = webresource;
53
- }
54
- },
55
- PRERESPOND: async ({ req, request, response, tx }) => {
56
- if (request.custom.isAction === 'beginUpload') {
57
- await tx.rollback();
58
- response.statusCode = 200;
59
- response.body = await (0, exports.beginUpload)(webResourceHandler, request, req.user?.actor);
60
- }
61
- else if (request.custom.isAction === 'commitUpload') {
62
- response.body = await webResourceHandler.onPreRespond(request.custom.commitUploadPayload);
63
- }
64
- },
65
- };
66
- };
67
- exports.multipartUploadHooks = multipartUploadHooks;
68
- const beginUpload = async (webResourceHandler, odataRequest, actorId) => {
69
- const payload = odataRequest.values;
70
- const fieldName = Object.keys(payload)[0];
71
- const metadata = payload[fieldName];
72
- const { fileKey, uploadId, uploadParts } = await webResourceHandler.beginMultipartUpload(fieldName, metadata);
73
- const uuid = (0, node_crypto_1.randomUUID)();
74
- try {
75
- await sbvr_utils_1.api.webresource.post({
76
- resource: 'multipart_upload',
77
- body: {
78
- uuid,
79
- resource_name: odataRequest.resourceName,
80
- field_name: fieldName,
81
- resource_id: odataRequest.affectedIds?.[0],
82
- upload_id: uploadId,
83
- file_key: fileKey,
84
- status: 'pending',
85
- filename: metadata.filename,
86
- content_type: metadata.content_type,
87
- size: metadata.size,
88
- chunk_size: metadata.chunk_size,
89
- expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000,
90
- is_created_by__actor: actorId,
91
- },
92
- });
93
- }
94
- catch (err) {
95
- console.error('failed to start multipart upload', err);
96
- throw new module_1.errors.BadRequestError('Failed to start multipart upload');
97
- }
98
- return { [fieldName]: { uuid, uploadParts } };
99
- };
100
- exports.beginUpload = beginUpload;
101
- const validateBeginUpload = async (request, applicationApi) => {
102
- if (request.odataQuery.key == null) {
103
- throw new module_1.errors.BadRequestError();
104
- }
105
- await applicationApi.post({
106
- url: request.url.substring(1).replace('beginUpload', 'canAccess'),
107
- body: { method: 'PATCH' },
108
- });
109
- const fieldNames = Object.keys(request.values);
110
- if (fieldNames.length !== 1) {
111
- throw new module_1.errors.BadRequestError('You can only get upload url for one field at a time');
112
- }
113
- const [fieldName] = fieldNames;
114
- const webResourceFields = (0, _1.getWebResourceFields)(request, false);
115
- if (!webResourceFields.includes(fieldName)) {
116
- throw new module_1.errors.BadRequestError(`You must provide a valid webresource field from: ${JSON.stringify(webResourceFields)}`);
117
- }
118
- const beginUploadPayload = parseBeginUploadPayload(request.values[fieldName]);
119
- if (beginUploadPayload == null) {
120
- throw new module_1.errors.BadRequestError('Invalid file metadata');
121
- }
122
- const uploadMetadataCheck = {
123
- ...beginUploadPayload,
124
- href: 'metadata_check',
125
- };
126
- return { [fieldName]: uploadMetadataCheck };
127
- };
128
- const parseBeginUploadPayload = (payload) => {
129
- if (typeof payload !== 'object') {
130
- return null;
131
- }
132
- let { filename, content_type, size, chunk_size } = payload;
133
- if (typeof filename !== 'string' ||
134
- typeof content_type !== 'string' ||
135
- typeof size !== 'number' ||
136
- (chunk_size != null && typeof chunk_size !== 'number') ||
137
- (chunk_size != null && chunk_size < 5 * MB)) {
138
- return null;
139
- }
140
- if (chunk_size == null) {
141
- chunk_size = 5 * MB;
142
- }
143
- return { filename, content_type, size, chunk_size };
144
- };
145
- const validateCommitUpload = async (request, applicationApi) => {
146
- if (request.odataQuery.key == null) {
147
- throw new module_1.errors.BadRequestError();
148
- }
149
- await applicationApi.post({
150
- url: request.url.substring(1).replace('commitUpload', 'canAccess'),
151
- body: { method: 'PATCH' },
152
- });
153
- const { uuid, providerCommitData } = request.values;
154
- if (typeof uuid !== 'string') {
155
- throw new module_1.errors.BadRequestError('Invalid uuid type');
156
- }
157
- const [multipartUpload] = (await sbvr_utils_1.api.webresource.get({
158
- resource: 'multipart_upload',
159
- options: {
160
- $select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
161
- $filter: {
162
- uuid,
163
- status: 'pending',
164
- expiry_date: { $gt: { $now: {} } },
165
- },
166
- },
167
- passthrough: {
168
- tx: request.tx,
169
- },
170
- }));
171
- if (multipartUpload == null) {
172
- throw new module_1.errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
173
- }
174
- const metadata = {
175
- fileKey: multipartUpload.file_key,
176
- uploadId: multipartUpload.upload_id,
177
- filename: multipartUpload.filename,
178
- fieldName: multipartUpload.field_name,
179
- };
180
- return { uuid, providerCommitData, metadata };
181
- };
182
- //# sourceMappingURL=multipartUpload.js.map