@balena/pinejs 17.0.0-build-v17-a23172f0d3c5aa4169ebb0f29d4939508c9074cf-2 → 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 (57) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +2297 -11
  3. package/CHANGELOG.md +860 -4
  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/webresource-handler/handlers/NoopHandler.d.ts +1 -3
  32. package/out/webresource-handler/handlers/NoopHandler.js +0 -6
  33. package/out/webresource-handler/handlers/NoopHandler.js.map +1 -1
  34. package/out/webresource-handler/handlers/S3Handler.d.ts +28 -0
  35. package/out/webresource-handler/handlers/S3Handler.js +104 -0
  36. package/out/webresource-handler/handlers/S3Handler.js.map +1 -0
  37. package/out/webresource-handler/handlers/index.d.ts +1 -0
  38. package/out/webresource-handler/handlers/index.js +1 -0
  39. package/out/webresource-handler/handlers/index.js.map +1 -1
  40. package/out/webresource-handler/index.d.ts +7 -25
  41. package/out/webresource-handler/index.js +18 -4
  42. package/out/webresource-handler/index.js.map +1 -1
  43. package/package.json +40 -39
  44. package/src/bin/abstract-sql-compiler.ts +1 -1
  45. package/src/bin/sbvr-compiler.ts +1 -1
  46. package/src/http-transactions/transactions.js +2 -2
  47. package/src/migrator/async.ts +10 -11
  48. package/src/migrator/sync.ts +6 -6
  49. package/src/sbvr-api/abstract-sql.ts +1 -1
  50. package/src/sbvr-api/permissions.ts +3 -3
  51. package/src/sbvr-api/sbvr-utils.ts +21 -30
  52. package/src/sbvr-api/uri-parser.ts +1 -1
  53. package/src/webresource-handler/handlers/NoopHandler.ts +1 -14
  54. package/src/webresource-handler/handlers/S3Handler.ts +143 -0
  55. package/src/webresource-handler/handlers/index.ts +1 -0
  56. package/src/webresource-handler/index.ts +21 -41
  57. package/tsconfig.json +1 -1
@@ -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,7 +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';
16
+ import { TypedError } from 'typed-error';
17
17
 
18
18
  export * from './handlers';
19
19
 
@@ -30,44 +30,19 @@ export interface UploadResponse {
30
30
  filename: string;
31
31
  }
32
32
 
33
- export interface BeginMultipartUploadPayload {
34
- filename: string;
35
- content_type: string;
36
- size: number;
37
- chunk_size: number;
38
- }
39
-
40
- export interface UploadPart {
41
- url: string;
42
- chunkSize: number;
43
- partNumber: number;
44
- }
45
-
46
- export interface BeginMultipartUploadHandlerResponse {
47
- uploadParts: UploadPart[];
48
- fileKey: string;
49
- uploadId: string;
50
- }
51
-
52
- export interface CommitMultipartUploadPayload {
53
- fileKey: string;
54
- uploadId: string;
55
- filename: string;
56
- providerCommitData?: AnyObject;
57
- }
58
-
59
33
  export interface WebResourceHandler {
60
34
  handleFile: (resource: IncomingFile) => Promise<UploadResponse>;
61
35
  removeFile: (fileReference: string) => Promise<void>;
62
36
  onPreRespond: (webResource: WebResource) => Promise<WebResource>;
37
+ }
38
+
39
+ export class WebResourceError extends TypedError {}
63
40
 
64
- beginMultipartUpload: (
65
- fieldName: string,
66
- payload: BeginMultipartUploadPayload,
67
- ) => Promise<BeginMultipartUploadHandlerResponse>;
68
- commitMultipartUpload: (
69
- commitInfo: CommitMultipartUploadPayload,
70
- ) => 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
+ }
71
46
  }
72
47
 
73
48
  type WebResourcesDbResponse = {
@@ -76,7 +51,7 @@ type WebResourcesDbResponse = {
76
51
 
77
52
  const getLogger = (vocab?: string): Console => {
78
53
  if (vocab) {
79
- return sbvrUtils.api[vocab]?.logger ?? console;
54
+ return sbvrUtils.logger[vocab] ?? console;
80
55
  }
81
56
  return console;
82
57
  };
@@ -218,12 +193,17 @@ export const getUploaderMiddlware = (
218
193
  next();
219
194
  } catch (err: any) {
220
195
  await clearFiles();
221
- getLogger(getApiRoot(req)).warn('Error uploading file', err);
222
- return sbvrUtils.handleHttpErrors(
223
- req,
224
- res,
225
- new errors.BadRequestError(err),
226
- );
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);
227
207
  }
228
208
  });
229
209
 
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,