@balena/pinejs 21.4.0-build-large-file-uploads-2-56700d672d31db4406ba01ca349d69af5c8611e7-1 → 21.4.0-build-add-actions-example-c20002c2bf051233b333158f3b7a0719f6cf6e4e-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 (38) hide show
  1. package/.versionbot/CHANGELOG.yml +3 -3
  2. package/CHANGELOG.md +1 -1
  3. package/out/actions/index.d.ts +32 -0
  4. package/out/actions/index.js +67 -0
  5. package/out/actions/index.js.map +1 -0
  6. package/out/config-loader/config-loader.js +7 -6
  7. package/out/config-loader/config-loader.js.map +1 -1
  8. package/out/sbvr-api/sbvr-utils.d.ts +1 -1
  9. package/out/sbvr-api/sbvr-utils.js +9 -2
  10. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  11. package/out/webresource-handler/actions/beginUpload.d.ts +3 -0
  12. package/out/webresource-handler/actions/beginUpload.js +99 -0
  13. package/out/webresource-handler/actions/beginUpload.js.map +1 -0
  14. package/out/webresource-handler/actions/canceUpload.d.ts +3 -0
  15. package/out/webresource-handler/actions/canceUpload.js +59 -0
  16. package/out/webresource-handler/actions/canceUpload.js.map +1 -0
  17. package/out/webresource-handler/actions/commitUpload.d.ts +3 -0
  18. package/out/webresource-handler/actions/commitUpload.js +85 -0
  19. package/out/webresource-handler/actions/commitUpload.js.map +1 -0
  20. package/out/webresource-handler/index.d.ts +1 -1
  21. package/out/webresource-handler/index.js +11 -6
  22. package/out/webresource-handler/index.js.map +1 -1
  23. package/out/webresource-handler/multiparUpload.d.ts +4 -0
  24. package/out/webresource-handler/multiparUpload.js +14 -0
  25. package/out/webresource-handler/multiparUpload.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/actions/index.ts +142 -0
  28. package/src/config-loader/config-loader.ts +8 -8
  29. package/src/sbvr-api/sbvr-utils.ts +11 -2
  30. package/src/webresource-handler/actions/beginUpload.ts +160 -0
  31. package/src/webresource-handler/actions/canceUpload.ts +93 -0
  32. package/src/webresource-handler/actions/commitUpload.ts +116 -0
  33. package/src/webresource-handler/index.ts +16 -14
  34. package/src/webresource-handler/multiparUpload.ts +23 -0
  35. package/out/webresource-handler/multipartUpload.d.ts +0 -12
  36. package/out/webresource-handler/multipartUpload.js +0 -297
  37. package/out/webresource-handler/multipartUpload.js.map +0 -1
  38. package/src/webresource-handler/multipartUpload.ts +0 -424
@@ -0,0 +1,160 @@
1
+ import type { AnyObject } from 'pinejs-client-core';
2
+ import type {
3
+ ODataActionArgs,
4
+ ODataActionRequest,
5
+ } from '../../actions/index.js';
6
+ import { api, type Response } from '../../sbvr-api/sbvr-utils.js';
7
+ import type { MultipartUploadHandler } from '../multiparUpload.js';
8
+ import { getMultipartUploadHandler } from '../multiparUpload.js';
9
+ import type { BeginMultipartUploadPayload } from '../index.js';
10
+ import { getWebResourceFields } from '../index.js';
11
+ import { BadRequestError, NotImplementedError } from '../../sbvr-api/errors.js';
12
+ import { permissions, sbvrUtils } from '../../server-glue/module.js';
13
+ import { TransactionClosedError } from '../../database-layer/db.js';
14
+ import { randomUUID } from 'crypto';
15
+ import type { WebResourceType as WebResource } from '@balena/sbvr-types';
16
+
17
+ type FakeWebResourcePatch = {
18
+ [key: string]: Omit<WebResource, 'href'> & {
19
+ href: 'fake_patch';
20
+ };
21
+ };
22
+
23
+ export const beginUploadAction = async ({
24
+ request,
25
+ tx,
26
+ affectedIds,
27
+ req,
28
+ }: ODataActionArgs): Promise<Response> => {
29
+ const affectedId = affectedIds[0];
30
+ if (typeof affectedId !== 'number') {
31
+ throw new NotImplementedError(
32
+ 'multipart upload do not yet support non-numeric ids',
33
+ );
34
+ }
35
+
36
+ const handler = getMultipartUploadHandler();
37
+ const { fieldName, beginUploadPayload } = parseBeginUpload(request, handler);
38
+
39
+ await runFakeDbPatch(request, {
40
+ [fieldName]: { ...beginUploadPayload, href: 'fake_patch' },
41
+ });
42
+
43
+ const { fileKey, uploadId, uploadParts } =
44
+ await handler.multipartUpload.begin(fieldName, beginUploadPayload);
45
+ const uuid = randomUUID();
46
+ await api.webresource.post({
47
+ resource: 'multipart_upload',
48
+ body: {
49
+ uuid,
50
+ resource_name: request.resourceName,
51
+ field_name: fieldName,
52
+ resource_id: affectedId,
53
+ upload_id: uploadId,
54
+ file_key: fileKey,
55
+ status: 'pending',
56
+ ...beginUploadPayload,
57
+ expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days in ms
58
+ is_created_by__actor: req.user?.actor,
59
+ },
60
+ passthrough: {
61
+ req: permissions.root,
62
+ tx,
63
+ },
64
+ });
65
+
66
+ return {
67
+ body: { [fieldName]: { uuid, uploadParts } },
68
+ statusCode: 200,
69
+ };
70
+ };
71
+
72
+ const parseBeginUpload = (
73
+ request: ODataActionRequest,
74
+ webResourceHandler: MultipartUploadHandler,
75
+ ) => {
76
+ const fieldNames = Object.keys(request.values);
77
+ if (fieldNames.length !== 1) {
78
+ throw new BadRequestError(
79
+ 'You can only get upload url for one field at a time',
80
+ );
81
+ }
82
+
83
+ const [fieldName] = fieldNames;
84
+ const webResourceFields = getWebResourceFields(request, false);
85
+ if (!webResourceFields.includes(fieldName)) {
86
+ throw new BadRequestError(
87
+ `The provided field '${fieldName}' is not a valid webresource`,
88
+ );
89
+ }
90
+
91
+ const beginUploadPayload = parseBeginUploadPayload(
92
+ request.values[fieldName],
93
+ webResourceHandler,
94
+ );
95
+ if (beginUploadPayload == null) {
96
+ throw new BadRequestError('Invalid file metadata');
97
+ }
98
+
99
+ return {
100
+ beginUploadPayload,
101
+ fieldName,
102
+ };
103
+ };
104
+
105
+ const parseBeginUploadPayload = (
106
+ payload: AnyObject,
107
+ webResourceHandler: MultipartUploadHandler,
108
+ ): BeginMultipartUploadPayload | null => {
109
+ if (payload == null || typeof payload !== 'object') {
110
+ return null;
111
+ }
112
+
113
+ let { filename, content_type, size, chunk_size } = payload;
114
+ if (
115
+ typeof filename !== 'string' ||
116
+ typeof content_type !== 'string' ||
117
+ typeof size !== 'number' ||
118
+ (chunk_size != null && typeof chunk_size !== 'number') ||
119
+ (chunk_size != null &&
120
+ chunk_size < webResourceHandler.multipartUpload.getMinimumPartSize())
121
+ ) {
122
+ return null;
123
+ }
124
+
125
+ chunk_size ??= webResourceHandler.multipartUpload.getDefaultPartSize();
126
+
127
+ return { filename, content_type, size, chunk_size };
128
+ };
129
+
130
+ // TODO: comment explaining why run this fake transaction is important
131
+ const runFakeDbPatch = async (
132
+ request: ODataActionRequest,
133
+ fakeDbPatch: FakeWebResourcePatch,
134
+ ) => {
135
+ try {
136
+ await sbvrUtils.db.transaction(async (fakeTx) => {
137
+ const newUrl = request.url.slice(1).replace(/\/beginUpload$/, '');
138
+ await api[request.vocabulary].request({
139
+ method: 'PATCH',
140
+ url: newUrl,
141
+ body: fakeDbPatch,
142
+ // TODO: comment explaining why root is necessary
143
+ passthrough: { tx: fakeTx, req: permissions.root },
144
+ });
145
+
146
+ await fakeTx.rollback();
147
+ });
148
+ } catch (e) {
149
+ if (
150
+ // calling rollback() always makes it throw, so we capture and move on in case
151
+ // the error was just because the tx was (on purpose) rolledback
152
+ !(
153
+ e instanceof TransactionClosedError &&
154
+ e.message === 'Transaction has been rolled back.'
155
+ )
156
+ ) {
157
+ throw e;
158
+ }
159
+ }
160
+ };
@@ -0,0 +1,93 @@
1
+ import type {
2
+ ODataActionArgs,
3
+ ODataActionRequest,
4
+ } from '../../actions/index.js';
5
+ import type { Tx } from '../../database-layer/db.js';
6
+ import {
7
+ BadRequestError,
8
+ NotImplementedError,
9
+ UnauthorizedError,
10
+ } from '../../sbvr-api/errors.js';
11
+ import { api, type Response } from '../../sbvr-api/sbvr-utils.js';
12
+ import { permissions } from '../../server-glue/module.js';
13
+ import { getMultipartUploadHandler } from '../multiparUpload.js';
14
+
15
+ export const cancelUploadAction = async ({
16
+ request,
17
+ tx,
18
+ affectedIds,
19
+ }: ODataActionArgs): Promise<Response> => {
20
+ const affectedId = affectedIds[0];
21
+ if (typeof affectedId !== 'number') {
22
+ throw new NotImplementedError(
23
+ 'multipart upload do not yet support non-numeric ids',
24
+ );
25
+ }
26
+ const { id, fileKey, uploadId } = await getOngoingUpload(
27
+ request,
28
+ affectedId,
29
+ tx,
30
+ );
31
+ const handler = getMultipartUploadHandler();
32
+
33
+ await api.webresource.patch({
34
+ resource: 'multipart_upload',
35
+ body: {
36
+ status: 'cancelled',
37
+ },
38
+ id,
39
+ passthrough: {
40
+ tx: tx,
41
+ req: permissions.root,
42
+ },
43
+ });
44
+
45
+ // Note that different then beginUpload/commitUpload where we first do the action on the external service
46
+ // and then reflect it on the DB, for cancel upload it is the other way around
47
+ // as the worst case scenario is having a canceled upload which is marked on the DB as something else
48
+ await handler.multipartUpload.cancel({ fileKey, uploadId });
49
+
50
+ return {
51
+ statusCode: 204,
52
+ };
53
+ };
54
+
55
+ const getOngoingUpload = async (
56
+ request: ODataActionRequest,
57
+ affectedId: number,
58
+ tx: Tx,
59
+ ) => {
60
+ const { uuid } = request.values;
61
+ if (!uuid || typeof uuid !== 'string') {
62
+ throw new BadRequestError('Invalid uuid type');
63
+ }
64
+
65
+ const [multipartUpload] = await api.webresource.get({
66
+ resource: 'multipart_upload',
67
+ options: {
68
+ $select: ['id', 'file_key', 'upload_id'],
69
+ $filter: {
70
+ // TODO: swap this into the ID to see if it works
71
+ uuid,
72
+ status: 'pending',
73
+ expiry_date: { $gt: { $now: {} } },
74
+ resource_name: request.resourceName,
75
+ resource_id: affectedId,
76
+ },
77
+ },
78
+ passthrough: {
79
+ tx,
80
+ req: permissions.rootRead,
81
+ },
82
+ });
83
+
84
+ if (multipartUpload == null) {
85
+ throw new UnauthorizedError();
86
+ }
87
+
88
+ return {
89
+ id: multipartUpload.id,
90
+ fileKey: multipartUpload.file_key,
91
+ uploadId: multipartUpload.upload_id,
92
+ };
93
+ };
@@ -0,0 +1,116 @@
1
+ import type {
2
+ ODataActionArgs,
3
+ ODataActionRequest,
4
+ } from '../../actions/index.js';
5
+ import type { Tx } from '../../database-layer/db.js';
6
+ import {
7
+ BadRequestError,
8
+ NotImplementedError,
9
+ UnauthorizedError,
10
+ } from '../../sbvr-api/errors.js';
11
+ import type { Response } from '../../sbvr-api/sbvr-utils.js';
12
+ import { api } from '../../sbvr-api/sbvr-utils.js';
13
+ import { permissions } from '../../server-glue/module.js';
14
+ import { getMultipartUploadHandler } from '../multiparUpload.js';
15
+
16
+ export const commitUploadAction = async ({
17
+ request,
18
+ tx,
19
+ affectedIds,
20
+ api: applicationApi,
21
+ }: ODataActionArgs): Promise<Response> => {
22
+ const affectedId = affectedIds[0];
23
+ if (typeof affectedId !== 'number') {
24
+ throw new NotImplementedError(
25
+ 'multipart upload do not yet support non-numeric ids',
26
+ );
27
+ }
28
+
29
+ const multipartUpload = await getOngoingUpload(request, affectedId, tx);
30
+ const handler = getMultipartUploadHandler();
31
+
32
+ const webresource = await handler.multipartUpload.commit({
33
+ fileKey: multipartUpload.fileKey,
34
+ uploadId: multipartUpload.uploadId,
35
+ filename: multipartUpload.filename,
36
+ providerCommitData: multipartUpload.providerCommitData,
37
+ });
38
+
39
+ await Promise.all([
40
+ api.webresource.patch({
41
+ resource: 'multipart_upload',
42
+ body: {
43
+ status: 'completed',
44
+ },
45
+ options: {
46
+ $filter: {
47
+ uuid: multipartUpload.uuid,
48
+ },
49
+ },
50
+ passthrough: {
51
+ tx: tx,
52
+ req: permissions.root,
53
+ },
54
+ }),
55
+ applicationApi.patch({
56
+ resource: request.resourceName,
57
+ id: affectedId,
58
+ body: {
59
+ [multipartUpload.fieldName]: webresource,
60
+ },
61
+ passthrough: {
62
+ tx: tx,
63
+ // Root is needed as, if you are not root, you are not allowed to directly modify the actual metadata
64
+ req: permissions.root,
65
+ },
66
+ }),
67
+ ]);
68
+
69
+ return {
70
+ body: webresource,
71
+ statusCode: 200,
72
+ };
73
+ };
74
+
75
+ const getOngoingUpload = async (
76
+ request: ODataActionRequest,
77
+ affectedId: number,
78
+ tx: Tx,
79
+ ) => {
80
+ const { uuid, providerCommitData } = request.values;
81
+ if (!uuid || typeof uuid !== 'string') {
82
+ throw new BadRequestError('Invalid uuid type');
83
+ }
84
+
85
+ const [multipartUpload] = await api.webresource.get({
86
+ resource: 'multipart_upload',
87
+ options: {
88
+ $select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
89
+ $filter: {
90
+ // TODO: swap this into the ID to see if it works
91
+ uuid,
92
+ status: 'pending',
93
+ expiry_date: { $gt: { $now: {} } },
94
+ resource_name: request.resourceName,
95
+ resource_id: affectedId,
96
+ },
97
+ },
98
+ passthrough: {
99
+ tx,
100
+ req: permissions.rootRead,
101
+ },
102
+ });
103
+
104
+ if (multipartUpload == null) {
105
+ throw new UnauthorizedError();
106
+ }
107
+
108
+ return {
109
+ uuid,
110
+ providerCommitData,
111
+ fileKey: multipartUpload.file_key,
112
+ uploadId: multipartUpload.upload_id,
113
+ filename: multipartUpload.filename,
114
+ fieldName: multipartUpload.field_name,
115
+ };
116
+ };
@@ -18,13 +18,13 @@ import { TypedError } from 'typed-error';
18
18
  import type { Resolvable } from '../sbvr-api/common-types.js';
