@balena/pinejs 20.1.0-build-add-large-file-uploads-interfaces-1aa101d2ee3103f42cf7f7bc29d703a8958011e8-1 → 20.1.0-build-large-file-uploads-2-b83400458570d926f7a6abddeb7a149e89f52abe-2

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.
@@ -11,9 +11,16 @@ import {
11
11
  odataNameToSqlName,
12
12
  sqlNameToODataName,
13
13
  } from '@balena/odata-to-abstract-sql';
14
+ import type { ConfigLoader } from '../server-glue/module.js';
14
15
  import { errors, permissions } from '../server-glue/module.js';
15
16
  import type { WebResourceType as WebResource } from '@balena/sbvr-types';
16
17
  import { TypedError } from 'typed-error';
18
+ import type WebresourceModel from './webresource.js';
19
+ import { importSBVR } from '../server-glue/sbvr-loader.js';
20
+ import {
21
+ isMultipartUploadAvailable,
22
+ multipartUploadHooks,
23
+ } from './multipartUpload.js';
17
24
 
18
25
  export * from './handlers/index.js';
19
26
 
@@ -102,30 +109,27 @@ export const setupWebresourceHandler = (handler: WebResourceHandler): void => {
102
109
  export const getWebresourceHandler = (): WebResourceHandler | undefined => {
103
110
  return configuredWebResourceHandler;
104
111
  };
112
+ const notValidUpload = () => Promise.resolve(false);
105
113
 
106
- const isFileInValidPath = async (
107
- fieldname: string,
114
+ const getRequestUploadValidator = async (
108
115
  req: Express.Request,
109
- ): Promise<boolean> => {
116
+ odataRequest: uriParser.ParsedODataRequest,
117
+ ): Promise<(fieldName: string) => Promise<boolean>> => {
110
118
  if (req.method !== 'POST' && req.method !== 'PATCH') {
111
- return false;
119
+ return notValidUpload;
112
120
  }
113
121
 
114
122
  const apiRoot = getApiRoot(req);
115
123
  if (apiRoot == null) {
116
- return false;
124
+ return notValidUpload;
117
125
  }
118
126
  const model = getModel(apiRoot);
119
- const odataRequest = uriParser.parseOData({
120
- url: req.url,
121
- method: req.method,
122
- });
123
127
  const sqlResourceName = sbvrUtils.resolveSynonym(odataRequest);
124
128
 
125
129
  const table = model.abstractSql.tables[sqlResourceName];
126
130
 
127
131
  if (table == null) {
128
- return false;
132
+ return notValidUpload;
129
133
  }
130
134
 
131
135
  const permission = req.method === 'POST' ? 'create' : 'update';
@@ -140,20 +144,22 @@ const isFileInValidPath = async (
140
144
  );
141
145
 
142
146
  if (!hasPermissions) {
143
- return false;
147
+ return notValidUpload;
144
148
  }
145
149
 
146
- const dbFieldName = odataNameToSqlName(fieldname);
147
- return table.fields.some(
148
- (field) =>
149
- field.fieldName === dbFieldName && field.dataType === 'WebResource',
150
- );
150
+ return async (fieldname: string) => {
151
+ const dbFieldName = odataNameToSqlName(fieldname);
152
+ return table.fields.some(
153
+ (field) =>
154
+ field.fieldName === dbFieldName && field.dataType === 'WebResource',
155
+ );
156
+ };
151
157
  };
152
158
 
153
159
  export const getUploaderMiddlware = (
154
160
  handler: WebResourceHandler,
155
161
  ): Express.RequestHandler => {
156
- return (req, res, next) => {
162
+ return async (req, res, next) => {
157
163
  if (!req.is('multipart')) {
158
164
  next();
159
165
  return;
@@ -164,6 +170,20 @@ export const getUploaderMiddlware = (
164
170
  const bb = busboy({ headers: req.headers });
165
171
  let isAborting = false;
166
172
 
173
+ const parsedOdataRequest = uriParser.parseOData({
174
+ url: req.url,
175
+ method: req.method,
176
+ });
177
+ const webResourcesFieldNames = getWebResourceFields(
178
+ parsedOdataRequest,
179
+ false,
180
+ );
181
+
182
+ const isValidUpload = await getRequestUploadValidator(
183
+ req,
184
+ parsedOdataRequest,
185
+ );
186
+
167
187
  const finishFileUpload = () => {
168
188
  req.unpipe(bb);
169
189
  req.on('readable', req.read.bind(req));
@@ -191,7 +211,7 @@ export const getUploaderMiddlware = (
191
211
  completeUploads.push(
192
212
  (async () => {
193
213
  try {
194
- if (!(await isFileInValidPath(fieldname, req))) {
214
+ if (!(await isValidUpload(fieldname))) {
195
215
  filestream.resume();
196
216
  return;
197
217
  }
@@ -213,7 +233,10 @@ export const getUploaderMiddlware = (
213
233
  uploadedFilePaths.push(result.filename);
214
234
  } catch (err: any) {
215
235
  filestream.resume();
216
- throw err;
236
+ bb.emit(
237
+ 'error',
238
+ new errors.BadRequestError(err.message ?? 'Error uploading file'),
239
+ );
217
240
  }
218
241
  })(),
219
242
  );
@@ -223,6 +246,14 @@ export const getUploaderMiddlware = (
223
246
  // This receives the form fields and transforms them into a standard JSON body
224
247
  // This is a similar behavior as previous multer library did
225
248
  bb.on('field', (name, val) => {
249
+ if (webResourcesFieldNames.includes(name)) {
250
+ isAborting = true;
251
+ bb.emit(
252
+ 'error',
253
+ new errors.BadRequestError('WebResource field must be a blob.'),
254
+ );
255
+ return;
256
+ }
226
257
  req.body[name] = val;
227
258
  });
228
259
 
@@ -248,7 +279,7 @@ export const getUploaderMiddlware = (
248
279
  }
249
280
  });
250
281
 
251
- bb.on('error', async (err) => {
282
+ bb.on('error', async (err: Error) => {
252
283
  finishFileUpload();
253
284
  await clearFiles();
254
285
 
@@ -260,15 +291,17 @@ export const getUploaderMiddlware = (
260
291
  );
261
292
  }
262
293
 
263
- getLogger(getApiRoot(req)).error('Error uploading file', err);
264
- next(err);
294
+ if (!sbvrUtils.handleHttpErrors(req, res, err)) {
295
+ getLogger(getApiRoot(req)).error('Error uploading file', err);
296
+ next(err);
297
+ }
265
298
  });
266
299
  req.pipe(bb);
267
300
  };
268
301
  };
269
302
 
270
- const getWebResourceFields = (
271
- request: uriParser.ODataRequest,
303
+ export const getWebResourceFields = (
304
+ request: uriParser.ParsedODataRequest,
272
305
  useTranslations = true,
273
306
  ): string[] => {
274
307
  // Translations will use modifyFields(translated) rather than fields(original) so we need to
@@ -300,6 +333,9 @@ const throwIfWebresourceNotInMultipart = (
300
333
  { req, request }: HookArgs,
301
334
  ) => {
302
335
  if (
336
+ request.custom.isAction !== 'beginUpload' &&
337
+ request.custom.isAction !== 'commitUpload' &&
338
+ request.custom.isAction !== 'cancelUpload' &&
303
339
  !req.is?.('multipart') &&
304
340
  webResourceFields.some((field) => request.values[field] != null)
305
341
  ) {
@@ -496,4 +532,37 @@ export const setupUploadHooks = (
496
532
  resourceName,
497
533
  getCreateWebResourceHooks(handler),
498
534
  );
535
+
536
+ if (isMultipartUploadAvailable(handler)) {
537
+ sbvrUtils.addPureHook(
538
+ 'POST',
539
+ apiRoot,
540
+ resourceName,
541
+ multipartUploadHooks(handler),
542
+ );
543
+ }
544
+ };
545
+
546
+ const initSql = `
547
+ CREATE INDEX IF NOT EXISTS idx_multipart_upload_uuid ON "multipart upload" (uuid);
548
+ CREATE INDEX IF NOT EXISTS idx_multipart_upload_status ON "multipart upload" (status);
549
+ `;
550
+
551
+ const modelText = await importSBVR('./webresource.sbvr', import.meta);
552
+
553
+ declare module '../sbvr-api/sbvr-utils.js' {
554
+ export interface API {
555
+ webresource: PinejsClient<WebresourceModel>;
556
+ }
557
+ }
558
+
559
+ export const config: ConfigLoader.Config = {
560
+ models: [
561
+ {
562
+ modelName: 'webresource',
563
+ apiRoot: 'webresource',
564
+ modelText,
565
+ initSql,
566
+ },
567
+ ],
499
568
  };
@@ -0,0 +1,371 @@
1
+ import type { WebResourceType as WebResource } from '@balena/sbvr-types';
2
+ import { randomUUID } from 'node:crypto';
3
+ import type { AnyObject } from 'pinejs-client-core';
4
+ import type {
5
+ BeginMultipartUploadPayload,
6
+ UploadPart,
7
+ WebResourceHandler,
8
+ } from './index.js';
9
+ import { getWebResourceFields } from './index.js';
10
+ import type { PinejsClient } from '../sbvr-api/sbvr-utils.js';
11
+ import { api } from '../sbvr-api/sbvr-utils.js';
12
+ import type { ODataRequest } from '../sbvr-api/uri-parser.js';
13
+ import { errors, sbvrUtils } from '../server-glue/module.js';
14
+ import { webResource as webResourceEnv } from '../config-loader/env.js';
15
+ import * as permissions from '../sbvr-api/permissions.js';
16
+
17
+ type BeginUploadDbCheck = BeginMultipartUploadPayload & WebResource;
18
+
19
+ export interface PendingUpload extends BeginMultipartUploadPayload {
20
+ fieldName: string;
21
+ fileKey: string;
22
+ uploadId: string;
23
+ }
24
+
25
+ export interface BeginUploadResponse {
26
+ [fieldName: string]: {
27
+ uuid: string;
28
+ uploadParts: UploadPart[];
29
+ };
30
+ }
31
+
32
+ // Is WebResourceHandler but with beginMultipartUpload and commitMultipartUpload as not optional
33
+ type MultipartUploadHandler = WebResourceHandler &
34
+ Required<Pick<WebResourceHandler, 'multipartUpload'>>;
35
+
36
+ const MB = 1024 * 1024;
37
+
38
+ export const isMultipartUploadAvailable = (
39
+ webResourceHandler: WebResourceHandler,
40
+ ): webResourceHandler is MultipartUploadHandler => {
41
+ return (
42
+ webResourceEnv.multipartUploadEnabled &&
43
+ webResourceHandler.multipartUpload != null
44
+ );
45
+ };
46
+
47
+ export const multipartUploadHooks = (
48
+ webResourceHandler: MultipartUploadHandler,
49
+ ): sbvrUtils.Hooks => {
50
+ return {
51
+ POSTPARSE: async ({ req, request, tx, api: applicationApi }) => {
52
+ if (request.odataQuery.property?.resource === 'beginUpload') {
53
+ const uploadParams = await validateBeginUpload(request, applicationApi);
54
+
55
+ // This transaction is necessary because beginUpload requests
56
+ // will rollback the transaction (in order to first validate)
57
+ // The metadata requested. If we don't pass any transaction
58
+ // It will use the default transaction handler which will error out
59
+ // on any rollback.
60
+ tx = await sbvrUtils.db.transaction();
61
+ req.tx = tx;
62
+ request.tx = tx;
63
+
64
+ request.method = 'PATCH';
65
+ request.values = uploadParams;
66
+ request.odataQuery.resource = request.resourceName;
67
+ delete request.odataQuery.property;
68
+ request.custom.isAction = 'beginUpload';
69
+ } else if (request.odataQuery.property?.resource === 'commitUpload') {
70
+ const commitPayload = await validateCommitUpload(
71
+ request,
72
+ applicationApi,
73
+ );
74
+
75
+ const webresource = await webResourceHandler.multipartUpload.commit({
76
+ fileKey: commitPayload.metadata.fileKey,
77
+ uploadId: commitPayload.metadata.uploadId,
78
+ filename: commitPayload.metadata.filename,
79
+ providerCommitData: commitPayload.providerCommitData,
80
+ });
81
+
82
+ await api.webresource.patch({
83
+ resource: 'multipart_upload',
84
+ body: {
85
+ status: 'completed',
86
+ },
87
+ options: {
88
+ $filter: {
89
+ uuid: commitPayload.uuid,
90
+ },
91
+ },
92
+ passthrough: {
93
+ tx: tx,
94
+ req: permissions.root,
95
+ },
96
+ });
97
+
98
+ request.method = 'PATCH';
99
+ request.values = {
100
+ [commitPayload.metadata.fieldName]: webresource,
101
+ };
102
+ request.odataQuery.resource = request.resourceName;
103
+ delete request.odataQuery.property;
104
+ request.custom.isAction = 'commitUpload';
105
+ request.custom.commitUploadPayload = webresource;
106
+ } else if (request.odataQuery.property?.resource === 'cancelUpload') {
107
+ const { uuid, fileKey, uploadId } = await validateCancelPayload(
108
+ request,
109
+ applicationApi,
110
+ );
111
+
112
+ await webResourceHandler.multipartUpload.cancel({ fileKey, uploadId });
113
+
114
+ await api.webresource.patch({
115
+ resource: 'multipart_upload',
116
+ body: {
117
+ status: 'cancelled',
118
+ },
119
+ options: {
120
+ $filter: { uuid },
121
+ },
122
+ passthrough: {
123
+ tx: tx,
124
+ req: permissions.root,
125
+ },
126
+ });
127
+
128
+ request.method = 'GET';
129
+ request.odataQuery.resource = request.resourceName;
130
+ delete request.odataQuery.property;
131
+ request.custom.isAction = 'cancelUpload';
132
+ }
133
+ },
134
+ PRERESPOND: async ({ req, request, response, tx }) => {
135
+ if (request.custom.isAction === 'beginUpload') {
136
+ // In the case where the transaction has failed because it had invalid payload
137
+ // such as breaking a db constraint, this hook wouldn't have been called
138
+ // and would rather throw with the rule it failed to validate
139
+ // We rollback here as the patch was just a way to validate the upload payload
140
+ await tx.rollback();
141
+
142
+ response.statusCode = 200;
143
+ response.body = await beginUpload({
144
+ webResourceHandler,
145
+ odataRequest: request,
146
+ actorId: req.user?.actor,
147
+ });
148
+ } else if (request.custom.isAction === 'commitUpload') {
149
+ response.body = await webResourceHandler.onPreRespond(
150
+ request.custom.commitUploadPayload,
151
+ );
152
+ } else if (request.custom.isAction === 'cancelUpload') {
153
+ response.statusCode = 204;
154
+ delete response.body;
155
+ }
156
+ },
157
+ };
158
+ };
159
+
160
+ const beginUpload = async ({
161
+ webResourceHandler,
162
+ odataRequest,
163
+ actorId,
164
+ }: {
165
+ webResourceHandler: MultipartUploadHandler;
166
+ odataRequest: ODataRequest;
167
+ actorId?: number;
168
+ }): Promise<BeginUploadResponse> => {
169
+ const payload = odataRequest.values as {
170
+ [x: string]: BeginMultipartUploadPayload;
171
+ };
172
+ const fieldName = Object.keys(payload)[0];
173
+ const metadata = payload[fieldName];
174
+ const { fileKey, uploadId, uploadParts } =
175
+ await webResourceHandler.multipartUpload.begin(fieldName, metadata);
176
+ const uuid = randomUUID();
177
+
178
+ return await sbvrUtils.db.transaction(async (tx) => {
179
+ try {
180
+ await api.webresource.post({
181
+ resource: 'multipart_upload',
182
+ body: {
183
+ uuid,
184
+ resource_name: odataRequest.resourceName,
185
+ field_name: fieldName,
186
+ resource_id: odataRequest.affectedIds?.[0],
187
+ upload_id: uploadId,
188
+ file_key: fileKey,
189
+ status: 'pending',
190
+ filename: metadata.filename,
191
+ content_type: metadata.content_type,
192
+ size: metadata.size,
193
+ chunk_size: metadata.chunk_size,
194
+ expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days in ms
195
+ is_created_by__actor: actorId,
196
+ },
197
+ passthrough: {
198
+ req: permissions.root,
199
+ tx,
200
+ },
201
+ });
202
+ return { [fieldName]: { uuid, uploadParts } };
203
+ } catch (err) {
204
+ console.error('failed to start multipart upload', err);
205
+ throw new errors.BadRequestError('Failed to start multipart upload');
206
+ }
207
+ });
208
+ };
209
+
210
+ const validateBeginUpload = async (
211
+ request: ODataRequest,
212
+ applicationApi: PinejsClient,
213
+ ) => {
214
+ if (request.odataQuery.key == null) {
215
+ throw new errors.BadRequestError();
216
+ }
217
+
218
+ await applicationApi.request({
219
+ method: 'POST',
220
+ url: request.url.substring(1).replace('beginUpload', 'canAccess'),
221
+ body: { method: 'PATCH' },
222
+ });
223
+
224
+ const fieldNames = Object.keys(request.values);
225
+ if (fieldNames.length !== 1) {
226
+ throw new errors.BadRequestError(
227
+ 'You can only get upload url for one field at a time',
228
+ );
229
+ }
230
+
231
+ const [fieldName] = fieldNames;
232
+ const webResourceFields = getWebResourceFields(request, false);
233
+ if (!webResourceFields.includes(fieldName)) {
234
+ throw new errors.BadRequestError(
235
+ `You must provide a valid webresource field from: ${JSON.stringify(webResourceFields)}`,
236
+ );
237
+ }
238
+
239
+ const beginUploadPayload = parseBeginUploadPayload(request.values[fieldName]);
240
+ if (beginUploadPayload == null) {
241
+ throw new errors.BadRequestError('Invalid file metadata');
242
+ }
243
+
244
+ const uploadMetadataCheck: BeginUploadDbCheck = {
245
+ ...beginUploadPayload,
246
+ // This is "probe" request. We don't actually store anything on the application table yet
247
+ // We just avoid creating the application record if it would fail anyway for some other db constraint
248
+ href: 'metadata_check_probe',
249
+ };
250
+
251
+ return { [fieldName]: uploadMetadataCheck };
252
+ };
253
+
254
+ const parseBeginUploadPayload = (
255
+ payload: AnyObject,
256
+ ): BeginMultipartUploadPayload | null => {
257
+ if (typeof payload !== 'object') {
258
+ return null;
259
+ }
260
+
261
+ let { filename, content_type, size, chunk_size } = payload;
262
+ if (
263
+ typeof filename !== 'string' ||
264
+ typeof content_type !== 'string' ||
265
+ typeof size !== 'number' ||
266
+ (chunk_size != null && typeof chunk_size !== 'number') ||
267
+ (chunk_size != null && chunk_size < 5 * MB)
268
+ ) {
269
+ return null;
270
+ }
271
+
272
+ if (chunk_size == null) {
273
+ chunk_size = 5 * MB;
274
+ }
275
+ return { filename, content_type, size, chunk_size };
276
+ };
277
+
278
+ const validateCommitUpload = async (
279
+ request: ODataRequest,
280
+ applicationApi: PinejsClient,
281
+ ) => {
282
+ if (request.odataQuery.key == null) {
283
+ throw new errors.BadRequestError();
284
+ }
285
+
286
+ await applicationApi.request({
287
+ method: 'POST',
288
+ url: request.url.substring(1).replace('commitUpload', 'canAccess'),
289
+ body: { method: 'PATCH' },
290
+ });
291
+
292
+ const { uuid, providerCommitData } = request.values;
293
+ if (typeof uuid !== 'string') {
294
+ throw new errors.BadRequestError('Invalid uuid type');
295
+ }
296
+
297
+ const [multipartUpload] = await api.webresource.get({
298
+ resource: 'multipart_upload',
299
+ options: {
300
+ $select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
301
+ $filter: {
302
+ uuid,
303
+ status: 'pending',
304
+ expiry_date: { $gt: { $now: {} } },
305
+ },
306
+ },
307
+ passthrough: {
308
+ tx: request.tx,
309
+ req: permissions.rootRead,
310
+ },
311
+ });
312
+
313
+ if (multipartUpload == null) {
314
+ throw new errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
315
+ }
316
+
317
+ const metadata = {
318
+ fileKey: multipartUpload.file_key,
319
+ uploadId: multipartUpload.upload_id,
320
+ filename: multipartUpload.filename,
321
+ fieldName: multipartUpload.field_name,
322
+ };
323
+
324
+ return { uuid, providerCommitData, metadata };
325
+ };
326
+
327
+ const validateCancelPayload = async (
328
+ request: ODataRequest,
329
+ applicationApi: PinejsClient,
330
+ ) => {
331
+ if (request.odataQuery.key == null) {
332
+ throw new errors.BadRequestError();
333
+ }
334
+
335
+ await applicationApi.request({
336
+ method: 'POST',
337
+ url: request.url.substring(1).replace('cancelUpload', 'canAccess'),
338
+ body: { method: 'PATCH' },
339
+ });
340
+
341
+ const { uuid } = request.values;
342
+ if (typeof uuid !== 'string') {
343
+ throw new errors.BadRequestError('Invalid uuid type');
344
+ }
345
+
346
+ const [multipartUpload] = await api.webresource.get({
347
+ resource: 'multipart_upload',
348
+ options: {
349
+ $select: ['id', 'file_key', 'upload_id'],
350
+ $filter: {
351
+ uuid,
352
+ status: 'pending',
353
+ expiry_date: { $gt: { $now: {} } },
354
+ },
355
+ },
356
+ passthrough: {
357
+ tx: request.tx,
358
+ req: permissions.rootRead,
359
+ },
360
+ });
361
+
362
+ if (multipartUpload == null) {
363
+ throw new errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
364
+ }
365
+
366
+ return {
367
+ uuid,
368
+ fileKey: multipartUpload.file_key,
369
+ uploadId: multipartUpload.upload_id,
370
+ };
371
+ };
@@ -0,0 +1,60 @@
1
+ Vocabulary: webresource
2
+
3
+ Term: actor
4
+ Concept Type: Integer (Type)
5
+ Term: expiry date
6
+ Concept Type: Date Time (Type)
7
+ Term: uuid
8
+ Concept Type: Short Text (Type)
9
+ Term: resource name
10
+ Concept Type: Short Text (Type)
11
+ Term: field name
12
+ Concept Type: Short Text (Type)
13
+ Term: resource id
14
+ Concept Type: Integer (Type)
15
+ Term: upload id
16
+ Concept Type: Short Text (Type)
17
+ Term: file key
18
+ Concept Type: Short Text (Type)
19
+ Term: status
20
+ Concept Type: Short Text (Type)
21
+ Term: filename
22
+ Concept Type: Short Text (Type)
23
+ Term: content type
24
+ Concept Type: Short Text (Type)
25
+ Term: size
26
+ Concept Type: Integer (Type)
27
+ Term: chunk size
28
+ Concept Type: Integer (Type)
29
+ Term: valid until date
30
+ Concept Type: Date Time (Type)
31
+
32
+ Term: multipart upload
33
+ Fact type: multipart upload has uuid
34
+ Necessity: each multipart upload has exactly one uuid
35
+ Necessity: each uuid is of exactly one multipart upload
36
+ Fact type: multipart upload has resource name
37
+ Necessity: each multipart upload has exactly one resource name
38
+ Fact type: multipart upload has field name
39
+ Necessity: each multipart upload has exactly one field name
40
+ Fact type: multipart upload has resource id
41
+ Necessity: each multipart upload has exactly one resource id
42
+ Fact type: multipart upload has upload id
43
+ Necessity: each multipart upload has exactly one upload id
44
+ Fact type: multipart upload has file key
45
+ Necessity: each multipart upload has exactly one file key
46
+ Fact type: multipart upload has status
47
+ Necessity: each multipart upload has exactly one status
48
+ Definition: "pending" or "completed" or "cancelled"
49
+ Fact type: multipart upload has filename
50
+ Necessity: each multipart upload has exactly one filename
51
+ Fact type: multipart upload has content type
52
+ Necessity: each multipart upload has exactly one content type
53
+ Fact type: multipart upload has size
54
+ Necessity: each multipart upload has exactly one size
55
+ Fact type: multipart upload has chunk size
56
+ Necessity: each multipart upload has exactly one chunk size
57
+ Fact type: multipart upload has expiry date
58
+ Necessity: each multipart upload has exactly one expiry date
59
+ Fact type: multipart upload is created by actor
60
+ Necessity: each multipart upload is created by at most one actor
@@ -0,0 +1,48 @@
1
+ // These types were generated by @balena/abstract-sql-to-typescript v5.1.0
2
+
3
+ import type { Types } from '@balena/abstract-sql-to-typescript';
4
+
5
+ export interface MultipartUpload {
6
+ Read: {
7
+ created_at: Types['Date Time']['Read'];
8
+ modified_at: Types['Date Time']['Read'];
9
+ id: Types['Serial']['Read'];
10
+ uuid: Types['Short Text']['Read'];
11
+ resource_name: Types['Short Text']['Read'];
12
+ field_name: Types['Short Text']['Read'];
13
+ resource_id: Types['Integer']['Read'];
14
+ upload_id: Types['Short Text']['Read'];
15
+ file_key: Types['Short Text']['Read'];
16
+ status: 'pending' | 'completed' | 'cancelled';
17
+ filename: Types['Short Text']['Read'];
18
+ content_type: Types['Short Text']['Read'];
19
+ size: Types['Integer']['Read'];
20
+ chunk_size: Types['Integer']['Read'];
21
+ expiry_date: Types['Date Time']['Read'];
22
+ is_created_by__actor: Types['Integer']['Read'] | null;
23
+ };
24
+ Write: {
25
+ created_at: Types['Date Time']['Write'];
26
+ modified_at: Types['Date Time']['Write'];
27
+ id: Types['Serial']['Write'];
28
+ uuid: Types['Short Text']['Write'];
29
+ resource_name: Types['Short Text']['Write'];
30
+ field_name: Types['Short Text']['Write'];
31
+ resource_id: Types['Integer']['Write'];
32
+ upload_id: Types['Short Text']['Write'];
33
+ file_key: Types['Short Text']['Write'];
34
+ status: 'pending' | 'completed' | 'cancelled';
35
+ filename: Types['Short Text']['Write'];
36
+ content_type: Types['Short Text']['Write'];
37
+ size: Types['Integer']['Write'];
38
+ chunk_size: Types['Integer']['Write'];
39
+ expiry_date: Types['Date Time']['Write'];
40
+ is_created_by__actor: Types['Integer']['Write'] | null;
41
+ };
42
+ }
43
+
44
+ export default interface $Model {
45
+ multipart_upload: MultipartUpload;
46
+
47
+
48
+ }