From 17e73abf5cc8bffef75a415453e303d5b315824d Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:53:59 +0200 Subject: [PATCH 01/19] chore: add abort api key --- config/default.json | 1 + openapi3.yaml | 62 +++++++++++++++++++ package-lock.json | 19 +++++- .../controllers/ingestionController.ts | 25 +++++++- src/ingestion/interfaces.ts | 4 ++ src/ingestion/models/ingestionManager.ts | 41 ++++++++++++ src/ingestion/routes/ingestionRouter.ts | 1 + src/serviceClients/jobManagerWrapper.ts | 15 +++++ 8 files changed, 166 insertions(+), 2 deletions(-) diff --git a/config/default.json b/config/default.json index 53d9e9a7..159a16aa 100644 --- a/config/default.json +++ b/config/default.json @@ -63,6 +63,7 @@ "ingestionUpdateJobType": "Ingestion_Update", "ingestionSwapUpdateJobType": "Ingestion_Swap_Update", "validationTaskType": "validation", + "finalizeTaskType": "finalize", "supportedIngestionSwapTypes": [ { "productType": "RasterVectorBest", diff --git a/openapi3.yaml b/openapi3.yaml index cb17ff4b..92bd8ae9 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -157,6 +157,67 @@ paths: schema: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + '422': + description: Unprocessable Content + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + '500': + description: Internal Server error + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + /ingestion/{jobId}/abort: + put: + operationId: abortIngestion + tags: + - ingestion + summary: abort a running ingestion job + description: >- + Aborts an ingestion job that is currently running or pending. The job cannot be aborted if it has already completed or if the finalize task has started (point of no return). + parameters: + - name: jobId + in: path + description: The id of the job to abort + required: true + schema: + type: string + format: uuid + responses: + '200': + description: OK - Job aborted successfully + '400': + description: Bad request - Job status does not allow abort (COMPLETED or ABORTED) + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + '404': + description: Not Found - Job does not exist + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + '409': + description: Conflict - Job has reached finalize task (point of no return) + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + '422': + description: Unprocessable Content + content: + application/json: + schema: + $ref: >- + ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '500': description: Internal Server error content: @@ -257,6 +318,7 @@ paths: schema: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage + components: schemas: CallbackUrls: diff --git a/package-lock.json b/package-lock.json index dd7abdc7..8b4cbe56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15193,6 +15193,21 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -23345,6 +23360,8 @@ }, "node_modules/xxhash-wasm": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, "node_modules/y18n": { @@ -23486,4 +23503,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/ingestion/controllers/ingestionController.ts b/src/ingestion/controllers/ingestionController.ts index 821cb256..9a725460 100644 --- a/src/ingestion/controllers/ingestionController.ts +++ b/src/ingestion/controllers/ingestionController.ts @@ -6,11 +6,12 @@ import { inject, injectable } from 'tsyringe'; import { GpkgError } from '../../serviceClients/database/errors'; import { INGESTION_SCHEMAS_VALIDATOR_SYMBOL, SchemasValidator } from '../../utils/validation/schemasValidator'; import { FileNotFoundError, UnsupportedEntityError, ValidationError } from '../errors/ingestionErrors'; -import type { IRetryRequestParams, IRecordRequestParams, ResponseId } from '../interfaces'; +import type { IRetryRequestParams, IRecordRequestParams, IAbortRequestParams, ResponseId } from '../interfaces'; import { IngestionManager } from '../models/ingestionManager'; type NewLayerHandler = RequestHandler; type RetryIngestionHandler = RequestHandler; +type AbortIngestionHandler = RequestHandler; type UpdateLayerHandler = RequestHandler; @injectable() @@ -84,4 +85,26 @@ export class IngestionController { next(error); } }; + + public abortIngestion: AbortIngestionHandler = async (req, res, next) => { + try { + await this.ingestionManager.abortIngestion(req.params.jobId); + + res.status(StatusCodes.OK).send(); + } catch (error) { + if (error instanceof ValidationError) { + (error as HttpError).status = StatusCodes.BAD_REQUEST; //400 + } + if (error instanceof NotFoundError) { + (error as HttpError).status = StatusCodes.NOT_FOUND; //404 + } + if (error instanceof ConflictError) { + (error as HttpError).status = StatusCodes.CONFLICT; //409 + } + if (error instanceof UnsupportedEntityError) { + (error as HttpError).status = StatusCodes.UNPROCESSABLE_ENTITY; //422 + } + next(error); + } + }; } diff --git a/src/ingestion/interfaces.ts b/src/ingestion/interfaces.ts index 67e086ac..a718b683 100644 --- a/src/ingestion/interfaces.ts +++ b/src/ingestion/interfaces.ts @@ -21,6 +21,10 @@ export interface IRetryRequestParams { jobId: string; } +export interface IAbortRequestParams { + jobId: string; +} + export interface PixelRange { min: number; max: number; diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index af981c05..d8624a56 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -68,6 +68,7 @@ export class IngestionManager { private readonly updateJobType: string; private readonly swapUpdateJobType: string; private readonly validationTaskType: string; + private readonly finalizeTaskType: string; private readonly sourceMount: string; private readonly jobTrackerServiceUrl: string; @@ -97,6 +98,7 @@ export class IngestionManager { this.updateJobType = config.get('jobManager.ingestionUpdateJobType'); this.swapUpdateJobType = config.get('jobManager.ingestionSwapUpdateJobType'); this.validationTaskType = config.get('jobManager.validationTaskType'); + this.finalizeTaskType = config.get('jobManager.finalizeTaskType'); this.sourceMount = config.get('storageExplorer.layerSourceDir'); this.jobTrackerServiceUrl = config.get('services.jobTrackerServiceURL'); } @@ -682,6 +684,45 @@ export class IngestionManager { return validStatuses.includes(status); } + @withSpanV4 + private isJobAbortable(status: OperationStatus): boolean { + const invalidStatuses = [OperationStatus.COMPLETED, OperationStatus.ABORTED]; + return !invalidStatuses.includes(status); + } + + @withSpanAsyncV4 + public async abortIngestion(jobId: string): Promise { + const logCtx: LogContext = { ...this.logContext, function: this.abortIngestion.name }; + const activeSpan = trace.getActiveSpan(); + activeSpan?.updateName('ingestionManager.abortIngestion'); + + this.logger.info({ msg: 'starting abort ingestion process', logContext: logCtx, jobId }); + + const job: IJobResponse = await this.jobManagerWrapper.getJob(jobId); + + if (!this.isJobAbortable(job.status)) { + throwInvalidJobStatusError(jobId, job.status, this.logger, activeSpan); + } + + const tasks = await this.jobManagerWrapper.getTasksForJob(jobId); + const finalizeTask = tasks.find((task) => task.type === this.finalizeTaskType); + + if (finalizeTask !== undefined) { + const errorMessage = `cannot abort job ${jobId} - finalize task already exists (point of no return)`; + this.logger.error({ msg: errorMessage, logContext: logCtx, jobId, finalizeTaskId: finalizeTask.id }); + activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage }); + throw new ConflictError(errorMessage); + } + + await this.jobManagerWrapper.abortJob(jobId); + + const { resourceId, productType } = this.parseAndValidateJobIdentifiers(job.resourceId, job.productType); + await this.polygonPartsManagerClient.deleteValidationEntity(resourceId, productType); + + this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); + activeSpan?.setStatus({ code: SpanStatusCode.OK }).addEvent('ingestionManager.abortIngestion.success', { abortSuccess: true, jobId }); + } + private convertChecksumsToRelativePaths(checksums: IChecksum[]): IChecksum[] { return checksums.map((checksum) => ({ ...checksum, diff --git a/src/ingestion/routes/ingestionRouter.ts b/src/ingestion/routes/ingestionRouter.ts index a9d90659..ff942eda 100644 --- a/src/ingestion/routes/ingestionRouter.ts +++ b/src/ingestion/routes/ingestionRouter.ts @@ -9,6 +9,7 @@ const ingestionRouterFactory: FactoryFunction = (dependencyContainer) => router.post('/', controller.newLayer.bind(controller)); router.put('/:id', controller.updateLayer.bind(controller)); router.put('/:jobId/retry', controller.retryIngestion.bind(controller)); + router.put('/:jobId/abort', controller.abortIngestion.bind(controller)); return router; }; diff --git a/src/serviceClients/jobManagerWrapper.ts b/src/serviceClients/jobManagerWrapper.ts index 19a545ea..5a1ceeba 100644 --- a/src/serviceClients/jobManagerWrapper.ts +++ b/src/serviceClients/jobManagerWrapper.ts @@ -60,4 +60,19 @@ export class JobManagerWrapper extends JobManagerClient { throw err; } } + + @withSpanAsyncV4 + public async abortJob(jobId: string): Promise { + const activeSpan = trace.getActiveSpan(); + activeSpan?.updateName('jobManagerWrapper.abortJob'); + + try { + await this.post(`${this.baseUrl}/jobs/${jobId}/abort`, {}); + this.logger.info({ msg: 'successfully aborted job', jobId }); + } catch (err) { + const message = `failed to abort job with id: ${jobId}`; + this.logger.error({ msg: message, err, jobId }); + throw err; + } + } } From ef0480bcca325c2442f303eda1eb709d1774b3f6 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:21:09 +0200 Subject: [PATCH 02/19] fix: correct Job Manager abort endpoint from /jobs/{id}/abort to /tasks/abort/{id} --- src/serviceClients/jobManagerWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serviceClients/jobManagerWrapper.ts b/src/serviceClients/jobManagerWrapper.ts index 5a1ceeba..a5a1515b 100644 --- a/src/serviceClients/jobManagerWrapper.ts +++ b/src/serviceClients/jobManagerWrapper.ts @@ -67,7 +67,7 @@ export class JobManagerWrapper extends JobManagerClient { activeSpan?.updateName('jobManagerWrapper.abortJob'); try { - await this.post(`${this.baseUrl}/jobs/${jobId}/abort`, {}); + await this.post(`${this.baseUrl}/tasks/abort/${jobId}`, {}); this.logger.info({ msg: 'successfully aborted job', jobId }); } catch (err) { const message = `failed to abort job with id: ${jobId}`; From 737b724043f2742c5df380f47b2b12e956ccfdd1 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:06:33 +0200 Subject: [PATCH 03/19] fix: address PR comments for abort API --- config/custom-environment-variables.json | 1 + helm/templates/configmap.yaml | 1 + helm/values.yaml | 2 ++ openapi3.yaml | 4 ++-- src/ingestion/errors/ingestionErrors.ts | 12 +++++++++--- src/ingestion/models/ingestionManager.ts | 17 ++++++++++++----- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index c2ae9096..8fc2238c 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -87,6 +87,7 @@ "jobDomain": "JOB_DOMAIN", "ingestionNewJobType": "INGESTION_NEW_JOB_TYPE", "validationTaskType": "VALIDATION_TASK_TYPE", + "finalizeTaskType": "FINALIZE_TASK_TYPE", "ingestionUpdateJobType": "INGESTION_UPDATE_JOB_TYPE", "ingestionSwapUpdateJobType": "INGESTION_SWAP_UPDATE_JOB_TYPE", "supportedIngestionSwapTypes": { diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 9a7492b7..b1f85d0e 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -37,6 +37,7 @@ data: INGESTION_UPDATE_JOB_TYPE: {{ $jobDefinitions.jobs.update.type | quote }} INGESTION_SWAP_UPDATE_JOB_TYPE: {{ $jobDefinitions.jobs.swapUpdate.type | quote }} VALIDATION_TASK_TYPE: {{ $jobDefinitions.tasks.validation.type | quote }} + FINALIZE_TASK_TYPE: {{ $jobDefinitions.tasks.finalize.type | quote }} CRS: {{ .Values.env.validationValuesByInfo.crs | toJson | quote}} FILE_FORMAT: {{ .Values.env.validationValuesByInfo.fileFormat | toJson | quote}} TILE_SIZE: {{ .Values.env.validationValuesByInfo.tileSize | quote}} diff --git a/helm/values.yaml b/helm/values.yaml index 0eefd8f6..106b10af 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -55,6 +55,8 @@ jobDefinitions: tasks: validation: type: "" + finalize: + type: "" storage: fs: diff --git a/openapi3.yaml b/openapi3.yaml index 92bd8ae9..cf628435 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -176,9 +176,9 @@ paths: operationId: abortIngestion tags: - ingestion - summary: abort a running ingestion job + summary: abort an active ingestion job description: >- - Aborts an ingestion job that is currently running or pending. The job cannot be aborted if it has already completed or if the finalize task has started (point of no return). + Aborts an ingestion job that is currently active or pending. The job cannot be aborted if it has already completed or if the finalize task has started (point of no return). parameters: - name: jobId in: path diff --git a/src/ingestion/errors/ingestionErrors.ts b/src/ingestion/errors/ingestionErrors.ts index 048013ab..30713300 100644 --- a/src/ingestion/errors/ingestionErrors.ts +++ b/src/ingestion/errors/ingestionErrors.ts @@ -43,9 +43,15 @@ export class ValidationError extends Error { } } -export function throwInvalidJobStatusError(jobId: string, currentStatus: OperationStatus, logger: Logger, span?: Span): never { - const validStatuses = [OperationStatus.FAILED, OperationStatus.SUSPENDED]; - const message = `Cannot retry job with id: ${jobId} because its status is: ${currentStatus}. Expected status: ${validStatuses.join(' or ')}`; +export function throwInvalidJobStatusError( + operation: string, + jobId: string, + currentStatus: OperationStatus, + validStatuses: OperationStatus[], + logger: Logger, + span?: Span +): never { + const message = `Cannot ${operation} job with id: ${jobId} because its status is: ${currentStatus}. Expected status: ${validStatuses.join(' or ')}`; logger.error({ msg: message, jobId, currentStatus, validStatuses }); diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index d8624a56..9647e92f 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -208,7 +208,7 @@ export class IngestionManager { const retryJob: IJobResponse = await this.jobManagerWrapper.getJob(jobId); if (!this.isJobRetryable(retryJob.status)) { - throwInvalidJobStatusError(jobId, retryJob.status, this.logger, activeSpan); + throwInvalidJobStatusError('retry', jobId, retryJob.status, [OperationStatus.FAILED, OperationStatus.SUSPENDED], this.logger, activeSpan); } const validationTask: ITaskResponse = await this.getValidationTask(jobId, logCtx); @@ -701,25 +701,32 @@ export class IngestionManager { const job: IJobResponse = await this.jobManagerWrapper.getJob(jobId); if (!this.isJobAbortable(job.status)) { - throwInvalidJobStatusError(jobId, job.status, this.logger, activeSpan); + throwInvalidJobStatusError( + 'abort', + jobId, + job.status, + [OperationStatus.FAILED, OperationStatus.SUSPENDED, OperationStatus.IN_PROGRESS, OperationStatus.PENDING], + this.logger, + activeSpan + ); } const tasks = await this.jobManagerWrapper.getTasksForJob(jobId); const finalizeTask = tasks.find((task) => task.type === this.finalizeTaskType); if (finalizeTask !== undefined) { - const errorMessage = `cannot abort job ${jobId} - finalize task already exists (point of no return)`; + const errorMessage = `cannot abort job ${jobId} - job already in finalization stage and cannot be aborted`; this.logger.error({ msg: errorMessage, logContext: logCtx, jobId, finalizeTaskId: finalizeTask.id }); activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage }); throw new ConflictError(errorMessage); } + this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); await this.jobManagerWrapper.abortJob(jobId); const { resourceId, productType } = this.parseAndValidateJobIdentifiers(job.resourceId, job.productType); + this.logger.debug({ msg: 'we are going to delete the validation entity', logContext: logCtx, jobId, resourceId, productType }); await this.polygonPartsManagerClient.deleteValidationEntity(resourceId, productType); - - this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); activeSpan?.setStatus({ code: SpanStatusCode.OK }).addEvent('ingestionManager.abortIngestion.success', { abortSuccess: true, jobId }); } From 32905850c364457f55eddebc2b8b1a488006b719 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:39:16 +0200 Subject: [PATCH 04/19] refactor: simplify error handling and improve abort validation logic --- src/ingestion/errors/ingestionErrors.ts | 5 ++--- src/ingestion/models/ingestionManager.ts | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/ingestion/errors/ingestionErrors.ts b/src/ingestion/errors/ingestionErrors.ts index 30713300..5755fd3b 100644 --- a/src/ingestion/errors/ingestionErrors.ts +++ b/src/ingestion/errors/ingestionErrors.ts @@ -47,13 +47,12 @@ export function throwInvalidJobStatusError( operation: string, jobId: string, currentStatus: OperationStatus, - validStatuses: OperationStatus[], logger: Logger, span?: Span ): never { - const message = `Cannot ${operation} job with id: ${jobId} because its status is: ${currentStatus}. Expected status: ${validStatuses.join(' or ')}`; + const message = `Cannot ${operation} job with id: ${jobId} because its status is: ${currentStatus}`; - logger.error({ msg: message, jobId, currentStatus, validStatuses }); + logger.error({ msg: message, jobId, currentStatus }); const error = new BadRequestError(message); span?.setAttribute('exception.type', error.status); diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 9647e92f..8175bee3 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -58,6 +58,11 @@ type InputFilesPaths = MapToRelativeAndAbsolute; type EnhancedIngestionNewLayer = ReplaceValuesOfKey; type EnhancedIngestionUpdateLayer = ReplaceValuesOfKey; +enum IngestionOperation { + RETRY = 'retry', + ABORT = 'abort', +} + @injectable() export class IngestionManager { private readonly logContext: LogContext; @@ -208,7 +213,7 @@ export class IngestionManager { const retryJob: IJobResponse = await this.jobManagerWrapper.getJob(jobId); if (!this.isJobRetryable(retryJob.status)) { - throwInvalidJobStatusError('retry', jobId, retryJob.status, [OperationStatus.FAILED, OperationStatus.SUSPENDED], this.logger, activeSpan); + throwInvalidJobStatusError(IngestionOperation.RETRY, jobId, retryJob.status, this.logger, activeSpan); } const validationTask: ITaskResponse = await this.getValidationTask(jobId, logCtx); @@ -701,22 +706,15 @@ export class IngestionManager { const job: IJobResponse = await this.jobManagerWrapper.getJob(jobId); if (!this.isJobAbortable(job.status)) { - throwInvalidJobStatusError( - 'abort', - jobId, - job.status, - [OperationStatus.FAILED, OperationStatus.SUSPENDED, OperationStatus.IN_PROGRESS, OperationStatus.PENDING], - this.logger, - activeSpan - ); + throwInvalidJobStatusError(IngestionOperation.ABORT, jobId, job.status, this.logger, activeSpan); } const tasks = await this.jobManagerWrapper.getTasksForJob(jobId); - const finalizeTask = tasks.find((task) => task.type === this.finalizeTaskType); + const hasFinalize = tasks.some((task) => task.type === this.finalizeTaskType); - if (finalizeTask !== undefined) { + if (hasFinalize) { const errorMessage = `cannot abort job ${jobId} - job already in finalization stage and cannot be aborted`; - this.logger.error({ msg: errorMessage, logContext: logCtx, jobId, finalizeTaskId: finalizeTask.id }); + this.logger.error({ msg: errorMessage, logContext: logCtx, jobId }); activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage }); throw new ConflictError(errorMessage); } @@ -725,7 +723,7 @@ export class IngestionManager { await this.jobManagerWrapper.abortJob(jobId); const { resourceId, productType } = this.parseAndValidateJobIdentifiers(job.resourceId, job.productType); - this.logger.debug({ msg: 'we are going to delete the validation entity', logContext: logCtx, jobId, resourceId, productType }); + this.logger.debug({ msg: 'deleting validation entity', logContext: logCtx, jobId, resourceId, productType }); await this.polygonPartsManagerClient.deleteValidationEntity(resourceId, productType); activeSpan?.setStatus({ code: SpanStatusCode.OK }).addEvent('ingestionManager.abortIngestion.success', { abortSuccess: true, jobId }); } From d0f99fb6d663aebf3b7376a72ac79ba48c0d4543 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:26:01 +0200 Subject: [PATCH 05/19] chore: fixed bad request end point --- src/ingestion/controllers/ingestionController.ts | 2 +- src/ingestion/interfaces.ts | 5 +++++ src/ingestion/models/ingestionManager.ts | 11 +++-------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ingestion/controllers/ingestionController.ts b/src/ingestion/controllers/ingestionController.ts index 9a725460..71b04ab4 100644 --- a/src/ingestion/controllers/ingestionController.ts +++ b/src/ingestion/controllers/ingestionController.ts @@ -1,4 +1,4 @@ -import { ConflictError, NotFoundError } from '@map-colonies/error-types'; +import { BadRequestError, ConflictError, NotFoundError } from '@map-colonies/error-types'; import { RequestHandler } from 'express'; import { HttpError } from 'express-openapi-validator/dist/framework/types'; import { StatusCodes } from 'http-status-codes'; diff --git a/src/ingestion/interfaces.ts b/src/ingestion/interfaces.ts index a718b683..7d706b2e 100644 --- a/src/ingestion/interfaces.ts +++ b/src/ingestion/interfaces.ts @@ -25,6 +25,11 @@ export interface IAbortRequestParams { jobId: string; } +export enum IngestionOperation { + RETRY = 'retry', + ABORT = 'abort', +} + export interface PixelRange { min: number; max: number; diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 8175bee3..bafe381d 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -1,6 +1,6 @@ import { relative } from 'node:path'; import { randomUUID } from 'node:crypto'; -import { ConflictError, NotFoundError } from '@map-colonies/error-types'; +import { BadRequestError, ConflictError, NotFoundError } from '@map-colonies/error-types'; import { Logger } from '@map-colonies/js-logger'; import { IFindJobsByCriteriaBody, @@ -39,7 +39,7 @@ import { getShapefileFiles } from '../../utils/shapefile'; import { ZodValidator } from '../../utils/validation/zodValidator'; import { ValidateManager } from '../../validate/models/validateManager'; import { ChecksumError, throwInvalidJobStatusError } from '../errors/ingestionErrors'; -import type { IngestionBaseJobParams, ResponseId } from '../interfaces'; +import type { IngestionBaseJobParams, IngestionOperation, ResponseId } from '../interfaces'; import type { RasterLayerMetadata } from '../schemas/layerCatalogSchema'; import type { IngestionNewLayer } from '../schemas/newLayerSchema'; import type { IngestionUpdateLayer } from '../schemas/updateLayerSchema'; @@ -58,11 +58,6 @@ type InputFilesPaths = MapToRelativeAndAbsolute; type EnhancedIngestionNewLayer = ReplaceValuesOfKey; type EnhancedIngestionUpdateLayer = ReplaceValuesOfKey; -enum IngestionOperation { - RETRY = 'retry', - ABORT = 'abort', -} - @injectable() export class IngestionManager { private readonly logContext: LogContext; @@ -716,7 +711,7 @@ export class IngestionManager { const errorMessage = `cannot abort job ${jobId} - job already in finalization stage and cannot be aborted`; this.logger.error({ msg: errorMessage, logContext: logCtx, jobId }); activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage }); - throw new ConflictError(errorMessage); + throw new BadRequestError(errorMessage); } this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); From a3a10de45d3c89a77d1e15f25bf6776bc7c44caa Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:47:59 +0200 Subject: [PATCH 06/19] fix: remove unsupported chartDirs parameter from workflow --- .github/workflows/pull_request.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 2c62401f..7caddf3e 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -6,5 +6,3 @@ jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 secrets: inherit - with: - chartDirs: 'helm' From 69caf682b5d4c31d142515034790af5e509c3911 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:53:00 +0200 Subject: [PATCH 07/19] style: fix prettier formatting --- .github/workflows/pull_request.yaml | 2 ++ src/ingestion/errors/ingestionErrors.ts | 8 +------- src/ingestion/models/ingestionManager.ts | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 7caddf3e..2c62401f 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -6,3 +6,5 @@ jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 secrets: inherit + with: + chartDirs: 'helm' diff --git a/src/ingestion/errors/ingestionErrors.ts b/src/ingestion/errors/ingestionErrors.ts index 5755fd3b..ccc52a59 100644 --- a/src/ingestion/errors/ingestionErrors.ts +++ b/src/ingestion/errors/ingestionErrors.ts @@ -43,13 +43,7 @@ export class ValidationError extends Error { } } -export function throwInvalidJobStatusError( - operation: string, - jobId: string, - currentStatus: OperationStatus, - logger: Logger, - span?: Span -): never { +export function throwInvalidJobStatusError(operation: string, jobId: string, currentStatus: OperationStatus, logger: Logger, span?: Span): never { const message = `Cannot ${operation} job with id: ${jobId} because its status is: ${currentStatus}`; logger.error({ msg: message, jobId, currentStatus }); diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index bafe381d..8b446dce 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -706,7 +706,7 @@ export class IngestionManager { const tasks = await this.jobManagerWrapper.getTasksForJob(jobId); const hasFinalize = tasks.some((task) => task.type === this.finalizeTaskType); - + if (hasFinalize) { const errorMessage = `cannot abort job ${jobId} - job already in finalization stage and cannot be aborted`; this.logger.error({ msg: errorMessage, logContext: logCtx, jobId }); From 95461c81ec0600cfc500e752ad89757e6cd1dec1 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:25:52 +0200 Subject: [PATCH 08/19] chore: comment out unsupported chartDirs parameter --- .github/workflows/pull_request.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 2c62401f..3c44566d 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -6,5 +6,6 @@ jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 secrets: inherit - with: - chartDirs: 'helm' + # with: + # chartDirs: 'helm' + \ No newline at end of file From 100b277481163a1e37c3fa6df69294da5b7dc72d Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:36:13 +0200 Subject: [PATCH 09/19] fix: import IngestionOperation as value, not type --- src/ingestion/models/ingestionManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 8b446dce..88a2fd9e 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -39,7 +39,8 @@ import { getShapefileFiles } from '../../utils/shapefile'; import { ZodValidator } from '../../utils/validation/zodValidator'; import { ValidateManager } from '../../validate/models/validateManager'; import { ChecksumError, throwInvalidJobStatusError } from '../errors/ingestionErrors'; -import type { IngestionBaseJobParams, IngestionOperation, ResponseId } from '../interfaces'; +import { IngestionOperation } from '../interfaces'; +import type { IngestionBaseJobParams, ResponseId } from '../interfaces'; import type { RasterLayerMetadata } from '../schemas/layerCatalogSchema'; import type { IngestionNewLayer } from '../schemas/newLayerSchema'; import type { IngestionUpdateLayer } from '../schemas/updateLayerSchema'; From b5231cc9af36fe1eab75dc6cfecc23f0ffe5af67 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:50:55 +0200 Subject: [PATCH 10/19] fix: remove unused BadRequestError import and restore chartDirs --- .github/workflows/pull_request.yaml | 4 ++-- src/ingestion/controllers/ingestionController.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 3c44566d..127dd0f4 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -6,6 +6,6 @@ jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 secrets: inherit - # with: - # chartDirs: 'helm' + with: + chartDirs: 'helm' \ No newline at end of file diff --git a/src/ingestion/controllers/ingestionController.ts b/src/ingestion/controllers/ingestionController.ts index 71b04ab4..9a725460 100644 --- a/src/ingestion/controllers/ingestionController.ts +++ b/src/ingestion/controllers/ingestionController.ts @@ -1,4 +1,4 @@ -import { BadRequestError, ConflictError, NotFoundError } from '@map-colonies/error-types'; +import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { RequestHandler } from 'express'; import { HttpError } from 'express-openapi-validator/dist/framework/types'; import { StatusCodes } from 'http-status-codes'; From fe85cead5bb37f141b819bc073089654f2198f01 Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Tue, 27 Jan 2026 02:56:32 +0200 Subject: [PATCH 11/19] fix: remove chartDirs parameter - not supported in shared workflow v5 --- .github/workflows/pull_request.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 127dd0f4..521367a3 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -5,7 +5,4 @@ on: [pull_request] jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 - secrets: inherit - with: - chartDirs: 'helm' - \ No newline at end of file + secrets: inherit \ No newline at end of file From e2ce0de4e6ccedc80873107e03ac80871b5bb678 Mon Sep 17 00:00:00 2001 From: shlomiko Date: Tue, 27 Jan 2026 10:57:17 +0200 Subject: [PATCH 12/19] chore: revert with chartDirs for test --- .github/workflows/pull_request.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 521367a3..01521628 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -5,4 +5,7 @@ on: [pull_request] jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 - secrets: inherit \ No newline at end of file + secrets: inherit + + with: + chartDirs: 'helm' \ No newline at end of file From adef95ec14b39f69bc1d9af6ccccbde8492f513b Mon Sep 17 00:00:00 2001 From: shlomiko Date: Tue, 27 Jan 2026 11:02:05 +0200 Subject: [PATCH 13/19] chore: removed chartDirs from pull_request workflow --- .github/workflows/pull_request.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 01521628..521367a3 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -5,7 +5,4 @@ on: [pull_request] jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 - secrets: inherit - - with: - chartDirs: 'helm' \ No newline at end of file + secrets: inherit \ No newline at end of file From 168593a16f5d950b58954dc67d73752d9fdd2008 Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:36:59 +0200 Subject: [PATCH 14/19] feat: abort deployment tests (MAPCO-8328) (#60) * test: add unit tests for abort deployment functionality * fix: remove unused isJobAbortableSpy * fix: return empty array from get task stub * chore: removed unnecessary tests and rearranged in it each fromat * chore: fixed tests description * chore: fixed untracked jobs unit tests * chore: fixed actions bad pattern * chore: removed unnecessary BadRequestError * chore: merge invalid status tests and use mock generator * chore: fixed bad jobmanager tests patterns * test: add job manager wrapper integration tests * chore: removed unnecessary integration tests * chore: fixed lint problems * test: fixed wrong http status code * test: add bad and sad abort integration tests * test: fixed infocontroller unit tests * chore: fix uncovered vlaidation unit tests * chore: removed unnecessary controllers unit testing due to prototype instance of conflict * chore: refactor abort mocks and extract abort statuses as const * test: adjust generateMockJob + abort expectations * test: removed unnecessary as const definition * style: fix lint errors * style: removed unused import * chore: fix eslint warnings --------- Co-authored-by: shlomiko --- package-lock.json | 4 - src/info/controllers/infoController.ts | 10 - src/ingestion/errors/ingestionErrors.ts | 4 +- src/ingestion/models/ingestionManager.ts | 5 +- .../controllers/validateController.ts | 5 - .../helpers/ingestionRequestSender.ts | 4 + tests/integration/ingestion/ingestion.spec.ts | 329 ++++++++---------- .../ingestion/jobManagerWrapper.spec.ts | 83 +++++ tests/mocks/mockFactory.ts | 35 +- .../ingestion/models/ingestionManager.spec.ts | 263 +++++++++++--- 10 files changed, 486 insertions(+), 256 deletions(-) create mode 100644 tests/integration/ingestion/jobManagerWrapper.spec.ts diff --git a/package-lock.json b/package-lock.json index 8b4cbe56..123e895e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4724,8 +4724,6 @@ }, "node_modules/@map-colonies/raster-shared": { "version": "7.8.0-alpha.1", - "resolved": "https://registry.npmjs.org/@map-colonies/raster-shared/-/raster-shared-7.8.0-alpha.1.tgz", - "integrity": "sha512-W//FWIGVQP674Siw2erS29edAlxv7LCGfCuMDeb+27W1SyY+saTwLHVVYGL20UQ++lMuLkmbqCtGcJxIKoX3oQ==", "license": "ISC", "dependencies": { "@map-colonies/mc-priority-queue": "^8.2.1", @@ -23360,8 +23358,6 @@ }, "node_modules/xxhash-wasm": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", - "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, "node_modules/y18n": { diff --git a/src/info/controllers/infoController.ts b/src/info/controllers/infoController.ts index 88f0d85c..186bff67 100644 --- a/src/info/controllers/infoController.ts +++ b/src/info/controllers/infoController.ts @@ -1,8 +1,6 @@ import { RequestHandler } from 'express'; -import { HttpError } from 'express-openapi-validator/dist/framework/types'; import { StatusCodes } from 'http-status-codes'; import { inject, injectable } from 'tsyringe'; -import { FileNotFoundError, GdalInfoError } from '../../ingestion/errors/ingestionErrors'; import { InfoData } from '../../ingestion/schemas/infoDataSchema'; import { GpkgInputFiles, INGESTION_SCHEMAS_VALIDATOR_SYMBOL, SchemasValidator } from '../../utils/validation/schemasValidator'; import { InfoManager } from '../models/infoManager'; @@ -23,14 +21,6 @@ export class InfoController { res.status(StatusCodes.OK).send(filesGdalInfoData); } catch (err) { - if (err instanceof FileNotFoundError) { - (err as HttpError).status = StatusCodes.NOT_FOUND; - } - - if (err instanceof GdalInfoError) { - (err as HttpError).status = StatusCodes.UNPROCESSABLE_ENTITY; - } - next(err); } }; diff --git a/src/ingestion/errors/ingestionErrors.ts b/src/ingestion/errors/ingestionErrors.ts index ccc52a59..1cd566b8 100644 --- a/src/ingestion/errors/ingestionErrors.ts +++ b/src/ingestion/errors/ingestionErrors.ts @@ -1,6 +1,6 @@ import { OperationStatus } from '@map-colonies/mc-priority-queue'; import { Logger } from '@map-colonies/js-logger'; -import { BadRequestError, NotFoundError } from '@map-colonies/error-types'; +import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { Span } from '@opentelemetry/api'; export class UnsupportedEntityError extends Error { @@ -48,7 +48,7 @@ export function throwInvalidJobStatusError(operation: string, jobId: string, cur logger.error({ msg: message, jobId, currentStatus }); - const error = new BadRequestError(message); + const error = new ConflictError(message); span?.setAttribute('exception.type', error.status); throw error; } diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 88a2fd9e..aea34b60 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -1,6 +1,6 @@ import { relative } from 'node:path'; import { randomUUID } from 'node:crypto'; -import { BadRequestError, ConflictError, NotFoundError } from '@map-colonies/error-types'; +import { ConflictError, NotFoundError } from '@map-colonies/error-types'; import { Logger } from '@map-colonies/js-logger'; import { IFindJobsByCriteriaBody, @@ -691,7 +691,6 @@ export class IngestionManager { return !invalidStatuses.includes(status); } - @withSpanAsyncV4 public async abortIngestion(jobId: string): Promise { const logCtx: LogContext = { ...this.logContext, function: this.abortIngestion.name }; const activeSpan = trace.getActiveSpan(); @@ -712,7 +711,7 @@ export class IngestionManager { const errorMessage = `cannot abort job ${jobId} - job already in finalization stage and cannot be aborted`; this.logger.error({ msg: errorMessage, logContext: logCtx, jobId }); activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage }); - throw new BadRequestError(errorMessage); + throw new ConflictError(errorMessage); } this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); diff --git a/src/validate/controllers/validateController.ts b/src/validate/controllers/validateController.ts index b6cc33e4..ed7b186f 100644 --- a/src/validate/controllers/validateController.ts +++ b/src/validate/controllers/validateController.ts @@ -1,8 +1,6 @@ import { RequestHandler } from 'express'; -import type { HttpError } from 'express-openapi-validator/dist/framework/types'; import { StatusCodes } from 'http-status-codes'; import { inject, injectable } from 'tsyringe'; -import { FileNotFoundError } from '../../ingestion/errors/ingestionErrors'; import { INGESTION_SCHEMAS_VALIDATOR_SYMBOL, SchemasValidator } from '../../utils/validation/schemasValidator'; import type { ValidateGpkgsResponse } from '../interfaces'; import { ValidateManager } from '../models/validateManager'; @@ -22,9 +20,6 @@ export class ValidateController { const response = await this.validateManager.validateGpkgs(validGpkgInputFilesRequestBody); res.status(StatusCodes.OK).send(response); } catch (error) { - if (error instanceof FileNotFoundError) { - (error as HttpError).status = StatusCodes.NOT_FOUND; //404 - } next(error); } }; diff --git a/tests/integration/ingestion/helpers/ingestionRequestSender.ts b/tests/integration/ingestion/helpers/ingestionRequestSender.ts index c7273494..24294824 100644 --- a/tests/integration/ingestion/helpers/ingestionRequestSender.ts +++ b/tests/integration/ingestion/helpers/ingestionRequestSender.ts @@ -16,4 +16,8 @@ export class IngestionRequestSender { public async retryIngestion(jobId: string): Promise { return supertest.agent(this.app).put(`/ingestion/${jobId}/retry`).set('Content-Type', 'application/json'); } + + public async abortIngestion(jobId: string): Promise { + return supertest.agent(this.app).put(`/ingestion/${jobId}/abort`).set('Content-Type', 'application/json'); + } } diff --git a/tests/integration/ingestion/ingestion.spec.ts b/tests/integration/ingestion/ingestion.spec.ts index c9098ba1..7a5bb41c 100644 --- a/tests/integration/ingestion/ingestion.spec.ts +++ b/tests/integration/ingestion/ingestion.spec.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import { faker } from '@faker-js/faker'; -import { OperationStatus, type ICreateJobResponse } from '@map-colonies/mc-priority-queue'; +import { IJobResponse, OperationStatus, type ICreateJobResponse } from '@map-colonies/mc-priority-queue'; import { CORE_VALIDATIONS, getMapServingLayerName, RasterProductTypes } from '@map-colonies/raster-shared'; import { SqliteError } from 'better-sqlite3'; import httpStatusCodes from 'http-status-codes'; @@ -22,6 +22,7 @@ import { createUpdateJobRequest, createUpdateLayerRequest, generateCallbackUrl, + generateMockJob, rasterLayerInputFilesGenerators, rasterLayerMetadataGenerators, } from '../../mocks/mockFactory'; @@ -1562,21 +1563,32 @@ describe('Ingestion', () => { productShapefilePath: `product/${validInputFiles.inputFiles.productShapefilePath}/Product.shp`, }; + const createRetryJob = (options: { + jobId: string; + productId: string; + productType: RasterProductTypes; + status: OperationStatus; + inputFiles?: unknown; + }): IJobResponse => { + const { jobId, productId, productType, status, inputFiles = storedInputFiles } = options; + return generateMockJob({ + id: jobId, + resourceId: productId, + productType, + status, + parameters: { + inputFiles, + }, + }); + }; + describe('Happy Path', () => { it('should return 200 status code when validation is valid and job is FAILED - easy reset job', async () => { const jobId = faker.string.uuid(); const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const validationTask = { id: taskId, jobId, @@ -1604,15 +1616,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.SUSPENDED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.SUSPENDED }); const validationTask = { id: taskId, jobId, @@ -1640,15 +1644,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); // Simulate old state with fewer checksums (3 items) - new files were added const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { @@ -1690,114 +1686,74 @@ describe('Ingestion', () => { }); describe('Bad Path', () => { - it('should return 400 BAD_REQUEST status code when job is in PENDING status', async () => { + it('should return 409 CONFLICT status code when job is in PENDING status', async () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.PENDING, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.PENDING }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); const response = await requestSender.retryIngestion(jobId); expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.status).toBe(httpStatusCodes.CONFLICT); }); - it('should return 400 BAD_REQUEST status code when job is in IN_PROGRESS status', async () => { + it('should return 409 CONFLICT status code when job is in IN_PROGRESS status', async () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.IN_PROGRESS, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.IN_PROGRESS }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); const response = await requestSender.retryIngestion(jobId); expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.status).toBe(httpStatusCodes.CONFLICT); }); - it('should return 400 BAD_REQUEST status code when job is in COMPLETED status', async () => { + it('should return 409 CONFLICT status code when job is in COMPLETED status', async () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.COMPLETED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.COMPLETED }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); const response = await requestSender.retryIngestion(jobId); expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.status).toBe(httpStatusCodes.CONFLICT); }); - it('should return 400 BAD_REQUEST status code when job is in EXPIRED status', async () => { + it('should return 409 CONFLICT status code when job is in EXPIRED status', async () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.EXPIRED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.EXPIRED }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); const response = await requestSender.retryIngestion(jobId); expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.status).toBe(httpStatusCodes.CONFLICT); }); - it('should return 400 BAD_REQUEST status code when job is in ABORTED status', async () => { + it('should return 409 CONFLICT status code when job is in ABORTED status', async () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.ABORTED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.ABORTED }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); const response = await requestSender.retryIngestion(jobId); expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.status).toBe(httpStatusCodes.CONFLICT); }); }); @@ -1806,15 +1762,7 @@ describe('Ingestion', () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const otherTask = { id: faker.string.uuid(), jobId, @@ -1838,15 +1786,7 @@ describe('Ingestion', () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); nock(jobManagerURL).get(`/jobs/${jobId}/tasks`).reply(httpStatusCodes.OK, []); @@ -1864,15 +1804,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const validationTask = { id: taskId, jobId, @@ -1901,15 +1833,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const validationTask = { id: taskId, jobId, @@ -1937,18 +1861,16 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, + const retryJob = createRetryJob({ + jobId, + productId, productType, status: OperationStatus.FAILED, - parameters: { - inputFiles: { - // Invalid structure - missing required fields - invalidField: 'invalid', - }, + inputFiles: { + // Invalid structure - missing required fields + invalidField: 'invalid', }, - }; + }); const validationTask = { id: taskId, jobId, @@ -1989,15 +1911,7 @@ describe('Ingestion', () => { const jobId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, retryJob); nock(jobManagerURL).get(`/jobs/${jobId}/tasks`).reply(httpStatusCodes.INTERNAL_SERVER_ERROR); @@ -2013,15 +1927,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { id: taskId, @@ -2050,15 +1956,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { id: taskId, @@ -2101,15 +1999,7 @@ describe('Ingestion', () => { const taskId = faker.string.uuid(); const productId = rasterLayerMetadataGenerators.productId(); const productType = rasterLayerMetadataGenerators.productType(); - const retryJob = { - id: jobId, - resourceId: productId, - productType, - status: OperationStatus.FAILED, - parameters: { - inputFiles: storedInputFiles, - }, - }; + const retryJob = createRetryJob({ jobId, productId, productType, status: OperationStatus.FAILED }); // Simulate old state with fewer checksums (3 items) - new files were added const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { @@ -2146,15 +2036,13 @@ describe('Ingestion', () => { metadataShapefilePath: 'metadata/nonexistent-shapefile/ShapeMetadata.shp', productShapefilePath: `product/${validInputFiles.inputFiles.productShapefilePath}/Product.shp`, }; - const retryJob = { - id: jobId, - resourceId: productId, + const retryJob = createRetryJob({ + jobId, + productId, productType, status: OperationStatus.FAILED, - parameters: { - inputFiles: nonExistentInputFiles, - }, - }; + inputFiles: nonExistentInputFiles, + }); const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { id: taskId, @@ -2189,15 +2077,13 @@ describe('Ingestion', () => { metadataShapefilePath: `metadata/${validInputFiles.inputFiles.metadataShapefilePath}/ShapeMetadata.shp`, productShapefilePath: `product/${validInputFiles.inputFiles.productShapefilePath}/Product.shp`, }; - const retryJob = { - id: jobId, - resourceId: productId, + const retryJob = createRetryJob({ + jobId, + productId, productType, status: OperationStatus.FAILED, - parameters: { - inputFiles: nonExistentInputFiles, - }, - }; + inputFiles: nonExistentInputFiles, + }); const oldChecksums = validInputFiles.checksums.slice(0, 3); const validationTask = { id: taskId, @@ -2223,4 +2109,91 @@ describe('Ingestion', () => { }); }); }); + + describe('PUT /ingestion/:jobId/abort', () => { + const abortableStatuses = [OperationStatus.FAILED, OperationStatus.SUSPENDED, OperationStatus.IN_PROGRESS, OperationStatus.PENDING]; + const nonAbortableStatuses = [OperationStatus.COMPLETED, OperationStatus.ABORTED]; + + describe('Happy Path', () => { + it.each(abortableStatuses)('should return 200 status code when aborting job with %s status', async (status) => { + const mockJob = generateMockJob({ status }); + const tasks = [ + { id: faker.string.uuid(), type: 'validation', status: OperationStatus.COMPLETED }, + { id: faker.string.uuid(), type: 'init', status: OperationStatus.COMPLETED }, + ]; + + nock(jobManagerURL).get(`/jobs/${mockJob.id}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, mockJob); + nock(jobManagerURL).get(`/jobs/${mockJob.id}/tasks`).reply(httpStatusCodes.OK, tasks); + nock(jobManagerURL).post(`/tasks/abort/${mockJob.id}`).reply(httpStatusCodes.OK); + nock(polygonPartsManagerURL) + .delete('/polygonParts/validate') + .query({ productType: mockJob.productType, productId: mockJob.resourceId }) + .reply(httpStatusCodes.NO_CONTENT); + + const response = await requestSender.abortIngestion(mockJob.id); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + }); + + it('should return 200 status code when aborting job with no tasks', async () => { + const mockJob = generateMockJob({ status: OperationStatus.FAILED }); + + nock(jobManagerURL).get(`/jobs/${mockJob.id}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, mockJob); + nock(jobManagerURL).get(`/jobs/${mockJob.id}/tasks`).reply(httpStatusCodes.OK, []); + nock(jobManagerURL).post(`/tasks/abort/${mockJob.id}`).reply(httpStatusCodes.OK); + nock(polygonPartsManagerURL) + .delete('/polygonParts/validate') + .query({ productType: mockJob.productType, productId: mockJob.resourceId }) + .reply(httpStatusCodes.NO_CONTENT); + + const response = await requestSender.abortIngestion(mockJob.id); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + }); + }); + + describe('Bad Path', () => { + it.each(nonAbortableStatuses)('should return 409 CONFLICT status code when job is in %s status', async (status) => { + const mockJob = generateMockJob({ status }); + + nock(jobManagerURL).get(`/jobs/${mockJob.id}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, mockJob); + + const response = await requestSender.abortIngestion(mockJob.id); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.CONFLICT); + }); + + it.each(abortableStatuses)('should return 409 CONFLICT status code when job with %s status has finalize task', async (status) => { + const mockJob = generateMockJob({ status }); + const tasks = [ + { id: faker.string.uuid(), type: 'validation', status: OperationStatus.COMPLETED }, + { id: faker.string.uuid(), type: configMock.get('jobManager.finalizeTaskType'), status: OperationStatus.PENDING }, + ]; + + nock(jobManagerURL).get(`/jobs/${mockJob.id}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.OK, mockJob); + nock(jobManagerURL).get(`/jobs/${mockJob.id}/tasks`).reply(httpStatusCodes.OK, tasks); + + const response = await requestSender.abortIngestion(mockJob.id); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.CONFLICT); + }); + }); + + describe('Sad Path', () => { + it('should return 404 NOT_FOUND status code when job does not exist', async () => { + const jobId = faker.string.uuid(); + + nock(jobManagerURL).get(`/jobs/${jobId}`).query({ shouldReturnTasks: false }).reply(httpStatusCodes.NOT_FOUND); + + const response = await requestSender.abortIngestion(jobId); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.NOT_FOUND); + }); + }); + }); }); diff --git a/tests/integration/ingestion/jobManagerWrapper.spec.ts b/tests/integration/ingestion/jobManagerWrapper.spec.ts new file mode 100644 index 00000000..6c1e2a7f --- /dev/null +++ b/tests/integration/ingestion/jobManagerWrapper.spec.ts @@ -0,0 +1,83 @@ +import { faker } from '@faker-js/faker'; +import jsLogger from '@map-colonies/js-logger'; +import { trace } from '@opentelemetry/api'; +import nock from 'nock'; +import { JobManagerWrapper } from '../../../src/serviceClients/jobManagerWrapper'; +import { clear as clearConfig, configMock, registerDefaultConfig } from '../../mocks/configMock'; + +describe('jobManagerWrapper integration', () => { + let jobManagerWrapper: JobManagerWrapper; + + beforeEach(() => { + registerDefaultConfig(); + jobManagerWrapper = new JobManagerWrapper(configMock, jsLogger({ enabled: false }), trace.getTracer('testTracer')); + }); + + afterEach(() => { + nock.cleanAll(); + clearConfig(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('abortJob', () => { + let jobId: string; + + beforeEach(() => { + jobId = faker.string.uuid(); + }); + + describe('Happy Path', () => { + it('should successfully send abort request to Job Manager', async () => { + nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).reply(200); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).resolves.not.toThrow(); + }); + + it('should call correct endpoint /tasks/abort/{jobId}', async () => { + const scope = nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).reply(200); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).resolves.not.toThrow(); + expect(scope.isDone()).toBe(true); + }); + }); + + describe('Sad Path', () => { + it('should throw error when job not found', async () => { + nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).reply(404, { message: 'Job not found' }); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).rejects.toThrow(); + }); + + it('should throw error when Job Manager has internal server error', async () => { + nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).reply(500, { message: 'Internal server error' }); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).rejects.toThrow(); + }); + + it('should handle network timeout', async () => { + nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).replyWithError({ code: 'ETIMEDOUT', message: 'Timeout' }); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).rejects.toThrow(); + }); + + it('should handle connection refused', async () => { + nock('http://jobmanagerurl').post(`/tasks/abort/${jobId}`).replyWithError({ code: 'ECONNREFUSED', message: 'Connection refused' }); + + const action = async () => jobManagerWrapper.abortJob(jobId); + + await expect(action()).rejects.toThrow(); + }); + }); + }); +}); diff --git a/tests/mocks/mockFactory.ts b/tests/mocks/mockFactory.ts index e562336d..31406201 100644 --- a/tests/mocks/mockFactory.ts +++ b/tests/mocks/mockFactory.ts @@ -2,7 +2,7 @@ import { join, relative } from 'node:path'; import { faker } from '@faker-js/faker'; import { RecordType, TileOutputFormat } from '@map-colonies/mc-model-types'; -import { OperationStatus, type ICreateJobBody, type IFindJobsByCriteriaBody } from '@map-colonies/mc-priority-queue'; +import { OperationStatus, type ICreateJobBody, type IFindJobsByCriteriaBody, type IJobResponse } from '@map-colonies/mc-priority-queue'; import { Checksum, CORE_VALIDATIONS, @@ -17,6 +17,7 @@ import { type InputFiles, type NewRasterLayerMetadata, type UpdateRasterLayerMetadata, + JobTypes, } from '@map-colonies/raster-shared'; import { Domain, RecordStatus, TilesMimeFormat } from '@map-colonies/types'; import { randomPolygon } from '@turf/turf'; @@ -228,6 +229,38 @@ export const generateChecksum = (): Checksum => { export const generateCallbackUrl = (): CallbackUrlsTargetArray[number] => faker.internet.url({ protocol: faker.helpers.arrayElement(['http', 'https']) }); +export const generateMockJob = (overrides: Partial> = {}): IJobResponse => { + const defaults: IJobResponse = { + id: faker.string.uuid(), + resourceId: rasterLayerMetadataGenerators.productId(), + version: rasterLayerMetadataGenerators.productVersion(), + type: JobTypes.Ingestion_New, + domain: Domain.RASTER, + productName: rasterLayerMetadataGenerators.productName(), + productType: rasterLayerMetadataGenerators.productType(), + status: faker.helpers.enumValue(OperationStatus), + created: faker.date.past().toISOString(), + updated: faker.date.recent().toISOString(), + priority: faker.number.int({ min: 0, max: 5 }), + internalId: faker.string.uuid(), + producerName: rasterLayerMetadataGenerators.producerName(), + parameters: {}, + percentage: faker.number.int({ min: 0, max: 100 }), + taskCount: 1, + completedTasks: 0, + inProgressTasks: 0, + failedTasks: 0, + pendingTasks: 1, + expiredTasks: 0, + abortedTasks: 0, + description: '', + reason: '', + isCleaned: false, + }; + + return { ...defaults, ...overrides }; +}; + export const rasterLayerMetadataGenerators: RasterLayerMetadataPropertiesGenerators = { id: (): string => faker.string.uuid(), classification: (): string => faker.number.int({ max: 100 }).toString(), diff --git a/tests/unit/ingestion/models/ingestionManager.spec.ts b/tests/unit/ingestion/models/ingestionManager.spec.ts index 8658d196..be2b5041 100644 --- a/tests/unit/ingestion/models/ingestionManager.spec.ts +++ b/tests/unit/ingestion/models/ingestionManager.spec.ts @@ -22,9 +22,9 @@ import { clear as clearConfig, configMock, registerDefaultConfig } from '../../. import { generateCatalogLayerResponse, generateChecksum, + generateMockJob, generateNewLayerRequest, generateUpdateLayerRequest, - rasterLayerMetadataGenerators, } from '../../../mocks/mockFactory'; import { ChecksumProcessor } from '../../../../src/utils/hash/interfaces'; import { CHECKSUM_PROCESSOR } from '../../../../src/utils/hash/constants'; @@ -532,11 +532,9 @@ describe('IngestionManager', () => { it('should reset job when validation task has no errors and job is Failed', async () => { const jobId = faker.string.uuid(); const taskId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -544,7 +542,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -571,11 +569,9 @@ describe('IngestionManager', () => { it('should reset job when job status is SUSPENDED and validation passed', async () => { const jobId = faker.string.uuid(); const taskId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.SUSPENDED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -583,7 +579,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -608,16 +604,14 @@ describe('IngestionManager', () => { }); it('should update task with new checksums when shapefile has changed and job is SUSPENDED', async () => { - const jobId = faker.string.uuid(); - const taskId = faker.string.uuid(); const oldChecksum = 'oldChecksum123'; const newChecksum = 'newChecksum456'; + const jobId = faker.string.uuid(); + const taskId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.SUSPENDED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -625,7 +619,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -683,16 +677,14 @@ describe('IngestionManager', () => { }); it('should update task with new checksums when shapefile has changed and job is FAILED', async () => { - const jobId = faker.string.uuid(); - const taskId = faker.string.uuid(); const oldChecksum = 'oldChecksum123'; const newChecksum = 'newChecksum456'; + const jobId = faker.string.uuid(); + const taskId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -700,7 +692,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -758,15 +750,13 @@ describe('IngestionManager', () => { }); it('should throw ConflictError when shapefile has not changed', async () => { + const existingChecksum = { fileName: 'metadata.shp', checksum: 'sameChecksum123' }; const jobId = faker.string.uuid(); const taskId = faker.string.uuid(); - const existingChecksum = { fileName: 'metadata.shp', checksum: 'sameChecksum123' }; - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -774,7 +764,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -800,12 +790,9 @@ describe('IngestionManager', () => { it('should throw BadRequestError when metadataShapefilePath is missing', async () => { const jobId = faker.string.uuid(); const taskId = faker.string.uuid(); - - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -813,7 +800,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockValidationTask = { id: taskId, jobId, @@ -835,13 +822,11 @@ describe('IngestionManager', () => { expect(resetJobSpy).not.toHaveBeenCalled(); }); - it('should throw BadRequestError when job status is PENDING', async () => { + it('should throw ConflictError when job status is PENDING', async () => { const jobId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.PENDING, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -849,23 +834,21 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); getJobSpy.mockResolvedValue(mockJob); - await expect(ingestionManager.retryIngestion(jobId)).rejects.toThrow(BadRequestError); + await expect(ingestionManager.retryIngestion(jobId)).rejects.toThrow(ConflictError); expect(getTasksForJobSpy).not.toHaveBeenCalled(); expect(updateTaskSpy).not.toHaveBeenCalled(); expect(resetJobSpy).not.toHaveBeenCalled(); }); - it('should throw BadRequestError when job status is IN_PROGRESS', async () => { + it('should throw ConflictError when job status is IN_PROGRESS', async () => { const jobId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.IN_PROGRESS, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -873,22 +856,20 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); getJobSpy.mockResolvedValue(mockJob); - await expect(ingestionManager.retryIngestion(jobId)).rejects.toThrow(BadRequestError); + await expect(ingestionManager.retryIngestion(jobId)).rejects.toThrow(ConflictError); expect(updateTaskSpy).not.toHaveBeenCalled(); expect(resetJobSpy).not.toHaveBeenCalled(); }); it('should throw NotFoundError when validation task is not found', async () => { const jobId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -896,7 +877,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); getJobSpy.mockResolvedValue(mockJob); getTasksForJobSpy.mockResolvedValue([]); @@ -908,12 +889,9 @@ describe('IngestionManager', () => { it('should find validation task among multiple tasks', async () => { const jobId = faker.string.uuid(); - const taskId = faker.string.uuid(); - const mockJob = { + const mockJob = generateMockJob({ id: jobId, status: OperationStatus.FAILED, - productType: 'Orthophoto', - resourceId: rasterLayerMetadataGenerators.productId(), parameters: { inputFiles: { gpkgFilesPath: ['/path/to/file.gpkg'], @@ -921,7 +899,7 @@ describe('IngestionManager', () => { productShapefilePath: '/path/to/product.shp', }, }, - }; + }); const mockTasks = [ { id: faker.string.uuid(), @@ -931,7 +909,7 @@ describe('IngestionManager', () => { parameters: {}, }, { - id: taskId, + id: faker.string.uuid(), jobId, type: 'validation', status: OperationStatus.COMPLETED, @@ -954,4 +932,183 @@ describe('IngestionManager', () => { expect(resetJobSpy).toHaveBeenCalledWith(jobId); }); }); + + describe('abortIngestion', () => { + let getJobSpy: jest.SpyInstance; + let getTasksForJobSpy: jest.SpyInstance; + let abortJobSpy: jest.SpyInstance; + + beforeEach(() => { + getJobSpy = jest.spyOn(JobManagerWrapper.prototype, 'getJob'); + getTasksForJobSpy = jest.spyOn(JobManagerWrapper.prototype, 'getTasksForJob'); + abortJobSpy = jest.spyOn(JobManagerWrapper.prototype, 'abortJob'); + mockPolygonPartsManagerClient.deleteValidationEntity.mockClear(); + }); + + it.each([[OperationStatus.FAILED], [OperationStatus.SUSPENDED], [OperationStatus.IN_PROGRESS], [OperationStatus.PENDING]])( + 'should successfully abort job with status %s', + async (status) => { + const mockJob = generateMockJob({ status }); + const mockTasks = [ + { id: faker.string.uuid(), type: 'validation', status: OperationStatus.COMPLETED }, + { id: faker.string.uuid(), type: 'create-tasks', status: OperationStatus.FAILED }, + ]; + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue(mockTasks); + abortJobSpy.mockResolvedValue(undefined); + mockPolygonPartsManagerClient.deleteValidationEntity.mockResolvedValue(undefined); + + await ingestionManager.abortIngestion(mockJob.id); + + expect(getJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(getTasksForJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(abortJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledWith(mockJob.resourceId, mockJob.productType); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledTimes(1); + } + ); + + it('should successfully abort when no tasks exist for the requested job', async () => { + const mockJob = generateMockJob({ status: OperationStatus.FAILED }); + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue([]); + abortJobSpy.mockResolvedValue(undefined); + mockPolygonPartsManagerClient.deleteValidationEntity.mockResolvedValue(undefined); + + await ingestionManager.abortIngestion(mockJob.id); + + expect(getJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(getTasksForJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(abortJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledWith(mockJob.resourceId, mockJob.productType); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledTimes(1); + }); + + it.each([[OperationStatus.COMPLETED], [OperationStatus.ABORTED]])('should reject abort for job with invalid status %s', async (status) => { + const mockJob = generateMockJob({ status }); + + getJobSpy.mockResolvedValue(mockJob); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(ConflictError); + expect(getTasksForJobSpy).not.toHaveBeenCalled(); + expect(abortJobSpy).not.toHaveBeenCalled(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + }); + + it.each([[OperationStatus.FAILED], [OperationStatus.SUSPENDED], [OperationStatus.IN_PROGRESS], [OperationStatus.PENDING]])( + 'should throw Conflict Error when finalize task exists for job with status %s', + async (status) => { + const finalizeTaskId = faker.string.uuid(); + const mockJob = generateMockJob({ status }); + const mockTasks = [ + { id: faker.string.uuid(), type: 'validation', status: OperationStatus.COMPLETED }, + { id: finalizeTaskId, type: 'finalize', status: OperationStatus.PENDING }, + ]; + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue(mockTasks); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(ConflictError); + expect(abortJobSpy).not.toHaveBeenCalled(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + } + ); + + it.each([['validation'], ['create-tasks'], ['merge']])( + 'should allow abort when only non-finalize tasks exist (task type: %s)', + async (taskType) => { + const mockJob = generateMockJob({ status: OperationStatus.FAILED }); + const mockTasks = [{ id: faker.string.uuid(), type: taskType, status: OperationStatus.COMPLETED }]; + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue(mockTasks); + abortJobSpy.mockResolvedValue(undefined); + mockPolygonPartsManagerClient.deleteValidationEntity.mockResolvedValue(undefined); + + await ingestionManager.abortIngestion(mockJob.id); + + expect(getJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(getTasksForJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(abortJobSpy).toHaveBeenCalledWith(mockJob.id); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledWith(mockJob.resourceId, mockJob.productType); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).toHaveBeenCalledTimes(1); + } + ); + + it('should throw NotFoundError when job does not exist', async () => { + const jobId = faker.string.uuid(); + getJobSpy.mockRejectedValue(new NotFoundError('Job not found')); + + const action = ingestionManager.abortIngestion(jobId); + + await expect(action).rejects.toThrow(NotFoundError); + expect(getTasksForJobSpy).not.toHaveBeenCalled(); + expect(abortJobSpy).not.toHaveBeenCalled(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + }); + + it.each([[OperationStatus.FAILED], [OperationStatus.SUSPENDED], [OperationStatus.IN_PROGRESS], [OperationStatus.PENDING]])( + 'should throw error when job has invalid productId for status %s', + async (status) => { + const mockJob = generateMockJob({ resourceId: undefined, status }); + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue([]); + abortJobSpy.mockResolvedValue(undefined); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + } + ); + + it.each([[OperationStatus.FAILED], [OperationStatus.SUSPENDED], [OperationStatus.IN_PROGRESS], [OperationStatus.PENDING]])( + 'should throw error when job has invalid productType for status %s', + async (status) => { + const mockJob = generateMockJob({ productType: undefined, status }); + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue([]); + abortJobSpy.mockResolvedValue(undefined); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + } + ); + + it('should propagate error when Job Manager abort fails', async () => { + const mockJob = generateMockJob({ status: OperationStatus.FAILED }); + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockResolvedValue([]); + abortJobSpy.mockRejectedValue(new Error('Job Manager failed')); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(Error); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + }); + + it('should handle getTasksForJob failure', async () => { + const mockJob = generateMockJob({ status: OperationStatus.FAILED }); + + getJobSpy.mockResolvedValue(mockJob); + getTasksForJobSpy.mockRejectedValue(new Error('Failed to fetch tasks')); + + const action = ingestionManager.abortIngestion(mockJob.id); + + await expect(action).rejects.toThrow(Error); + expect(abortJobSpy).not.toHaveBeenCalled(); + expect(mockPolygonPartsManagerClient.deleteValidationEntity).not.toHaveBeenCalled(); + }); + }); }); From d99b30b13217b1b362eec21459098cdf3630011f Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:05:37 +0200 Subject: [PATCH 15/19] fix: move abort log before abortJob call and update message --- package-lock.json | 592 +++++++++++++++-------- src/ingestion/models/ingestionManager.ts | 2 +- 2 files changed, 380 insertions(+), 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index 123e895e..3a0f9add 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2545,6 +2545,8 @@ }, "node_modules/@humanwhocodes/momoa": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2571,7 +2573,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -2660,6 +2664,8 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -3116,6 +3122,8 @@ }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", "dev": true, "license": "MIT", "engines": { @@ -3127,6 +3135,8 @@ }, "node_modules/@jsep-plugin/regex": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "dev": true, "license": "MIT", "engines": { @@ -5183,71 +5193,88 @@ } }, "node_modules/@npmcli/agent": { - "version": "3.0.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", + "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "license": "ISC" + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@npmcli/fs": { - "version": "4.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@oozcitak/dom": { - "version": "1.15.10", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", "license": "MIT", "dependencies": { - "@oozcitak/infra": "1.0.8", - "@oozcitak/url": "1.0.4", - "@oozcitak/util": "8.3.8" + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" }, "engines": { - "node": ">=8.0" + "node": ">=20.0" } }, "node_modules/@oozcitak/infra": { - "version": "1.0.8", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", "license": "MIT", "dependencies": { - "@oozcitak/util": "8.3.8" + "@oozcitak/util": "^10.0.0" }, "engines": { - "node": ">=6.0" + "node": ">=20.0" } }, "node_modules/@oozcitak/url": { - "version": "1.0.4", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", "license": "MIT", "dependencies": { - "@oozcitak/infra": "1.0.8", - "@oozcitak/util": "8.3.8" + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" }, "engines": { - "node": ">=8.0" + "node": ">=20.0" } }, "node_modules/@oozcitak/util": { - "version": "8.3.8", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", "license": "MIT", "engines": { - "node": ">=8.0" + "node": ">=20.0" } }, "node_modules/@opentelemetry/api": { @@ -7190,7 +7217,9 @@ } }, "node_modules/@redocly/cli": { - "version": "1.34.5", + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.6.tgz", + "integrity": "sha512-V03jtLOXLm6+wpTuFNw9+eLHE6R3wywZo4Clt9XMPnulafbJcpCFz+J0e5/4Cw4zZB087xjU7WvRdI/bZ+pHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -7200,8 +7229,8 @@ "@opentelemetry/sdk-trace-node": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0", "@redocly/config": "^0.22.0", - "@redocly/openapi-core": "1.34.5", - "@redocly/respect-core": "1.34.5", + "@redocly/openapi-core": "1.34.6", + "@redocly/respect-core": "1.34.6", "abort-controller": "^3.0.0", "chokidar": "^3.5.1", "colorette": "^1.2.0", @@ -7213,8 +7242,8 @@ "handlebars": "^4.7.6", "mobx": "^6.0.4", "pluralize": "^8.0.0", - "react": "^17.0.0 || ^18.2.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0", + "react": "^17.0.0 || ^18.2.0 || ^19.2.1", + "react-dom": "^17.0.0 || ^18.2.0 || ^19.2.1", "redoc": "2.5.0", "semver": "^7.5.2", "simple-websocket": "^9.0.0", @@ -7413,7 +7442,9 @@ "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.34.5", + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", + "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", "dev": true, "license": "MIT", "dependencies": { @@ -7433,13 +7464,15 @@ } }, "node_modules/@redocly/respect-core": { - "version": "1.34.5", + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.6.tgz", + "integrity": "sha512-nXFBRctoB4CPCLR2it2WxDsuAE/nLd4EnW9mQ+IUKrIFAjMv1O6rgggxkgdlyKUyenYkajJIHSKwVbRS6FwlEQ==", "dev": true, "license": "MIT", "dependencies": { "@faker-js/faker": "^7.6.0", "@redocly/ajv": "8.11.2", - "@redocly/openapi-core": "1.34.5", + "@redocly/openapi-core": "1.34.6", "better-ajv-errors": "^1.2.0", "colorette": "^2.0.20", "concat-stream": "^2.0.0", @@ -7464,6 +7497,8 @@ }, "node_modules/@redocly/respect-core/node_modules/@faker-js/faker": { "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", "dev": true, "license": "MIT", "engines": { @@ -7473,6 +7508,8 @@ }, "node_modules/@redocly/respect-core/node_modules/@redocly/ajv": { "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", "dev": true, "license": "MIT", "dependencies": { @@ -7488,11 +7525,15 @@ }, "node_modules/@redocly/respect-core/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/@redocly/respect-core/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -10241,10 +10282,12 @@ "license": "ISC" }, "node_modules/abbrev": { - "version": "3.0.1", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/abort-controller": { @@ -11064,6 +11107,8 @@ }, "node_modules/better-ajv-errors": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz", + "integrity": "sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11140,21 +11185,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -11168,10 +11215,39 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "license": "MIT", @@ -11275,6 +11351,8 @@ }, "node_modules/bundle-name": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11304,83 +11382,63 @@ } }, "node_modules/cacache": { - "version": "19.0.1", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", "license": "ISC", "dependencies": { - "@npmcli/fs": "^4.0.0", + "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "license": "ISC", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "minimatch": "^10.1.2", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "path-scurry": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/jackspeak": { - "version": "3.4.3", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "license": "ISC" - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "20 || >=22" } }, - "node_modules/cacache/node_modules/path-scurry": { - "version": "1.11.1", + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11564,6 +11622,8 @@ }, "node_modules/chownr": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -12430,7 +12490,8 @@ }, "node_modules/cookie": { "version": "0.7.2", - "dev": true, + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12938,7 +12999,9 @@ } }, "node_modules/default-browser": { - "version": "5.4.0", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -12954,6 +13017,8 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -12991,6 +13056,8 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { @@ -13080,7 +13147,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13137,6 +13206,8 @@ }, "node_modules/dotenv": { "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -13313,6 +13384,8 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" @@ -13334,6 +13407,8 @@ }, "node_modules/err-code": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, "node_modules/error-ex": { @@ -14489,6 +14564,7 @@ }, "node_modules/esprima": { "version": "4.0.1", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -14633,40 +14709,44 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.21.2", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -14710,19 +14790,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-openapi-validator/node_modules/qs": { - "version": "6.14.0", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express-prom-bundle": { "version": "6.6.0", "license": "MIT", @@ -14737,13 +14804,6 @@ "prom-client": ">=12.0.0" } }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -15179,6 +15239,8 @@ }, "node_modules/fs-minipass": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -15266,7 +15328,9 @@ } }, "node_modules/gdal-async": { - "version": "3.11.5", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/gdal-async/-/gdal-async-3.12.1.tgz", + "integrity": "sha512-KmxY5Vk571aBcoxzbSwPnXcJvYfzyEIiMMADpOuZlnXp4v7LaTlOYSqXvdzyH3xwcO8N/+265rTFv7ArPJL8Fg==", "bundleDependencies": [ "@mapbox/node-pre-gyp" ], @@ -15275,9 +15339,9 @@ "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@petamoriken/float16": "^3.9.2", - "nan": "^2.17.0", - "node-gyp": "^11.0.0", - "xmlbuilder2": "^3.0.2", + "nan": "^2.23.0", + "node-gyp": "^12.1.0", + "xmlbuilder2": "^4.0.0", "yatag": "^1.2.0" }, "engines": { @@ -16126,6 +16190,8 @@ }, "node_modules/http-cache-semantics": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -16144,6 +16210,8 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -16352,6 +16420,8 @@ }, "node_modules/ip-address": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -16512,6 +16582,8 @@ }, "node_modules/is-docker": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { @@ -16590,6 +16662,8 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { @@ -16861,6 +16935,8 @@ }, "node_modules/is-wsl": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { @@ -17803,6 +17879,8 @@ }, "node_modules/jsep": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", "engines": { @@ -17891,6 +17969,8 @@ }, "node_modules/jsonpath-plus": { "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", "dev": true, "license": "MIT", "dependencies": { @@ -17908,6 +17988,8 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, "license": "MIT", "engines": { @@ -18260,27 +18342,31 @@ "license": "ISC" }, "node_modules/make-fetch-happen": { - "version": "14.0.3", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "ssri": "^13.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/make-fetch-happen/node_modules/negotiator": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -18506,6 +18592,8 @@ }, "node_modules/minipass-collect": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" @@ -18515,15 +18603,17 @@ } }, "node_modules/minipass-fetch": { - "version": "4.0.1", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", + "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -18531,6 +18621,8 @@ }, "node_modules/minipass-flush": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -18541,6 +18633,8 @@ }, "node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -18551,10 +18645,14 @@ }, "node_modules/minipass-flush/node_modules/yallist": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/minipass-pipeline": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -18565,6 +18663,8 @@ }, "node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -18575,34 +18675,26 @@ }, "node_modules/minipass-pipeline/node_modules/yallist": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/minipass-sized": { - "version": "1.0.3", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -18842,36 +18934,42 @@ } }, "node_modules/node-gyp": { - "version": "11.5.0", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "license": "ISC", "engines": { "node": ">=16" } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -18880,7 +18978,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-int64": { @@ -18928,16 +19026,18 @@ "license": "MIT" }, "node_modules/nopt": { - "version": "8.1.0", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-package-data": { @@ -19208,6 +19308,8 @@ }, "node_modules/open": { "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { @@ -19370,6 +19472,8 @@ }, "node_modules/outdent": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", "dev": true, "license": "MIT" }, @@ -19417,6 +19521,8 @@ }, "node_modules/p-map": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "license": "MIT", "engines": { "node": ">=18" @@ -20099,10 +20205,12 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/process": { @@ -20133,6 +20241,8 @@ }, "node_modules/promise-retry": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "license": "MIT", "dependencies": { "err-code": "^2.0.2", @@ -20254,10 +20364,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -20327,18 +20439,49 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rbush": { "version": "3.0.1", "license": "MIT", @@ -20367,7 +20510,9 @@ } }, "node_modules/react": { - "version": "19.2.0", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "dev": true, "license": "MIT", "engines": { @@ -20375,14 +20520,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.0", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.4" } }, "node_modules/react-is": { @@ -20835,6 +20982,8 @@ }, "node_modules/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", "engines": { "node": ">= 4" @@ -20935,6 +21084,8 @@ }, "node_modules/run-applescript": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -21155,6 +21306,8 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "dev": true, "license": "MIT" }, @@ -21473,6 +21626,8 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -21481,6 +21636,8 @@ }, "node_modules/socks": { "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { "ip-address": "^10.0.1", @@ -21493,6 +21650,8 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -21600,16 +21759,19 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/ssri": { - "version": "12.0.0", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/stack-utils": { @@ -22190,7 +22352,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -22254,6 +22418,8 @@ }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -22390,6 +22556,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -22404,6 +22572,8 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -22419,6 +22589,8 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -22901,7 +23073,9 @@ } }, "node_modules/undici": { - "version": "6.22.0", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", "engines": { @@ -22945,23 +23119,27 @@ } }, "node_modules/unique-filename": { - "version": "4.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", "license": "ISC", "dependencies": { - "unique-slug": "^5.0.0" + "unique-slug": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/unique-slug": { - "version": "5.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/universalify": { @@ -23025,6 +23203,8 @@ }, "node_modules/uri-js-replace": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", "dev": true, "license": "MIT" }, @@ -23306,6 +23486,8 @@ }, "node_modules/wsl-utils": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", "dependencies": { @@ -23319,34 +23501,18 @@ } }, "node_modules/xmlbuilder2": { - "version": "3.1.1", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", "license": "MIT", "dependencies": { - "@oozcitak/dom": "1.15.10", - "@oozcitak/infra": "1.0.8", - "@oozcitak/util": "8.3.8", - "js-yaml": "3.14.1" + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" }, "engines": { - "node": ">=12.0" - } - }, - "node_modules/xmlbuilder2/node_modules/argparse": { - "version": "1.0.10", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/xmlbuilder2/node_modules/js-yaml": { - "version": "3.14.1", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "node": ">=20.0" } }, "node_modules/xtend": { diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index aea34b60..289cebb1 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -714,7 +714,7 @@ export class IngestionManager { throw new ConflictError(errorMessage); } - this.logger.info({ msg: 'successfully aborted ingestion job', logContext: logCtx, jobId }); + this.logger.info({ msg: 'aborting job', logContext: logCtx, jobId }); await this.jobManagerWrapper.abortJob(jobId); const { resourceId, productType } = this.parseAndValidateJobIdentifiers(job.resourceId, job.productType); From efc713ec14df2eef9ab60142586b2e0b83f0d36f Mon Sep 17 00:00:00 2001 From: roicohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:18:47 +0200 Subject: [PATCH 16/19] fix: remove unnecessary blank line in IngestionManager class --- src/ingestion/models/ingestionManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ingestion/models/ingestionManager.ts b/src/ingestion/models/ingestionManager.ts index 9a616107..80f1ec01 100644 --- a/src/ingestion/models/ingestionManager.ts +++ b/src/ingestion/models/ingestionManager.ts @@ -215,7 +215,7 @@ export class IngestionManager { const validationTask: ITaskResponse = await this.getValidationTask(jobId, logCtx); const { resourceId, productType } = this.parseAndValidateJobIdentifiers(retryJob.resourceId, retryJob.productType); await this.zodValidator.validate(ingestionValidationTaskParamsSchema, validationTask.parameters); - + if (validationTask.parameters.isValid === true) { await this.softReset(jobId, logCtx); } else { From 3e6d475f7344804650f834e3f9e89a5b178906a7 Mon Sep 17 00:00:00 2001 From: Roi Cohen <213414225+roicohen326@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:51:46 +0200 Subject: [PATCH 17/19] Fix formatting in pull_request.yaml --- .github/workflows/pull_request.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 521367a3..0c23d9b9 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -5,4 +5,5 @@ on: [pull_request] jobs: pull_request: uses: MapColonies/shared-workflows/.github/workflows/pull_request.yaml@v5 - secrets: inherit \ No newline at end of file + secrets: inherit + From 13fbe59637fa49dba9081128a0b91a7d630271bc Mon Sep 17 00:00:00 2001 From: shlomiko Date: Sun, 8 Feb 2026 10:22:11 +0200 Subject: [PATCH 18/19] fix: abort api description --- openapi3.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index cf628435..c87a9baa 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -178,7 +178,7 @@ paths: - ingestion summary: abort an active ingestion job description: >- - Aborts an ingestion job that is currently active or pending. The job cannot be aborted if it has already completed or if the finalize task has started (point of no return). + Aborts an ingestion job that is currently active or pending, preventing any further processing. parameters: - name: jobId in: path @@ -205,7 +205,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '409': - description: Conflict - Job has reached finalize task (point of no return) + description: Conflict - Unable to abort job due to current state (e.g., already completed or in finalization) content: application/json: schema: From db717a219039bed32f5a213dc85c3f5e953e3d01 Mon Sep 17 00:00:00 2001 From: shlomiko Date: Sun, 8 Feb 2026 10:24:49 +0200 Subject: [PATCH 19/19] fix: error code description --- openapi3.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index c87a9baa..2a2ce3ff 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -50,7 +50,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '500': - description: Server error + description: Internal Server Error content: application/json: schema: @@ -111,7 +111,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '500': - description: Invalid request + description: Internal Server Error content: application/json: schema: @@ -165,7 +165,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '500': - description: Internal Server error + description: Internal Server Error content: application/json: schema: @@ -219,7 +219,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/errorMessage '500': - description: Internal Server error + description: Internal Server Error content: application/json: schema: @@ -249,7 +249,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/validateSourcesResponse '400': - description: Invalid request + description: Bad Request content: application/json: schema: @@ -291,7 +291,7 @@ paths: $ref: >- ./Schema/ingestionTrigger/responses/ingestionTriggerResponses.yaml#/components/schemas/sourcesInfoResponse '400': - description: Invalid request + description: Bad Request content: application/json: schema: