@balena/pinejs 21.4.0-build-add-actions-example-00cabf6eb1629a826638b8c81dcf6d45a534872b-1 → 21.4.0-build-add-actions-example-6ac2b3e43d47ee50b22cbf3985c736f454ad73bf-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.
@@ -0,0 +1,42 @@
1
+ import { addTaskHandler } from '../tasks/index.js';
2
+ import { getWebresourceHandler } from './index.js';
3
+
4
+ const deleteFileSchema = {
5
+ type: 'object',
6
+ properties: {
7
+ fileKey: {
8
+ type: 'string',
9
+ },
10
+ },
11
+ required: ['fileKey'],
12
+ additionalProperties: false,
13
+ } as const;
14
+
15
+ export const addDeleteFileTaskHandler = () => {
16
+ addTaskHandler(
17
+ 'delete_webresource_file',
18
+ async (task) => {
19
+ const handler = getWebresourceHandler();
20
+ if (!handler) {
21
+ return {
22
+ error: 'Webresource handler not available',
23
+ status: 'failed',
24
+ };
25
+ }
26
+
27
+ try {
28
+ await handler.removeFile(task.params.fileKey);
29
+ return {
30
+ status: 'succeeded',
31
+ };
32
+ } catch (error) {
33
+ console.error('Error deleting file:', error);
34
+ return {
35
+ error: `${error}`,
36
+ status: 'failed',
37
+ };
38
+ }
39
+ },
40
+ deleteFileSchema,
41
+ );
42
+ };
@@ -16,6 +16,8 @@ import { errors, permissions } from '../server-glue/module.js';
16
16
  import type { WebResourceType as WebResource } from '@balena/sbvr-types';
17
17
  import { TypedError } from 'typed-error';
18
18
  import type { Resolvable } from '../sbvr-api/common-types.js';
19
+ import type { Tx } from '../database-layer/db.js';
20
+ import { isPineTasksAvailable } from '../tasks/index.js';
19
21
  import type WebresourceModel from './webresource.js';
20
22
  import { importSBVR } from '../server-glue/sbvr-loader.js';
21
23
  import { beginUploadAction } from './actions/beginUpload.js';
@@ -324,12 +326,34 @@ export const getWebResourceFields = (
324
326
  .map((f) => sqlNameToODataName(f.fieldName));
325
327
  };
326
328
 