19
19
  import type WebresourceModel from './webresource.js';
20
20
  import { importSBVR } from '../server-glue/sbvr-loader.js';
21
- import {
22
- isMultipartUploadAvailable,
23
- multipartUploadHooks,
24
- } from './multipartUpload.js';
21
+ import { addAction } from '../actions/index.js';
22
+ import { beginUploadAction } from './actions/beginUpload.js';
23
+ import { isMultipartUploadAvailable } from './multiparUpload.js';
24
+ import { commitUploadAction } from './actions/commitUpload.js';
25
+ import { cancelUploadAction } from './actions/canceUpload.js';
25
26
 
26
27
  export * from './handlers/index.js';
27
- export type { BeginUploadResponse } from './multipartUpload.js';
28
28
 
29
29
  export interface IncomingFile {
30
30
  fieldname: string;
@@ -337,9 +337,6 @@ const throwIfWebresourceNotInMultipart = (
337
337
  { req, request }: HookArgs,
338
338
  ) => {
339
339
  if (
340
- request.custom.isAction !== 'beginUpload' &&
341
- request.custom.isAction !== 'commitUpload' &&
342
- request.custom.isAction !== 'cancelUpload' &&
343
340
  req.user !== permissions.root.user &&
344
341
  !req.is?.('multipart') &&
345
342
  webResourceFields.some((field) => request.values[field] != null)
@@ -537,14 +534,19 @@ export const setupUploadHooks = (
537
534
  resourceName,
538
535
  getCreateWebResourceHooks(handler),
539
536
  );
537
+ };
540
538
 
539
+ export const setupUploadActions = (
540
+ handler: WebResourceHandler,
541
+ apiRoot: string,
542
+ resourceName: string,
543
+ ) => {
541
544
  if (isMultipartUploadAvailable(handler)) {
542
- sbvrUtils.addPureHook(
543
- 'POST',
544
- apiRoot,
545
- resourceName,
546
- multipartUploadHooks(handler),
547
- );
545
+ addAction(apiRoot, resourceName, 'beginUpload', beginUploadAction);
546
+
547
+ addAction(apiRoot, resourceName, 'commitUpload', commitUploadAction);
548
+
549
+ addAction(apiRoot, resourceName, 'cancelUpload', cancelUploadAction);
548
550
  }
549
551
  };
550
552
 
@@ -0,0 +1,23 @@
1
+ import { webResource as webResourceEnv } from '../config-loader/env.js';
2
+ import { NotImplementedError } from '../sbvr-api/errors.js';
3
+ import type { WebResourceHandler } from './index.js';
4
+ import { getWebresourceHandler } from './index.js';
5
+
6
+ export type MultipartUploadHandler = WebResourceHandler &
7
+ Required<Pick<WebResourceHandler, 'multipartUpload'>>;
8
+
9
+ export const isMultipartUploadAvailable = (
10
+ handler: WebResourceHandler | undefined,
11
+ ): handler is MultipartUploadHandler => {
12
+ return (
13
+ webResourceEnv.multipartUploadEnabled && handler?.multipartUpload != null
14
+ );
15
+ };
16
+
17
+ export const getMultipartUploadHandler = () => {
18
+ const handler = getWebresourceHandler();
19
+ if (!isMultipartUploadAvailable(handler)) {
20
+ throw new NotImplementedError('Multipart uploads not available');
21
+ }
22
+ return handler;
23
+ };
@@ -1,12 +0,0 @@
1
- import type { UploadPart, WebResourceHandler } from './index.js';
2
- import { sbvrUtils } from '../server-glue/module.js';
3
- export interface BeginUploadResponse {
4
- [fieldName: string]: {
5
- uuid: string;
6
- uploadParts: UploadPart[];
7
- };
8
- }
9
- type MultipartUploadHandler = WebResourceHandler & Required<Pick<WebResourceHandler, 'multipartUpload'>>;
10
- export declare const isMultipartUploadAvailable: (webResourceHandler: WebResourceHandler) => webResourceHandler is MultipartUploadHandler;
11
- export declare const multipartUploadHooks: (webResourceHandler: MultipartUploadHandler) => sbvrUtils.Hooks;
12
- export {};