@balena/pinejs 21.3.1-build-webresource-delete-reliable-7d16043f72b2a015f309d0e0a504d9ce22844008-1 → 21.3.2-build-renovate-minio-mc-2025-x-da1cec8047050b0fc2defc6939dbe2b5e5302ab4-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.
@@ -15,8 +15,6 @@ import { errors, permissions } from '../server-glue/module.js';
15
15
  import type { WebResourceType as WebResource } from '@balena/sbvr-types';
16
16
  import { TypedError } from 'typed-error';
17
17
  import type { Resolvable } from '../sbvr-api/common-types.js';
18
- import type { Tx } from '../database-layer/db.js';
19
- import { isPineTasksAvailable } from '../tasks/index.js';
20
18
 
21
19
  export * from './handlers/index.js';
22
20
 
@@ -318,34 +316,12 @@ export const getWebResourceFields = (
318
316
  .map((f) => sqlNameToODataName(f.fieldName));
319
317
  };
320
318
 
321
- const deleteOnStorage = async (keysToDelete: string[], tx?: Tx) => {
322
- if (isPineTasksAvailable()) {
323
- await Promise.all(
324
- keysToDelete.map(async (fileKey) => {
325
- return await sbvrUtils.api.tasks.post({
326
- resource: 'task',
327
- passthrough: {
328
- req: permissions.root,
329
- tx,
330
- },
331
- body: {
332
- key: crypto.randomUUID(),
333
- is_executed_by__handler: 'delete_webresource_file',
334
- is_executed_with__parameter_set: {
335
- fileKey: fileKey,
336
- },
337
- attempt_limit: 2 ** 31 - 1,
338
- },
339
- });
340
- }),
341
- );
342
- } else {
343
- await Promise.all(
344
- keysToDelete.map(async (fileKey) => {
345
- return await configuredWebResourceHandler?.removeFile(fileKey);
346
- }),
347
- );
348
- }
319
+ const deleteFiles = async (
320
+ keysToDelete: string[],
321
+ webResourceHandler: WebResourceHandler,
322
+ ) => {
323
+ const promises = keysToDelete.map((r) => webResourceHandler.removeFile(r));
324
+ await Promise.all(promises);
349
325
  };
350
326
 
351
327
  const throwIfWebresourceNotInMultipart = (
@@ -362,22 +338,18 @@ const throwIfWebresourceNotInMultipart = (
362
338
  }
363
339
  };
364
340
 
365
- const getCreateWebResourceHooks = (): sbvrUtils.Hooks => {
341
+ const getCreateWebResourceHooks = (
342
+ webResourceHandler: WebResourceHandler,
343
+ ): sbvrUtils.Hooks => {
366
344
  return {
367
345
  PRERUN: (hookArgs) => {
368
346
  const webResourceFields = getWebResourceFields(hookArgs.request);
369
347
  throwIfWebresourceNotInMultipart(webResourceFields, hookArgs);
370
348
  },
371
- 'POSTRUN-ERROR': async ({ request }) => {
372
- const fields = getWebResourceFields(request);
373
-
374
- if (fields.length === 0) {
375
- return;
376
- }
377
-
378
- const keysToDelete = getWebResourcesKeysFromRequest(fields, request);
379
- // Explicitely not passing tx as it will be rolledback as it is an error hook
380
- await deleteOnStorage(keysToDelete);
349
+ 'POSTRUN-ERROR': ({ tx, request }) => {
350
+ tx.on('rollback', () => {
351
+ void deleteRollbackPendingFields(request, webResourceHandler);
352
+ });
381
353
  },
382
354
  };
383
355
  };
@@ -406,12 +378,21 @@ const getWebResourcesKeysFromRequest = (
406
378
  .filter((href) => href != null);
407
379
  };
408
380
 
409
- const getRemoveWebResourceHooks = (): sbvrUtils.Hooks => {
381
+ const getRemoveWebResourceHooks = (
382
+ webResourceHandler: WebResourceHandler,
383
+ ): sbvrUtils.Hooks => {
410
384
  return {
411
385
  PRERUN: async (args) => {
412
386
  const { api, request, tx } = args;
413
387
  let webResourceFields = getWebResourceFields(request);
414
388
 
389
+ throwIfWebresourceNotInMultipart(webResourceFields, args);
390
+
391
+ // Request failed on DB roundtrip (e.g. DB constraint) and pending files need to be deleted
392
+ tx.on('rollback', () => {
393
+ void deleteRollbackPendingFields(request, webResourceHandler);
394
+ });
395
+
415
396
  if (request.method === 'PATCH') {
416
397
  webResourceFields = Object.entries(request.values)
417
398
  .filter(
@@ -420,8 +401,6 @@ const getRemoveWebResourceHooks = (): sbvrUtils.Hooks => {
420
401
  )
421
402
  .map(([key]) => key);
422
403
  }
423
- request.custom.webResourceFields = webResourceFields;
424
- throwIfWebresourceNotInMultipart(webResourceFields, args);
425
404
 
426
405
  if (webResourceFields.length === 0) {
427
406
  // No need to delete anything as no file is in the wire
@@ -432,32 +411,43 @@ const getRemoveWebResourceHooks = (): sbvrUtils.Hooks => {
432
411
  // This can only be validated here because we need to first ensure the
433
412
  // request is actually modifying a webresource before erroring out
434
413
  if (request.method === 'PATCH' && request.odataQuery?.key == null) {
414
+ // When we get here, files have already been uploaded. We need to mark them for deletion.
415
+ const keysToDelete = getWebResourcesKeysFromRequest(
416
+ webResourceFields,
417
+ request,
418
+ );
419
+
420
+ // Set deletion of files on the wire as request will throw
421
+ tx.on('end', () => {
422
+ deletePendingFiles(keysToDelete, request, webResourceHandler);
423
+ });
424
+
435
425
  throw new errors.BadRequestError(
436
426
  'WebResources can only be updated when providing a resource key.',
437
427
  );
438
428
  }
439
429
 
430
+ // This can be > 1 in both DELETE requests or PATCH requests to not accessible IDs.
440
431
  const ids = await sbvrUtils.getAffectedIds(args);
441
432
  if (ids.length === 0) {
442
433
  // Set deletion of files on the wire as no resource was affected
443
- request.custom.onPostRunDelete = getWebResourcesKeysFromRequest(
434
+ // Note that for DELETE requests it should not find any request on the wire
435
+ const keysToDelete = getWebResourcesKeysFromRequest(
444
436
  webResourceFields,
445
437
  request,
446
438
  );
439
+ deletePendingFiles(keysToDelete, request, webResourceHandler);
447
440
  return;
448
441
  }
449
442
 
450
- // If it reaches here, it means that it will try to patch/delete the webresource
451
- // So we need (before postrun) get what are the current keys to be deleted
452
- // if post run succeeds
453
443
  const webResources = (await api.get({
454
444
  resource: request.resourceName,
455
445
  passthrough: {
456
- tx,
446
+ tx: args.tx,
457
447
  req: permissions.root,
458
448
  },
459
449
  options: {
460
- $select: request.custom.webResourceFields,
450
+ $select: webResourceFields,
461
451
  $filter: {
462
452
  id: {
463
453
  $in: ids,
@@ -465,34 +455,60 @@ const getRemoveWebResourceHooks = (): sbvrUtils.Hooks => {
465
455
  },
466
456
  },
467
457
  })) as WebResourcesDbResponse[] | undefined | null;
468
- request.custom.onPostRunDelete = getWebResourcesHrefs(webResources);
469
- },
470
458
 
471
- POSTRUN: async ({ request, tx }) => {
472
- // Either patch or delete worked. In either case, schedule the previous existing files to delete
473
- await deleteOnStorage(request.custom.onPostRunDelete ?? [], tx);
474
- },
475
- 'POSTRUN-ERROR': async ({ request }) => {
476
- const keysToDelete = getWebResourcesKeysFromRequest(
477
- request.custom.webResourceFields,
478
- request,
479
- );
480
- // Explicitely not passing tx as it will be rolledback as it is an error hook
481
- await deleteOnStorage(keysToDelete);
459
+ // Deletes previous stored resources in case they were patched or the whole entity was deleted
460
+ tx.on('end', () => {
461
+ deletePendingFiles(
462
+ getWebResourcesHrefs(webResources),
463
+ request,
464
+ webResourceHandler,
465
+ );
466
+ });
482
467
  },
483
468
  };
484
469
  };
485
470
 
471
+ const deleteRollbackPendingFields = async (
472
+ request: uriParser.ODataRequest,
473
+ webResourceHandler: WebResourceHandler,
474
+ ) => {
475
+ const fields = getWebResourceFields(request);
476
+
477
+ if (fields.length === 0) {
478
+ return;
479
+ }
480
+
481
+ const keysToDelete = getWebResourcesKeysFromRequest(fields, request);
482
+ await deleteFiles(keysToDelete, webResourceHandler);
483
+ };
484
+
485
+ const deletePendingFiles = (
486
+ keysToDelete: string[],
487
+ request: uriParser.ODataRequest,
488
+ webResourceHandler: WebResourceHandler,
489
+ ): void => {
490
+ // on purpose does not await for this promise to resolve
491
+ try {
492
+ void deleteFiles(keysToDelete, webResourceHandler);
493
+ } catch (err) {
494
+ getLogger(request.vocabulary).error(`Failed to delete pending files`, err);
495
+ }
496
+ };
497
+
486
498
  export const getDefaultHandler = (): WebResourceHandler => {
487
499
  return new NoopHandler();
488
500
  };
489
501
 
490
- export const setupUploadHooks = (apiRoot: string, resourceName: string) => {
502
+ export const setupUploadHooks = (
503
+ handler: WebResourceHandler,
504
+ apiRoot: string,
505
+ resourceName: string,
506
+ ) => {
491
507
  sbvrUtils.addPureHook(
492
508
  'DELETE',
493
509
  apiRoot,
494
510
  resourceName,
495
- getRemoveWebResourceHooks(),
511
+ getRemoveWebResourceHooks(handler),
496
512
  );
497
513
 
498
514
  sbvrUtils.addPureHook(
@@ -500,13 +516,13 @@ export const setupUploadHooks = (apiRoot: string, resourceName: string) => {
500
516
  apiRoot,
501
517
  resourceName,
502
518
  // PATCH also needs to remove the old resource in case a webresource was modified
503
- getRemoveWebResourceHooks(),
519
+ getRemoveWebResourceHooks(handler),
504
520
  );
505
521
 
506
522
  sbvrUtils.addPureHook(
507
523
  'POST',
508
524
  apiRoot,
509
525
  resourceName,
510
- getCreateWebResourceHooks(),
526
+ getCreateWebResourceHooks(handler),
511
527
  );
512
528
  };
@@ -1 +0,0 @@
1
- export declare const addPineTaskHandlers: () => void;
@@ -1,7 +0,0 @@
1
- import { tasks } from '../server-glue/module.js';
2
- import { addDeleteFileTaskHandler } from '../webresource-handler/delete-file-task.js';
3
- export const addPineTaskHandlers = () => {
4
- addDeleteFileTaskHandler();
5
- void tasks.worker?.start();
6
- };
7
- //# sourceMappingURL=pine-tasks.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pine-tasks.js","sourceRoot":"","sources":["../../src/tasks/pine-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAEtF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,EAAE;IACvC,wBAAwB,EAAE,CAAC;IAC3B,KAAK,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC,CAAC"}
@@ -1 +0,0 @@
1
- export declare const addDeleteFileTaskHandler: () => void;
@@ -1,37 +0,0 @@
1
- import { addTaskHandler } from '../tasks/index.js';
2
- import { getWebresourceHandler } from './index.js';
3
- const deleteFileSchema = {
4
- type: 'object',
5
- properties: {
6
- fileKey: {
7
- type: 'string',
8
- },
9
- },
10
- required: ['fileKey'],
11
- additionalProperties: false,
12
- };
13
- export const addDeleteFileTaskHandler = () => {
14
- addTaskHandler('delete_webresource_file', async (task) => {
15
- const handler = getWebresourceHandler();
16
- if (!handler) {
17
- return {
18
- error: 'Webresource handler not available',
19
- status: 'failed',
20
- };
21
- }
22
- try {
23
- await handler.removeFile(task.params.fileKey);
24
- return {
25
- status: 'succeeded',
26
- };
27
- }
28
- catch (error) {
29
- console.error('Error deleting file:', error);
30
- return {
31
- error: `${error}`,
32
- status: 'failed',
33
- };
34
- }
35
- }, deleteFileSchema);
36
- };
37
- //# sourceMappingURL=delete-file-task.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"delete-file-task.js","sourceRoot":"","sources":["../../src/webresource-handler/delete-file-task.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,gBAAgB,GAAG;IACxB,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACX,OAAO,EAAE;YACR,IAAI,EAAE,QAAQ;SACd;KACD;IACD,QAAQ,EAAE,CAAC,SAAS,CAAC;IACrB,oBAAoB,EAAE,KAAK;CAClB,CAAC;AAEX,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,EAAE;IAC5C,cAAc,CACb,yBAAyB,EACzB,KAAK,EAAE,IAAI,EAAE,EAAE;QACd,MAAM,OAAO,GAAG,qBAAqB,EAAE,CAAC;QACxC,IAAI,CAAC,OAAO,EAAE,CAAC;YACd,OAAO;gBACN,KAAK,EAAE,mCAAmC;gBAC1C,MAAM,EAAE,QAAQ;aAChB,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC9C,OAAO;gBACN,MAAM,EAAE,WAAW;aACnB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,OAAO;gBACN,KAAK,EAAE,GAAG,KAAK,EAAE;gBACjB,MAAM,EAAE,QAAQ;aAChB,CAAC;QACH,CAAC;IACF,CAAC,EACD,gBAAgB,CAChB,CAAC;AACH,CAAC,CAAC"}
@@ -1,7 +0,0 @@
1
- import { tasks } from '../server-glue/module.js';
2
- import { addDeleteFileTaskHandler } from '../webresource-handler/delete-file-task.js';
3
-
4
- export const addPineTaskHandlers = () => {
5
- addDeleteFileTaskHandler();
6
- void tasks.worker?.start();
7
- };
@@ -1,42 +0,0 @@
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
- };