327
- const deleteFiles = async (
328
- keysToDelete: string[],
329
- webResourceHandler: WebResourceHandler,
330
- ) => {
331
- const promises = keysToDelete.map((r) => webResourceHandler.removeFile(r));
332
- await Promise.all(promises);
329
+ const deleteOnStorage = async (keysToDelete: string[], tx?: Tx) => {
330
+ if (isPineTasksAvailable()) {
331
+ await Promise.all(
332
+ keysToDelete.map(async (fileKey) => {
333
+ return await sbvrUtils.api.tasks.post({
334
+ resource: 'task',
335
+ passthrough: {
336
+ req: permissions.root,
337
+ tx,
338
+ },
339
+ body: {
340
+ key: crypto.randomUUID(),
341
+ is_executed_by__handler: 'delete_webresource_file',
342
+ is_executed_with__parameter_set: {
343
+ fileKey: fileKey,
344
+ },
345
+ attempt_limit: 2 ** 31 - 1,
346
+ },
347
+ });
348
+ }),
349
+ );
350
+ } else {
351
+ await Promise.all(
352
+ keysToDelete.map(async (fileKey) => {
353
+ return await configuredWebResourceHandler?.removeFile(fileKey);
354
+ }),
355
+ );
356
+ }
333
357
  };
334
358
 
335
359
  const throwIfWebresourceNotInMultipart = (
@@ -353,18 +377,22 @@ const throwIfWebresourceNotInMultipart = (
353
377
  }
354
378
  };
355
379
 
356
- const getCreateWebResourceHooks = (
357
- webResourceHandler: WebResourceHandler,
358
- ): sbvrUtils.Hooks => {
380
+ const getCreateWebResourceHooks = (): sbvrUtils.Hooks => {
359
381
  return {
360
382
  PRERUN: (hookArgs) => {
361
383
  const webResourceFields = getWebResourceFields(hookArgs.request);
362
384
  throwIfWebresourceNotInMultipart(webResourceFields, hookArgs);
363
385
  },
364
- 'POSTRUN-ERROR': ({ tx, request }) => {
365
- tx.on('rollback', () => {
366
- void deleteRollbackPendingFields(request, webResourceHandler);
367
- });
386
+ 'POSTRUN-ERROR': async ({ request }) => {
387
+ const fields = getWebResourceFields(request);
388
+
389
+ if (fields.length === 0) {
390
+ return;
391
+ }
392
+
393
+ const keysToDelete = getWebResourcesKeysFromRequest(fields, request);
394
+ // Explicitely not passing tx as it will be rolledback as it is an error hook
395
+ await deleteOnStorage(keysToDelete);
368
396
  },
369
397
  };
370
398
  };
@@ -393,21 +421,12 @@ const getWebResourcesKeysFromRequest = (
393
421
  .filter((href) => href != null);
394
422
  };
395
423
 
396
- const getRemoveWebResourceHooks = (
397
- webResourceHandler: WebResourceHandler,
398
- ): sbvrUtils.Hooks => {
424
+ const getRemoveWebResourceHooks = (): sbvrUtils.Hooks => {
399
425
  return {
400
426
  PRERUN: async (args) => {
401
427
  const { api, request, tx } = args;
402
428
  let webResourceFields = getWebResourceFields(request);
403
429
 
404
- throwIfWebresourceNotInMultipart(webResourceFields, args);
405
-
406
- // Request failed on DB roundtrip (e.g. DB constraint) and pending files need to be deleted
407
- tx.on('rollback', () => {
408
- void deleteRollbackPendingFields(request, webResourceHandler);
409
- });
410
-
411
430
  if (request.method === 'PATCH') {
412
431
  webResourceFields = Object.entries(request.values)
413
432
  .filter(
@@ -416,6 +435,8 @@ const getRemoveWebResourceHooks = (
416
435
  )
417
436
  .map(([key]) => key);
418
437
  }
438
+ request.custom.webResourceFields = webResourceFields;
439
+ throwIfWebresourceNotInMultipart(webResourceFields, args);
419
440
 
420
441
  if (webResourceFields.length === 0) {
421
442
  // No need to delete anything as no file is in the wire
@@ -426,43 +447,32 @@ const getRemoveWebResourceHooks = (
426
447
  // This can only be validated here because we need to first ensure the
427
448
  // request is actually modifying a webresource before erroring out
428
449
  if (request.method === 'PATCH' && request.odataQuery?.key == null) {
429
- // When we get here, files have already been uploaded. We need to mark them for deletion.
430
- const keysToDelete = getWebResourcesKeysFromRequest(
431
- webResourceFields,
432
- request,
433
- );
434
-
435
- // Set deletion of files on the wire as request will throw
436
- tx.on('end', () => {
437
- deletePendingFiles(keysToDelete, request, webResourceHandler);
438
- });
439
-
440
450
  throw new errors.BadRequestError(
441
451
  'WebResources can only be updated when providing a resource key.',
442
452
  );
443
453
  }
444
454
 
445
- // This can be > 1 in both DELETE requests or PATCH requests to not accessible IDs.
446
455
  const ids = await sbvrUtils.getAffectedIds(args);
447
456
  if (ids.length === 0) {
448
457
  // Set deletion of files on the wire as no resource was affected
449
- // Note that for DELETE requests it should not find any request on the wire
450
- const keysToDelete = getWebResourcesKeysFromRequest(
458
+ request.custom.onPostRunDelete = getWebResourcesKeysFromRequest(
451
459
  webResourceFields,
452
460
  request,
453
461
  );
454
- deletePendingFiles(keysToDelete, request, webResourceHandler);
455
462
  return;
456
463
  }
457
464
 
465
+ // If it reaches here, it means that it will try to patch/delete the webresource
466
+ // So we need (before postrun) get what are the current keys to be deleted
467
+ // if post run succeeds
458
468
  const webResources = (await api.get({
459
469
  resource: request.resourceName,
460
470
  passthrough: {
461
- tx: args.tx,
471
+ tx,
462
472
  req: permissions.root,
463
473
  },
464
474
  options: {
465
- $select: webResourceFields,
475
+ $select: request.custom.webResourceFields,
466
476
  $filter: {
467
477
  id: {
468
478
  $in: ids,
@@ -470,60 +480,34 @@ const getRemoveWebResourceHooks = (
470
480
  },
471
481
  },
472
482
  })) as WebResourcesDbResponse[] | undefined | null;
483
+ request.custom.onPostRunDelete = getWebResourcesHrefs(webResources);
484
+ },
473
485
 
474
- // Deletes previous stored resources in case they were patched or the whole entity was deleted
475
- tx.on('end', () => {
476
- deletePendingFiles(
477
- getWebResourcesHrefs(webResources),
478
- request,
479
- webResourceHandler,
480
- );
481
- });
486
+ POSTRUN: async ({ request, tx }) => {
487
+ // Either patch or delete worked. In either case, schedule the previous existing files to delete
488
+ await deleteOnStorage(request.custom.onPostRunDelete ?? [], tx);
489
+ },
490
+ 'POSTRUN-ERROR': async ({ request }) => {
491
+ const keysToDelete = getWebResourcesKeysFromRequest(
492
+ request.custom.webResourceFields,
493
+ request,
494
+ );
495
+ // Explicitely not passing tx as it will be rolledback as it is an error hook
496
+ await deleteOnStorage(keysToDelete);
482
497
  },
483
498
  };
484
499
  };
485
500
 
486
- const deleteRollbackPendingFields = async (
487
- request: uriParser.ODataRequest,
488
- webResourceHandler: WebResourceHandler,
489
- ) => {
490
- const fields = getWebResourceFields(request);
491
-
492
- if (fields.length === 0) {
493
- return;
494
- }
495
-
496
- const keysToDelete = getWebResourcesKeysFromRequest(fields, request);
497
- await deleteFiles(keysToDelete, webResourceHandler);
498
- };
499
-
500
- const deletePendingFiles = (
501
- keysToDelete: string[],
502
- request: uriParser.ODataRequest,
503
- webResourceHandler: WebResourceHandler,
504
- ): void => {
505
- // on purpose does not await for this promise to resolve
506
- try {
507
- void deleteFiles(keysToDelete, webResourceHandler);
508
- } catch (err) {
509
- getLogger(request.vocabulary).error(`Failed to delete pending files`, err);
510
- }
511
- };
512
-
513
501
  export const getDefaultHandler = (): WebResourceHandler => {
514
502
  return new NoopHandler();
515
503
  };
516
504
 
517
- export const setupUploadHooks = (
518
- handler: WebResourceHandler,
519
- apiRoot: string,
520
- resourceName: string,
521
- ) => {
505
+ export const setupUploadHooks = (apiRoot: string, resourceName: string) => {
522
506
  sbvrUtils.addPureHook(
523
507
  'DELETE',
524
508
  apiRoot,
525
509
  resourceName,
526
- getRemoveWebResourceHooks(handler),
510
+ getRemoveWebResourceHooks(),
527
511
  );
528
512
 
529
513
  sbvrUtils.addPureHook(
@@ -531,24 +515,20 @@ export const setupUploadHooks = (
531
515
  apiRoot,
532
516
  resourceName,
533
517
  // PATCH also needs to remove the old resource in case a webresource was modified
534
- getRemoveWebResourceHooks(handler),
518
+ getRemoveWebResourceHooks(),
535
519
  );
536
520
 
537
521
  sbvrUtils.addPureHook(
538
522
  'POST',
539
523
  apiRoot,
540
524
  resourceName,
541
- getCreateWebResourceHooks(handler),
525
+ getCreateWebResourceHooks(),
542
526
  );
543
527
  };
544
528
 
545
- export const setupUploadActions = (
546
- handler: WebResourceHandler,
547
- apiRoot: string,
548
- resourceName: string,
549
- ) => {
529
+ export const setupUploadActions = (apiRoot: string, resourceName: string) => {
550
530
  const resource = sqlNameToODataName(resourceName);
551
- if (isMultipartUploadAvailable(handler)) {
531
+ if (isMultipartUploadAvailable(configuredWebResourceHandler)) {
552
532
  addAction(apiRoot, resource, 'beginUpload', beginUploadAction);
553
533
 
554
534
  addAction(apiRoot, resource, 'commitUpload', commitUploadAction);
@@ -23,7 +23,7 @@ Term: filename
23
23
  Term: content type
24
24
  Concept Type: Short Text (Type)
25
25
  Term: size
26
- Concept Type: Integer (Type)
26
+ Concept Type: Big Integer (Type)
27
27
  Term: chunk size
28
28
  Concept Type: Integer (Type)
29
29
  Term: valid until date
@@ -16,7 +16,7 @@ export interface MultipartUpload {
16
16
  status: 'pending' | 'completed' | 'cancelled';
17
17
  filename: Types['Short Text']['Read'];
18
18
  content_type: Types['Short Text']['Read'];
19
- size: Types['Integer']['Read'];
19
+ size: Types['Big Integer']['Read'];
20
20
  chunk_size: Types['Integer']['Read'];
21
21
  expiry_date: Types['Date Time']['Read'];
22
22
  is_created_by__actor: Types['Integer']['Read'] | null;
@@ -34,7 +34,7 @@ export interface MultipartUpload {
34
34
  status: 'pending' | 'completed' | 'cancelled';
35
35
  filename: Types['Short Text']['Write'];
36
36
  content_type: Types['Short Text']['Write'];
37
- size: Types['Integer']['Write'];
37
+ size: Types['Big Integer']['Write'];
38
38
  chunk_size: Types['Integer']['Write'];
39
39
  expiry_date: Types['Date Time']['Write'];
40
40
  is_created_by__actor: Types['Integer']['Write'] | null;