From fe3b9957c97b9890d41d532e2ff89cf4127bb514 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:46:54 +0300 Subject: [PATCH 01/16] feat(jobnik-openapi): unify branded-id generation in jobnik-openapi Move Brand/JobId/StageId/TaskId from jobnik-sdk into jobnik-openapi and wire generateTypes.mts to brand jobId/stageId/taskId schema refs, so jobnik-openapi becomes the single place OpenAPI-derived types (brands included) are generated. Co-Authored-By: Claude Sonnet 5 --- CONTEXT.md | 17 +++++++++ .../jobnik-openapi/scripts/generateTypes.mts | 13 +++++++ packages/jobnik-openapi/src/brands.ts | 36 +++++++++++++++++++ packages/jobnik-openapi/src/index.ts | 1 + packages/jobnik-openapi/src/openapi.d.ts | 7 ++-- 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 CONTEXT.md create mode 100644 packages/jobnik-openapi/src/brands.ts diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..7d7c930 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,17 @@ +# Jobnik + +A job orchestration system: Jobs are composed of Stages, which are composed of Tasks. + +## Language + +**Branded ID**: +A nominally-typed wrapper (`JobId`, `StageId`, `TaskId`) around a plain `string` id, generated as part of the OpenAPI types. Its purpose is to catch a job/stage/task id being passed where a different kind of id is expected — a compile-time guard against mixing up ids, not a runtime check that the id exists or is well-formed (that's the OpenAPI request validator's job). +_Avoid_: validated ID, typed ID + +**API model**: +A type derived directly from the OpenAPI spec (`components['schemas'][...]`), describing the wire contract of a request or response. +_Avoid_: DTO, schema type + +**Persistence model**: +The Prisma-derived shape of a row as stored in the database. Deliberately not required to mirror the API model — the two are distinct representations of the same entity, and Prisma is expected to be replaced later. +_Avoid_: DB model, entity (when meaning the Prisma type specifically) diff --git a/packages/jobnik-openapi/scripts/generateTypes.mts b/packages/jobnik-openapi/scripts/generateTypes.mts index 3cc93b0..4b70863 100644 --- a/packages/jobnik-openapi/scripts/generateTypes.mts +++ b/packages/jobnik-openapi/scripts/generateTypes.mts @@ -1,4 +1,5 @@ import { generateTypes } from '@map-colonies/openapi-helpers/generators'; +import { factory } from 'typescript'; const OPENAPI_PATH = 'openapi3.yaml'; const TYPES_DESTINATION_PATH = 'src/openapi.d.ts'; @@ -6,4 +7,16 @@ const TYPES_DESTINATION_PATH = 'src/openapi.d.ts'; await generateTypes(OPENAPI_PATH, TYPES_DESTINATION_PATH, { addTypedRequestHandler: true, shouldFormat: true, + inject: 'import type { JobId, StageId, TaskId } from "./brands";', + transform(schemaObject, metadata) { + if (metadata.path === '#/components/schemas/taskId') { + return factory.createTypeReferenceNode('TaskId', undefined); + } + if (metadata.path === '#/components/schemas/jobId') { + return factory.createTypeReferenceNode('JobId', undefined); + } + if (metadata.path === '#/components/schemas/stageId') { + return factory.createTypeReferenceNode('StageId', undefined); + } + }, }); diff --git a/packages/jobnik-openapi/src/brands.ts b/packages/jobnik-openapi/src/brands.ts new file mode 100644 index 0000000..7d36021 --- /dev/null +++ b/packages/jobnik-openapi/src/brands.ts @@ -0,0 +1,36 @@ +declare const brand: unique symbol; + +type Brand = T & { [brand]: Brand }; + +/** + * Branded type for job identifiers. + * Prevents accidentally mixing job IDs with stage or task IDs at compile time. + * + * @example + * ```typescript + * const jobId = 'job-123' as JobId; + * ``` + */ +export type JobId = Brand; + +/** + * Branded type for stage identifiers. + * Prevents accidentally mixing stage IDs with job or task IDs at compile time. + * + * @example + * ```typescript + * const stageId = 'stage-456' as StageId; + * ``` + */ +export type StageId = Brand; + +/** + * Branded type for task identifiers. + * Prevents accidentally mixing task IDs with job or stage IDs at compile time. + * + * @example + * ```typescript + * const taskId = 'task-789' as TaskId; + * ``` + */ +export type TaskId = Brand; diff --git a/packages/jobnik-openapi/src/index.ts b/packages/jobnik-openapi/src/index.ts index c78af31..825f656 100644 --- a/packages/jobnik-openapi/src/index.ts +++ b/packages/jobnik-openapi/src/index.ts @@ -1,4 +1,5 @@ export type { paths, components, operations, TypedRequestHandlers } from './openapi'; +export type { JobId, StageId, TaskId } from './brands'; /** * Absolute path to the bundled OpenAPI specification file, resolved through the module diff --git a/packages/jobnik-openapi/src/openapi.d.ts b/packages/jobnik-openapi/src/openapi.d.ts index ce6309a..9068ce3 100644 --- a/packages/jobnik-openapi/src/openapi.d.ts +++ b/packages/jobnik-openapi/src/openapi.d.ts @@ -3,6 +3,7 @@ // To update, run the error generation script again. import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-helpers/typedRequestHandler'; +import type { JobId, StageId, TaskId } from './brands'; export type paths = { '/v1/jobs': { parameters: { @@ -559,7 +560,7 @@ export type components = { * Format: uuid * @description Unique identifier for a job */ - jobId: string; + jobId: JobId; /** @description Custom job configuration data containing job-specific parameters */ jobPayload: { [key: string]: unknown; @@ -574,7 +575,7 @@ export type components = { * Format: uuid * @description Unique identifier for a stage */ - stageId: string; + stageId: StageId; /** @description Custom stage configuration data containing stage-specific parameters */ stagePayload: { [key: string]: unknown; @@ -778,7 +779,7 @@ export type components = { * Format: uuid * @description Unique identifier for a task, generated by the system upon task creation */ - taskId: string; + taskId: TaskId; /** * @description Custom task configuration data containing operation-specific parameters. * The schema varies based on task type and contains all necessary information From 5f5d52960b6f8558a66d5eb727c507f0f4f30481 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:48:32 +0300 Subject: [PATCH 02/16] feat(jobnik-sdk): consume jobnik-openapi types directly Stop regenerating a duplicate openapi.ts/brands.ts and import paths/components/operations/JobId/StageId/TaskId straight from jobnik-openapi, removing the second independently-generated copy. Co-Authored-By: Claude Sonnet 5 --- packages/jobnik-sdk/package.json | 2 +- packages/jobnik-sdk/scripts/openapi.mts | 21 +- packages/jobnik-sdk/src/clients/consumer.ts | 3 +- packages/jobnik-sdk/src/clients/producer.ts | 3 +- packages/jobnik-sdk/src/clients/worker.ts | 2 +- packages/jobnik-sdk/src/index.ts | 2 +- packages/jobnik-sdk/src/types/brands.ts | 36 - packages/jobnik-sdk/src/types/consumer.ts | 2 +- packages/jobnik-sdk/src/types/index.ts | 2 +- packages/jobnik-sdk/src/types/job.ts | 2 +- packages/jobnik-sdk/src/types/openapi.ts | 2154 ----------------- packages/jobnik-sdk/src/types/producer.ts | 2 +- packages/jobnik-sdk/src/types/stage.ts | 2 +- packages/jobnik-sdk/src/types/task.ts | 2 +- packages/jobnik-sdk/src/types/worker.ts | 2 +- packages/jobnik-sdk/tests/apiClient.spec.ts | 2 +- .../tests/consumer/consumer.spec.ts | 2 +- .../tests/producer/producer.spec.ts | 2 +- .../tests/producer/producer.test-d.ts | 2 +- .../jobnik-sdk/tests/worker/worker.spec.ts | 2 +- .../jobnik-sdk/tests/worker/worker.test-d.ts | 2 +- 21 files changed, 19 insertions(+), 2230 deletions(-) delete mode 100644 packages/jobnik-sdk/src/types/brands.ts delete mode 100644 packages/jobnik-sdk/src/types/openapi.ts diff --git a/packages/jobnik-sdk/package.json b/packages/jobnik-sdk/package.json index 1efbfac..71b5d96 100644 --- a/packages/jobnik-sdk/package.json +++ b/packages/jobnik-sdk/package.json @@ -25,7 +25,7 @@ "clean": "rimraf dist", "copy:openapi": "node scripts/copySpec.mts", "generate:openapi": "node scripts/openapi.mts", - "generate:check": "npm run generate:openapi && git diff --exit-code -- src/types/openapi.ts src/generated/openapi-errors.ts", + "generate:check": "npm run generate:openapi && git diff --exit-code -- src/generated/openapi-errors.ts", "prepack": "npm run build", "type-check": "tsc --noEmit -p tsconfig.lint.json", "check-pack": "publint && attw --pack . && node scripts/checkPack.mts" diff --git a/packages/jobnik-sdk/scripts/openapi.mts b/packages/jobnik-sdk/scripts/openapi.mts index 48016a4..4ca4420 100644 --- a/packages/jobnik-sdk/scripts/openapi.mts +++ b/packages/jobnik-sdk/scripts/openapi.mts @@ -1,26 +1,7 @@ -import { generateErrors, generateTypes } from '@map-colonies/openapi-helpers/generators'; -import { factory } from 'typescript'; +import { generateErrors } from '@map-colonies/openapi-helpers/generators'; import { openapiFilePath } from 'jobnik-openapi'; const OPENAPI_PATH = openapiFilePath; -const TYPES_DESTINATION_PATH = 'src/types/openapi.ts'; const ERRORS_DESTINATION_PATH = 'src/generated/openapi-errors.ts'; -await generateTypes(OPENAPI_PATH, TYPES_DESTINATION_PATH, { - addTypedRequestHandler: false, - shouldFormat: true, - inject: 'import type { JobId, StageId, TaskId } from "./brands";', - transform(schemaObject, metadata) { - if (metadata.path === '#/components/schemas/taskId') { - return factory.createTypeReferenceNode('TaskId', undefined); - } - if (metadata.path === '#/components/schemas/jobId') { - return factory.createTypeReferenceNode('JobId', undefined); - } - if (metadata.path === '#/components/schemas/stageId') { - return factory.createTypeReferenceNode('StageId', undefined); - } - }, -}); - await generateErrors(OPENAPI_PATH, ERRORS_DESTINATION_PATH, { shouldFormat: true, includeErrorClasses: false, includeMapping: true }); diff --git a/packages/jobnik-sdk/src/clients/consumer.ts b/packages/jobnik-sdk/src/clients/consumer.ts index 1b325b8..b3951f7 100644 --- a/packages/jobnik-sdk/src/clients/consumer.ts +++ b/packages/jobnik-sdk/src/clients/consumer.ts @@ -2,10 +2,9 @@ import { SpanKind, context, propagation, trace } from '@opentelemetry/api'; import { StatusCodes } from 'http-status-codes'; import { INFRA_JOBNIK_TASK_ATTEMPTS, INFRA_JOBNIK_TASK_STATUS } from '@map-colonies/semantic-conventions'; import type { ApiClient } from '../api'; -import type { TaskId } from '../types/brands'; +import type { TaskId, components } from 'jobnik-openapi'; import type { InferTaskData, Task } from '../types/task'; import type { ValidStageType, StageTypesTemplate } from '../types/stage'; -import type { components } from '../types/openapi'; import { DEFAULT_SPAN_CONTEXT, withSpan } from '../telemetry/trace'; import { ATTR_MESSAGING_DESTINATION_NAME, ATTR_MESSAGING_MESSAGE_ID } from '../telemetry/semconv'; import type { Logger } from '../types'; diff --git a/packages/jobnik-sdk/src/clients/producer.ts b/packages/jobnik-sdk/src/clients/producer.ts index 9e01643..cf9c886 100644 --- a/packages/jobnik-sdk/src/clients/producer.ts +++ b/packages/jobnik-sdk/src/clients/producer.ts @@ -1,8 +1,7 @@ import { type Span, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; import { INFRA_JOBNIK_JOB_NAME, INFRA_JOBNIK_JOB_PRIORITY, INFRA_JOBNIK_STAGE_ID } from '@map-colonies/semantic-conventions'; import type { ApiClient } from '../api'; -import type { components } from '../types/openapi'; -import type { JobId, StageId } from '../types/brands'; +import type { components, JobId, StageId } from 'jobnik-openapi'; import type { Job, JobData, JobTypesTemplate, NewJob, ValidJobType } from '../types/job'; import type { InferStageData, NewStage, Stage, StageData, StageTypesTemplate, ValidStageType } from '../types/stage'; import type { InferTaskData, NewTask, Task } from '../types/task'; diff --git a/packages/jobnik-sdk/src/clients/worker.ts b/packages/jobnik-sdk/src/clients/worker.ts index ef149c7..6368462 100644 --- a/packages/jobnik-sdk/src/clients/worker.ts +++ b/packages/jobnik-sdk/src/clients/worker.ts @@ -13,7 +13,7 @@ import { BASE_ATTRIBUTES, tracer } from '../telemetry/trace'; import type { IWorker, TaskHandler, TaskHandlerContext, WorkerOptions } from '../types/worker'; import type { IProducer } from '../types/producer'; import { WorkerError } from '../errors/sdkErrors'; -import type { JobId, StageId, TaskId } from '../types/brands'; +import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import { CIRCUIT_BREAKER_STATES, MILLISECOND_IN_SECOND } from '../common/constants'; import type { JobnikMetrics } from '../telemetry/metrics'; import { categorizeError } from '../telemetry/metrics-utils'; diff --git a/packages/jobnik-sdk/src/index.ts b/packages/jobnik-sdk/src/index.ts index 5c5f4a9..33d125f 100644 --- a/packages/jobnik-sdk/src/index.ts +++ b/packages/jobnik-sdk/src/index.ts @@ -1,6 +1,6 @@ export type { ApiClient } from './api'; export type { HttpClientOptions } from './network/httpClient'; -export type { JobId, StageId, TaskId } from './types/brands'; +export type { JobId, StageId, TaskId } from 'jobnik-openapi'; export type { Logger } from './types'; export type { Job, NewJob, JobData } from './types/job'; export type { Stage, NewStage, StageData } from './types/stage'; diff --git a/packages/jobnik-sdk/src/types/brands.ts b/packages/jobnik-sdk/src/types/brands.ts deleted file mode 100644 index 7d36021..0000000 --- a/packages/jobnik-sdk/src/types/brands.ts +++ /dev/null @@ -1,36 +0,0 @@ -declare const brand: unique symbol; - -type Brand = T & { [brand]: Brand }; - -/** - * Branded type for job identifiers. - * Prevents accidentally mixing job IDs with stage or task IDs at compile time. - * - * @example - * ```typescript - * const jobId = 'job-123' as JobId; - * ``` - */ -export type JobId = Brand; - -/** - * Branded type for stage identifiers. - * Prevents accidentally mixing stage IDs with job or task IDs at compile time. - * - * @example - * ```typescript - * const stageId = 'stage-456' as StageId; - * ``` - */ -export type StageId = Brand; - -/** - * Branded type for task identifiers. - * Prevents accidentally mixing task IDs with job or stage IDs at compile time. - * - * @example - * ```typescript - * const taskId = 'task-789' as TaskId; - * ``` - */ -export type TaskId = Brand; diff --git a/packages/jobnik-sdk/src/types/consumer.ts b/packages/jobnik-sdk/src/types/consumer.ts index e7fe3ec..790681d 100644 --- a/packages/jobnik-sdk/src/types/consumer.ts +++ b/packages/jobnik-sdk/src/types/consumer.ts @@ -5,7 +5,7 @@ * @template StageTypes - Interface defining stage types with their metadata, data, and task schemas */ -import type { TaskId } from './brands'; +import type { TaskId } from 'jobnik-openapi'; import type { StageTypesTemplate, ValidStageType } from './stage'; import type { InferTaskData, Task } from './task'; diff --git a/packages/jobnik-sdk/src/types/index.ts b/packages/jobnik-sdk/src/types/index.ts index fa0c77b..ffa58b3 100644 --- a/packages/jobnik-sdk/src/types/index.ts +++ b/packages/jobnik-sdk/src/types/index.ts @@ -1,3 +1,3 @@ export * from '../telemetry/logger'; -export type { paths } from './openapi'; +export type { paths } from 'jobnik-openapi'; diff --git a/packages/jobnik-sdk/src/types/job.ts b/packages/jobnik-sdk/src/types/job.ts index d7bcbb6..b6bd24d 100644 --- a/packages/jobnik-sdk/src/types/job.ts +++ b/packages/jobnik-sdk/src/types/job.ts @@ -1,4 +1,4 @@ -import type { components } from './openapi'; +import type { components } from 'jobnik-openapi'; import type { Prettify } from './utils'; type UserMetadata = components['schemas']['userMetadata']; diff --git a/packages/jobnik-sdk/src/types/openapi.ts b/packages/jobnik-sdk/src/types/openapi.ts deleted file mode 100644 index c840dc0..0000000 --- a/packages/jobnik-sdk/src/types/openapi.ts +++ /dev/null @@ -1,2154 +0,0 @@ -/* eslint-disable */ -// This file was auto-generated. Do not edit manually. -// To update, run the error generation script again. - -import type { JobId, StageId, TaskId } from './brands'; -export type paths = { - '/v1/jobs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieve jobs matching criteria - * @description Filter jobs by name, date range, priority. Optional stage inclusion. - * Returns empty array if no matches. - */ - get: operations['findJobsV1']; - put?: never; - /** - * Create job - * @description Create job with config and metadata. Initial status: PENDING. - */ - post: operations['createJobV1']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/jobs/{jobId}': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - /** - * Retrieve a specific job by its ID - * @description Fetches detailed information about a job using its unique identifier. - * Includes job configuration, status, metadata, and completion percentage. - * - * Optional inclusion of related stage data via the should_return_stages parameter, - * which allows clients to retrieve the complete job hierarchy in a single request. - */ - get: operations['getJobByIdV1']; - put?: never; - post?: never; - /** - * Delete a job and all its associated resources (stages, tasks) - * @description Permanently removes a job and all its associated stages and tasks from the system. - * This operation cascades to delete all child resources and cannot be undone. - * - * The job must exist in the system for this operation to succeed. - * Jobs can only be deleted when they are in a finite state (COMPLETED, FAILED, or ABORTED). - * Attempting to delete a job in any other state will result in a 400 error. - * - * Returns a success message with code JOB_DELETED_SUCCESSFULLY when completed. - */ - delete: operations['deleteJobV1']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/jobs/{jobId}/user-metadata': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update job's custom metadata - * @description Updates the user-defined metadata object for a specific job. - * This endpoint allows clients to attach or modify arbitrary data related to a job - * without affecting the job's core properties or execution status. - * - * User metadata is useful for storing application-specific context, tracking information, - * or any custom data needed by client applications. - */ - patch: operations['updateUserMetadataV1']; - trace?: never; - }; - '/v1/jobs/{jobId}/priority': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify job's priority level - * @description Updates the priority level for a specific job. - * This affects how the job is scheduled relative to other jobs in the system. - * - * Higher priority jobs will be processed before lower priority ones when resources - * are constrained. Priority changes take effect immediately and apply to all - * pending tasks associated with the job. - */ - patch: operations['updateJobPriorityV1']; - trace?: never; - }; - '/v1/jobs/{jobId}/status': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - get?: never; - /** - * Update job status for workflow control - * @description Updates the operational status of a job to control its workflow execution. This endpoint - * supports user-initiated operations including PENDING (resume), ABORTED (cancel), and - * PAUSED (suspend), which may cascade changes to related stages and tasks. - * - * Internal status transitions (such as IN_PROGRESS, COMPLETED, FAILED, CREATED) are managed - * automatically by the system based on stage and task completion, and cannot be set through - * this endpoint. - * - * Status changes follow a state machine that enforces valid transitions. When a job's status - * is changed, the system will automatically update timestamps and completion percentages as - * appropriate. - */ - put: operations['updateStatusV1']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/jobs/{jobId}/stages': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - /** - * Retrieve all stages for a specific job in execution order - * @description Fetches all stages associated with the specified job ID, automatically ordered by - * their execution sequence (order field). Provides complete information about each - * stage including type, status, progress, and sequential order number. - * - * Stages are returned in ascending order based on their order field (1, 2, 3, etc.) - * to maintain the correct workflow execution sequence defined when stages were created. - * - * Optional inclusion of related task data via the should_return_tasks parameter, - * allowing clients to retrieve the complete job hierarchy in a single request. - */ - get: operations['getStagesByJobIdV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/jobs/{jobId}/stage': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add a new stage as the last stage in the job workflow - * @description Appends a new stage to an existing job with automatic order assignment. - * The stage will be added after any existing stages in the job's workflow sequence. - * Each stage is automatically assigned an incremental order number (1, 2, 3, etc.) - * within the scope of its parent job to maintain execution sequence. - * - * This endpoint allows for extending job workflows at runtime by adding new processing steps. - * The order field ensures stages are processed in the correct sequence when retrieved. - * - * The job must exist and be in a valid state to accept new stages. - */ - post: operations['addStageV1']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieve stages matching specified criteria - * @description Returns a filtered list of stages based on the provided query parameters. - * Supports filtering by job ID, stage type, and status. - * - * Optional inclusion of related task data via the should_return_tasks parameter - * allows clients to retrieve the complete stage hierarchy in a single request. - */ - get: operations['getStagesV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages/{stageId}': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - /** - * Retrieve a specific stage by its ID - * @description Fetches detailed information about a stage using its unique identifier. - * Includes stage configuration, status, metadata, and completion information. - * - * Optional inclusion of related task data via the should_return_tasks parameter, - * which allows clients to retrieve the complete stage hierarchy in a single request. - */ - get: operations['getStageByIdV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages/{stageId}/summary': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get stage progress summary - * @description Retrieves aggregated statistics about the tasks within a specific stage. - * Provides counts of tasks by status (pending, in progress, completed, etc.) - * and a total task count for monitoring stage progress. - * - * This endpoint is useful for displaying progress indicators or status dashboards - * without needing to retrieve and process all individual task details. - */ - get: operations['getStageSummaryV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages/{stageId}/user-metadata': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update stage's custom metadata - * @description Updates the user-defined metadata object for a specific stage. - * This endpoint allows clients to attach or modify arbitrary data related to a stage - * without affecting the stage's core properties or execution status. - * - * User metadata is useful for storing application-specific context, tracking information, - * or any custom data needed by client applications. - */ - patch: operations['updateStageUserMetadataV1']; - trace?: never; - }; - '/v1/stages/{stageId}/status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** - * Update stage status to trigger workflow progression - * @description Updates the operational status of a stage to PENDING, which may trigger workflow - * transitions and cascade changes to related tasks and the parent job. This endpoint - * is restricted to manually advancing stages in the workflow sequence. - * - * Common use cases: - * - Resuming a WAITING stage (e.g., after manual intervention or approval) - * - Unblocking workflow progression when a stage is ready to proceed - * - * Internal status transitions (such as IN_PROGRESS, COMPLETED, FAILED, ABORTED, - * WAITING, CREATED) are managed automatically by the system based on task completion, - * job state, and workflow rules. - * - * Status changes follow a state machine that enforces valid transitions. When a stage's - * status changes, it may automatically update the parent job's status and trigger - * transitions in subsequent stages (e.g., activating the next stage in the sequence). - */ - put: operations['updateStageStatusV1']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages/{stageId}/tasks': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - /** - * Retrieve all tasks for a specific stage - * @description Fetches all tasks associated with the specified stage ID. - * Provides complete information about each task including type, status, and attempt count. - */ - get: operations['getTasksByStageIdV1']; - put?: never; - /** - * Add new tasks to an existing stage - * @description Creates and appends new tasks to an existing stage. - * This endpoint allows for extending stage processing capabilities by adding more work units. - * - * Task objects require type and data properties, with optional user metadata and - * maximum attempt configuration. Tasks are created with an initial status of PENDING. - * - * The stage must exist and be in a valid state to accept new tasks. - */ - post: operations['addTasksV1']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/stages/{stageType}/tasks/dequeue': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Stage type identifier for dequeuing tasks */ - stageType: components['parameters']['stageType']; - }; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Find and claim the highest priority pending task of specified stage type - * @description Retrieves the highest priority task of the specified stage type that is in PENDING or RETRIED status, - * and automatically updates its status to IN_PROGRESS. This endpoint implements a priority-based - * work queue pattern where workers can claim the next available task. - * - * The endpoint considers task priority (inherited from the parent job), searches only for tasks - * that are in valid states (PENDING or RETRIED), and updates related stage and job status if needed. - * - * If successful, returns the complete task details with status updated to IN_PROGRESS. - */ - patch: operations['dequeueTaskV1']; - trace?: never; - }; - '/v1/tasks': { - parameters: { - query?: { - /** @description Filter results by stage identifier */ - stage_id?: components['parameters']['paramStageId']; - /** @description Filter results by stage type (e.g., processing, validation) */ - stage_type?: components['parameters']['paramStageType']; - /** @description Filter results by update time, starting from this date/time */ - from_date?: components['parameters']['fromDate']; - /** @description Filter results by update time, ending at this date/time */ - end_date?: components['parameters']['endDate']; - /** @description Filter tasks by their operational status */ - status?: components['parameters']['paramsTaskStatus']; - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - page?: components['parameters']['pageParam']; - /** @description Number of items to return per page. */ - page_size?: components['parameters']['pageSizeParam']; - }; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieve tasks matching specified criteria - * @description Returns a filtered list of tasks based on the provided query parameters. - * Supports filtering by stage ID, task type, date range, and operational status. - * - * This endpoint is useful for monitoring task progress across multiple stages and jobs, - * enabling clients to build custom dashboards or track specific task types. - */ - get: operations['getTasksByCriteriaV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/tasks/{taskId}': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the task */ - taskId: components['parameters']['taskId']; - }; - cookie?: never; - }; - /** - * Retrieve a specific task by its ID - * @description Fetches detailed information about a task using its unique identifier. - * Returns complete task data including type, status, payload, and attempt information. - */ - get: operations['getTaskByIdV1']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/v1/tasks/{taskId}/user-metadata': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update task's custom metadata - * @description Updates the user-defined metadata object for a specific task. - * This endpoint allows clients to attach or modify arbitrary data related to a task - * without affecting the task's core properties or execution status. - * - * User metadata is useful for storing application-specific context, tracking information, - * or any custom data needed by client applications. - */ - patch: operations['updateTaskUserMetadataV1']; - trace?: never; - }; - '/v1/tasks/{taskId}/status': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the task */ - taskId: components['parameters']['taskId']; - }; - cookie?: never; - }; - get?: never; - /** - * Mark task as completed or failed - * @description Updates the operational status of a task to either COMPLETED or FAILED, which may trigger - * cascading updates to the parent stage and job. This endpoint is restricted to marking tasks - * as finished by external workers. - * - * Internal status transitions (such as PENDING to IN_PROGRESS) are managed automatically - * by the system through the dequeue operation and cannot be set through this endpoint. - * - * Status changes follow a state machine that enforces valid transitions. When a task's status - * is changed, the system will automatically update the parent stage's summary statistics and - * may affect the stage's overall status. - */ - put: operations['updateTaskStatusV1']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -}; -export type webhooks = Record; -export type components = { - schemas: { - /** - * Format: date-time - * @description Timestamp indicating when the resource was created - */ - creationTime: string; - /** - * Format: date-time - * @description Timestamp indicating when the resource was last updated - */ - updateTime: string; - /** - * Format: date-time - * @description Timestamp indicating when the resource began processing (in-progress state) - */ - startTime: string; - /** - * Format: date-time - * @description Timestamp indicating when the resource reached a finite state (e.g., COMPLETED, FAILED, ABORTED) - */ - endTime: string; - /** - * Format: uuid - * @description Unique identifier for a job - */ - jobId: JobId; - /** @description Custom job configuration data containing job-specific parameters */ - jobPayload: { - [key: string]: unknown; - }; - /** @description Completion percentage of a job, stage, or task (0-100) */ - percentage: number; - /** @description Number of times a task has been attempted */ - attempts: number; - /** @description Maximum number of retries allowed for a task */ - maxAttempts: number; - /** - * Format: uuid - * @description Unique identifier for a stage - */ - stageId: StageId; - /** @description Custom stage configuration data containing stage-specific parameters */ - stagePayload: { - [key: string]: unknown; - }; - /** - * @description Stage execution order within job - * @example 1 - */ - order: number; - /** - * @description Job priority for processing order. Higher priority processed first. - * VERY_HIGH > HIGH > MEDIUM > LOW > VERY_LOW - * @example LOW - * @enum {string} - */ - priority: 'VERY_HIGH' | 'HIGH' | 'MEDIUM' | 'LOW' | 'VERY_LOW'; - /** - * @description W3C traceparent for distributed tracing. Auto-injected if not provided. - * See [W3C Trace Context](https://www.w3.org/TR/trace-context/). - * @example 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 - */ - traceparent: string; - /** - * @description W3C tracestate for distributed tracing. Optional, auto-injected if available. - * @example rojo=00f067aa0ba902b7,congo=t61rcWkgMzE - */ - tracestate: string; - /** - * @description Success response codes - * @example JOB_MODIFIED_SUCCESSFULLY - * @enum {string} - */ - successMessages: 'JOB_MODIFIED_SUCCESSFULLY' | 'TASK_MODIFIED_SUCCESSFULLY' | 'STAGE_MODIFIED_SUCCESSFULLY' | 'JOB_DELETED_SUCCESSFULLY'; - /** - * @description User-controllable job statuses: PENDING (resume), ABORTED (cancel), PAUSED (suspend). - * System-managed: IN_PROGRESS, COMPLETED, FAILED, CREATED. - * @example PENDING - * @enum {string} - */ - jobOperationStatus: 'PENDING' | 'ABORTED' | 'PAUSED'; - /** - * @description All job states. User-controllable: PENDING, ABORTED, PAUSED. System-managed: IN_PROGRESS, COMPLETED, FAILED, CREATED. - * Terminal states: COMPLETED, FAILED, ABORTED. - * @example PENDING - * @enum {string} - */ - jobOperationStatusResponse: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'ABORTED' | 'PAUSED' | 'CREATED'; - /** - * @description User-controllable stage status: PENDING only. - * System-managed: IN_PROGRESS, COMPLETED, FAILED, ABORTED, WAITING, CREATED. - * @enum {string} - */ - stageOperationStatus: 'PENDING'; - /** - * @description All stage states. User-controllable: PENDING. System-managed: IN_PROGRESS, COMPLETED, FAILED, ABORTED, WAITING, CREATED. - * Terminal states: COMPLETED, FAILED, ABORTED. - * @example PENDING - * @enum {string} - */ - stageOperationStatusResponse: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'ABORTED' | 'WAITING' | 'CREATED'; - /** - * @description User-controllable task statuses: COMPLETED, FAILED. - * System-managed: PENDING, IN_PROGRESS, CREATED, RETRIED. - * @example COMPLETED - * @enum {string} - */ - taskOperationStatus: 'COMPLETED' | 'FAILED'; - /** - * @description All task states including RETRIED for retry handling. - * Terminal states: COMPLETED, FAILED. - * @example PENDING - * @enum {string} - */ - taskOperationStatusResponse: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'CREATED' | 'RETRIED'; - /** - * @description Job category/type for filtering and behavior - * @example DEFAULT - */ - jobName: string; - /** - * @description Free-form string identifier for stage functionality, allowing flexible categorization - * of stage operations. Used for routing tasks to appropriate workers and - * for filtering in API requests. Can be any descriptive name up to 50 characters. - * @example unknown - */ - stageType: string; - /** @description Flag indicating whether to include complete stage details in job response payloads */ - returnStage: boolean; - /** @description Flag indicating whether to include complete task details in stage response payloads */ - returnTask: boolean; - /** - * @description Application-specific custom data container that can store arbitrary client information without affecting core operations - * @default {} - */ - userMetadata: { - [key: string]: unknown; - }; - /** - * @description Aggregated task statistics grouped by operational status, providing a complete overview of stage progress. - * Used for monitoring progress, generating dashboards, and determining when stages/jobs are complete. - * The total field should always equal the sum of all other status counts. - */ - summary: { - /** @description Number of tasks awaiting execution */ - pending: number; - /** @description Number of tasks currently being processed */ - inProgress: number; - /** @description Number of tasks that finished successfully */ - completed: number; - /** @description Number of tasks that encountered errors and could not be completed */ - failed: number; - /** @description Number of tasks in initial state before becoming pending */ - created: number; - /** @description Number of tasks scheduled for re-execution after failure */ - retried: number; - /** @description Total count of tasks belonging to the stage */ - total: number; - }; - /** - * @description Input payload for creating a new job in the system. - * Contains all required configuration for job execution, including processing mode, - * custom parameters, metadata. - * - * Tracing fields (traceparent, tracestate) are optional: - * - If traceparent is provided, user's trace context is used (tracestate defaults to null if not provided) - * - If traceparent is not provided, the system automatically injects both traceparent and tracestate - * from the active OpenTelemetry context using propagation.inject() (tracestate may still be null if not available) - */ - createJobPayload: { - name: components['schemas']['jobName']; - data: components['schemas']['jobPayload']; - priority?: components['schemas']['priority'] & unknown; - userMetadata?: components['schemas']['userMetadata']; - traceparent?: components['schemas']['traceparent']; - tracestate?: components['schemas']['tracestate']; - }; - /** - * @description Complete job information with status and metadata. - * - * Comprehensive job response model containing all job details including configuration, - * execution status, progress tracking, and associated metadata. This schema represents - * the complete state of a job in the system and is returned by job retrieval operations. - * - * Includes optional stage data when requested via query parameters, allowing clients - * to retrieve the complete job hierarchy in a single request. - */ - job: { - id: components['schemas']['jobId']; - status: components['schemas']['jobOperationStatusResponse']; - percentage: components['schemas']['percentage']; - creationTime: components['schemas']['creationTime']; - updateTime: components['schemas']['updateTime']; - name: components['schemas']['jobName']; - data: components['schemas']['jobPayload']; - priority: components['schemas']['priority']; - userMetadata: components['schemas']['userMetadata']; - traceparent: components['schemas']['traceparent']; - tracestate?: components['schemas']['tracestate']; - stages?: components['schemas']['stageResponse'][]; - }; - createStagePayloadRequest: components['schemas']['createStagePayload'] & { - /** - * @description Optional flag indicating whether the stage should be created in a waiting state. - * If true, the stage will not start processing immediately and will require - * manual intervention to begin execution. Useful for staging workflows where - * stages need to be prepared but not executed until all dependencies are met. - * @example false - */ - startAsWaiting?: boolean; - }; - /** - * @description Input payload for creating a new stage within a job. - * Contains stage type, operational parameters, and optional user metadata. - * - * Tracing fields (traceparent, tracestate) are optional: - * - If traceparent is provided, user's trace context is used (tracestate defaults to null if not provided) - * - If traceparent is not provided, the system automatically injects both traceparent and tracestate - * from the active OpenTelemetry context using propagation.inject() (tracestate may still be null if not available) - */ - createStagePayload: { - type: components['schemas']['stageType']; - data: components['schemas']['stagePayload']; - userMetadata?: components['schemas']['userMetadata']; - traceparent?: components['schemas']['traceparent']; - tracestate?: components['schemas']['tracestate']; - }; - stageResponse: components['schemas']['createStagePayload'] & { - id: components['schemas']['stageId']; - summary: components['schemas']['summary']; - percentage: components['schemas']['percentage']; - status: components['schemas']['stageOperationStatusResponse']; - jobId: components['schemas']['jobId']; - order: components['schemas']['order']; - traceparent: components['schemas']['traceparent']; - }; - getStageResponse: components['schemas']['stageResponse'] & { - /** @description Associated tasks belonging to this stage */ - tasks?: components['schemas']['taskResponse'][]; - }; - /** - * Format: uuid - * @description Unique identifier for a task, generated by the system upon task creation - */ - taskId: TaskId; - /** - * @description Custom task configuration data containing operation-specific parameters. - * The schema varies based on task type and contains all necessary information - * for task execution by workers. - */ - taskPayload: { - [key: string]: unknown; - }; - /** - * @description Input payload for creating a new task within a stage. - * Contains task type, operational parameters, and optional retry configuration. - * Used when adding tasks to existing stages. - * - * Trace propagation (traceparent, tracestate) are optional: - * - If traceparent is provided, user's trace context is used (tracestate defaults to null if not provided) - * - If traceparent is not provided, the system automatically injects both traceparent and tracestate - * from the active OpenTelemetry context using propagation.inject() (tracestate may still be null if not available) - */ - createTaskPayload: { - data: components['schemas']['taskPayload']; - userMetadata?: components['schemas']['userMetadata']; - maxAttempts?: components['schemas']['maxAttempts']; - traceparent?: components['schemas']['traceparent']; - tracestate?: components['schemas']['tracestate']; - }; - /** - * @description Complete task information returned by the API, including all configuration - * data along with execution status, attempt tracking, and associated stage reference. - * Used when retrieving task details or after task creation. - */ - taskResponse: { - id: components['schemas']['taskId']; - data: components['schemas']['taskPayload']; - stageId: components['schemas']['stageId']; - userMetadata: components['schemas']['userMetadata']; - creationTime: components['schemas']['creationTime']; - updateTime: components['schemas']['updateTime']; - startTime?: components['schemas']['startTime']; - endTime?: components['schemas']['endTime']; - status: components['schemas']['taskOperationStatusResponse']; - attempts: components['schemas']['attempts']; - maxAttempts: components['schemas']['maxAttempts']; - traceparent: components['schemas']['traceparent']; - tracestate?: components['schemas']['tracestate']; - }; - /** - * @description Standard success response structure used for operations that don't - * return entity data, providing a standardized confirmation message. - */ - defaultOkMessage: { - code: components['schemas']['successMessages']; - }; - /** - * @description Foundation schema for standardized error responses across all API endpoints. - * Provides consistent structure with human-readable message and common-readable code fields - * that are extended by individual endpoints to define their specific error scenarios. - */ - baseErrorResponse: { - /** @description Human-readable error message describing the issue */ - message: string; - /** @description common-readable error code indicating the specific error type */ - code: string; - }; - internalErrorsResponse: components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'DATABASE_RELATED_ERROR' | 'UNKNOWN_ERROR'; - }; - badRequestResponse: components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'VALIDATION_ERROR'; - }; - jobNotFoundResponse: components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'JOB_NOT_FOUND'; - }; - stageNotFoundResponse: components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'STAGE_NOT_FOUND'; - }; - taskNotFoundResponse: components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'TASK_NOT_FOUND'; - }; - /** @description Paginated list of jobs with total count. */ - jobsPaginatedResponse: { - /** @description Total number of jobs matching the filter criteria */ - total: number; - /** @description Page of job records */ - items: components['schemas']['job'][]; - }; - /** @description Paginated list of stages with total count. */ - stagesPaginatedResponse: { - /** @description Total number of stages matching the filter criteria */ - total: number; - /** @description Page of stage records */ - items: components['schemas']['getStageResponse'][]; - }; - /** @description Paginated list of tasks with total count. */ - tasksPaginatedResponse: { - /** @description Total number of tasks matching the filter criteria */ - total: number; - /** @description Page of task records */ - items: components['schemas']['taskResponse'][]; - }; - }; - responses: never; - parameters: { - /** @description Unique identifier for the job */ - jobId: components['schemas']['jobId']; - /** @description Unique identifier for the stage */ - stageId: components['schemas']['stageId']; - /** @description Unique identifier for the task */ - taskId: components['schemas']['taskId']; - /** @description Filter tasks by their operational status */ - paramsTaskStatus: components['schemas']['taskOperationStatusResponse']; - /** @description Filter jobs by their name/type */ - jobNameQueryParam: components['schemas']['jobName']; - /** @description Filter jobs by their priority level */ - priority: components['schemas']['priority']; - /** @description Filter results by update time, starting from this date/time */ - fromDate: string; - /** @description Filter results by update time, ending at this date/time */ - endDate: string; - /** @description When true, includes stage data in the response */ - includeStages: components['schemas']['returnStage']; - /** @description When true, includes task data in the response */ - includeTasks: components['schemas']['returnTask']; - /** @description Filter results by stage identifier */ - paramStageId: components['schemas']['stageId']; - /** @description Filter results by job identifier */ - paramJobId: components['schemas']['jobId']; - /** @description Filter results by stage type (e.g., processing, validation) */ - paramStageType: components['schemas']['stageType']; - /** @description Stage type identifier for dequeuing tasks */ - stageType: components['schemas']['stageType']; - /** - * @description Filter results by stage operational status (e.g., PENDING, IN_PROGRESS). - * Used to find stages in specific execution states. - */ - stageStatus: components['schemas']['stageOperationStatusResponse']; - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - pageParam: number; - /** @description Number of items to return per page. */ - pageSizeParam: number; - }; - requestBodies: never; - headers: never; - pathItems: never; -}; -export type $defs = Record; -export interface operations { - findJobsV1: { - parameters: { - query?: { - /** @description Filter jobs by their name/type */ - job_name?: components['parameters']['jobNameQueryParam']; - /** @description Filter results by update time, starting from this date/time */ - from_date?: components['parameters']['fromDate']; - /** @description Filter results by update time, ending at this date/time */ - end_date?: components['parameters']['endDate']; - /** @description Filter jobs by their priority level */ - priority?: components['parameters']['priority']; - /** @description When true, includes stage data in the response */ - should_return_stages?: components['parameters']['includeStages']; - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - page?: components['parameters']['pageParam']; - /** @description Number of items to return per page. */ - page_size?: components['parameters']['pageSizeParam']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved matching jobs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobsPaginatedResponse']; - }; - }; - /** @description Invalid query parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - createJobV1: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['createJobPayload']; - }; - }; - responses: { - /** @description Job created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['job']; - }; - }; - /** @description Invalid request, could not create job */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getJobByIdV1: { - parameters: { - query?: { - /** @description When true, includes stage data in the response */ - should_return_stages?: components['parameters']['includeStages']; - }; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Job data retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['job']; - }; - }; - /** @description Invalid request, could not get job */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - deleteJobV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Job deleted successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "JOB_DELETED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid parameters or validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code: 'VALIDATION_ERROR' | 'JOB_NOT_IN_FINITE_STATE'; - }; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateUserMetadataV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['userMetadata']; - }; - }; - responses: { - /** @description User metadata successfully updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "JOB_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid metadata format or validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateJobPriorityV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - priority: components['schemas']['priority']; - }; - }; - }; - responses: { - /** @description Job priority successfully changed */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "JOB_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description No change made - requested priority equals current priority */ - 204: { - headers: { - /** @description Won't change priority if equal to current */ - Reason?: string; - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid priority value or other request error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateStatusV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - status: components['schemas']['jobOperationStatus']; - }; - }; - }; - responses: { - /** @description Job status successfully changed */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "JOB_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid status or illegal state transition */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'VALIDATION_ERROR' | 'ILLEGAL_JOB_STATUS_TRANSITION'; - }; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getStagesByJobIdV1: { - parameters: { - query?: { - /** @description When true, includes task data in the response */ - should_return_tasks?: components['parameters']['includeTasks']; - }; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved stages for the specified job */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['getStageResponse'][]; - }; - }; - /** @description Invalid job ID format or other parameter error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - addStageV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the job */ - jobId: components['parameters']['jobId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['createStagePayloadRequest']; - }; - }; - responses: { - /** @description Stage successfully created and added to the job */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageResponse']; - }; - }; - /** @description Invalid request format. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'VALIDATION_ERROR' | 'JOB_IN_FINITE_STATE'; - }; - }; - }; - /** @description Job not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['jobNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getStagesV1: { - parameters: { - query?: { - /** @description Filter results by job identifier */ - job_id?: components['parameters']['paramJobId']; - /** @description Filter results by stage type (e.g., processing, validation) */ - stage_type?: components['parameters']['paramStageType']; - /** - * @description Filter results by stage operational status (e.g., PENDING, IN_PROGRESS). - * Used to find stages in specific execution states. - */ - stage_operation_status?: components['parameters']['stageStatus']; - /** @description When true, includes task data in the response */ - should_return_tasks?: components['parameters']['includeTasks']; - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - page?: components['parameters']['pageParam']; - /** @description Number of items to return per page. */ - page_size?: components['parameters']['pageSizeParam']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved matching stages */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stagesPaginatedResponse']; - }; - }; - /** @description Invalid query parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getStageByIdV1: { - parameters: { - query?: { - /** @description When true, includes task data in the response */ - should_return_tasks?: components['parameters']['includeTasks']; - }; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Stage data retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['getStageResponse']; - }; - }; - /** @description Invalid request format or parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getStageSummaryV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Stage summary retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['summary']; - }; - }; - /** @description Invalid stage ID format */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateStageUserMetadataV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['userMetadata']; - }; - }; - responses: { - /** @description User metadata successfully updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "STAGE_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid metadata format or validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateStageStatusV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - status: components['schemas']['stageOperationStatus']; - }; - }; - }; - responses: { - /** @description Stage status successfully changed */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "STAGE_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid status or illegal state transition */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'VALIDATION_ERROR' | 'ILLEGAL_STAGE_STATUS_TRANSITION'; - }; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'DATABASE_RELATED_ERROR' | 'UNKNOWN_ERROR' | 'ILLEGAL_JOB_STATUS_TRANSITION' | 'JOB_NOT_FOUND'; - }; - }; - }; - }; - }; - getTasksByStageIdV1: { - parameters: { - query?: { - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - page?: components['parameters']['pageParam']; - /** @description Number of items to return per page. */ - page_size?: components['parameters']['pageSizeParam']; - }; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved tasks for the specified stage */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['tasksPaginatedResponse']; - }; - }; - /** @description Invalid stage ID format or other parameter error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - addTasksV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the stage */ - stageId: components['parameters']['stageId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['createTaskPayload'][]; - }; - }; - responses: { - /** @description Tasks successfully created and added to the stage */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskResponse'][]; - }; - }; - /** @description Invalid request format or validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'VALIDATION_ERROR' | 'STAGE_IN_FINITE_STATE' | 'NOT_ALLOWED_TO_ADD_TASKS_TO_IN_PROGRESS_STAGE'; - }; - }; - }; - /** @description Stage not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['stageNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - dequeueTaskV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Stage type identifier for dequeuing tasks */ - stageType: components['parameters']['stageType']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Task successfully dequeued and status updated to IN_PROGRESS */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskResponse']; - }; - }; - /** @description Invalid stageType parameter or other validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description No pending tasks of requested type are available */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskNotFoundResponse']; - }; - }; - /** @description task was claimed by another worker. This occurs when multiple workers attempt to dequeue the same task simultaneously. The client should retry the dequeue operation to get a different task. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'TASK_STATUS_UPDATE_FAILED'; - }; - }; - }; - /** @description Internal server error or invalid state transition */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: - | 'DATABASE_RELATED_ERROR' - | 'UNKNOWN_ERROR' - | 'TASK_STATUS_UPDATE_FAILED' - | 'ILLEGAL_TASK_STATUS_TRANSITION' - | 'ILLEGAL_STAGE_STATUS_TRANSITION' - | 'ILLEGAL_JOB_STATUS_TRANSITION' - | 'STAGE_NOT_FOUND' - | 'JOB_NOT_FOUND'; - }; - }; - }; - }; - }; - getTasksByCriteriaV1: { - parameters: { - query?: { - /** @description Filter results by stage identifier */ - stage_id?: components['parameters']['paramStageId']; - /** @description Filter results by stage type (e.g., processing, validation) */ - stage_type?: components['parameters']['paramStageType']; - /** @description Filter results by update time, starting from this date/time */ - from_date?: components['parameters']['fromDate']; - /** @description Filter results by update time, ending at this date/time */ - end_date?: components['parameters']['endDate']; - /** @description Filter tasks by their operational status */ - status?: components['parameters']['paramsTaskStatus']; - /** @description 1-based page number for pagination. Requesting beyond the last page returns an empty items array. */ - page?: components['parameters']['pageParam']; - /** @description Number of items to return per page. */ - page_size?: components['parameters']['pageSizeParam']; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved matching tasks */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['tasksPaginatedResponse']; - }; - }; - /** @description Invalid query parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - getTaskByIdV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the task */ - taskId: components['parameters']['taskId']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Task data retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskResponse']; - }; - }; - /** @description Invalid task ID format or other parameter error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Task not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateTaskUserMetadataV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the task */ - taskId: components['parameters']['taskId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['userMetadata']; - }; - }; - responses: { - /** @description User metadata successfully updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "code": "TASK_MODIFIED_SUCCESSFULLY" - * } - */ - 'application/json': components['schemas']['defaultOkMessage']; - }; - }; - /** @description Invalid metadata format or validation error */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['badRequestResponse']; - }; - }; - /** @description Task not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskNotFoundResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['internalErrorsResponse']; - }; - }; - }; - }; - updateTaskStatusV1: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Unique identifier for the task */ - taskId: components['parameters']['taskId']; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - status: components['schemas']['taskOperationStatus']; - }; - }; - }; - responses: { - /** @description Task status successfully changed */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskResponse']; - }; - }; - /** @description Invalid status or illegal state transition */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'VALIDATION_ERROR' | 'ILLEGAL_TASK_STATUS_TRANSITION'; - }; - }; - }; - /** @description Task not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['taskNotFoundResponse']; - }; - }; - /** @description task status was modified by another request. This occurs when multiple workers attempt to update the same task simultaneously. The current state of the task has changed since it was retrieved. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: 'TASK_STATUS_UPDATE_FAILED'; - }; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['baseErrorResponse'] & { - /** @enum {unknown} */ - code?: - | 'DATABASE_RELATED_ERROR' - | 'UNKNOWN_ERROR' - | 'TASK_STATUS_UPDATE_FAILED' - | 'ILLEGAL_TASK_STATUS_TRANSITION' - | 'ILLEGAL_STAGE_STATUS_TRANSITION' - | 'ILLEGAL_JOB_STATUS_TRANSITION' - | 'STAGE_NOT_FOUND' - | 'JOB_NOT_FOUND'; - }; - }; - }; - }; - }; -} diff --git a/packages/jobnik-sdk/src/types/producer.ts b/packages/jobnik-sdk/src/types/producer.ts index 34e99ed..e430c0b 100644 --- a/packages/jobnik-sdk/src/types/producer.ts +++ b/packages/jobnik-sdk/src/types/producer.ts @@ -1,4 +1,4 @@ -import type { JobId, StageId } from './brands'; +import type { JobId, StageId } from 'jobnik-openapi'; import type { Job, JobData, JobTypesTemplate, NewJob, ValidJobType } from './job'; import type { InferStageData, NewStage, Stage, StageData, StageTypesTemplate, ValidStageType } from './stage'; import type { InferTaskData, NewTask, Task } from './task'; diff --git a/packages/jobnik-sdk/src/types/stage.ts b/packages/jobnik-sdk/src/types/stage.ts index f7f2693..f09ffd0 100644 --- a/packages/jobnik-sdk/src/types/stage.ts +++ b/packages/jobnik-sdk/src/types/stage.ts @@ -1,4 +1,4 @@ -import type { components } from './openapi'; +import type { components } from 'jobnik-openapi'; import type { TaskData } from './task'; import type { Prettify } from './utils'; diff --git a/packages/jobnik-sdk/src/types/task.ts b/packages/jobnik-sdk/src/types/task.ts index 92a5f85..5dae3ad 100644 --- a/packages/jobnik-sdk/src/types/task.ts +++ b/packages/jobnik-sdk/src/types/task.ts @@ -1,4 +1,4 @@ -import type { components } from './openapi'; +import type { components } from 'jobnik-openapi'; import type { StageTypesTemplate } from './stage'; import type { Prettify } from './utils'; diff --git a/packages/jobnik-sdk/src/types/worker.ts b/packages/jobnik-sdk/src/types/worker.ts index 85eca56..90f7b4e 100644 --- a/packages/jobnik-sdk/src/types/worker.ts +++ b/packages/jobnik-sdk/src/types/worker.ts @@ -2,7 +2,7 @@ import type { Options as OpossumOptions } from 'opossum'; import type { Logger } from '../telemetry/logger'; import type { ScopedApiClient } from '../api'; import type { InferTaskData, Task } from './task'; -import type { TaskId } from './brands'; +import type { TaskId } from 'jobnik-openapi'; import type { InferStageData, Stage, StageData, StageTypesTemplate, ValidStageType } from './stage'; import type { IProducer } from './producer'; import type { InferJobData, Job, JobData, JobTypesTemplate, ValidJobType } from './job'; diff --git a/packages/jobnik-sdk/tests/apiClient.spec.ts b/packages/jobnik-sdk/tests/apiClient.spec.ts index 6567ab3..051f87f 100644 --- a/packages/jobnik-sdk/tests/apiClient.spec.ts +++ b/packages/jobnik-sdk/tests/apiClient.spec.ts @@ -6,7 +6,7 @@ import type { MockPool } from 'undici'; import { MockAgent } from 'undici'; import { createApiClient } from '../src/api/index'; import { NetworkError, APIError } from '../src/errors/sdkErrors'; -import type { JobId } from '../src/types/brands'; +import type { JobId } from 'jobnik-openapi'; /* eslint-disable */ // Add type declaration for global mockAgent diff --git a/packages/jobnik-sdk/tests/consumer/consumer.spec.ts b/packages/jobnik-sdk/tests/consumer/consumer.spec.ts index f33cb89..42cda64 100644 --- a/packages/jobnik-sdk/tests/consumer/consumer.spec.ts +++ b/packages/jobnik-sdk/tests/consumer/consumer.spec.ts @@ -6,7 +6,7 @@ import { Registry } from 'prom-client'; import { createApiClient } from '../../src/api/index'; import { Consumer } from '../../src/clients/consumer'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { StageId, TaskId } from '../../src/types/brands'; +import type { StageId, TaskId } from 'jobnik-openapi'; import { ConsumerError, API_ERROR_CODES } from '../../src/errors'; import type { Task } from '../../src/types/task'; import { Metrics } from '../../src/telemetry/metrics'; diff --git a/packages/jobnik-sdk/tests/producer/producer.spec.ts b/packages/jobnik-sdk/tests/producer/producer.spec.ts index 34f9409..87162de 100644 --- a/packages/jobnik-sdk/tests/producer/producer.spec.ts +++ b/packages/jobnik-sdk/tests/producer/producer.spec.ts @@ -7,7 +7,7 @@ import { Registry } from 'prom-client'; import { createApiClient } from '../../src/api/index'; import { Producer } from '../../src/clients/producer'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { JobId, StageId } from '../../src/types/brands'; +import type { JobId, StageId } from 'jobnik-openapi'; import { ProducerError, API_ERROR_CODES } from '../../src/errors'; import { Metrics } from '../../src/telemetry/metrics'; diff --git a/packages/jobnik-sdk/tests/producer/producer.test-d.ts b/packages/jobnik-sdk/tests/producer/producer.test-d.ts index 33fd36e..b1b46a9 100644 --- a/packages/jobnik-sdk/tests/producer/producer.test-d.ts +++ b/packages/jobnik-sdk/tests/producer/producer.test-d.ts @@ -3,7 +3,7 @@ import { describe, it, expectTypeOf } from 'vitest'; import { Registry } from 'prom-client'; import { Producer } from '../../src/clients/producer'; import type { ApiClient } from '../../src/api'; -import type { JobId, StageId, TaskId } from '../../src/types/brands'; +import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import type { NewJob, JobData } from '../../src/types/job'; import type { NewStage, InferStageData, StageData } from '../../src/types/stage'; import type { NewTask, InferTaskData } from '../../src/types/task'; diff --git a/packages/jobnik-sdk/tests/worker/worker.spec.ts b/packages/jobnik-sdk/tests/worker/worker.spec.ts index fe0bdc6..bfd0a24 100644 --- a/packages/jobnik-sdk/tests/worker/worker.spec.ts +++ b/packages/jobnik-sdk/tests/worker/worker.spec.ts @@ -8,7 +8,7 @@ import { Registry } from 'prom-client'; import type { createApiClient } from '../../src/api/index'; import { Worker } from '../../src/clients/worker'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { StageId, TaskId } from '../../src/types/brands'; +import type { StageId, TaskId } from 'jobnik-openapi'; import type { Task } from '../../src/types/task'; import type { Logger } from '../../src/types'; import type { TaskHandler, WorkerOptions } from '../../src/types/worker'; diff --git a/packages/jobnik-sdk/tests/worker/worker.test-d.ts b/packages/jobnik-sdk/tests/worker/worker.test-d.ts index 9204548..6e807b3 100644 --- a/packages/jobnik-sdk/tests/worker/worker.test-d.ts +++ b/packages/jobnik-sdk/tests/worker/worker.test-d.ts @@ -1,7 +1,7 @@ import { describe, it, expectTypeOf } from 'vitest'; import { Worker } from '../../src/clients/worker'; import type { ApiClient } from '../../src/api'; -import type { TaskId } from '../../src/types/brands'; +import type { TaskId } from 'jobnik-openapi'; import type { NoopLogger } from '../../src/telemetry/noopLogger'; import type { TaskHandler, WorkerOptions } from '../../src/types/worker'; import type { IProducer } from '../../src/types/producer'; From db8e803cba3a6e1b51578102facb634d727e60df Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:54:18 +0300 Subject: [PATCH 03/16] feat(jobnik-manager): brand job ids end-to-end Retype every jobId parameter in the job service layer and the job-id flow through the stage manager to JobId from jobnik-openapi, so the compiler rejects a StageId/TaskId passed where a job id is expected. Prisma-sourced job ids are cast at the DB boundary, since Prisma keeps plain strings while the branded id only exists in the API/service layer. Co-Authored-By: Claude Sonnet 5 --- .../jobnik-manager/src/jobs/models/manager.ts | 15 +++---- .../src/stages/models/manager.ts | 19 ++++----- .../tests/integration/tasks/tasks.spec.ts | 12 +++--- .../tests/unit/jobs/jobs.spec.ts | 39 ++++++++++--------- .../tests/unit/stages/stages.spec.ts | 29 +++++++------- 5 files changed, 60 insertions(+), 54 deletions(-) diff --git a/apps/jobnik-manager/src/jobs/models/manager.ts b/apps/jobnik-manager/src/jobs/models/manager.ts index 3486586..0b54924 100644 --- a/apps/jobnik-manager/src/jobs/models/manager.ts +++ b/apps/jobnik-manager/src/jobs/models/manager.ts @@ -5,6 +5,7 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; +import type { JobId } from 'jobnik-openapi'; import type { PrismaClient, Priority } from '@prismaClient'; import { Prisma, JobOperationStatus } from '@prismaClient'; import { SERVICES } from '@common/constants'; @@ -88,7 +89,7 @@ export class JobManager { } @withSpanAsyncV4 - public async getJobById(jobId: string, includeStages?: boolean): Promise { + public async getJobById(jobId: JobId, includeStages?: boolean): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, }); @@ -103,7 +104,7 @@ export class JobManager { } @withSpanAsyncV4 - public async updateUserMetadata(jobId: string, userMetadata: Record): Promise { + public async updateUserMetadata(jobId: JobId, userMetadata: Record): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, }); @@ -128,7 +129,7 @@ export class JobManager { } @withSpanAsyncV4 - public async updatePriority(jobId: string, priority: Priority): Promise { + public async updatePriority(jobId: JobId, priority: Priority): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, [INFRA_CONVENTIONS.infra.jobnik.job.priority]: priority, @@ -157,7 +158,7 @@ export class JobManager { } @withSpanAsyncV4 - public async updateStatus(jobId: string, status: JobOperationStatus, tx?: PrismaTransaction): Promise { + public async updateStatus(jobId: JobId, status: JobOperationStatus, tx?: PrismaTransaction): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, [INFRA_CONVENTIONS.infra.jobnik.job.status]: status, @@ -173,7 +174,7 @@ export class JobManager { } @withSpanAsyncV4 - public async deleteJob(jobId: string): Promise { + public async deleteJob(jobId: JobId): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, }); @@ -207,7 +208,7 @@ export class JobManager { */ @withSpanAsyncV4 public async getJobEntityById( - jobId: string, + jobId: JobId, options: { includeStages?: IncludeStages; tx?: PrismaTransaction } = {} ): Promise | null> { const spanActive = trace.getActiveSpan(); @@ -229,7 +230,7 @@ export class JobManager { } @withSpanAsyncV4 - private async executeUpdateStatus(jobId: string, status: JobOperationStatus, tx: PrismaTransaction): Promise { + private async executeUpdateStatus(jobId: JobId, status: JobOperationStatus, tx: PrismaTransaction): Promise { const job = await this.getJobEntityById(jobId, { tx }); if (!job) { diff --git a/apps/jobnik-manager/src/stages/models/manager.ts b/apps/jobnik-manager/src/stages/models/manager.ts index ae087a2..714fcb1 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -4,6 +4,7 @@ import { createActor } from 'xstate'; import { trace, type Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; +import type { JobId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { JobOperationStatus, Prisma, StageOperationStatus } from '@prismaClient'; import { JobManager } from '@src/jobs/models/manager'; @@ -55,7 +56,7 @@ export class StageManager { ) {} @withSpanAsyncV4 - public async addStage(jobId: string, stagePayload: StageCreateModel): Promise { + public async addStage(jobId: JobId, stagePayload: StageCreateModel): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, @@ -172,7 +173,7 @@ export class StageManager { } @withSpanAsyncV4 - public async getStagesByJobId(jobId: string, includeTasks?: boolean): Promise { + public async getStagesByJobId(jobId: JobId, includeTasks?: boolean): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, @@ -396,9 +397,9 @@ export class StageManager { trace.getActiveSpan()?.addEvent('Next stage set to PENDING', { nextStageId: nextStage.id }); } - const { completedStages, totalStages } = await this.updateJobCompletionProgress(stage.jobId, tx); + const { completedStages, totalStages } = await this.updateJobCompletionProgress(stage.jobId as JobId, tx); if (completedStages === totalStages) { - await this.jobManager.updateStatus(stage.jobId, JobOperationStatus.COMPLETED, tx); + await this.jobManager.updateStatus(stage.jobId as JobId, JobOperationStatus.COMPLETED, tx); this.logger.info({ msg: 'Job completed as all stages are done', jobId: stage.jobId, @@ -410,11 +411,11 @@ export class StageManager { if (targetStatus === StageOperationStatus.IN_PROGRESS && stage.job.status === JobOperationStatus.PENDING) { // Update job status to IN_PROGRESS - await this.jobManager.updateStatus(stage.job.id, JobOperationStatus.IN_PROGRESS, tx); + await this.jobManager.updateStatus(stage.job.id as JobId, JobOperationStatus.IN_PROGRESS, tx); trace.getActiveSpan()?.addEvent('Job status set to IN_PROGRESS because first stage is being processed', { jobId: stage.jobId }); } else if (targetStatus === StageOperationStatus.FAILED) { // Update job status to FAILED - await this.jobManager.updateStatus(stage.jobId, JobOperationStatus.FAILED, tx); + await this.jobManager.updateStatus(stage.jobId as JobId, JobOperationStatus.FAILED, tx); trace.getActiveSpan()?.addEvent('Job set to FAILED because its stage failed', { jobId: stage.jobId }); } @@ -449,7 +450,7 @@ export class StageManager { stageId: stage.id, jobId: stage.jobId, }); - await this.updateJobCompletionProgress(stage.jobId, tx); + await this.updateJobCompletionProgress(stage.jobId as JobId, tx); trace.getActiveSpan()?.addEvent('Stage set to COMPLETED', { stageId: stage.id }); } } @@ -462,7 +463,7 @@ export class StageManager { * @returns The next order number for a new stage in the job. */ @withSpanAsyncV4 - private async getNextStageOrder(jobId: string): Promise { + private async getNextStageOrder(jobId: JobId): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, @@ -486,7 +487,7 @@ export class StageManager { * @param tx transaction context. */ @withSpanAsyncV4 - private async updateJobCompletionProgress(jobId: string, tx?: PrismaTransaction): Promise<{ completedStages: number; totalStages: number }> { + private async updateJobCompletionProgress(jobId: JobId, tx?: PrismaTransaction): Promise<{ completedStages: number; totalStages: number }> { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: jobId, diff --git a/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts index 12d91c5..22afa20 100644 --- a/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts @@ -11,7 +11,7 @@ import { type RequestSender, } from '@map-colonies/openapi-helpers/requestSender'; import { faker } from '@faker-js/faker'; -import { openapiFilePath, type paths, type operations } from 'jobnik-openapi'; +import { openapiFilePath, type paths, type operations, type JobId } from 'jobnik-openapi'; import { JobOperationStatus, Priority, Prisma, StageOperationStatus, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import type { PrismaTransaction } from '@src/db/types'; import { getApp } from '@src/app'; @@ -969,7 +969,7 @@ describe('task', function () { ); const secondStage = await requestSender.addStageV1({ - pathParams: { jobId: stage.jobId }, + pathParams: { jobId: stage.jobId as JobId }, requestBody: { type: 'Second Stage', data: {}, @@ -990,7 +990,7 @@ describe('task', function () { const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); const getSecondStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: secondStage.body.id } }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId as JobId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedTaskStatus); @@ -1023,7 +1023,7 @@ describe('task', function () { const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId as JobId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedTaskStatus); @@ -1239,7 +1239,7 @@ describe('task', function () { }); const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ @@ -1596,7 +1596,7 @@ describe('task', function () { const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ diff --git a/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts index 961ffdd..c33d184 100644 --- a/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts +++ b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts @@ -9,6 +9,7 @@ import { illegalStatusTransitionErrorMessage, prismaKnownErrors } from '@src/com import { JobManager } from '@src/jobs/models/manager'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; import type { JobCreateModel } from '@src/jobs/models/models'; +import type { JobId } from 'jobnik-openapi'; import { randomUuid } from '@tests/unit/generator'; import { SERVICE_NAME } from '@src/common/constants'; import { jobEntityWithAbortStatus, jobEntityWithoutStages, jobEntityWithStages } from '../data'; @@ -119,7 +120,7 @@ describe('JobManager', () => { it('should return a job matching the provided id', async function () { prisma.job.findUnique.mockResolvedValue(jobEntityWithoutStages); - const jobs = await jobManager.getJobById(jobEntityWithoutStages.id); + const jobs = await jobManager.getJobById(jobEntityWithoutStages.id as JobId); const { xstate, stage, ...rest } = jobEntityWithoutStages; const expectedJob = { @@ -138,7 +139,7 @@ describe('JobManager', () => { it("should fail with a 'job not found' error when retrieving the job", async function () { prisma.job.findUnique.mockResolvedValue(null); - await expect(jobManager.getJobById('some_id')).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(jobManager.getJobById('some_id' as JobId)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); }); @@ -146,7 +147,7 @@ describe('JobManager', () => { it('should fail with a database error when retrieving a job', async function () { prisma.job.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(jobManager.getJobById('some_id')).rejects.toThrow('db connection error'); + await expect(jobManager.getJobById('some_id' as JobId)).rejects.toThrow('db connection error'); }); }); }); @@ -156,7 +157,7 @@ describe('JobManager', () => { it("should successfully update job's user metadata by id", async function () { prisma.job.update.mockResolvedValue(jobEntityWithoutStages); - await expect(jobManager.updateUserMetadata(jobEntityWithoutStages.id, { newData: 'test' })).toResolve(); + await expect(jobManager.updateUserMetadata(jobEntityWithoutStages.id as JobId, { newData: 'test' })).toResolve(); }); }); @@ -164,7 +165,9 @@ describe('JobManager', () => { it('should fail when updating user metadata of a non-existent job', async function () { prisma.job.update.mockRejectedValue(jobNotFoundError); - await expect(jobManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(jobManager.updateUserMetadata('someId' as JobId, { testData: 'some new data' })).rejects.toThrow( + jobsErrorMessages.jobNotFound + ); }); }); @@ -172,7 +175,7 @@ describe('JobManager', () => { it('should fail with a database error when updating user metadata', async function () { prisma.job.update.mockRejectedValueOnce(new Error('db connection error')); - await expect(jobManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow('db connection error'); + await expect(jobManager.updateUserMetadata('someId' as JobId, { testData: 'some new data' })).rejects.toThrow('db connection error'); }); }); }); @@ -183,13 +186,13 @@ describe('JobManager', () => { prisma.job.findUnique.mockResolvedValue(jobEntityWithoutStages); prisma.job.update.mockResolvedValue(jobEntityWithoutStages); - await expect(jobManager.updatePriority(jobEntityWithoutStages.id, Priority.MEDIUM)).toResolve(); + await expect(jobManager.updatePriority(jobEntityWithoutStages.id as JobId, Priority.MEDIUM)).toResolve(); }); it('should not perform a job priority update when the provided priority matches the current job priority', async function () { prisma.job.findUnique.mockResolvedValue({ ...jobEntityWithoutStages, priority: Priority.HIGH }); - await expect(jobManager.updatePriority(jobEntityWithoutStages.id, Priority.HIGH)).rejects.toThrow( + await expect(jobManager.updatePriority(jobEntityWithoutStages.id as JobId, Priority.HIGH)).rejects.toThrow( jobsErrorMessages.priorityCannotBeUpdatedToSameValue ); }); @@ -199,7 +202,7 @@ describe('JobManager', () => { it('should fail when updating priority of a non-existent job', async function () { prisma.job.findUnique.mockResolvedValue(null); - await expect(jobManager.updatePriority('someId', Priority.MEDIUM)).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(jobManager.updatePriority('someId' as JobId, Priority.MEDIUM)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); }); @@ -207,7 +210,7 @@ describe('JobManager', () => { it('should fail with a database error when updating priority', async function () { prisma.job.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(jobManager.updatePriority('someId', Priority.MEDIUM)).rejects.toThrow('db connection error'); + await expect(jobManager.updatePriority('someId' as JobId, Priority.MEDIUM)).rejects.toThrow('db connection error'); }); }); }); @@ -228,7 +231,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(jobManager.updateStatus(jobId, JobOperationStatus.PENDING)).toResolve(); + await expect(jobManager.updateStatus(jobId as JobId, JobOperationStatus.PENDING)).toResolve(); }); }); @@ -244,7 +247,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(jobManager.updateStatus('someId', JobOperationStatus.PENDING)).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(jobManager.updateStatus('someId' as JobId, JobOperationStatus.PENDING)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); it('should fail on invalid status transition', async function () { @@ -258,7 +261,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(jobManager.updateStatus(jobEntityWithoutStages.id, JobOperationStatus.COMPLETED)).rejects.toThrow( + await expect(jobManager.updateStatus(jobEntityWithoutStages.id as JobId, JobOperationStatus.COMPLETED)).rejects.toThrow( illegalStatusTransitionErrorMessage(jobEntityWithoutStages.status, JobOperationStatus.COMPLETED) ); }); @@ -276,7 +279,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(jobManager.updateStatus('someId', JobOperationStatus.COMPLETED)).rejects.toThrow('db connection error'); + await expect(jobManager.updateStatus('someId' as JobId, JobOperationStatus.COMPLETED)).rejects.toThrow('db connection error'); }); }); }); @@ -287,7 +290,7 @@ describe('JobManager', () => { prisma.job.findUnique.mockResolvedValue(jobEntityWithAbortStatus); prisma.job.delete.mockResolvedValue(jobEntityWithAbortStatus); - await expect(jobManager.deleteJob(jobEntityWithAbortStatus.id)).toResolve(); + await expect(jobManager.deleteJob(jobEntityWithAbortStatus.id as JobId)).toResolve(); }); }); @@ -295,13 +298,13 @@ describe('JobManager', () => { it('should return an error for a request to delete non finalized-status job', async function () { prisma.job.findUnique.mockResolvedValue(jobEntityWithoutStages); - await expect(jobManager.deleteJob(jobEntityWithoutStages.id)).rejects.toThrow(jobsErrorMessages.jobNotInFiniteState); + await expect(jobManager.deleteJob(jobEntityWithoutStages.id as JobId)).rejects.toThrow(jobsErrorMessages.jobNotInFiniteState); }); it('should return an error for a request to delete a non-existent job', async function () { prisma.job.findUnique.mockResolvedValue(null); - await expect(jobManager.deleteJob('someId')).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(jobManager.deleteJob('someId' as JobId)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); }); @@ -309,7 +312,7 @@ describe('JobManager', () => { it('should fail with a database error when deleting a job', async function () { prisma.job.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(jobManager.deleteJob('someId')).rejects.toThrow('db connection error'); + await expect(jobManager.deleteJob('someId' as JobId)).rejects.toThrow('db connection error'); }); }); }); diff --git a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts index 24a4000..616aff1 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -7,6 +7,7 @@ import { trace } from '@opentelemetry/api'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, JobOperationStatus } from '@prismaClient'; +import type { JobId } from 'jobnik-openapi'; import { StageManager } from '@src/stages/models/manager'; import { JobManager } from '@src/jobs/models/manager'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; @@ -177,7 +178,7 @@ describe('JobManager', () => { prisma.job.findUnique.mockResolvedValue(jobEntityWithStages); prisma.stage.findMany.mockResolvedValue([stageEntity]); - const stage = await stageManager.getStagesByJobId(stageEntity.jobId); + const stage = await stageManager.getStagesByJobId(stageEntity.jobId as JobId); const { xstate, task, tracestate, ...rest } = stageEntity; const expectedStage = [rest]; @@ -194,7 +195,7 @@ describe('JobManager', () => { prisma.job.findUnique.mockResolvedValue(jobEntityWithStages); prisma.stage.findMany.mockResolvedValue([stageEntity]); - const stage = await stageManager.getStagesByJobId(stageEntity.jobId, true); + const stage = await stageManager.getStagesByJobId(stageEntity.jobId as JobId, true); const { xstate, task, tracestate, ...rest } = stageEntity; @@ -216,7 +217,7 @@ describe('JobManager', () => { prisma.job.findUnique.mockResolvedValue(jobEntityWithStages); prisma.stage.findMany.mockResolvedValue(orderedStages); - const stages = await stageManager.getStagesByJobId(jobId); + const stages = await stageManager.getStagesByJobId(jobId as JobId); expect(stages).toMatchObject([ { id: stage1.id, order: 1 }, @@ -230,7 +231,7 @@ describe('JobManager', () => { it('should failed on not founded stage when getting by non exists job', async function () { prisma.job.findUnique.mockResolvedValue(null); - await expect(stageManager.getStagesByJobId('some_id')).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(stageManager.getStagesByJobId('some_id' as JobId)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); }); @@ -238,7 +239,7 @@ describe('JobManager', () => { it('should failed on db error when getting desired stage', async function () { prisma.job.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(stageManager.getStagesByJobId('some_id')).rejects.toThrow('db connection error'); + await expect(stageManager.getStagesByJobId('some_id' as JobId)).rejects.toThrow('db connection error'); }); }); }); @@ -327,7 +328,7 @@ describe('JobManager', () => { prisma.stage.create.mockResolvedValue(anotherStageEntity); - const stagesResponse = await stageManager.addStage(uniqueJobId, anotherStagePayload); + const stagesResponse = await stageManager.addStage(uniqueJobId as JobId, anotherStagePayload); // Extract unnecessary fields from the stage object and assemble the expected result const { xstate, task, tracestate, ...rest } = anotherStageEntity; @@ -360,7 +361,7 @@ describe('JobManager', () => { prisma.stage.create.mockResolvedValue(anotherStageEntity); - const stagesResponse = await stageManager.addStage(uniqueJobId, anotherStagePayload); + const stagesResponse = await stageManager.addStage(uniqueJobId as JobId, anotherStagePayload); // Extract unnecessary fields from the stage object and assemble the expected result const { xstate, task, tracestate, ...rest } = anotherStageEntity; @@ -392,7 +393,7 @@ describe('JobManager', () => { prisma.stage.create.mockResolvedValue(expectedStageEntity); - const result = await stageManager.addStage(uniqueJobId, stagePayload); + const result = await stageManager.addStage(uniqueJobId as JobId, stagePayload); expect(result).toMatchObject({ id: expectedStageEntity.id, @@ -424,7 +425,7 @@ describe('JobManager', () => { prisma.stage.create.mockResolvedValue(expectedStageEntity); - const result = await stageManager.addStage(uniqueJobId, stagePayload); + const result = await stageManager.addStage(uniqueJobId as JobId, stagePayload); expect(result).toMatchObject({ id: expectedStageEntity.id, @@ -473,8 +474,8 @@ describe('JobManager', () => { prisma.stage.create.mockResolvedValueOnce(expectedStageEntity1).mockResolvedValueOnce(expectedStageEntity2); - const result1 = await stageManager.addStage(jobId1, stagePayload1); - const result2 = await stageManager.addStage(jobId2, stagePayload2); + const result1 = await stageManager.addStage(jobId1 as JobId, stagePayload1); + const result2 = await stageManager.addStage(jobId2 as JobId, stagePayload2); expect(result1).toMatchObject({ id: expectedStageEntity1.id, @@ -491,13 +492,13 @@ describe('JobManager', () => { it('should reject adding stage to a non-existent job', async function () { prisma.job.findUnique.mockResolvedValue(null); - await expect(stageManager.addStage('someId', {} as unknown as StageCreateModel)).rejects.toThrow(jobsErrorMessages.jobNotFound); + await expect(stageManager.addStage('someId' as JobId, {} as unknown as StageCreateModel)).rejects.toThrow(jobsErrorMessages.jobNotFound); }); it('should reject adding stage to a finite job', async function () { prisma.job.findUnique.mockResolvedValue({ ...jobEntityWithAbortStatus }); - await expect(stageManager.addStage('someId', {} as unknown as StageCreateModel)).rejects.toThrow( + await expect(stageManager.addStage('someId' as JobId, {} as unknown as StageCreateModel)).rejects.toThrow( new JobInFiniteStateError(jobsErrorMessages.jobAlreadyFinishedStagesError) ); }); @@ -510,7 +511,7 @@ describe('JobManager', () => { prisma.stage.aggregate.mockResolvedValueOnce({ _max: { order: null } } as StageAggregateResult); // No existing stages prisma.stage.create.mockRejectedValueOnce(new Error('db connection error')); - await expect(stageManager.addStage(jobEntity.id, {} as unknown as StageCreateModel)).rejects.toThrow('db connection error'); + await expect(stageManager.addStage(jobEntity.id as JobId, {} as unknown as StageCreateModel)).rejects.toThrow('db connection error'); }); }); }); From 3e73e82d07442a2e4fba08183036b0169f07ecef Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:00:12 +0300 Subject: [PATCH 04/16] feat(jobnik-manager): brand stage ids end-to-end Retype every stageId parameter in the stage service layer, the stage-id flow through the task manager, and stageRepository. updateStageSummary to StageId from jobnik-openapi, so getStageById and the repository reject a JobId/TaskId at compile time. Prisma-sourced stage ids are cast at the DB boundary, matching the JobId pattern from the previous commit. Also casts the Prisma->API boundary in convertPrismaToStageResponse, and closes a couple of JobId gaps in the stages integration spec that the job-id-branding commit missed. Co-Authored-By: Claude Sonnet 5 --- .../src/stages/DAL/stageRepository.ts | 3 +- .../src/stages/models/helper.ts | 2 +- .../src/stages/models/manager.ts | 20 +-- .../src/tasks/models/manager.ts | 13 +- .../tests/integration/stages/stages.spec.ts | 141 +++++++++--------- .../tests/unit/stages/repository.spec.ts | 9 +- .../tests/unit/stages/stages.spec.ts | 52 ++++--- 7 files changed, 126 insertions(+), 114 deletions(-) diff --git a/apps/jobnik-manager/src/stages/DAL/stageRepository.ts b/apps/jobnik-manager/src/stages/DAL/stageRepository.ts index 29e9738..53f5aa8 100644 --- a/apps/jobnik-manager/src/stages/DAL/stageRepository.ts +++ b/apps/jobnik-manager/src/stages/DAL/stageRepository.ts @@ -1,5 +1,6 @@ import { inject, Lifecycle, scoped } from 'tsyringe'; import { type Logger } from '@map-colonies/js-logger'; +import type { StageId } from 'jobnik-openapi'; import { Prisma, PrismaClient } from '@prismaClient'; import { SERVICES } from '@src/common/constants'; import { PrismaTransaction } from '@src/db/types'; @@ -13,7 +14,7 @@ export class StageRepository { @inject(SERVICES.PRISMA) private readonly prisma: PrismaClient ) {} - public async updateStageSummary(stageId: string, summaryPayload: UpdateSummaryCount, tx: PrismaTransaction): Promise { + public async updateStageSummary(stageId: StageId, summaryPayload: UpdateSummaryCount, tx: PrismaTransaction): Promise { const addStatus = summaryCountsMapper[summaryPayload.add.status]; const addCount = summaryPayload.add.count; diff --git a/apps/jobnik-manager/src/stages/models/helper.ts b/apps/jobnik-manager/src/stages/models/helper.ts index 0ef6227..e13b6f1 100644 --- a/apps/jobnik-manager/src/stages/models/helper.ts +++ b/apps/jobnik-manager/src/stages/models/helper.ts @@ -39,7 +39,7 @@ function convertPrismaToStageResponse(prismaObjects: StagePrismaObject): StageMo tasks: Array.isArray(task) ? convertArrayPrismaTaskToTaskResponse(task) : undefined, tracestate: tracestate ?? undefined, }; - return Object.assign(rest, transformedFields); + return Object.assign(rest, transformedFields) as StageModel; } /** diff --git a/apps/jobnik-manager/src/stages/models/manager.ts b/apps/jobnik-manager/src/stages/models/manager.ts index 714fcb1..5867af5 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -4,7 +4,7 @@ import { createActor } from 'xstate'; import { trace, type Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; -import type { JobId } from 'jobnik-openapi'; +import type { JobId, StageId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { JobOperationStatus, Prisma, StageOperationStatus } from '@prismaClient'; import { JobManager } from '@src/jobs/models/manager'; @@ -153,7 +153,7 @@ export class StageManager { } @withSpanAsyncV4 - public async getStageById(stageId: string, includeTasks?: boolean): Promise { + public async getStageById(stageId: StageId, includeTasks?: boolean): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, @@ -200,7 +200,7 @@ export class StageManager { } @withSpanAsyncV4 - public async getSummaryByStageId(stageId: string): Promise { + public async getSummaryByStageId(stageId: StageId): Promise { trace.getActiveSpan()?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, }); @@ -213,7 +213,7 @@ export class StageManager { } @withSpanAsyncV4 - public async updateUserMetadata(stageId: string, userMetadata: Record): Promise { + public async updateUserMetadata(stageId: StageId, userMetadata: Record): Promise { trace.getActiveSpan()?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, }); @@ -238,7 +238,7 @@ export class StageManager { } @withSpanAsyncV4 - public async updateStatus(stageId: string, status: StageOperationStatus, tx?: PrismaTransaction): Promise { + public async updateStatus(stageId: StageId, status: StageOperationStatus, tx?: PrismaTransaction): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, @@ -266,7 +266,7 @@ export class StageManager { */ @withSpanAsyncV4 public async getStageEntityById( - stageId: string, + stageId: StageId, options: T = {} as T ): Promise> { trace.getActiveSpan()?.setAttributes({ @@ -295,7 +295,7 @@ export class StageManager { * @param summary summary object containing the current progress aggregated task data of the stage. */ @withSpanAsyncV4 - public async updateStageProgressFromTaskChanges(stageId: string, summaryUpdatePayload: UpdateSummaryCount, tx: PrismaTransaction): Promise { + public async updateStageProgressFromTaskChanges(stageId: StageId, summaryUpdatePayload: UpdateSummaryCount, tx: PrismaTransaction): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, @@ -320,7 +320,7 @@ export class StageManager { } @withSpanAsyncV4 - private async executeUpdateStatus(stageId: string, targetStatus: StageOperationStatus, tx: PrismaTransaction): Promise { + private async executeUpdateStatus(stageId: StageId, targetStatus: StageOperationStatus, tx: PrismaTransaction): Promise { const stage = await this.getStageEntityById(stageId, { includeJob: true, tx }); if (!stage) { @@ -393,7 +393,7 @@ export class StageManager { }); if (nextStage?.status === StageOperationStatus.CREATED) { - await this.executeUpdateStatus(nextStage.id, StageOperationStatus.PENDING, tx); + await this.executeUpdateStatus(nextStage.id as StageId, StageOperationStatus.PENDING, tx); trace.getActiveSpan()?.addEvent('Next stage set to PENDING', { nextStageId: nextStage.id }); } @@ -443,7 +443,7 @@ export class StageManager { await tx.stage.update({ where: { id: stage.id }, data: stageUpdatedData }); if (summary.total === summary.completed) { - await this.updateStatus(stage.id, StageOperationStatus.COMPLETED, tx); + await this.updateStatus(stage.id as StageId, StageOperationStatus.COMPLETED, tx); this.logger.info({ msg: 'Stage completed, updating job progress', diff --git a/apps/jobnik-manager/src/tasks/models/manager.ts b/apps/jobnik-manager/src/tasks/models/manager.ts index e0fcd37..08b5e80 100644 --- a/apps/jobnik-manager/src/tasks/models/manager.ts +++ b/apps/jobnik-manager/src/tasks/models/manager.ts @@ -5,6 +5,7 @@ import { trace, type Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { subMinutes } from 'date-fns'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; +import type { StageId } from 'jobnik-openapi'; import { Prisma, StageOperationStatus, Task, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import { SERVICES, XSTATE_DONE_STATE } from '@common/constants'; import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; @@ -42,7 +43,7 @@ export class TaskManager { ) {} @withSpanAsyncV4 - public async addTasks(stageId: string, tasksPayload: TaskCreateModel[]): Promise { + public async addTasks(stageId: StageId, tasksPayload: TaskCreateModel[]): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, @@ -155,7 +156,7 @@ export class TaskManager { } @withSpanAsyncV4 - public async getTasksByStageId(stageId: string, query: TasksByStageIdQuery): Promise { + public async getTasksByStageId(stageId: StageId, query: TasksByStageIdQuery): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [INFRA_CONVENTIONS.infra.jobnik.stage.id]: stageId, @@ -442,13 +443,13 @@ export class TaskManager { throw new TaskStatusUpdateFailedError(tasksErrorMessages.taskStatusUpdateFailed); } - await this.updateStageSummary(task.stageId, previousStatus, nextStatus, tx); + await this.updateStageSummary(task.stageId as StageId, previousStatus, nextStatus, tx); // TODO - Check if this stage type should propagate failure to parent job // For now, all task failures cause stage failure, but in future versions // some stages may be configured as optional (non-blocking) if (nextStatus === TaskOperationStatus.FAILED) { - const stage = await this.stageManager.getStageEntityById(task.stageId, { tx }); + const stage = await this.stageManager.getStageEntityById(task.stageId as StageId, { tx }); /* v8 ignore next 7 -- @preserve */ if (!stage) { @@ -466,7 +467,7 @@ export class TaskManager { stageId: task.stageId, }); - await this.stageManager.updateStatus(task.stageId, StageOperationStatus.FAILED, tx); + await this.stageManager.updateStatus(task.stageId as StageId, StageOperationStatus.FAILED, tx); trace.getActiveSpan()?.addEvent('Stage set to FAILED', { stageId: task.stageId }); } @@ -515,7 +516,7 @@ export class TaskManager { } private async updateStageSummary( - stageId: string, + stageId: StageId, previousStatus: TaskOperationStatus, nextStatus: TaskOperationStatus, tx: PrismaTransaction diff --git a/apps/jobnik-manager/tests/integration/stages/stages.spec.ts b/apps/jobnik-manager/tests/integration/stages/stages.spec.ts index ca84f07..61e073a 100644 --- a/apps/jobnik-manager/tests/integration/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/integration/stages/stages.spec.ts @@ -11,7 +11,7 @@ import { type RequestSender, } from '@map-colonies/openapi-helpers/requestSender'; import { faker } from '@faker-js/faker'; -import { openapiFilePath, type paths, type operations } from 'jobnik-openapi'; +import { openapiFilePath, type paths, type operations, type JobId, type StageId } from 'jobnik-openapi'; import { JobOperationStatus, StageOperationStatus, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import type { PrismaTransaction } from '@src/db/types'; import { getApp } from '@src/app'; @@ -82,7 +82,7 @@ describe('stage', function () { }); const jobId = job.id; - const response = await requestSender.getStagesV1({ queryParams: { job_id: jobId } }); + const response = await requestSender.getStagesV1({ queryParams: { job_id: jobId as JobId } }); expect(response).toMatchObject({ status: StatusCodes.OK, @@ -102,7 +102,7 @@ describe('stage', function () { }); it('should return 200 status code and empty array', async function () { - const response = await requestSender.getStagesV1({ queryParams: { job_id: faker.string.uuid() } }); + const response = await requestSender.getStagesV1({ queryParams: { job_id: faker.string.uuid() as JobId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -169,7 +169,7 @@ describe('stage', function () { const jobId = job.id; - const response = await requestSender.getStagesV1({ queryParams: { job_id: jobId, should_return_tasks: true } }); + const response = await requestSender.getStagesV1({ queryParams: { job_id: jobId as JobId, should_return_tasks: true } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -197,7 +197,7 @@ describe('stage', function () { [{}] ); - const response = await requestSender.getStagesV1({ queryParams: { job_id: job.id, should_return_tasks: false } }); + const response = await requestSender.getStagesV1({ queryParams: { job_id: job.id as JobId, should_return_tasks: false } }); expectResponseStatus(response, 200); @@ -276,7 +276,7 @@ describe('stage', function () { ); const getStageResponse = await requestSender.getStageByIdV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, queryParams: { should_return_tasks: undefined }, }); @@ -294,7 +294,7 @@ describe('stage', function () { ]); const getStageResponse = await requestSender.getStageByIdV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, queryParams: { should_return_tasks: true }, }); @@ -310,7 +310,7 @@ describe('stage', function () { ]); const getStageResponse = await requestSender.getStageByIdV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, queryParams: { should_return_tasks: false }, }); @@ -322,7 +322,7 @@ describe('stage', function () { describe('Bad Path', function () { it('should return a 404 status code with a validation error message if the requested stage does not exist', async function () { - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: faker.string.uuid() } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: faker.string.uuid() as StageId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -332,7 +332,7 @@ describe('stage', function () { }); it('should return status code 400 when supplying bad uuid as part of the request', async function () { - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: 'someInvalidJobId' } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: 'someInvalidJobId' as StageId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -348,7 +348,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStageByIdV1({ pathParams: { stageId: dumpUuid } }); + const response = await requestSender.getStageByIdV1({ pathParams: { stageId: dumpUuid as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -362,7 +362,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStageByIdV1({ pathParams: { stageId: dumpUuid } }); + const response = await requestSender.getStageByIdV1({ pathParams: { stageId: dumpUuid as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -378,7 +378,7 @@ describe('stage', function () { it('should return 200 status code and return the stages', async function () { const { stage, job } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); - const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: job.id } }); + const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: job.id as JobId } }); expectResponseStatus(getStageResponse, 200); @@ -393,7 +393,7 @@ describe('stage', function () { ]); const getStageResponse = await requestSender.getStagesByJobIdV1({ - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, queryParams: { should_return_tasks: true }, }); @@ -411,7 +411,7 @@ describe('stage', function () { ]); const getStageResponse = await requestSender.getStagesByJobIdV1({ - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, queryParams: { should_return_tasks: false }, }); @@ -425,7 +425,7 @@ describe('stage', function () { it('should return a 200 status code with empty array object if no stages exists for the requested job', async function () { const { job } = await createJobnikTree(prisma, {}, {}, [], { createStage: false, createTasks: false }); - const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: job.id } }); + const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: job.id as JobId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -440,7 +440,7 @@ describe('stage', function () { // Create multiple stages for the same job using API endpoints to ensure proper order assignment await requestSender.addStageV1({ - pathParams: { jobId: createdJobId }, + pathParams: { jobId: createdJobId as JobId }, requestBody: { type: 'FIRST_STAGE', data: {}, @@ -449,7 +449,7 @@ describe('stage', function () { }); await requestSender.addStageV1({ - pathParams: { jobId: createdJobId }, + pathParams: { jobId: createdJobId as JobId }, requestBody: { type: 'SECOND_STAGE', data: {}, @@ -458,7 +458,7 @@ describe('stage', function () { }); await requestSender.addStageV1({ - pathParams: { jobId: createdJobId }, + pathParams: { jobId: createdJobId as JobId }, requestBody: { type: 'THIRD_STAGE', data: {}, @@ -466,7 +466,7 @@ describe('stage', function () { }, }); - const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: createdJobId } }); + const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: createdJobId as JobId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -482,7 +482,7 @@ describe('stage', function () { describe('Bad Path', function () { it('should return status code 400 when supplying bad uuid', async function () { - const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: 'someInvalidJobId' } }); + const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: 'someInvalidJobId' as JobId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -492,7 +492,7 @@ describe('stage', function () { }); it('should return status code 404 when a job with the given uuid does not exists', async function () { - const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() } }); + const getStageResponse = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() as JobId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -508,7 +508,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() } }); + const response = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() as JobId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -522,7 +522,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() } }); + const response = await requestSender.getStagesByJobIdV1({ pathParams: { jobId: faker.string.uuid() as JobId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -538,7 +538,7 @@ describe('stage', function () { it("should return 200 status code and return the stage's summary", async function () { const { stage } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); - const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: stage.id } }); + const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: stage.id as StageId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ status: StatusCodes.OK, body: defaultStatusCounts }); @@ -547,7 +547,7 @@ describe('stage', function () { describe('Bad Path', function () { it("should return a 404 status code and a validation error indicating the stage's non-existence should be returned", async function () { - const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid } }); + const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid as StageId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -557,7 +557,7 @@ describe('stage', function () { }); it('should return status code 400 when supplying bad uuid as part of the request', async function () { - const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: 'someInvalidJobId' } }); + const getStageResponse = await requestSender.getStageSummaryV1({ pathParams: { stageId: 'someInvalidJobId' as StageId } }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -573,7 +573,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid } }); + const response = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -587,7 +587,7 @@ describe('stage', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid } }); + const response = await requestSender.getStageSummaryV1({ pathParams: { stageId: dumpUuid as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -606,11 +606,11 @@ describe('stage', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const updateUserMetadataResponse = await requestSender.updateStageUserMetadataV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, requestBody: userMetadataInput, }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id as StageId } }); expect(updateUserMetadataResponse).toSatisfyApiSpec(); expect(getStageResponse.body).toMatchObject({ userMetadata: userMetadataInput }); @@ -619,7 +619,10 @@ describe('stage', function () { describe('Bad Path', function () { it('should return a 404 status code along with a message that specifies that a stage with the given id was not found', async function () { - const getStageResponse = await requestSender.updateStageUserMetadataV1({ pathParams: { stageId: dumpUuid }, requestBody: { avi: 'avi' } }); + const getStageResponse = await requestSender.updateStageUserMetadataV1({ + pathParams: { stageId: dumpUuid as StageId }, + requestBody: { avi: 'avi' }, + }); expect(getStageResponse).toSatisfyApiSpec(); expect(getStageResponse).toMatchObject({ @@ -632,7 +635,7 @@ describe('stage', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const response = await requestSender.updateStageUserMetadataV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, requestBody: 'badInputString' as unknown as { [key: string]: string }, }); @@ -650,7 +653,7 @@ describe('stage', function () { const updateSpy = createProxyMock(prisma.stage, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateStageUserMetadataV1({ pathParams: { stageId: faker.string.uuid() }, requestBody: {} }); + const response = await requestSender.updateStageUserMetadataV1({ pathParams: { stageId: faker.string.uuid() as StageId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -664,7 +667,7 @@ describe('stage', function () { const updateSpy = createProxyMock(prisma.stage, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateStageUserMetadataV1({ pathParams: { stageId: faker.string.uuid() }, requestBody: {} }); + const response = await requestSender.updateStageUserMetadataV1({ pathParams: { stageId: faker.string.uuid() as StageId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -688,7 +691,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -710,7 +713,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -732,7 +735,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -757,7 +760,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -778,7 +781,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagePayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -804,7 +807,7 @@ describe('stage', function () { const stage1Response = await requestSender.addStageV1({ requestBody: stage1Payload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(stage1Response).toMatchObject({ @@ -823,7 +826,7 @@ describe('stage', function () { const stage2Response = await requestSender.addStageV1({ requestBody: stage2Payload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(stage2Response).toMatchObject({ @@ -842,7 +845,7 @@ describe('stage', function () { const stage3Response = await requestSender.addStageV1({ requestBody: stage3Payload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(stage3Response).toMatchObject({ @@ -880,17 +883,17 @@ describe('stage', function () { // Create stages const job1Stage1Response = await requestSender.addStageV1({ requestBody: job1Stage1Payload, - pathParams: { jobId: job1.id }, + pathParams: { jobId: job1.id as JobId }, }); const job1Stage2Response = await requestSender.addStageV1({ requestBody: job1Stage2Payload, - pathParams: { jobId: job1.id }, + pathParams: { jobId: job1.id as JobId }, }); const job2Stage1Response = await requestSender.addStageV1({ requestBody: job2Stage1Payload, - pathParams: { jobId: job2.id }, + pathParams: { jobId: job2.id as JobId }, }); expect(job1Stage1Response).toMatchObject({ @@ -931,13 +934,13 @@ describe('stage', function () { ...stageData, userMetadata: {}, } satisfies StageCreateModel, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); } // Fetch all stages for the job const getStagesResponse = await requestSender.getStagesByJobIdV1({ - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(getStagesResponse).toMatchObject({ @@ -957,7 +960,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); await memoryExporter.forceFlush(); @@ -984,7 +987,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -1010,7 +1013,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -1036,7 +1039,7 @@ describe('stage', function () { data: {}, userMetadata: {}, } satisfies StageCreateModel, - pathParams: { jobId: 'someInvalidJobId' }, + pathParams: { jobId: 'someInvalidJobId' as JobId }, }); expect(addStageResponse).toMatchObject({ @@ -1049,7 +1052,7 @@ describe('stage', function () { const { job } = await createJobnikTree(prisma, {}, {}, [], { createStage: false, createTasks: false }); const addStageResponse = await requestSender.addStageV1({ - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, requestBody: {} as unknown as StageCreateModel, }); @@ -1067,7 +1070,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: { data: {}, userMetadata: {}, type: 'SOME_STAGE_TYPE' } satisfies StageCreateModel, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -1089,7 +1092,7 @@ describe('stage', function () { const addStageResponse = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: job.id }, + pathParams: { jobId: job.id as JobId }, }); expect(addStageResponse).toSatisfyApiSpec(); @@ -1108,7 +1111,7 @@ describe('stage', function () { const response = await requestSender.addStageV1({ requestBody: createStagesPayload, - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, }); expect(response).toSatisfyApiSpec(); @@ -1127,7 +1130,7 @@ describe('stage', function () { const response = await requestSender.addStageV1({ requestBody: { data: {}, type: 'SOME_STAGE_TYPE', userMetadata: {} }, - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, }); expect(response).toSatisfyApiSpec(); @@ -1144,7 +1147,7 @@ describe('stage', function () { const response = await requestSender.addStageV1({ requestBody: { data: {}, type: 'SOME_STAGE_TYPE', userMetadata: {} }, - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, }); expect(response).toSatisfyApiSpec(); @@ -1162,14 +1165,14 @@ describe('stage', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const setStatusResponse = await requestSender.updateStageStatusV1({ - pathParams: { stageId: stage.id }, + pathParams: { stageId: stage.id as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); expect(setStatusResponse).toSatisfyApiSpec(); expect(setStatusResponse).toHaveProperty('status', StatusCodes.OK); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id as StageId } }); expect(getStageResponse).toHaveProperty('body.status', StageOperationStatus.PENDING); }); @@ -1177,7 +1180,7 @@ describe('stage', function () { it("should return 201 status code and modify stages's status by order (stage is first ordered before other)", async function () { const { stage: stage1 } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const secondStageResponse = await requestSender.addStageV1({ - pathParams: { jobId: stage1.jobId }, + pathParams: { jobId: stage1.jobId as JobId }, requestBody: { type: 'SECOND_STAGE', data: {}, userMetadata: {} }, }); @@ -1188,14 +1191,14 @@ describe('stage', function () { expect(stage2).toHaveProperty('order', 2); const setStatusResponse = await requestSender.updateStageStatusV1({ - pathParams: { stageId: stage1.id }, + pathParams: { stageId: stage1.id as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); expect(setStatusResponse).toSatisfyApiSpec(); expect(setStatusResponse).toHaveProperty('status', StatusCodes.OK); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage1.id } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage1.id as StageId } }); expect(getStageResponse).toHaveProperty('body.status', StageOperationStatus.PENDING); }); @@ -1228,7 +1231,7 @@ describe('stage', function () { const stageId = stage.id; const updateStageResponse = await requestSender.updateStageStatusV1({ - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); @@ -1248,7 +1251,7 @@ describe('stage', function () { const stageId = stage.id; const updateStageResponse = await requestSender.updateStageStatusV1({ - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, requestBody: { status: StageOperationStatus.COMPLETED as unknown as 'PENDING' }, }); @@ -1266,7 +1269,7 @@ describe('stage', function () { const { stage: stage1 } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const secondStageResponse = await requestSender.addStageV1({ - pathParams: { jobId: stage1.jobId }, + pathParams: { jobId: stage1.jobId as JobId }, requestBody: { type: 'SECOND_STAGE', data: {}, userMetadata: {} }, }); @@ -1289,7 +1292,7 @@ describe('stage', function () { it('should return 404 with specific error message for non-existent stage', async function () { const updateStageResponse = await requestSender.updateStageStatusV1({ - pathParams: { stageId: testStageId }, + pathParams: { stageId: testStageId as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); @@ -1315,7 +1318,7 @@ describe('stage', function () { }); const response = await requestSender.updateStageStatusV1({ - pathParams: { stageId: testStageId }, + pathParams: { stageId: testStageId as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); @@ -1341,7 +1344,7 @@ describe('stage', function () { }); const response = await requestSender.updateStageStatusV1({ - pathParams: { stageId: testStageId }, + pathParams: { stageId: testStageId as StageId }, requestBody: { status: StageOperationStatus.PENDING }, }); diff --git a/apps/jobnik-manager/tests/unit/stages/repository.spec.ts b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts index 730f852..d4ccb3c 100644 --- a/apps/jobnik-manager/tests/unit/stages/repository.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts @@ -6,6 +6,7 @@ import type { PrismaClient } from '@prismaClient'; import { TaskOperationStatus } from '@prismaClient'; import type { UpdateSummaryCount } from '@src/stages/models/models'; import { defaultStatusCounts } from '@src/stages/models/helper'; +import type { StageId } from 'jobnik-openapi'; import { StageRepository } from '@src/stages/DAL/stageRepository'; import { createStageEntity } from '../generator'; @@ -36,7 +37,7 @@ describe('JobManager', () => { add: { status: TaskOperationStatus.CREATED, count: 1 }, } satisfies UpdateSummaryCount; - await expect(stageRepository.updateStageSummary(stageEntity.id, summaryUpdatePayload, mockTx)).toResolve(); + await expect(stageRepository.updateStageSummary(stageEntity.id as StageId, summaryUpdatePayload, mockTx)).toResolve(); }); it('should not increase total count and change counting of other', async function () { @@ -52,7 +53,7 @@ describe('JobManager', () => { remove: { status: TaskOperationStatus.CREATED, count: 1 }, } satisfies UpdateSummaryCount; - await expect(stageRepository.updateStageSummary(stageEntity.id, summaryUpdatePayload, mockTx)).toResolve(); + await expect(stageRepository.updateStageSummary(stageEntity.id as StageId, summaryUpdatePayload, mockTx)).toResolve(); }); }); @@ -70,7 +71,7 @@ describe('JobManager', () => { remove: { status: TaskOperationStatus.CREATED, count: 1 }, } satisfies UpdateSummaryCount; - await expect(stageRepository.updateStageSummary(stageEntity.id, summaryUpdatePayload, mockTx)).rejects.toThrow( + await expect(stageRepository.updateStageSummary(stageEntity.id as StageId, summaryUpdatePayload, mockTx)).rejects.toThrow( 'Failed to update stage summary: No summary returned from database.' ); }); @@ -85,7 +86,7 @@ describe('JobManager', () => { $queryRaw: vi.fn().mockRejectedValueOnce(new Error('db connection error')), } as unknown as Omit; - await expect(stageRepository.updateStageSummary('someId', summaryUpdatePayload, mockTx)).rejects.toThrow('db connection error'); + await expect(stageRepository.updateStageSummary('someId' as StageId, summaryUpdatePayload, mockTx)).rejects.toThrow('db connection error'); }); }); }); diff --git a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts index 616aff1..442af42 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -7,7 +7,7 @@ import { trace } from '@opentelemetry/api'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, JobOperationStatus } from '@prismaClient'; -import type { JobId } from 'jobnik-openapi'; +import type { JobId, StageId } from 'jobnik-openapi'; import { StageManager } from '@src/stages/models/manager'; import { JobManager } from '@src/jobs/models/manager'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; @@ -127,7 +127,7 @@ describe('JobManager', () => { const stageId = stageEntity.id; prisma.stage.findUnique.mockResolvedValue(stageEntity); - const stage = await stageManager.getStageById(stageId); + const stage = await stageManager.getStageById(stageId as StageId); const { xstate, task, tracestate, ...rest } = stageEntity; @@ -144,7 +144,7 @@ describe('JobManager', () => { prisma.stage.findUnique.mockResolvedValue(stageEntity); - const stage = await stageManager.getStageById(stageId); + const stage = await stageManager.getStageById(stageId as StageId); const { xstate, task, tracestate, ...rest } = stageEntity; @@ -159,7 +159,7 @@ describe('JobManager', () => { it('should result in failure when attempting to retrieve a job with a non-existent stage', async function () { prisma.stage.findUnique.mockResolvedValue(null); - await expect(stageManager.getStageById('some_id')).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(stageManager.getStageById('some_id' as StageId)).rejects.toThrow(stagesErrorMessages.stageNotFound); }); }); @@ -167,7 +167,7 @@ describe('JobManager', () => { it('should fail and throw an error if prisma throws an error', async function () { prisma.stage.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(stageManager.getStageById('some_id')).rejects.toThrow('db connection error'); + await expect(stageManager.getStageById('some_id' as StageId)).rejects.toThrow('db connection error'); }); }); }); @@ -249,7 +249,7 @@ describe('JobManager', () => { it("should return stage's summary object by provided stage id", async function () { prisma.stage.findUnique.mockResolvedValue(stageEntity); - const stage = await stageManager.getSummaryByStageId(stageEntity.id); + const stage = await stageManager.getSummaryByStageId(stageEntity.id as StageId); expect(stage).toMatchObject({}); }); @@ -259,7 +259,7 @@ describe('JobManager', () => { it('should failed on not founded stage when getting by non exists job', async function () { prisma.stage.findUnique.mockResolvedValue(null); - await expect(stageManager.getSummaryByStageId('some_id')).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(stageManager.getSummaryByStageId('some_id' as StageId)).rejects.toThrow(stagesErrorMessages.stageNotFound); }); }); @@ -267,7 +267,7 @@ describe('JobManager', () => { it('should failed on db error when getting desired stage', async function () { prisma.stage.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(stageManager.getSummaryByStageId('some_id')).rejects.toThrow('db connection error'); + await expect(stageManager.getSummaryByStageId('some_id' as StageId)).rejects.toThrow('db connection error'); }); }); }); @@ -279,7 +279,7 @@ describe('JobManager', () => { const stageId = stageEntity.id; const prismaUpdateStageMock = prisma.stage.update.mockResolvedValue(stageEntity); - await stageManager.updateUserMetadata(stageId, { newData: 'test' }); + await stageManager.updateUserMetadata(stageId as StageId, { newData: 'test' }); expect(prismaUpdateStageMock).toHaveBeenCalledTimes(1); }); @@ -289,7 +289,9 @@ describe('JobManager', () => { it('should failed on for not exists stage when update user metadata of desired stage', async function () { prisma.stage.update.mockRejectedValue(notFoundError); - await expect(stageManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(stageManager.updateUserMetadata('someId' as StageId, { testData: 'some new data' })).rejects.toThrow( + stagesErrorMessages.stageNotFound + ); }); }); @@ -297,7 +299,7 @@ describe('JobManager', () => { it('should failed on db error when update user metadata of desired stage', async function () { prisma.stage.update.mockRejectedValueOnce(new Error('db connection error')); - await expect(stageManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow('db connection error'); + await expect(stageManager.updateUserMetadata('someId' as StageId, { testData: 'some new data' })).rejects.toThrow('db connection error'); }); }); }); @@ -533,7 +535,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.PENDING)).toResolve(); + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.PENDING)).toResolve(); }); it('should successfully update stage status by id with previous stage check', async function () { @@ -557,7 +559,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.PENDING)).toResolve(); + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.PENDING)).toResolve(); }); it('should successfully update next ordered stage status to pending after completion of current', async function () { @@ -607,7 +609,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageId, StageOperationStatus.COMPLETED)).toResolve(); + await expect(stageManager.updateStatus(stageId as StageId, StageOperationStatus.COMPLETED)).toResolve(); }); it('should successfully complete the final stage and also complete the job', async function () { @@ -646,7 +648,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.COMPLETED)).toResolve(); + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.COMPLETED)).toResolve(); }); it("should successfully complete stage and also update in-progress job's percentage", async function () { @@ -692,7 +694,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.COMPLETED)).toResolve(); + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.COMPLETED)).toResolve(); }); it('should successfully update stage to IN_PROGRESS and move also the PENDING job to IN_PROGRESS', async function () { @@ -722,7 +724,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.IN_PROGRESS)).toResolve(); + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.IN_PROGRESS)).toResolve(); }); }); @@ -738,7 +740,9 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus('someId', StageOperationStatus.PENDING)).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(stageManager.updateStatus('someId' as StageId, StageOperationStatus.PENDING)).rejects.toThrow( + stagesErrorMessages.stageNotFound + ); }); it('should fail when updating status for a stage before previous completed', async function () { @@ -763,7 +767,9 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageId, StageOperationStatus.PENDING)).rejects.toThrow('Previous stage is not COMPLETED'); + await expect(stageManager.updateStatus(stageId as StageId, StageOperationStatus.PENDING)).rejects.toThrow( + 'Previous stage is not COMPLETED' + ); }); it('should fail on invalid status transition', async function () { @@ -777,7 +783,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus(stageEntity.id, StageOperationStatus.COMPLETED)).rejects.toThrow( + await expect(stageManager.updateStatus(stageEntity.id as StageId, StageOperationStatus.COMPLETED)).rejects.toThrow( illegalStatusTransitionErrorMessage(stageEntity.status, StageOperationStatus.COMPLETED) ); }); @@ -795,7 +801,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(stageManager.updateStatus('someId', StageOperationStatus.COMPLETED)).rejects.toThrow('db connection error'); + await expect(stageManager.updateStatus('someId' as StageId, StageOperationStatus.COMPLETED)).rejects.toThrow('db connection error'); }); }); }); @@ -829,7 +835,7 @@ describe('JobManager', () => { vi.spyOn(stageRepository, 'updateStageSummary').mockResolvedValueOnce({ ...defaultStatusCounts, total: 2, inProgress: 1 }); vi.spyOn(stageManager, 'updateStatus').mockResolvedValueOnce(undefined); - await expect(stageManager.updateStageProgressFromTaskChanges(stageId, updateSummaryCount, mockTx)).toResolve(); + await expect(stageManager.updateStageProgressFromTaskChanges(stageId as StageId, updateSummaryCount, mockTx)).toResolve(); }); it('should update stage data according with auto completed', async function () { @@ -866,7 +872,7 @@ describe('JobManager', () => { vi.spyOn(stageRepository, 'updateStageSummary').mockResolvedValueOnce({ ...defaultStatusCounts, total: 2, completed: 2 }); vi.spyOn(stageManager, 'updateStatus').mockResolvedValueOnce(undefined); - await expect(stageManager.updateStageProgressFromTaskChanges(stageId, updateSummaryCount, mockTx)).toResolve(); + await expect(stageManager.updateStageProgressFromTaskChanges(stageId as StageId, updateSummaryCount, mockTx)).toResolve(); }); }); }); From c72fec6e7681ce88d4a38575c754e0dd8a5a6960 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:06:31 +0300 Subject: [PATCH 05/16] feat(jobnik-manager): brand task ids end-to-end Retype every taskId parameter in the task service layer to TaskId from jobnik-openapi, so getTaskById rejects a JobId/StageId at compile time. Casts the Prisma->API boundary in convertPrismaToTaskResponse and convertPrismaToJobResponse (the latter closes a JobId gap the job-id-branding commit missed), matching the pattern already used for JobId/StageId. This is the last of the three id-branding commits; jobnik-manager now type-checks, builds, and its full test suite passes with the branded ids threaded end-to-end. Also fixes import ordering (eslint import-x/order) flagged in a couple of test files touched by the earlier job/stage id commits. Co-Authored-By: Claude Sonnet 5 --- .../jobnik-manager/src/jobs/models/manager.ts | 2 +- .../jobnik-manager/src/tasks/models/helper.ts | 2 +- .../src/tasks/models/manager.ts | 18 +- .../tests/integration/jobs/jobs.spec.ts | 90 +++++----- .../tests/integration/tasks/tasks.spec.ts | 162 +++++++++--------- .../tests/unit/jobs/jobs.spec.ts | 2 +- .../tests/unit/stages/repository.spec.ts | 2 +- .../tests/unit/stages/stages.spec.ts | 2 +- .../tests/unit/tasks/tasks.spec.ts | 55 +++--- 9 files changed, 178 insertions(+), 157 deletions(-) diff --git a/apps/jobnik-manager/src/jobs/models/manager.ts b/apps/jobnik-manager/src/jobs/models/manager.ts index 0b54924..1e2e2e8 100644 --- a/apps/jobnik-manager/src/jobs/models/manager.ts +++ b/apps/jobnik-manager/src/jobs/models/manager.ts @@ -280,6 +280,6 @@ export class JobManager { stages: Array.isArray(stage) ? convertArrayPrismaStageToStageResponse(stage) : undefined, }; - return Object.assign(rest, transformedFields); + return Object.assign(rest, transformedFields) as unknown as JobModel; } } diff --git a/apps/jobnik-manager/src/tasks/models/helper.ts b/apps/jobnik-manager/src/tasks/models/helper.ts index 8e78828..d5341cf 100644 --- a/apps/jobnik-manager/src/tasks/models/helper.ts +++ b/apps/jobnik-manager/src/tasks/models/helper.ts @@ -21,7 +21,7 @@ export function convertPrismaToTaskResponse(prismaObjects: Prisma.TaskGetPayload endTime: endTime ? endTime.toISOString() : undefined, }; - return Object.assign(rest, transformedFields); + return Object.assign(rest, transformedFields) as TaskModel; } /** diff --git a/apps/jobnik-manager/src/tasks/models/manager.ts b/apps/jobnik-manager/src/tasks/models/manager.ts index 08b5e80..1e87e20 100644 --- a/apps/jobnik-manager/src/tasks/models/manager.ts +++ b/apps/jobnik-manager/src/tasks/models/manager.ts @@ -5,7 +5,7 @@ import { trace, type Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { subMinutes } from 'date-fns'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; -import type { StageId } from 'jobnik-openapi'; +import type { StageId, TaskId } from 'jobnik-openapi'; import { Prisma, StageOperationStatus, Task, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import { SERVICES, XSTATE_DONE_STATE } from '@common/constants'; import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; @@ -140,7 +140,7 @@ export class TaskManager { } @withSpanAsyncV4 - public async getTaskById(taskId: string): Promise { + public async getTaskById(taskId: TaskId): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_ID]: taskId, @@ -176,7 +176,7 @@ export class TaskManager { } @withSpanAsyncV4 - public async updateUserMetadata(taskId: string, userMetadata: Record): Promise { + public async updateUserMetadata(taskId: TaskId, userMetadata: Record): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_ID]: taskId, @@ -201,7 +201,7 @@ export class TaskManager { } @withSpanAsyncV4 - public async updateStatus(taskId: string, status: TaskOperationStatus, tx?: PrismaTransaction): Promise { + public async updateStatus(taskId: TaskId, status: TaskOperationStatus, tx?: PrismaTransaction): Promise { const spanActive = trace.getActiveSpan(); spanActive?.setAttributes({ [ATTR_MESSAGING_MESSAGE_ID]: taskId, @@ -248,7 +248,7 @@ export class TaskManager { * @returns The task entity if found, otherwise null. */ @withSpanAsyncV4 - public async getTaskEntityById(taskId: string, tx?: PrismaTransaction): Promise { + public async getTaskEntityById(taskId: TaskId, tx?: PrismaTransaction): Promise { trace.getActiveSpan()?.setAttributes({ [ATTR_MESSAGING_MESSAGE_ID]: taskId, }); @@ -353,7 +353,7 @@ export class TaskManager { * @returns The updated task model */ @withSpanAsyncV4 - private async executeUpdateStatus(taskId: string, status: TaskOperationStatus, tx: PrismaTransaction): Promise { + private async executeUpdateStatus(taskId: TaskId, status: TaskOperationStatus, tx: PrismaTransaction): Promise { const task = await this.getTaskEntityById(taskId, tx); if (!task) { @@ -425,7 +425,7 @@ export class TaskManager { // Create update query with race condition protection for IN_PROGRESS const updateQueryBody = { - where: this.createUpdateWhereClause(task.id, previousStatus), + where: this.createUpdateWhereClause(task.id as TaskId, previousStatus), data: { ...taskDataToUpdate, status: nextStatus, xstate: newPersistedSnapshot, startTime, endTime }, }; @@ -511,7 +511,7 @@ export class TaskManager { * @param previousStatus - The expected current status to prevent race conditions. * @returns The filter object for the update query. */ - private createUpdateWhereClause(taskId: string, previousStatus: TaskOperationStatus): { id: string; status: TaskOperationStatus } { + private createUpdateWhereClause(taskId: TaskId, previousStatus: TaskOperationStatus): { id: string; status: TaskOperationStatus } { return { id: taskId, status: previousStatus }; } @@ -544,7 +544,7 @@ export class TaskManager { // Process tasks sequentially to avoid overwhelming the database for (const task of staleTasks) { try { - await this.updateStatus(task.id, TaskOperationStatus.FAILED); + await this.updateStatus(task.id as TaskId, TaskOperationStatus.FAILED); successCount++; this.logger.debug({ diff --git a/apps/jobnik-manager/tests/integration/jobs/jobs.spec.ts b/apps/jobnik-manager/tests/integration/jobs/jobs.spec.ts index e7e5740..eddd52d 100644 --- a/apps/jobnik-manager/tests/integration/jobs/jobs.spec.ts +++ b/apps/jobnik-manager/tests/integration/jobs/jobs.spec.ts @@ -10,7 +10,7 @@ import { type ExpectResponseStatus, type RequestSender, } from '@map-colonies/openapi-helpers/requestSender'; -import { openapiFilePath, type paths, type operations } from 'jobnik-openapi'; +import { openapiFilePath, type paths, type operations, type JobId } from 'jobnik-openapi'; import { JobOperationStatus, Priority, StageOperationStatus, type PrismaClient } from '@prismaClient'; import type { PrismaTransaction } from '@src/db/types'; import { getApp } from '@src/app'; @@ -373,7 +373,7 @@ describe('job', function () { const { job } = await createJobnikTree(prisma, { name: 'SOME_UNIQUE_NAME' }, {}, [], { createStage: true, createTasks: false }); const jobId = job.id; - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ status: StatusCodes.OK, body: { status: JobOperationStatus.CREATED, name: 'SOME_UNIQUE_NAME' } }); @@ -387,7 +387,10 @@ describe('job', function () { }); const jobId = job.id; - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId }, queryParams: { should_return_stages: true } }); + const getJobResponse = await requestSender.getJobByIdV1({ + pathParams: { jobId: jobId as JobId }, + queryParams: { should_return_stages: true }, + }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -404,7 +407,10 @@ describe('job', function () { }); const jobId = job.id; - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId }, queryParams: { should_return_stages: false } }); + const getJobResponse = await requestSender.getJobByIdV1({ + pathParams: { jobId: jobId as JobId }, + queryParams: { should_return_stages: false }, + }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ status: StatusCodes.OK, body: { status: JobOperationStatus.CREATED, name: 'SOME_UNIQUE_NAME' } }); @@ -414,7 +420,7 @@ describe('job', function () { describe('Bad Path', function () { it('should return a 404 status code along with a specific validation error message detailing the non exists job', async function () { - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId as JobId } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -424,7 +430,7 @@ describe('job', function () { }); it('should return status code 400 when supplying bad uuid as part of the request', async function () { - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: 'someInvalidJobId' } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: 'someInvalidJobId' as JobId } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -440,7 +446,7 @@ describe('job', function () { const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId as JobId } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -453,7 +459,7 @@ describe('job', function () { const error = createMockUnknownDbError(); const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: testJobId as JobId } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -475,11 +481,11 @@ describe('job', function () { const jobId = job.id; const updateUserMetadataResponse = await requestSender.updateUserMetadataV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, requestBody: userMetadataInput, }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(updateUserMetadataResponse).toSatisfyApiSpec(); expect(getJobResponse.body).toMatchObject({ userMetadata: userMetadataInput }); @@ -488,7 +494,7 @@ describe('job', function () { describe('Bad Path', function () { it('should return a 404 status code along with a message that specifies that a job with the given id was not found', async function () { - const getJobResponse = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId }, requestBody: { avi: 'avi' } }); + const getJobResponse = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId as JobId }, requestBody: { avi: 'avi' } }); expect(getJobResponse).toSatisfyApiSpec(); expect(getJobResponse).toMatchObject({ @@ -499,7 +505,7 @@ describe('job', function () { it('should return a 400 status code along with a message that specifies that body not valid (should be json)', async function () { const getJobResponse = await requestSender.updateUserMetadataV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: 'badType' as unknown as { avi: 'avi' }, }); @@ -517,7 +523,7 @@ describe('job', function () { const updateSpy = createProxyMock(prisma.job, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId }, requestBody: {} }); + const response = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId as JobId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -531,7 +537,7 @@ describe('job', function () { const updateSpy = createProxyMock(prisma.job, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId }, requestBody: {} }); + const response = await requestSender.updateUserMetadataV1({ pathParams: { jobId: testJobId as JobId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -552,11 +558,11 @@ describe('job', function () { const jobId = job.id; const setPriorityResponse = await requestSender.updateJobPriorityV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, requestBody: { priority: Priority.VERY_HIGH }, }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(setPriorityResponse).toSatisfyApiSpec(); expect(getJobResponse.body).toMatchObject({ priority: Priority.VERY_HIGH }); @@ -570,7 +576,7 @@ describe('job', function () { const jobId = job.id; const setPriorityResponse = await requestSender.updateJobPriorityV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, requestBody: { priority: Priority.VERY_HIGH }, }); @@ -586,7 +592,7 @@ describe('job', function () { describe('Bad Path', function () { it('should return 404 with specific error message for non-existent job', async function () { const getJobResponse = await requestSender.updateJobPriorityV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: { priority: Priority.VERY_HIGH }, }); @@ -599,7 +605,7 @@ describe('job', function () { it('should return 400 with specific error message for non-existent priority', async function () { const getJobResponse = await requestSender.updateJobPriorityV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: { priority: 'MEGA_HIGH' as unknown as Priority }, }); @@ -620,7 +626,10 @@ describe('job', function () { const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateJobPriorityV1({ pathParams: { jobId: testJobId }, requestBody: { priority: Priority.VERY_HIGH } }); + const response = await requestSender.updateJobPriorityV1({ + pathParams: { jobId: testJobId as JobId }, + requestBody: { priority: Priority.VERY_HIGH }, + }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -634,7 +643,10 @@ describe('job', function () { const findUniqueSpy = createProxyMock(prisma.job, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateJobPriorityV1({ pathParams: { jobId: testJobId }, requestBody: { priority: Priority.VERY_HIGH } }); + const response = await requestSender.updateJobPriorityV1({ + pathParams: { jobId: testJobId as JobId }, + requestBody: { priority: Priority.VERY_HIGH }, + }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -655,14 +667,14 @@ describe('job', function () { const jobId = job.id; const setStatusResponse = await requestSender.updateStatusV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, requestBody: { status: JobOperationStatus.PENDING }, }); expect(setStatusResponse).toSatisfyApiSpec(); expect(setStatusResponse).toHaveProperty('status', StatusCodes.OK); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(getJobResponse).toHaveProperty('body.status', JobOperationStatus.PENDING); }); @@ -683,7 +695,7 @@ describe('job', function () { const jobId = job.id; const setStatusResponse = await requestSender.updateStatusV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, requestBody: { status: JobOperationStatus.PAUSED }, }); @@ -702,7 +714,7 @@ describe('job', function () { const jobId = job.id; const setStatusResponse = await requestSender.updateStatusV1({ - pathParams: { jobId }, + pathParams: { jobId: jobId as JobId }, // @ts-expect-error - COMPLETED is a system-managed status and cannot be set via the user-controllable status update endpoint; this test ensures such values are rejected by the API requestBody: { status: JobOperationStatus.COMPLETED }, }); @@ -719,7 +731,7 @@ describe('job', function () { it('should return 404 with specific error message for non-existent job', async function () { const getJobResponse = await requestSender.updateStatusV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: { status: JobOperationStatus.PAUSED }, }); @@ -732,7 +744,7 @@ describe('job', function () { it('should return status code 400 when supplying bad uuid as part of the request', async function () { const getJobResponse = await requestSender.updateStatusV1({ - pathParams: { jobId: 'someBadUuid' }, + pathParams: { jobId: 'someBadUuid' as JobId }, requestBody: { status: JobOperationStatus.PENDING }, }); @@ -760,7 +772,7 @@ describe('job', function () { }); const response = await requestSender.updateStatusV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: { status: JobOperationStatus.PENDING }, }); @@ -787,7 +799,7 @@ describe('job', function () { }); const response = await requestSender.updateStatusV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, requestBody: { status: JobOperationStatus.PENDING }, }); @@ -803,9 +815,9 @@ describe('job', function () { const job = await createJobRecord(createJobRequestBody, prisma); const createdJobId = job.id; - await requestSender.updateStatusV1({ pathParams: { jobId: createdJobId }, requestBody: { status: JobOperationStatus.ABORTED } }); - const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: createdJobId } }); - const validateDeletionResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: createdJobId } }); + await requestSender.updateStatusV1({ pathParams: { jobId: createdJobId as JobId }, requestBody: { status: JobOperationStatus.ABORTED } }); + const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: createdJobId as JobId } }); + const validateDeletionResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: createdJobId as JobId } }); expect(deleteJobResponse).toSatisfyApiSpec(); expect(deleteJobResponse).toMatchObject({ @@ -826,8 +838,8 @@ describe('job', function () { }); const jobId = job.id; - const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId } }); - const validateDeletionResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: jobId as JobId } }); + const validateDeletionResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(deleteJobResponse).toSatisfyApiSpec(); expect(deleteJobResponse).toMatchObject({ @@ -844,7 +856,7 @@ describe('job', function () { describe('Bad Path', function () { it('should return status code 400 when supplying bad uuid as part of the request', async function () { - const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: 'someInvalidJobId' } }); + const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: 'someInvalidJobId' as JobId } }); expect(deleteJobResponse).toSatisfyApiSpec(); expect(deleteJobResponse).toMatchObject({ @@ -860,7 +872,7 @@ describe('job', function () { }); const jobId = job.id; - const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId } }); + const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: jobId as JobId } }); expect(deleteJobResponse).toSatisfyApiSpec(); expect(deleteJobResponse).toMatchObject({ @@ -870,7 +882,7 @@ describe('job', function () { }); it('should return 404 with specific error message for non-existent job', async function () { - const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: testJobId } }); + const deleteJobResponse = await requestSender.deleteJobV1({ pathParams: { jobId: testJobId as JobId } }); expect(deleteJobResponse).toSatisfyApiSpec(); expect(deleteJobResponse).toMatchObject({ @@ -887,7 +899,7 @@ describe('job', function () { findUniqueSpy.mockRejectedValueOnce(error); const deleteJobResponse = await requestSender.deleteJobV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, }); expect(deleteJobResponse).toSatisfyApiSpec(); @@ -903,7 +915,7 @@ describe('job', function () { findUniqueSpy.mockRejectedValueOnce(error); const deleteJobResponse = await requestSender.deleteJobV1({ - pathParams: { jobId: testJobId }, + pathParams: { jobId: testJobId as JobId }, }); expect(deleteJobResponse).toSatisfyApiSpec(); diff --git a/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts index 22afa20..aa653d7 100644 --- a/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts @@ -11,7 +11,7 @@ import { type RequestSender, } from '@map-colonies/openapi-helpers/requestSender'; import { faker } from '@faker-js/faker'; -import { openapiFilePath, type paths, type operations, type JobId } from 'jobnik-openapi'; +import { openapiFilePath, type paths, type operations, type JobId, type StageId, type TaskId } from 'jobnik-openapi'; import { JobOperationStatus, Priority, Prisma, StageOperationStatus, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import type { PrismaTransaction } from '@src/db/types'; import { getApp } from '@src/app'; @@ -80,7 +80,7 @@ describe('task', function () { const taskId = tasks[0]!.id; const stageId = stage.id; - const response = await requestSender.getTasksByCriteriaV1({ queryParams: { stage_id: stageId } }); + const response = await requestSender.getTasksByCriteriaV1({ queryParams: { stage_id: stageId as StageId } }); expect(response).toMatchObject({ status: StatusCodes.OK, @@ -90,7 +90,7 @@ describe('task', function () { it('should return 200 with empty array', async function () { const someRandomUuid = faker.string.uuid(); - const response = await requestSender.getTasksByCriteriaV1({ queryParams: { stage_id: someRandomUuid } }); + const response = await requestSender.getTasksByCriteriaV1({ queryParams: { stage_id: someRandomUuid as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -221,7 +221,7 @@ describe('task', function () { const { tasks } = await createJobnikTree(prisma); const taskId = tasks[0]!.id; - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ status: StatusCodes.OK, body: { status: TaskOperationStatus.CREATED } }); @@ -230,7 +230,7 @@ describe('task', function () { describe('Bad Path', function () { it('should return 404 when task does not exist', async function () { - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() as TaskId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ @@ -240,7 +240,7 @@ describe('task', function () { }); it('should return 400 when supplying bad uuid', async function () { - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: 'badUuid' } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: 'badUuid' as TaskId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ @@ -256,7 +256,7 @@ describe('task', function () { const findUniqueSpy = createProxyMock(prisma.task, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() } }); + const response = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() as TaskId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -270,7 +270,7 @@ describe('task', function () { const findUniqueSpy = createProxyMock(prisma.task, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() } }); + const response = await requestSender.getTaskByIdV1({ pathParams: { taskId: faker.string.uuid() as TaskId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -287,7 +287,7 @@ describe('task', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [{}, {}]); const stageId = stage.id; - const getTasksResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId } }); + const getTasksResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: stageId as StageId } }); expect(getTasksResponse).toSatisfyApiSpec(); @@ -310,7 +310,7 @@ describe('task', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [], { createStage: true, createTasks: false }); const stageId = stage.id; - const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: stageId as StageId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ @@ -323,7 +323,10 @@ describe('task', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [{}, {}, {}]); const stageId = stage.id; - const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId }, queryParams: { page: 1, page_size: 2 } }); + const response = await requestSender.getTasksByStageIdV1({ + pathParams: { stageId: stageId as StageId }, + queryParams: { page: 1, page_size: 2 }, + }); expect(response).toSatisfyApiSpec(); @@ -337,7 +340,10 @@ describe('task', function () { const { stage } = await createJobnikTree(prisma, {}, {}, [{}]); const stageId = stage.id; - const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId }, queryParams: { page: 9999, page_size: 10 } }); + const response = await requestSender.getTasksByStageIdV1({ + pathParams: { stageId: stageId as StageId }, + queryParams: { page: 9999, page_size: 10 }, + }); expect(response).toSatisfyApiSpec(); @@ -350,7 +356,7 @@ describe('task', function () { describe('Bad Path', function () { it('should return 400 when supplying bad uuid', async function () { - const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: 'someInvalidStageId' } }); + const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: 'someInvalidStageId' as StageId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ @@ -360,7 +366,7 @@ describe('task', function () { }); it('should return 404 when stage does not exist', async function () { - const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() } }); + const getTaskResponse = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() as StageId } }); expect(getTaskResponse).toSatisfyApiSpec(); expect(getTaskResponse).toMatchObject({ @@ -376,7 +382,7 @@ describe('task', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() } }); + const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -390,7 +396,7 @@ describe('task', function () { const findUniqueSpy = createProxyMock(prisma.stage, 'findUnique'); findUniqueSpy.mockRejectedValueOnce(error); - const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() } }); + const response = await requestSender.getTasksByStageIdV1({ pathParams: { stageId: faker.string.uuid() as StageId } }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -409,11 +415,11 @@ describe('task', function () { const taskId = tasks[0]!.id; const updateUserMetadataResponse = await requestSender.updateTaskUserMetadataV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: userMetadataInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: tasks[0]!.id } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: tasks[0]!.id as TaskId } }); expect(updateUserMetadataResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject({ userMetadata: userMetadataInput }); @@ -423,7 +429,7 @@ describe('task', function () { describe('Bad Path', function () { it('should return 404 when task not found', async function () { const getTaskResponse = await requestSender.updateTaskUserMetadataV1({ - pathParams: { taskId: faker.string.uuid() }, + pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: { avi: 'avi' }, }); @@ -439,7 +445,7 @@ describe('task', function () { const taskId = tasks[0]!.id; const response = await requestSender.updateTaskUserMetadataV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: 'badInputString' as unknown as { [key: string]: string }, }); @@ -457,7 +463,7 @@ describe('task', function () { const updateSpy = createProxyMock(prisma.task, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateTaskUserMetadataV1({ pathParams: { taskId: faker.string.uuid() }, requestBody: {} }); + const response = await requestSender.updateTaskUserMetadataV1({ pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -471,7 +477,7 @@ describe('task', function () { const updateSpy = createProxyMock(prisma.task, 'update'); updateSpy.mockRejectedValueOnce(error); - const response = await requestSender.updateTaskUserMetadataV1({ pathParams: { taskId: faker.string.uuid() }, requestBody: {} }); + const response = await requestSender.updateTaskUserMetadataV1({ pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: {} }); expect(response).toSatisfyApiSpec(); expect(response).toMatchObject({ @@ -495,7 +501,7 @@ describe('task', function () { const response = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); expect(response).toSatisfyApiSpec(); @@ -519,7 +525,7 @@ describe('task', function () { const response = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); expect(response).toSatisfyApiSpec(); @@ -540,7 +546,7 @@ describe('task', function () { const createTaskResponse = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); await memoryExporter.forceFlush(); @@ -566,7 +572,7 @@ describe('task', function () { const createTaskResponse = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); expect(createTaskResponse).toSatisfyApiSpec(); @@ -592,7 +598,7 @@ describe('task', function () { const createTaskResponse = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); if (!Array.isArray(createTaskResponse.body)) { @@ -615,7 +621,7 @@ describe('task', function () { describe('Bad Path', function () { it('should return 400 when supplying bad uuid', async function () { - const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId: 'someInvalidStageId' } }); + const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId: 'someInvalidStageId' as StageId } }); expect(addTasksResponse).toMatchObject({ status: StatusCodes.BAD_REQUEST, @@ -628,7 +634,7 @@ describe('task', function () { const stageId = stage.id; const addTasksResponse = await requestSender.addTasksV1({ - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, requestBody: {} as unknown as [], }); @@ -648,7 +654,7 @@ describe('task', function () { ); const stageId = stage.id; - const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId } }); + const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId: stageId as StageId } }); expect(addTasksResponse).toSatisfyApiSpec(); expect(addTasksResponse).toMatchObject({ @@ -667,7 +673,7 @@ describe('task', function () { ); const stageId = stage.id; - const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId } }); + const addTasksResponse = await requestSender.addTasksV1({ requestBody: [], pathParams: { stageId: stageId as StageId } }); expect(addTasksResponse).toSatisfyApiSpec(); expect(addTasksResponse).toMatchObject({ @@ -688,7 +694,7 @@ describe('task', function () { const createTaskResponse = await requestSender.addTasksV1({ requestBody: [createTasksPayload], - pathParams: { stageId }, + pathParams: { stageId: stageId as StageId }, }); expect(createTaskResponse).toMatchObject({ @@ -705,7 +711,7 @@ describe('task', function () { const response = await requestSender.addTasksV1({ requestBody: [createTaskPayload], - pathParams: { stageId: faker.string.uuid() }, + pathParams: { stageId: faker.string.uuid() as StageId }, }); expect(response).toSatisfyApiSpec(); @@ -724,7 +730,7 @@ describe('task', function () { const response = await requestSender.addTasksV1({ requestBody: [], - pathParams: { stageId: faker.string.uuid() }, + pathParams: { stageId: faker.string.uuid() as StageId }, }); expect(response).toSatisfyApiSpec(); @@ -741,7 +747,7 @@ describe('task', function () { const response = await requestSender.addTasksV1({ requestBody: [], - pathParams: { stageId: faker.string.uuid() }, + pathParams: { stageId: faker.string.uuid() as StageId }, }); expect(response).toSatisfyApiSpec(); @@ -770,12 +776,12 @@ describe('task', function () { const taskId = tasks[0]!.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stage.id as StageId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(updateStatusInput); @@ -793,14 +799,14 @@ describe('task', function () { ); const taskId = tasks[0]!.id; - const getTaskResponseBeforeUpdate = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseBeforeUpdate = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponseBeforeUpdate.body).not.toHaveProperty('endTime'); @@ -822,14 +828,14 @@ describe('task', function () { ); const taskId = tasks[0]!.id; - const getTaskResponseBeforeUpdate = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseBeforeUpdate = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponseBeforeUpdate.body).not.toHaveProperty('endTime'); @@ -852,12 +858,12 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(updateStatusInput); @@ -881,12 +887,12 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedStatus); @@ -910,11 +916,11 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedStatus); @@ -942,12 +948,12 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedTaskStatus); @@ -983,12 +989,12 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); const getSecondStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: secondStage.body.id } }); const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId as JobId } }); @@ -1017,12 +1023,12 @@ describe('task', function () { const stageId = stage.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: updateStatusInput, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId as JobId } }); expect(updateStatusResponse).toSatisfyApiSpec(); @@ -1044,7 +1050,7 @@ describe('task', function () { const taskId = tasks[0]!.id; const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); @@ -1060,7 +1066,7 @@ describe('task', function () { it('should return 400 when supplying bad uuid', async function () { const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId: 'badUuid' }, + pathParams: { taskId: 'badUuid' as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); @@ -1073,7 +1079,7 @@ describe('task', function () { it('should return 404 when task not found', async function () { const updateStatusResponse = await requestSender.updateTaskStatusV1({ - pathParams: { taskId: faker.string.uuid() }, + pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); @@ -1101,7 +1107,7 @@ describe('task', function () { }); const response = await requestSender.updateTaskStatusV1({ - pathParams: { taskId: faker.string.uuid() }, + pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); @@ -1127,7 +1133,7 @@ describe('task', function () { }); const response = await requestSender.updateTaskStatusV1({ - pathParams: { taskId: faker.string.uuid() }, + pathParams: { taskId: faker.string.uuid() as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); @@ -1164,7 +1170,7 @@ describe('task', function () { pathParams: { stageType: 'SOME_TEST_TYPE_1' }, }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ @@ -1201,7 +1207,7 @@ describe('task', function () { pathParams: { stageType: 'SOME_TEST_TYPE_2' }, }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ @@ -1238,7 +1244,7 @@ describe('task', function () { pathParams: { stageType: 'SOME_TEST_TYPE_3' }, }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(dequeueResponse).toSatisfyApiSpec(); @@ -1399,14 +1405,14 @@ describe('task', function () { const taskId = tasks[0]!.id; // Get task before dequeue to verify it doesn't have startTime - const getTaskResponseBeforeDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseBeforeDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); const dequeueResponse = await requestSender.dequeueTaskV1({ pathParams: { stageType: 'SOME_TEST_TYPE_STARTIME_CHECK' }, }); // Get task after dequeue to verify it has startTime - const getTaskResponseAfterDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseAfterDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ @@ -1439,7 +1445,7 @@ describe('task', function () { const taskId = tasks[0]!.id; // Get task before dequeue to capture previous startTime - const getTaskResponseBeforeDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseBeforeDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); const taskBeforeDequeue = getTaskResponseBeforeDequeue.body as TaskModel; const previousStartTime = taskBeforeDequeue.startTime; @@ -1448,7 +1454,7 @@ describe('task', function () { }); // Get task after dequeue to verify startTime was updated - const getTaskResponseAfterDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); + const getTaskResponseAfterDequeue = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); const taskAfterDequeue = getTaskResponseAfterDequeue.body as TaskModel; expect(dequeueResponse).toSatisfyApiSpec(); @@ -1594,8 +1600,8 @@ describe('task', function () { pathParams: { stageType: 'SOME_TEST_TYPE_FAILED_TRANSACTION' }, }); - const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId } }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); + const getTaskResponse = await requestSender.getTaskByIdV1({ pathParams: { taskId: taskId as TaskId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(dequeueResponse).toSatisfyApiSpec(); @@ -1754,11 +1760,11 @@ describe('task', function () { // Start both update operations concurrently (simulating 2 workers completing the same task) const firstUpdatePromise = requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); const secondUpdatePromise = requestSender.updateTaskStatusV1({ - pathParams: { taskId }, + pathParams: { taskId: taskId as TaskId }, requestBody: { status: TaskOperationStatus.COMPLETED }, }); diff --git a/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts index c33d184..2aa2790 100644 --- a/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts +++ b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts @@ -3,13 +3,13 @@ import type { Logger } from '@map-colonies/js-logger'; import { jsLogger } from '@map-colonies/js-logger'; import { trace } from '@opentelemetry/api'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; +import type { JobId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, JobOperationStatus, Priority } from '@prismaClient'; import { illegalStatusTransitionErrorMessage, prismaKnownErrors } from '@src/common/errors'; import { JobManager } from '@src/jobs/models/manager'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; import type { JobCreateModel } from '@src/jobs/models/models'; -import type { JobId } from 'jobnik-openapi'; import { randomUuid } from '@tests/unit/generator'; import { SERVICE_NAME } from '@src/common/constants'; import { jobEntityWithAbortStatus, jobEntityWithoutStages, jobEntityWithStages } from '../data'; diff --git a/apps/jobnik-manager/tests/unit/stages/repository.spec.ts b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts index d4ccb3c..4abe80c 100644 --- a/apps/jobnik-manager/tests/unit/stages/repository.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts @@ -2,11 +2,11 @@ import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; import { faker } from '@faker-js/faker'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; +import type { StageId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { TaskOperationStatus } from '@prismaClient'; import type { UpdateSummaryCount } from '@src/stages/models/models'; import { defaultStatusCounts } from '@src/stages/models/helper'; -import type { StageId } from 'jobnik-openapi'; import { StageRepository } from '@src/stages/DAL/stageRepository'; import { createStageEntity } from '../generator'; diff --git a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts index 442af42..0dc7cce 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -5,9 +5,9 @@ import { jsLogger } from '@map-colonies/js-logger'; import { faker } from '@faker-js/faker'; import { trace } from '@opentelemetry/api'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; +import type { JobId, StageId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, JobOperationStatus } from '@prismaClient'; -import type { JobId, StageId } from 'jobnik-openapi'; import { StageManager } from '@src/stages/models/manager'; import { JobManager } from '@src/jobs/models/manager'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; diff --git a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts index 9f707d2..5c5b942 100644 --- a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts @@ -5,6 +5,7 @@ import { faker } from '@faker-js/faker'; import { trace } from '@opentelemetry/api'; import { subHours, subMinutes } from 'date-fns'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; +import type { StageId, TaskId } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, TaskOperationStatus, JobOperationStatus } from '@prismaClient'; import { StageManager } from '@src/stages/models/manager'; @@ -143,7 +144,7 @@ describe('JobManager', () => { const taskId = taskEntity.id; prisma.task.findUnique.mockResolvedValue(taskEntity); - const task = await taskManager.getTaskById(taskId); + const task = await taskManager.getTaskById(taskId as TaskId); const { creationTime, updateTime, xstate, startTime, endTime, ...rest } = taskEntity; const expectedTask = { ...rest, tracestate: undefined, creationTime: creationTime.toISOString(), updateTime: updateTime.toISOString() }; @@ -156,7 +157,7 @@ describe('JobManager', () => { it('should result in failure when attempting to retrieve a task with a non-existent id', async function () { prisma.task.findUnique.mockResolvedValue(null); - await expect(taskManager.getTaskById('some_id')).rejects.toThrow(tasksErrorMessages.taskNotFound); + await expect(taskManager.getTaskById('some_id' as TaskId)).rejects.toThrow(tasksErrorMessages.taskNotFound); }); }); @@ -164,7 +165,7 @@ describe('JobManager', () => { it('should fail and throw an error if prisma throws an error', async function () { prisma.task.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(taskManager.getTaskById('some_id')).rejects.toThrow('db connection error'); + await expect(taskManager.getTaskById('some_id' as TaskId)).rejects.toThrow('db connection error'); }); }); }); @@ -178,7 +179,7 @@ describe('JobManager', () => { prisma.task.findMany.mockResolvedValue([taskEntity]); prisma.task.count.mockResolvedValue(1); - const result = await taskManager.getTasksByStageId(stageEntity.id, {}); + const result = await taskManager.getTasksByStageId(stageEntity.id as StageId, {}); const { creationTime, updateTime, xstate, startTime, endTime, ...rest } = taskEntity; const expectedTask = [{ ...rest, tracestate: undefined, creationTime: creationTime.toISOString(), updateTime: updateTime.toISOString() }]; @@ -192,7 +193,7 @@ describe('JobManager', () => { it('should failed on not founded task when getting by non exists stage', async function () { prisma.stage.findUnique.mockResolvedValue(null); - await expect(taskManager.getTasksByStageId('some_id', {})).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(taskManager.getTasksByStageId('some_id' as StageId, {})).rejects.toThrow(stagesErrorMessages.stageNotFound); }); }); @@ -200,7 +201,7 @@ describe('JobManager', () => { it('should fail and throw an error if prisma throws an error', async function () { prisma.stage.findUnique.mockRejectedValueOnce(new Error('db connection error')); - await expect(taskManager.getTasksByStageId('some_id', {})).rejects.toThrow('db connection error'); + await expect(taskManager.getTasksByStageId('some_id' as StageId, {})).rejects.toThrow('db connection error'); }); }); }); @@ -212,7 +213,7 @@ describe('JobManager', () => { prisma.task.update.mockResolvedValue(taskEntity); - await expect(taskManager.updateUserMetadata(taskEntity.id, { newData: 'test' })).toResolve(); + await expect(taskManager.updateUserMetadata(taskEntity.id as TaskId, { newData: 'test' })).toResolve(); }); }); @@ -220,7 +221,9 @@ describe('JobManager', () => { it('should failed on for not exists task when update user metadata', async function () { prisma.task.update.mockRejectedValue(notFoundError); - await expect(taskManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow(tasksErrorMessages.taskNotFound); + await expect(taskManager.updateUserMetadata('someId' as TaskId, { testData: 'some new data' })).rejects.toThrow( + tasksErrorMessages.taskNotFound + ); }); }); @@ -228,7 +231,7 @@ describe('JobManager', () => { it('should fail and throw an error if prisma throws an error', async function () { prisma.task.update.mockRejectedValueOnce(new Error('db connection error')); - await expect(taskManager.updateUserMetadata('someId', { testData: 'some new data' })).rejects.toThrow('db connection error'); + await expect(taskManager.updateUserMetadata('someId' as TaskId, { testData: 'some new data' })).rejects.toThrow('db connection error'); }); }); }); @@ -262,7 +265,7 @@ describe('JobManager', () => { userMetadata: { someData: '123' }, } satisfies TaskCreateModel; - const tasksResponse = await taskManager.addTasks(stageId, [taskPayload]); + const tasksResponse = await taskManager.addTasks(stageId as StageId, [taskPayload]); // Extract unnecessary fields from the job object and assemble the expected result const { creationTime, updateTime, xstate, startTime, endTime, ...rest } = taskEntity; @@ -277,7 +280,7 @@ describe('JobManager', () => { it('should reject adding tasks to a non-existent stage', async function () { prisma.stage.findUnique.mockResolvedValue(null); - await expect(taskManager.addTasks('someId', [])).rejects.toThrow(stagesErrorMessages.stageNotFound); + await expect(taskManager.addTasks('someId' as StageId, [])).rejects.toThrow(stagesErrorMessages.stageNotFound); }); it('should reject adding tasks to job with IN_PROGRESS stage', async function () { @@ -295,7 +298,7 @@ describe('JobManager', () => { prisma.stage.findUnique.mockResolvedValue(stageEntity); prisma.job.findUnique.mockResolvedValue(jobEntity); - await expect(taskManager.addTasks('someId', [])).rejects.toThrow( + await expect(taskManager.addTasks('someId' as StageId, [])).rejects.toThrow( new NotAllowedToAddTasksToInProgressStageError(tasksErrorMessages.addTaskNotAllowed) ); }); @@ -309,7 +312,7 @@ describe('JobManager', () => { prisma.stage.findUnique.mockResolvedValue(stageEntity); - await expect(taskManager.addTasks('someId', [])).rejects.toThrow( + await expect(taskManager.addTasks('someId' as StageId, [])).rejects.toThrow( new StageInFiniteStateError(stagesErrorMessages.stageAlreadyFinishedTasksError) ); }); @@ -336,7 +339,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.addTasks(jobEntity.id, [])).rejects.toThrow('db connection error'); + await expect(taskManager.addTasks(jobEntity.id as StageId, [])).rejects.toThrow('db connection error'); }); }); }); @@ -376,7 +379,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStatus').mockResolvedValue(undefined); vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.COMPLETED)).toResolve(); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.COMPLETED)).toResolve(); }); it('should update task status to RETRIED', async function () { @@ -412,7 +415,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.FAILED)).toResolve(); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.FAILED)).toResolve(); }); it('should update task status to IN_PROGRESS and add startTime', async function () { @@ -448,7 +451,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); }); it('should update task status to FAILED and add endTime', async function () { @@ -495,7 +498,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.FAILED)).toResolve(); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.FAILED)).toResolve(); }); it('should update task status to IN_PROGRESS', async function () { @@ -531,7 +534,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); }); }); @@ -547,7 +550,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.updateStatus('someId', TaskOperationStatus.COMPLETED)).rejects.toThrow(tasksErrorMessages.taskNotFound); + await expect(taskManager.updateStatus('someId' as TaskId, TaskOperationStatus.COMPLETED)).rejects.toThrow(tasksErrorMessages.taskNotFound); }); it("should reject update invalid task's status [from IN_PROGRESS to CREATED]", async function () { @@ -574,7 +577,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.updateStatus(taskId, TaskOperationStatus.CREATED)).rejects.toThrow(IllegalTaskStatusTransitionError); + await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.CREATED)).rejects.toThrow(IllegalTaskStatusTransitionError); }); }); @@ -590,7 +593,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.updateStatus(faker.string.uuid(), TaskOperationStatus.PENDING)).rejects.toThrow('db connection error'); + await expect(taskManager.updateStatus(faker.string.uuid() as TaskId, TaskOperationStatus.PENDING)).rejects.toThrow('db connection error'); }); }); }); @@ -697,8 +700,8 @@ describe('JobManager', () => { it('should successfully clean stale tasks and update them to FAILED status', async function () { prisma.task.findMany.mockResolvedValue([staleTaskOneHour, staleTaskFortyFiveMinutes]); const updateStatusMock = vi.spyOn(taskManager, 'updateStatus').mockResolvedValue({ - id: staleTaskOneHour.id, - stageId: staleTaskOneHour.stageId, + id: staleTaskOneHour.id as TaskId, + stageId: staleTaskOneHour.stageId as StageId, status: TaskOperationStatus.FAILED, attempts: 0, maxAttempts: 2, @@ -731,8 +734,8 @@ describe('JobManager', () => { const updateStatusMock = vi .spyOn(taskManager, 'updateStatus') .mockResolvedValueOnce({ - id: staleTaskOneHour.id, - stageId: staleTaskOneHour.stageId, + id: staleTaskOneHour.id as TaskId, + stageId: staleTaskOneHour.stageId as StageId, status: TaskOperationStatus.FAILED, attempts: 0, maxAttempts: 2, From 55345b4a65da84859c3cb5f5b15cdcfdcbca2abe Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:14:13 +0300 Subject: [PATCH 06/16] fix(jobnik-openapi,jobnik-manager): address code review findings - Export Brand from jobnik-openapi's brands.ts/index.ts, matching ticket 01's acceptance checklist (JobId/StageId/TaskId were already re-exported, Brand itself was missed). - Hoist the repeated stage.jobId/stage.job.id as JobId cast in StageManager.executeUpdateStatus into a single local, removing four near-identical casts and a stage.jobId/stage.job.id inconsistency. - Fix a test that cast a job id to StageId and passed it to addTasks instead of the actual stage id (tests/unit/tasks/tasks.spec.ts). Co-Authored-By: Claude Sonnet 5 --- .../src/stages/models/manager.ts | 18 ++++++++++-------- .../tests/unit/tasks/tasks.spec.ts | 2 +- packages/jobnik-openapi/src/brands.ts | 2 +- packages/jobnik-openapi/src/index.ts | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/jobnik-manager/src/stages/models/manager.ts b/apps/jobnik-manager/src/stages/models/manager.ts index 5867af5..7da6728 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -327,6 +327,8 @@ export class StageManager { throw new StageNotFoundError(stagesErrorMessages.stageNotFound); } + const jobId = stage.jobId as JobId; + // Idempotent status update: if already in target status, no-op // This prevents errors during race conditions where multiple workers // try to set the same status (e.g., multiple tasks setting stage to IN_PROGRESS) @@ -397,26 +399,26 @@ export class StageManager { trace.getActiveSpan()?.addEvent('Next stage set to PENDING', { nextStageId: nextStage.id }); } - const { completedStages, totalStages } = await this.updateJobCompletionProgress(stage.jobId as JobId, tx); + const { completedStages, totalStages } = await this.updateJobCompletionProgress(jobId, tx); if (completedStages === totalStages) { - await this.jobManager.updateStatus(stage.jobId as JobId, JobOperationStatus.COMPLETED, tx); + await this.jobManager.updateStatus(jobId, JobOperationStatus.COMPLETED, tx); this.logger.info({ msg: 'Job completed as all stages are done', - jobId: stage.jobId, + jobId, }); - trace.getActiveSpan()?.addEvent('Job set to COMPLETED', { jobId: stage.jobId }); + trace.getActiveSpan()?.addEvent('Job set to COMPLETED', { jobId }); } } if (targetStatus === StageOperationStatus.IN_PROGRESS && stage.job.status === JobOperationStatus.PENDING) { // Update job status to IN_PROGRESS - await this.jobManager.updateStatus(stage.job.id as JobId, JobOperationStatus.IN_PROGRESS, tx); - trace.getActiveSpan()?.addEvent('Job status set to IN_PROGRESS because first stage is being processed', { jobId: stage.jobId }); + await this.jobManager.updateStatus(jobId, JobOperationStatus.IN_PROGRESS, tx); + trace.getActiveSpan()?.addEvent('Job status set to IN_PROGRESS because first stage is being processed', { jobId }); } else if (targetStatus === StageOperationStatus.FAILED) { // Update job status to FAILED - await this.jobManager.updateStatus(stage.jobId as JobId, JobOperationStatus.FAILED, tx); - trace.getActiveSpan()?.addEvent('Job set to FAILED because its stage failed', { jobId: stage.jobId }); + await this.jobManager.updateStatus(jobId, JobOperationStatus.FAILED, tx); + trace.getActiveSpan()?.addEvent('Job set to FAILED because its stage failed', { jobId }); } //#endregion diff --git a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts index 5c5b942..eb996e6 100644 --- a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts @@ -339,7 +339,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.addTasks(jobEntity.id as StageId, [])).rejects.toThrow('db connection error'); + await expect(taskManager.addTasks(stageEntity.id as StageId, [])).rejects.toThrow('db connection error'); }); }); }); diff --git a/packages/jobnik-openapi/src/brands.ts b/packages/jobnik-openapi/src/brands.ts index 7d36021..30882cd 100644 --- a/packages/jobnik-openapi/src/brands.ts +++ b/packages/jobnik-openapi/src/brands.ts @@ -1,6 +1,6 @@ declare const brand: unique symbol; -type Brand = T & { [brand]: Brand }; +export type Brand = T & { [brand]: Brand }; /** * Branded type for job identifiers. diff --git a/packages/jobnik-openapi/src/index.ts b/packages/jobnik-openapi/src/index.ts index 825f656..432929c 100644 --- a/packages/jobnik-openapi/src/index.ts +++ b/packages/jobnik-openapi/src/index.ts @@ -1,5 +1,5 @@ export type { paths, components, operations, TypedRequestHandlers } from './openapi'; -export type { JobId, StageId, TaskId } from './brands'; +export type { Brand, JobId, StageId, TaskId } from './brands'; /** * Absolute path to the bundled OpenAPI specification file, resolved through the module From de63f33032fc3771c06e1403fc2ea23c29dc230b Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:58:43 +0300 Subject: [PATCH 07/16] fix(jobnik-sdk): satisfy import-x/order for jobnik-openapi imports Co-Authored-By: Claude Sonnet 5 --- packages/jobnik-sdk/src/clients/consumer.ts | 2 +- packages/jobnik-sdk/src/clients/producer.ts | 2 +- packages/jobnik-sdk/src/clients/worker.ts | 2 +- packages/jobnik-sdk/src/types/worker.ts | 2 +- packages/jobnik-sdk/tests/apiClient.spec.ts | 2 +- packages/jobnik-sdk/tests/consumer/consumer.spec.ts | 2 +- packages/jobnik-sdk/tests/producer/producer.spec.ts | 2 +- packages/jobnik-sdk/tests/producer/producer.test-d.ts | 2 +- packages/jobnik-sdk/tests/worker/worker.spec.ts | 2 +- packages/jobnik-sdk/tests/worker/worker.test-d.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/jobnik-sdk/src/clients/consumer.ts b/packages/jobnik-sdk/src/clients/consumer.ts index b3951f7..4f315c1 100644 --- a/packages/jobnik-sdk/src/clients/consumer.ts +++ b/packages/jobnik-sdk/src/clients/consumer.ts @@ -1,8 +1,8 @@ import { SpanKind, context, propagation, trace } from '@opentelemetry/api'; import { StatusCodes } from 'http-status-codes'; import { INFRA_JOBNIK_TASK_ATTEMPTS, INFRA_JOBNIK_TASK_STATUS } from '@map-colonies/semantic-conventions'; -import type { ApiClient } from '../api'; import type { TaskId, components } from 'jobnik-openapi'; +import type { ApiClient } from '../api'; import type { InferTaskData, Task } from '../types/task'; import type { ValidStageType, StageTypesTemplate } from '../types/stage'; import { DEFAULT_SPAN_CONTEXT, withSpan } from '../telemetry/trace'; diff --git a/packages/jobnik-sdk/src/clients/producer.ts b/packages/jobnik-sdk/src/clients/producer.ts index cf9c886..a0d0551 100644 --- a/packages/jobnik-sdk/src/clients/producer.ts +++ b/packages/jobnik-sdk/src/clients/producer.ts @@ -1,7 +1,7 @@ import { type Span, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; import { INFRA_JOBNIK_JOB_NAME, INFRA_JOBNIK_JOB_PRIORITY, INFRA_JOBNIK_STAGE_ID } from '@map-colonies/semantic-conventions'; -import type { ApiClient } from '../api'; import type { components, JobId, StageId } from 'jobnik-openapi'; +import type { ApiClient } from '../api'; import type { Job, JobData, JobTypesTemplate, NewJob, ValidJobType } from '../types/job'; import type { InferStageData, NewStage, Stage, StageData, StageTypesTemplate, ValidStageType } from '../types/stage'; import type { InferTaskData, NewTask, Task } from '../types/task'; diff --git a/packages/jobnik-sdk/src/clients/worker.ts b/packages/jobnik-sdk/src/clients/worker.ts index 6368462..d5bf3f3 100644 --- a/packages/jobnik-sdk/src/clients/worker.ts +++ b/packages/jobnik-sdk/src/clients/worker.ts @@ -2,6 +2,7 @@ import { setTimeout as sleep } from 'node:timers/promises'; import circuitBreaker, { type Options as OpossumOptions } from 'opossum'; import { context, propagation, type Span, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api'; import { INFRA_JOBNIK_STAGE_ID } from '@map-colonies/semantic-conventions'; +import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import type { ApiClient } from '../api'; import type { Logger } from '../types'; import type { Stage, StageData, StageTypesTemplate, ValidStageType } from '../types/stage'; @@ -13,7 +14,6 @@ import { BASE_ATTRIBUTES, tracer } from '../telemetry/trace'; import type { IWorker, TaskHandler, TaskHandlerContext, WorkerOptions } from '../types/worker'; import type { IProducer } from '../types/producer'; import { WorkerError } from '../errors/sdkErrors'; -import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import { CIRCUIT_BREAKER_STATES, MILLISECOND_IN_SECOND } from '../common/constants'; import type { JobnikMetrics } from '../telemetry/metrics'; import { categorizeError } from '../telemetry/metrics-utils'; diff --git a/packages/jobnik-sdk/src/types/worker.ts b/packages/jobnik-sdk/src/types/worker.ts index 90f7b4e..e18df49 100644 --- a/packages/jobnik-sdk/src/types/worker.ts +++ b/packages/jobnik-sdk/src/types/worker.ts @@ -1,8 +1,8 @@ import type { Options as OpossumOptions } from 'opossum'; +import type { TaskId } from 'jobnik-openapi'; import type { Logger } from '../telemetry/logger'; import type { ScopedApiClient } from '../api'; import type { InferTaskData, Task } from './task'; -import type { TaskId } from 'jobnik-openapi'; import type { InferStageData, Stage, StageData, StageTypesTemplate, ValidStageType } from './stage'; import type { IProducer } from './producer'; import type { InferJobData, Job, JobData, JobTypesTemplate, ValidJobType } from './job'; diff --git a/packages/jobnik-sdk/tests/apiClient.spec.ts b/packages/jobnik-sdk/tests/apiClient.spec.ts index 051f87f..a192f6f 100644 --- a/packages/jobnik-sdk/tests/apiClient.spec.ts +++ b/packages/jobnik-sdk/tests/apiClient.spec.ts @@ -4,9 +4,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import type { MockPool } from 'undici'; import { MockAgent } from 'undici'; +import type { JobId } from 'jobnik-openapi'; import { createApiClient } from '../src/api/index'; import { NetworkError, APIError } from '../src/errors/sdkErrors'; -import type { JobId } from 'jobnik-openapi'; /* eslint-disable */ // Add type declaration for global mockAgent diff --git a/packages/jobnik-sdk/tests/consumer/consumer.spec.ts b/packages/jobnik-sdk/tests/consumer/consumer.spec.ts index 42cda64..9f418fe 100644 --- a/packages/jobnik-sdk/tests/consumer/consumer.spec.ts +++ b/packages/jobnik-sdk/tests/consumer/consumer.spec.ts @@ -3,10 +3,10 @@ import { propagation } from '@opentelemetry/api'; import { W3CTraceContextPropagator } from '@opentelemetry/core'; import { MockAgent, type MockPool } from 'undici'; import { Registry } from 'prom-client'; +import type { StageId, TaskId } from 'jobnik-openapi'; import { createApiClient } from '../../src/api/index'; import { Consumer } from '../../src/clients/consumer'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { StageId, TaskId } from 'jobnik-openapi'; import { ConsumerError, API_ERROR_CODES } from '../../src/errors'; import type { Task } from '../../src/types/task'; import { Metrics } from '../../src/telemetry/metrics'; diff --git a/packages/jobnik-sdk/tests/producer/producer.spec.ts b/packages/jobnik-sdk/tests/producer/producer.spec.ts index 87162de..334414b 100644 --- a/packages/jobnik-sdk/tests/producer/producer.spec.ts +++ b/packages/jobnik-sdk/tests/producer/producer.spec.ts @@ -4,10 +4,10 @@ import { W3CTraceContextPropagator } from '@opentelemetry/core'; import type { MockPool } from 'undici'; import { MockAgent } from 'undici'; import { Registry } from 'prom-client'; +import type { JobId, StageId } from 'jobnik-openapi'; import { createApiClient } from '../../src/api/index'; import { Producer } from '../../src/clients/producer'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { JobId, StageId } from 'jobnik-openapi'; import { ProducerError, API_ERROR_CODES } from '../../src/errors'; import { Metrics } from '../../src/telemetry/metrics'; diff --git a/packages/jobnik-sdk/tests/producer/producer.test-d.ts b/packages/jobnik-sdk/tests/producer/producer.test-d.ts index b1b46a9..b2ad567 100644 --- a/packages/jobnik-sdk/tests/producer/producer.test-d.ts +++ b/packages/jobnik-sdk/tests/producer/producer.test-d.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { describe, it, expectTypeOf } from 'vitest'; import { Registry } from 'prom-client'; +import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import { Producer } from '../../src/clients/producer'; import type { ApiClient } from '../../src/api'; -import type { JobId, StageId, TaskId } from 'jobnik-openapi'; import type { NewJob, JobData } from '../../src/types/job'; import type { NewStage, InferStageData, StageData } from '../../src/types/stage'; import type { NewTask, InferTaskData } from '../../src/types/task'; diff --git a/packages/jobnik-sdk/tests/worker/worker.spec.ts b/packages/jobnik-sdk/tests/worker/worker.spec.ts index bfd0a24..a37daf3 100644 --- a/packages/jobnik-sdk/tests/worker/worker.spec.ts +++ b/packages/jobnik-sdk/tests/worker/worker.spec.ts @@ -5,10 +5,10 @@ import { W3CTraceContextPropagator } from '@opentelemetry/core'; import { MockAgent, type MockPool } from 'undici'; import createClient from 'openapi-fetch'; import { Registry } from 'prom-client'; +import type { StageId, TaskId } from 'jobnik-openapi'; import type { createApiClient } from '../../src/api/index'; import { Worker } from '../../src/clients/worker'; import { NoopLogger } from '../../src/telemetry/noopLogger'; -import type { StageId, TaskId } from 'jobnik-openapi'; import type { Task } from '../../src/types/task'; import type { Logger } from '../../src/types'; import type { TaskHandler, WorkerOptions } from '../../src/types/worker'; diff --git a/packages/jobnik-sdk/tests/worker/worker.test-d.ts b/packages/jobnik-sdk/tests/worker/worker.test-d.ts index 6e807b3..755f6c7 100644 --- a/packages/jobnik-sdk/tests/worker/worker.test-d.ts +++ b/packages/jobnik-sdk/tests/worker/worker.test-d.ts @@ -1,7 +1,7 @@ import { describe, it, expectTypeOf } from 'vitest'; +import type { TaskId } from 'jobnik-openapi'; import { Worker } from '../../src/clients/worker'; import type { ApiClient } from '../../src/api'; -import type { TaskId } from 'jobnik-openapi'; import type { NoopLogger } from '../../src/telemetry/noopLogger'; import type { TaskHandler, WorkerOptions } from '../../src/types/worker'; import type { IProducer } from '../../src/types/producer'; From 467c03f229a0ab590e8522c7a5c248206cc80d00 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 17 Sep 2026 09:49:33 +0300 Subject: [PATCH 08/16] refactor(jobnik-openapi,jobnik-sdk,jobnik-manager): unify generated error classes/mapping jobnik-sdk and jobnik-manager each independently regenerated error classes/mapping from the same spec. jobnik-openapi now generates both once; jobnik-manager imports them directly (never published, so a runtime dependency is safe), while jobnik-sdk stages a physical local copy at build time via a new copy:errors step, since it publishes standalone and jobnik-openapi is a private, unpublished package. Co-Authored-By: Claude Sonnet 5 --- apps/jobnik-manager/eslint.config.mjs | 2 +- apps/jobnik-manager/package.json | 2 -- .../scripts/generateOpenApiErrors.mts | 10 -------- .../src/api/v1/jobs/controller.ts | 2 +- .../src/api/v1/stages/controller.ts | 2 +- .../src/api/v1/tasks/controller.ts | 4 ++-- .../jobnik-manager/src/jobs/models/manager.ts | 2 +- .../src/stages/models/manager.ts | 2 +- .../src/tasks/models/manager.ts | 14 +++++------ .../src/tasks/models/taskStateMachine.ts | 2 +- .../tests/unit/stages/stages.spec.ts | 2 +- .../tests/unit/tasks/tasks.spec.ts | 2 +- knip.json | 2 +- packages/jobnik-openapi/package.json | 7 +++--- .../scripts/generateOpenApiErrors.mts | 6 +++++ .../jobnik-openapi/src}/generated/errors.ts | 16 +++++++++++++ packages/jobnik-openapi/src/index.ts | 24 +++++++++++++++++++ packages/jobnik-sdk/.gitignore | 1 + packages/jobnik-sdk/package.json | 6 ++--- packages/jobnik-sdk/scripts/copyErrors.mts | 13 ++++++++++ packages/jobnik-sdk/scripts/openapi.mts | 7 ------ packages/jobnik-sdk/scripts/tsconfig.json | 2 +- .../src/generated/openapi-errors.ts | 20 ---------------- turbo.json | 2 +- 24 files changed, 86 insertions(+), 66 deletions(-) delete mode 100644 apps/jobnik-manager/scripts/generateOpenApiErrors.mts create mode 100644 packages/jobnik-openapi/scripts/generateOpenApiErrors.mts rename {apps/jobnik-manager/src/common => packages/jobnik-openapi/src}/generated/errors.ts (88%) create mode 100644 packages/jobnik-sdk/scripts/copyErrors.mts delete mode 100644 packages/jobnik-sdk/scripts/openapi.mts delete mode 100644 packages/jobnik-sdk/src/generated/openapi-errors.ts diff --git a/apps/jobnik-manager/eslint.config.mjs b/apps/jobnik-manager/eslint.config.mjs index 9aec36d..bcbaffb 100644 --- a/apps/jobnik-manager/eslint.config.mjs +++ b/apps/jobnik-manager/eslint.config.mjs @@ -32,5 +32,5 @@ const customConfig = { }; export default defineConfig(vitestConfig, tsBaseConfig, customConfig, { - ignores: ['src/db/prisma/generated', 'src/common/generated', 'vitest.config.mts'], + ignores: ['src/db/prisma/generated', 'vitest.config.mts'], }); diff --git a/apps/jobnik-manager/package.json b/apps/jobnik-manager/package.json index ba0cb10..a5ad370 100644 --- a/apps/jobnik-manager/package.json +++ b/apps/jobnik-manager/package.json @@ -29,8 +29,6 @@ "assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles ./package.json dist && copyfiles -u 4 \"./src/db/prisma/migrations/**/*\" ./dist/db/prisma/migrations && copyfiles -f ./src/db/prisma/schema.prisma ./dist/db/prisma", "prisma:copy": "node ./scripts/copyDirectories.mjs ./src/db/prisma/generated ./dist/db/prisma/generated", "clean": "rimraf dist", - "generate:openapi-errors": "node scripts/generateOpenApiErrors.mts", - "generate:check": "npm run generate:openapi-errors && git diff --exit-code -- src/common/generated/errors.ts", "type-check": "tsc --noEmit", "migration:validate": "npx prisma validate --schema ./src/db/prisma/schema.prisma ", "migration:format": "npx prisma format --check --schema ./src/db/prisma/schema.prisma ", diff --git a/apps/jobnik-manager/scripts/generateOpenApiErrors.mts b/apps/jobnik-manager/scripts/generateOpenApiErrors.mts deleted file mode 100644 index ec6dd39..0000000 --- a/apps/jobnik-manager/scripts/generateOpenApiErrors.mts +++ /dev/null @@ -1,10 +0,0 @@ -import { generateErrors } from '@map-colonies/openapi-helpers/generators'; -import { openapiFilePath } from 'jobnik-openapi'; - -const ERRORS_DESTINATION_PATH = 'src/common/generated/errors.ts'; - -await generateErrors(openapiFilePath, ERRORS_DESTINATION_PATH, { - shouldFormat: true, - includeMapping: false, - includeErrorClasses: true, -}); diff --git a/apps/jobnik-manager/src/api/v1/jobs/controller.ts b/apps/jobnik-manager/src/api/v1/jobs/controller.ts index bb408a6..5266049 100644 --- a/apps/jobnik-manager/src/api/v1/jobs/controller.ts +++ b/apps/jobnik-manager/src/api/v1/jobs/controller.ts @@ -3,9 +3,9 @@ import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { HttpError } from '@map-colonies/error-express-handler'; import type { TypedRequestHandlers } from 'jobnik-openapi'; +import { IllegalJobStatusTransitionError, JobNotInFiniteStateError, JobNotFoundError } from 'jobnik-openapi'; import { SERVICES, successMessages } from '@common/constants'; import { SamePriorityChangeError } from '@src/jobs/models/errors'; -import { IllegalJobStatusTransitionError, JobNotInFiniteStateError, JobNotFoundError } from '@src/common/generated/errors'; import { type JobFindCriteriaArg } from '@src/jobs/models/models'; import { JobManager } from '@src/jobs/models/manager'; diff --git a/apps/jobnik-manager/src/api/v1/stages/controller.ts b/apps/jobnik-manager/src/api/v1/stages/controller.ts index 50df633..f23304a 100644 --- a/apps/jobnik-manager/src/api/v1/stages/controller.ts +++ b/apps/jobnik-manager/src/api/v1/stages/controller.ts @@ -3,8 +3,8 @@ import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { HttpError } from '@map-colonies/error-express-handler'; import type { TypedRequestHandlers } from 'jobnik-openapi'; +import { IllegalStageStatusTransitionError, JobInFiniteStateError, JobNotFoundError, StageNotFoundError } from 'jobnik-openapi'; import { SERVICES, successMessages } from '@common/constants'; -import { IllegalStageStatusTransitionError, JobInFiniteStateError, JobNotFoundError, StageNotFoundError } from '@src/common/generated/errors'; import { StageManager } from '@src/stages/models/manager'; import type { StageFindCriteriaArg } from '@src/stages/models/models'; diff --git a/apps/jobnik-manager/src/api/v1/tasks/controller.ts b/apps/jobnik-manager/src/api/v1/tasks/controller.ts index 60a6117..72714b6 100644 --- a/apps/jobnik-manager/src/api/v1/tasks/controller.ts +++ b/apps/jobnik-manager/src/api/v1/tasks/controller.ts @@ -3,7 +3,6 @@ import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { HttpError } from '@map-colonies/error-express-handler'; import type { TypedRequestHandlers } from 'jobnik-openapi'; -import { SERVICES, successMessages } from '@common/constants'; import { IllegalJobStatusTransitionError, IllegalStageStatusTransitionError, @@ -14,7 +13,8 @@ import { StageNotFoundError, TaskStatusUpdateFailedError, TaskNotFoundError, -} from '@src/common/generated/errors'; +} from 'jobnik-openapi'; +import { SERVICES, successMessages } from '@common/constants'; import { TaskManager } from '@src/tasks/models/manager'; import { type TasksFindCriteriaArg } from '@src/tasks/models/models'; diff --git a/apps/jobnik-manager/src/jobs/models/manager.ts b/apps/jobnik-manager/src/jobs/models/manager.ts index 1e2e2e8..aac83b0 100644 --- a/apps/jobnik-manager/src/jobs/models/manager.ts +++ b/apps/jobnik-manager/src/jobs/models/manager.ts @@ -6,6 +6,7 @@ import { trace } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; import type { JobId } from 'jobnik-openapi'; +import { IllegalJobStatusTransitionError, JobNotInFiniteStateError, JobNotFoundError } from 'jobnik-openapi'; import type { PrismaClient, Priority } from '@prismaClient'; import { Prisma, JobOperationStatus } from '@prismaClient'; import { SERVICES } from '@common/constants'; @@ -13,7 +14,6 @@ import { convertArrayPrismaStageToStageResponse } from '@src/stages/models/helpe import { illegalStatusTransitionErrorMessage, prismaKnownErrors } from '@common/errors'; import { type PrismaTransaction } from '@src/db/types'; import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; -import { IllegalJobStatusTransitionError, JobNotInFiniteStateError, JobNotFoundError } from '@src/common/generated/errors'; import { ATTR_MESSAGING_MESSAGE_CONVERSATION_ID } from '@src/common/semconv'; import { paginate } from '@src/common/utils/pagination'; import { errorMessages as jobsErrorMessages, SamePriorityChangeError } from './errors'; diff --git a/apps/jobnik-manager/src/stages/models/manager.ts b/apps/jobnik-manager/src/stages/models/manager.ts index 7da6728..88ebd78 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -5,6 +5,7 @@ import { trace, type Tracer } from '@opentelemetry/api'; import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; import type { JobId, StageId } from 'jobnik-openapi'; +import { IllegalStageStatusTransitionError, JobInFiniteStateError, JobNotFoundError, StageNotFoundError } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { JobOperationStatus, Prisma, StageOperationStatus } from '@prismaClient'; import { JobManager } from '@src/jobs/models/manager'; @@ -13,7 +14,6 @@ import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; import { jobStateMachine } from '@src/jobs/models/jobStateMachine'; import { illegalStatusTransitionErrorMessage, prismaKnownErrors } from '@src/common/errors'; import { errorMessages as jobsErrorMessages } from '@src/jobs/models/errors'; -import { IllegalStageStatusTransitionError, JobInFiniteStateError, JobNotFoundError, StageNotFoundError } from '@src/common/generated/errors'; import { errorMessages as stagesErrorMessages } from '@src/stages/models/errors'; import type { PrismaTransaction } from '@src/db/types'; import { ATTR_MESSAGING_DESTINATION_NAME, ATTR_MESSAGING_MESSAGE_CONVERSATION_ID } from '@src/common/semconv'; diff --git a/apps/jobnik-manager/src/tasks/models/manager.ts b/apps/jobnik-manager/src/tasks/models/manager.ts index 1e87e20..860843d 100644 --- a/apps/jobnik-manager/src/tasks/models/manager.ts +++ b/apps/jobnik-manager/src/tasks/models/manager.ts @@ -6,6 +6,13 @@ import { withSpanAsyncV4 } from '@map-colonies/tracing-utils'; import { subMinutes } from 'date-fns'; import { INFRA_CONVENTIONS } from '@map-colonies/semantic-conventions'; import type { StageId, TaskId } from 'jobnik-openapi'; +import { + NotAllowedToAddTasksToInProgressStageError, + StageInFiniteStateError, + StageNotFoundError, + TaskNotFoundError, + TaskStatusUpdateFailedError, +} from 'jobnik-openapi'; import { Prisma, StageOperationStatus, Task, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import { SERVICES, XSTATE_DONE_STATE } from '@common/constants'; import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; @@ -17,13 +24,6 @@ import { stageStateMachine } from '@src/stages/models/stageStateMachine'; import { type ConfigType } from '@src/common/config'; import type { UpdateSummaryCount } from '@src/stages/models/models'; import type { PrismaTransaction } from '@src/db/types'; -import { - NotAllowedToAddTasksToInProgressStageError, - StageInFiniteStateError, - StageNotFoundError, - TaskNotFoundError, - TaskStatusUpdateFailedError, -} from '@src/common/generated/errors'; import { ATTR_MESSAGING_DESTINATION_NAME, ATTR_MESSAGING_MESSAGE_ID } from '@src/common/semconv'; import { paginate } from '@src/common/utils/pagination'; import { TaskRepository } from '../DAL/taskRepository'; diff --git a/apps/jobnik-manager/src/tasks/models/taskStateMachine.ts b/apps/jobnik-manager/src/tasks/models/taskStateMachine.ts index 111442a..6701c42 100644 --- a/apps/jobnik-manager/src/tasks/models/taskStateMachine.ts +++ b/apps/jobnik-manager/src/tasks/models/taskStateMachine.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { Snapshot } from 'xstate'; import { createActor, setup } from 'xstate'; +import { IllegalTaskStatusTransitionError } from 'jobnik-openapi'; import { TaskOperationStatus } from '@prismaClient'; -import { IllegalTaskStatusTransitionError } from '@src/common/generated/errors'; import { illegalStatusTransitionErrorMessage } from '@src/common/errors'; type changeStatusOperations = 'pend' | 'complete' | 'retry' | 'process' | 'fail' | 'create'; diff --git a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts index 0dc7cce..9d2ceb8 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -6,6 +6,7 @@ import { faker } from '@faker-js/faker'; import { trace } from '@opentelemetry/api'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; import type { JobId, StageId } from 'jobnik-openapi'; +import { JobInFiniteStateError } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, JobOperationStatus } from '@prismaClient'; import { StageManager } from '@src/stages/models/manager'; @@ -18,7 +19,6 @@ import { defaultStatusCounts } from '@src/stages/models/helper'; import { StageRepository } from '@src/stages/DAL/stageRepository'; import type { JobPrismaObject } from '@src/jobs/models/models'; import { SERVICE_NAME } from '@src/common/constants'; -import { JobInFiniteStateError } from '@src/common/generated/errors'; import { completedStageXstatePersistentSnapshot, inProgressStageXstatePersistentSnapshot, diff --git a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts index eb996e6..36f2f0b 100644 --- a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts @@ -6,6 +6,7 @@ import { trace } from '@opentelemetry/api'; import { subHours, subMinutes } from 'date-fns'; import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; import type { StageId, TaskId } from 'jobnik-openapi'; +import { IllegalTaskStatusTransitionError, NotAllowedToAddTasksToInProgressStageError, StageInFiniteStateError } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, TaskOperationStatus, JobOperationStatus } from '@prismaClient'; import { StageManager } from '@src/stages/models/manager'; @@ -18,7 +19,6 @@ import type { TaskCreateModel } from '@src/tasks/models/models'; import { StageRepository } from '@src/stages/DAL/stageRepository'; import { TaskRepository } from '@src/tasks/DAL/taskRepository'; import { SERVICE_NAME } from '@src/common/constants'; -import { IllegalTaskStatusTransitionError, NotAllowedToAddTasksToInProgressStageError, StageInFiniteStateError } from '@src/common/generated/errors'; import { getConfig, initConfig } from '@src/common/config'; import { DEFAULT_TRACEPARENT } from '@src/common/utils/tracingHelpers'; import { createJobEntity, createStageEntity, createTaskEntity } from '../generator'; diff --git a/knip.json b/knip.json index 3b4dd89..b56d6d7 100644 --- a/knip.json +++ b/knip.json @@ -3,7 +3,7 @@ "workspaces": { "apps/jobnik-manager": { "entry": ["src/instrumentation.mts"], - "ignore": ["src/db/prisma/generated/**", "src/common/generated/**"], + "ignore": ["src/db/prisma/generated/**"], "ignoreUnresolved": ["./instrumentation.mjs"], "ignoreDependencies": ["@types/express-serve-static-core", "@types/multer", "@types/swagger-ui-express", "@prismaClient"] }, diff --git a/packages/jobnik-openapi/package.json b/packages/jobnik-openapi/package.json index fd001e7..6ded2f2 100644 --- a/packages/jobnik-openapi/package.json +++ b/packages/jobnik-openapi/package.json @@ -20,12 +20,13 @@ "lint": "eslint .", "lint:fix": "eslint --fix .", "prebuild": "npm run clean", - "build": "tsc --project tsconfig.build.json && copyfiles -f ./openapi3.yaml ./src/openapi.d.ts ./dist", + "build": "tsc --project tsconfig.build.json && copyfiles -f ./openapi3.yaml ./src/openapi.d.ts ./dist && copyfiles -u 1 ./src/generated/errors.ts ./dist", "clean": "rimraf dist", "generate:openapi-build": "node scripts/generateVersionedOpenApi.mts", "generate:openapi-types": "node scripts/generateTypes.mts", - "generate": "npm run generate:openapi-build && npm run generate:openapi-types", - "generate:check": "npm run generate && git diff --exit-code -- openapi3.yaml src/openapi.d.ts", + "generate:openapi-errors": "node scripts/generateOpenApiErrors.mts", + "generate": "npm run generate:openapi-build && npm run generate:openapi-types && npm run generate:openapi-errors", + "generate:check": "npm run generate && git diff --exit-code -- openapi3.yaml src/openapi.d.ts src/generated/errors.ts", "type-check": "tsc --noEmit" }, "author": "MapColonies", diff --git a/packages/jobnik-openapi/scripts/generateOpenApiErrors.mts b/packages/jobnik-openapi/scripts/generateOpenApiErrors.mts new file mode 100644 index 0000000..546458c --- /dev/null +++ b/packages/jobnik-openapi/scripts/generateOpenApiErrors.mts @@ -0,0 +1,6 @@ +import { generateErrors } from '@map-colonies/openapi-helpers/generators'; + +const OPENAPI_PATH = 'openapi3.yaml'; +const ERRORS_DESTINATION_PATH = 'src/generated/errors.ts'; + +await generateErrors(OPENAPI_PATH, ERRORS_DESTINATION_PATH, { shouldFormat: true, includeErrorClasses: true, includeMapping: true }); diff --git a/apps/jobnik-manager/src/common/generated/errors.ts b/packages/jobnik-openapi/src/generated/errors.ts similarity index 88% rename from apps/jobnik-manager/src/common/generated/errors.ts rename to packages/jobnik-openapi/src/generated/errors.ts index 9b3694e..3176c0f 100644 --- a/apps/jobnik-manager/src/common/generated/errors.ts +++ b/packages/jobnik-openapi/src/generated/errors.ts @@ -183,3 +183,19 @@ export class IllegalTaskStatusTransitionError extends Error { Object.setPrototypeOf(this, new.target.prototype); } } +export const API_ERRORS_MAP = { + VALIDATION_ERROR: 'VALIDATION_ERROR', + DATABASE_RELATED_ERROR: 'DATABASE_RELATED_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', + JOB_NOT_FOUND: 'JOB_NOT_FOUND', + JOB_NOT_IN_FINITE_STATE: 'JOB_NOT_IN_FINITE_STATE', + ILLEGAL_JOB_STATUS_TRANSITION: 'ILLEGAL_JOB_STATUS_TRANSITION', + JOB_IN_FINITE_STATE: 'JOB_IN_FINITE_STATE', + STAGE_NOT_FOUND: 'STAGE_NOT_FOUND', + ILLEGAL_STAGE_STATUS_TRANSITION: 'ILLEGAL_STAGE_STATUS_TRANSITION', + STAGE_IN_FINITE_STATE: 'STAGE_IN_FINITE_STATE', + NOT_ALLOWED_TO_ADD_TASKS_TO_IN_PROGRESS_STAGE: 'NOT_ALLOWED_TO_ADD_TASKS_TO_IN_PROGRESS_STAGE', + TASK_NOT_FOUND: 'TASK_NOT_FOUND', + TASK_STATUS_UPDATE_FAILED: 'TASK_STATUS_UPDATE_FAILED', + ILLEGAL_TASK_STATUS_TRANSITION: 'ILLEGAL_TASK_STATUS_TRANSITION', +} as const; diff --git a/packages/jobnik-openapi/src/index.ts b/packages/jobnik-openapi/src/index.ts index 432929c..4069a1b 100644 --- a/packages/jobnik-openapi/src/index.ts +++ b/packages/jobnik-openapi/src/index.ts @@ -1,8 +1,32 @@ export type { paths, components, operations, TypedRequestHandlers } from './openapi'; export type { Brand, JobId, StageId, TaskId } from './brands'; +export { + ValidationError, + DatabaseRelatedError, + UnknownError, + JobNotFoundError, + JobNotInFiniteStateError, + IllegalJobStatusTransitionError, + JobInFiniteStateError, + StageNotFoundError, + IllegalStageStatusTransitionError, + StageInFiniteStateError, + NotAllowedToAddTasksToInProgressStageError, + TaskNotFoundError, + TaskStatusUpdateFailedError, + IllegalTaskStatusTransitionError, + API_ERRORS_MAP, +} from './generated/errors'; /** * Absolute path to the bundled OpenAPI specification file, resolved through the module * system rather than through a copy of the file living next to the consumer. */ export const openapiFilePath: string = require.resolve('./openapi3.yaml'); + +/** + * Absolute path to the generated error classes/mapping source file, for consumers (such as + * jobnik-sdk) that need to stage a physical local copy rather than import the runtime module + * directly — e.g. because they publish standalone and can't depend on this private package. + */ +export const errorsFilePath: string = require.resolve('./generated/errors.ts'); diff --git a/packages/jobnik-sdk/.gitignore b/packages/jobnik-sdk/.gitignore index d4198d8..1e9daaa 100644 --- a/packages/jobnik-sdk/.gitignore +++ b/packages/jobnik-sdk/.gitignore @@ -113,3 +113,4 @@ html/ # staged from jobnik-openapi; the specification exists once, in that package src/openapi3.yaml +src/generated/openapi-errors.ts diff --git a/packages/jobnik-sdk/package.json b/packages/jobnik-sdk/package.json index 71b5d96..e5c3aca 100644 --- a/packages/jobnik-sdk/package.json +++ b/packages/jobnik-sdk/package.json @@ -19,13 +19,12 @@ "prelint": "npm run format", "lint": "eslint .", "lint:fix": "eslint --fix .", - "prebuild": "npm run clean && npm run copy:openapi", + "prebuild": "npm run clean && npm run copy:openapi && npm run copy:errors", "build": "tsc --project tsconfig.build.json && copyfiles -f src/openapi3.yaml dist", "start": "npm run build && cd dist && node ./index.js", "clean": "rimraf dist", "copy:openapi": "node scripts/copySpec.mts", - "generate:openapi": "node scripts/openapi.mts", - "generate:check": "npm run generate:openapi && git diff --exit-code -- src/generated/openapi-errors.ts", + "copy:errors": "node scripts/copyErrors.mts", "prepack": "npm run build", "type-check": "tsc --noEmit -p tsconfig.lint.json", "check-pack": "publint && attw --pack . && node scripts/checkPack.mts" @@ -53,7 +52,6 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", "@map-colonies/eslint-config": "catalog:", - "@map-colonies/openapi-helpers": "catalog:", "@map-colonies/tsconfig": "catalog:", "@opentelemetry/context-async-hooks": "catalog:", "@opentelemetry/core": "catalog:", diff --git a/packages/jobnik-sdk/scripts/copyErrors.mts b/packages/jobnik-sdk/scripts/copyErrors.mts new file mode 100644 index 0000000..6ad4fc9 --- /dev/null +++ b/packages/jobnik-sdk/scripts/copyErrors.mts @@ -0,0 +1,13 @@ +import { copyFileSync, mkdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { errorsFilePath } from 'jobnik-openapi'; + +// Stages a local copy of the generated error classes/mapping, resolved through jobnik-openapi, +// next to the SDK's own source. The SDK ships this copy in its build output and imports it at +// runtime, so a physical file has to exist here — jobnik-openapi is private and never published, +// so the SDK can't depend on it at runtime once installed by a real consumer. +const destinationDir = fileURLToPath(new URL('../src/generated', import.meta.url)); +const destination = fileURLToPath(new URL('../src/generated/openapi-errors.ts', import.meta.url)); + +mkdirSync(destinationDir, { recursive: true }); +copyFileSync(errorsFilePath, destination); diff --git a/packages/jobnik-sdk/scripts/openapi.mts b/packages/jobnik-sdk/scripts/openapi.mts deleted file mode 100644 index 4ca4420..0000000 --- a/packages/jobnik-sdk/scripts/openapi.mts +++ /dev/null @@ -1,7 +0,0 @@ -import { generateErrors } from '@map-colonies/openapi-helpers/generators'; -import { openapiFilePath } from 'jobnik-openapi'; - -const OPENAPI_PATH = openapiFilePath; -const ERRORS_DESTINATION_PATH = 'src/generated/openapi-errors.ts'; - -await generateErrors(OPENAPI_PATH, ERRORS_DESTINATION_PATH, { shouldFormat: true, includeErrorClasses: false, includeMapping: true }); diff --git a/packages/jobnik-sdk/scripts/tsconfig.json b/packages/jobnik-sdk/scripts/tsconfig.json index dd58a29..0938ccb 100644 --- a/packages/jobnik-sdk/scripts/tsconfig.json +++ b/packages/jobnik-sdk/scripts/tsconfig.json @@ -4,5 +4,5 @@ "erasableSyntaxOnly": true, "lib": ["ESNext"] }, - "include": ["openapi.mts", "copySpec.mts", "checkPack.mts"] + "include": ["copySpec.mts", "copyErrors.mts", "checkPack.mts"] } diff --git a/packages/jobnik-sdk/src/generated/openapi-errors.ts b/packages/jobnik-sdk/src/generated/openapi-errors.ts deleted file mode 100644 index 62458a4..0000000 --- a/packages/jobnik-sdk/src/generated/openapi-errors.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable */ -// This file was auto-generated. Do not edit manually. -// To update, run the error generation script again. - -export const API_ERRORS_MAP = { - VALIDATION_ERROR: 'VALIDATION_ERROR', - DATABASE_RELATED_ERROR: 'DATABASE_RELATED_ERROR', - UNKNOWN_ERROR: 'UNKNOWN_ERROR', - JOB_NOT_FOUND: 'JOB_NOT_FOUND', - JOB_NOT_IN_FINITE_STATE: 'JOB_NOT_IN_FINITE_STATE', - ILLEGAL_JOB_STATUS_TRANSITION: 'ILLEGAL_JOB_STATUS_TRANSITION', - JOB_IN_FINITE_STATE: 'JOB_IN_FINITE_STATE', - STAGE_NOT_FOUND: 'STAGE_NOT_FOUND', - ILLEGAL_STAGE_STATUS_TRANSITION: 'ILLEGAL_STAGE_STATUS_TRANSITION', - STAGE_IN_FINITE_STATE: 'STAGE_IN_FINITE_STATE', - NOT_ALLOWED_TO_ADD_TASKS_TO_IN_PROGRESS_STAGE: 'NOT_ALLOWED_TO_ADD_TASKS_TO_IN_PROGRESS_STAGE', - TASK_NOT_FOUND: 'TASK_NOT_FOUND', - TASK_STATUS_UPDATE_FAILED: 'TASK_STATUS_UPDATE_FAILED', - ILLEGAL_TASK_STATUS_TRANSITION: 'ILLEGAL_TASK_STATUS_TRANSITION', -} as const; diff --git a/turbo.json b/turbo.json index 866d968..499decf 100644 --- a/turbo.json +++ b/turbo.json @@ -6,7 +6,7 @@ "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", "src/db/prisma/schema.prisma"], - "outputs": ["dist/**", "src/db/prisma/generated/**", "src/openapi3.yaml"], + "outputs": ["dist/**", "src/db/prisma/generated/**", "src/openapi3.yaml", "src/generated/openapi-errors.ts"], "outputLogs": "errors-only" }, "build:docker": { From 483a7cd158eb370f92f7f065d2c4ef184a63689e Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:35:16 +0300 Subject: [PATCH 09/16] fix(jobnik-sdk): sync lockfile after openapi-helpers devDependency removal pnpm-lock.yaml still listed @map-colonies/openapi-helpers for jobnik-sdk after 467c03f dropped it from package.json, breaking frozen-lockfile CI installs. Co-Authored-By: Claude Sonnet 5 --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78af796..af61b5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -464,9 +464,6 @@ importers: '@map-colonies/eslint-config': specifier: 'catalog:' version: 9.0.0(@typescript-eslint/utils@8.67.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.27(@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3)(vitest@4.1.11))(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3) - '@map-colonies/openapi-helpers': - specifier: 'catalog:' - version: 5.1.0(@types/express@4.17.25)(@types/json-schema@7.0.15)(openapi-typescript@7.13.0(typescript@5.9.3))(prettier@3.9.6)(supertest@7.2.2)(typescript@5.9.3) '@map-colonies/tsconfig': specifier: 'catalog:' version: 2.0.0 From b292a7e1ce31c55c429c86337b1905985e5748a1 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:35:30 +0300 Subject: [PATCH 10/16] refactor(jobnik-manager): brand ids at the Prisma query boundary instead of at each cast site getStageEntityById/getJobEntityById/getTaskEntityById and their raw ad-hoc query call sites now cast Prisma results to JobId/StageId/TaskId once, at the point the value leaves Prisma, instead of scattering `as JobId`/ `as StageId`/`as TaskId` casts across every call site that reads the field. Prisma has no concept of branded scalars, so this can't be pushed further down into schema.prisma: prisma-json-types-generator throws on `@db.Uuid` string columns (breaks its UuidFilter handling and silently drops Json overrides for later models). Revisit once that's fixed upstream. Co-Authored-By: Claude Sonnet 5 --- .../src/stages/models/manager.ts | 37 +++++--- .../src/stages/models/models.ts | 10 ++- .../src/tasks/models/manager.ts | 28 +++--- .../jobnik-manager/src/tasks/models/models.ts | 10 ++- .../tests/integration/tasks/helpers.ts | 4 +- apps/jobnik-manager/tests/unit/generator.ts | 5 +- .../tests/unit/stages/stages.spec.ts | 8 +- .../tests/unit/tasks/tasks.spec.ts | 88 ++++++++++--------- 8 files changed, 112 insertions(+), 78 deletions(-) diff --git a/apps/jobnik-manager/src/stages/models/manager.ts b/apps/jobnik-manager/src/stages/models/manager.ts index 88ebd78..acb16b3 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -38,13 +38,22 @@ import { } from './helper'; import { OperationStatusMapper, stageStateMachine } from './stageStateMachine'; -type GetStageEntityByIdReturnType = TOptions extends { includeTasks: true; includeJob: true } - ? Prisma.StageGetPayload<{ include: { task: true; job: true } }> - : TOptions extends { includeTasks: true } - ? Prisma.StageGetPayload<{ include: { task: true } }> - : TOptions extends { includeJob: true } - ? Prisma.StageGetPayload<{ include: { job: true } }> - : Prisma.StageGetPayload>; +/** + * TODO: `id`/`jobId` are re-branded here instead of at the Prisma schema level because + * prisma-json-types-generator throws on `@db.Uuid` string columns (breaks its `UuidFilter` + * handling and silently drops Json overrides for later models). Move this branding into + * schema.prisma once that upstream bug is fixed. + */ +type GetStageEntityByIdReturnType = Omit< + TOptions extends { includeTasks: true; includeJob: true } + ? Prisma.StageGetPayload<{ include: { task: true; job: true } }> + : TOptions extends { includeTasks: true } + ? Prisma.StageGetPayload<{ include: { task: true } }> + : TOptions extends { includeJob: true } + ? Prisma.StageGetPayload<{ include: { job: true } }> + : Prisma.StageGetPayload>, + 'id' | 'jobId' +> & { id: StageId; jobId: JobId }; @injectable() export class StageManager { public constructor( @@ -327,7 +336,7 @@ export class StageManager { throw new StageNotFoundError(stagesErrorMessages.stageNotFound); } - const jobId = stage.jobId as JobId; + const jobId = stage.jobId; // Idempotent status update: if already in target status, no-op // This prevents errors during race conditions where multiple workers @@ -387,15 +396,17 @@ export class StageManager { // If the stage is marked as completed, and there is a next stage in the job, update the next stage status to PENDING if (targetStatus === StageOperationStatus.COMPLETED) { const nextStageOrder = stage.order + 1; - const nextStage = await tx.stage.findFirst({ + // TODO: cast here, not at usage below, because Prisma has no concept of branded scalars + // (see GetStageEntityByIdReturnType above for why); revisit once that's fixed upstream. + const nextStage = (await tx.stage.findFirst({ where: { jobId: stage.jobId, order: nextStageOrder, }, - }); + })) as (Prisma.StageGetPayload & { id: StageId }) | null; if (nextStage?.status === StageOperationStatus.CREATED) { - await this.executeUpdateStatus(nextStage.id as StageId, StageOperationStatus.PENDING, tx); + await this.executeUpdateStatus(nextStage.id, StageOperationStatus.PENDING, tx); trace.getActiveSpan()?.addEvent('Next stage set to PENDING', { nextStageId: nextStage.id }); } @@ -445,14 +456,14 @@ export class StageManager { await tx.stage.update({ where: { id: stage.id }, data: stageUpdatedData }); if (summary.total === summary.completed) { - await this.updateStatus(stage.id as StageId, StageOperationStatus.COMPLETED, tx); + await this.updateStatus(stage.id, StageOperationStatus.COMPLETED, tx); this.logger.info({ msg: 'Stage completed, updating job progress', stageId: stage.id, jobId: stage.jobId, }); - await this.updateJobCompletionProgress(stage.jobId as JobId, tx); + await this.updateJobCompletionProgress(stage.jobId, tx); trace.getActiveSpan()?.addEvent('Stage set to COMPLETED', { stageId: stage.id }); } } diff --git a/apps/jobnik-manager/src/stages/models/models.ts b/apps/jobnik-manager/src/stages/models/models.ts index d60cff8..16fff0e 100644 --- a/apps/jobnik-manager/src/stages/models/models.ts +++ b/apps/jobnik-manager/src/stages/models/models.ts @@ -1,4 +1,4 @@ -import type { components, operations } from 'jobnik-openapi'; +import type { components, operations, JobId, StageId } from 'jobnik-openapi'; import type { Prisma, TaskOperationStatus } from '@prismaClient'; import type { JobPrismaObject } from '@src/jobs/models/models'; import type { PrismaTransaction } from '@src/db/types'; @@ -8,7 +8,13 @@ type StageCreateModel = components['schemas']['createStagePayloadRequest']; type StageSummary = components['schemas']['summary']; type StageFindCriteriaArg = operations['getStagesV1']['parameters']['query']; type StagesPaginatedResponse = components['schemas']['stagesPaginatedResponse']; -type StageIncludingJob = StagePrismaObject & { job: JobPrismaObject }; +/** + * TODO: `id`/`jobId` are re-branded here instead of at the Prisma schema level because + * prisma-json-types-generator throws on `@db.Uuid` string columns (breaks its `UuidFilter` + * handling and silently drops Json overrides for later models). Move this branding into + * schema.prisma once that upstream bug is fixed. + */ +type StageIncludingJob = Omit & { id: StageId; jobId: JobId; job: JobPrismaObject }; interface UpdateSummaryCount { add: { status: TaskOperationStatus; count: number }; remove?: { status: TaskOperationStatus; count: number }; diff --git a/apps/jobnik-manager/src/tasks/models/manager.ts b/apps/jobnik-manager/src/tasks/models/manager.ts index 860843d..68eefeb 100644 --- a/apps/jobnik-manager/src/tasks/models/manager.ts +++ b/apps/jobnik-manager/src/tasks/models/manager.ts @@ -13,7 +13,7 @@ import { TaskNotFoundError, TaskStatusUpdateFailedError, } from 'jobnik-openapi'; -import { Prisma, StageOperationStatus, Task, TaskOperationStatus, type PrismaClient } from '@prismaClient'; +import { Prisma, StageOperationStatus, TaskOperationStatus, type PrismaClient } from '@prismaClient'; import { SERVICES, XSTATE_DONE_STATE } from '@common/constants'; import { resolveTraceContext } from '@src/common/utils/tracingHelpers'; import { StageManager } from '@src/stages/models/manager'; @@ -260,8 +260,10 @@ export class TaskManager { }, }; + // TODO: cast here, not at usage, because Prisma has no concept of branded scalars + // (see TaskPrismaObject in models.ts for why); revisit once that's fixed upstream. const task = await prisma.task.findUnique(queryBody); - return task; + return task as TaskPrismaObject | null; } /** @@ -280,7 +282,9 @@ export class TaskManager { const cutoffTime = subMinutes(new Date(), staleTaskThresholdInMinutes); // Find tasks that are stuck in IN_PROGRESS state beyond the time threshold - const staleTasks = await this.prisma.task.findMany({ + // TODO: cast here, not at usage, because Prisma has no concept of branded scalars + // (see TaskPrismaObject in models.ts for why); revisit once that's fixed upstream. + const staleTasks = (await this.prisma.task.findMany({ where: { status: TaskOperationStatus.IN_PROGRESS, startTime: { @@ -292,7 +296,7 @@ export class TaskManager { stageId: true, startTime: true, }, - }); + })) as Pick[]; if (staleTasks.length === 0) { this.logger.debug({ msg: 'No stale tasks found for cleanup' }); @@ -425,11 +429,13 @@ export class TaskManager { // Create update query with race condition protection for IN_PROGRESS const updateQueryBody = { - where: this.createUpdateWhereClause(task.id as TaskId, previousStatus), + where: this.createUpdateWhereClause(task.id, previousStatus), data: { ...taskDataToUpdate, status: nextStatus, xstate: newPersistedSnapshot, startTime, endTime }, }; - const updatedTasks = await tx.task.updateManyAndReturn(updateQueryBody); + // TODO: cast here, not at usage below, because Prisma has no concept of branded scalars + // (see TaskPrismaObject in models.ts for why); revisit once that's fixed upstream. + const updatedTasks = (await tx.task.updateManyAndReturn(updateQueryBody)) as TaskPrismaObject[]; if (updatedTasks[0] === undefined) { // Race condition detected: another process already modified this task this.logger.warn({ @@ -443,13 +449,13 @@ export class TaskManager { throw new TaskStatusUpdateFailedError(tasksErrorMessages.taskStatusUpdateFailed); } - await this.updateStageSummary(task.stageId as StageId, previousStatus, nextStatus, tx); + await this.updateStageSummary(task.stageId, previousStatus, nextStatus, tx); // TODO - Check if this stage type should propagate failure to parent job // For now, all task failures cause stage failure, but in future versions // some stages may be configured as optional (non-blocking) if (nextStatus === TaskOperationStatus.FAILED) { - const stage = await this.stageManager.getStageEntityById(task.stageId as StageId, { tx }); + const stage = await this.stageManager.getStageEntityById(task.stageId, { tx }); /* v8 ignore next 7 -- @preserve */ if (!stage) { @@ -467,7 +473,7 @@ export class TaskManager { stageId: task.stageId, }); - await this.stageManager.updateStatus(task.stageId as StageId, StageOperationStatus.FAILED, tx); + await this.stageManager.updateStatus(task.stageId, StageOperationStatus.FAILED, tx); trace.getActiveSpan()?.addEvent('Stage set to FAILED', { stageId: task.stageId }); } @@ -534,7 +540,7 @@ export class TaskManager { * @param staleTasks - Array of stale task objects * @returns Object containing success and failure counts */ - private async updateStaleTasksStatus(staleTasks: Pick[]): Promise<{ + private async updateStaleTasksStatus(staleTasks: Pick[]): Promise<{ successCount: number; failureCount: number; }> { @@ -544,7 +550,7 @@ export class TaskManager { // Process tasks sequentially to avoid overwhelming the database for (const task of staleTasks) { try { - await this.updateStatus(task.id as TaskId, TaskOperationStatus.FAILED); + await this.updateStatus(task.id, TaskOperationStatus.FAILED); successCount++; this.logger.debug({ diff --git a/apps/jobnik-manager/src/tasks/models/models.ts b/apps/jobnik-manager/src/tasks/models/models.ts index c854d44..7ae62ee 100644 --- a/apps/jobnik-manager/src/tasks/models/models.ts +++ b/apps/jobnik-manager/src/tasks/models/models.ts @@ -1,10 +1,16 @@ -import type { components, operations } from 'jobnik-openapi'; +import type { components, operations, StageId, TaskId } from 'jobnik-openapi'; import type { Prisma } from '@prismaClient'; type TaskModel = components['schemas']['taskResponse']; type TaskCreateModel = components['schemas']['createTaskPayload']; type TasksFindCriteriaArg = operations['getTasksByCriteriaV1']['parameters']['query']; -type TaskPrismaObject = Prisma.TaskGetPayload; +/** + * TODO: `id`/`stageId` are re-branded here instead of at the Prisma schema level because + * prisma-json-types-generator throws on `@db.Uuid` string columns (breaks its `UuidFilter` + * handling and silently drops Json overrides for later models). Move this branding into + * schema.prisma once that upstream bug is fixed. + */ +type TaskPrismaObject = Omit, 'id' | 'stageId'> & { id: TaskId; stageId: StageId }; type TasksPaginatedResponse = components['schemas']['tasksPaginatedResponse']; type TasksByStageIdQuery = operations['getTasksByStageIdV1']['parameters']['query']; diff --git a/apps/jobnik-manager/tests/integration/tasks/helpers.ts b/apps/jobnik-manager/tests/integration/tasks/helpers.ts index f44efd2..76da563 100644 --- a/apps/jobnik-manager/tests/integration/tasks/helpers.ts +++ b/apps/jobnik-manager/tests/integration/tasks/helpers.ts @@ -8,8 +8,10 @@ import { DEFAULT_TRACEPARENT } from '@src/common/utils/tracingHelpers'; const persistedSnapshot = createActor(taskStateMachine).start().getPersistedSnapshot(); export const createTaskRecords = async (body: Prisma.TaskCreateManyInput[], prisma: PrismaClient): Promise => { + // TODO: cast here because Prisma has no concept of branded scalars (see TaskPrismaObject in + // src/tasks/models/models.ts for why); revisit once that's fixed upstream. const res = await prisma.task.createManyAndReturn({ data: body }); - return res; + return res as TaskPrismaObject[]; }; export const createTaskBody = { diff --git a/apps/jobnik-manager/tests/unit/generator.ts b/apps/jobnik-manager/tests/unit/generator.ts index e5cd4db..842c615 100644 --- a/apps/jobnik-manager/tests/unit/generator.ts +++ b/apps/jobnik-manager/tests/unit/generator.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; import { createActor } from 'xstate'; +import type { StageId, TaskId } from 'jobnik-openapi'; import type { Prisma, Stage, Task } from '@prismaClient'; import { JobOperationStatus, Priority, StageOperationStatus, TaskOperationStatus } from '@prismaClient'; import type { findAndLockTask } from '@src/db/prisma/generated/client/sql'; @@ -62,8 +63,8 @@ export const createStageEntity = (override: Partial): StageWithT export const createTaskEntity = (override: Partial): TaskPrismaObject => { const taskEntity = { data: {}, - stageId: faker.string.uuid(), - id: faker.string.uuid(), + stageId: faker.string.uuid() as StageId, + id: faker.string.uuid() as TaskId, status: TaskOperationStatus.CREATED, userMetadata: {}, attempts: 0, diff --git a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts index 9d2ceb8..f427d37 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -78,7 +78,7 @@ describe('JobManager', () => { }); it('should return array with single stage formatted object by criteria with related tasks', async function () { - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const taskEntity = createTaskEntity({ stageId }); const stageEntity = createStageEntity({ id: stageId, task: [taskEntity], type: 'SOME_STAGE_TYPE' }); prisma.stage.findMany.mockResolvedValue([stageEntity]); @@ -138,13 +138,13 @@ describe('JobManager', () => { }); it('should return stage object by provided id with related tasks', async function () { - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const taskEntity = createTaskEntity({ stageId }); const stageEntity = createStageEntity({ id: stageId, task: [taskEntity] }); prisma.stage.findUnique.mockResolvedValue(stageEntity); - const stage = await stageManager.getStageById(stageId as StageId); + const stage = await stageManager.getStageById(stageId); const { xstate, task, tracestate, ...rest } = stageEntity; @@ -188,7 +188,7 @@ describe('JobManager', () => { }); it('should return stage object by provided job id with related tasks', async function () { - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const taskEntity = createTaskEntity({ stageId }); const stageEntity = createStageEntity({ id: stageId, task: [taskEntity] }); diff --git a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts index 36f2f0b..90f6ee8 100644 --- a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts @@ -144,7 +144,7 @@ describe('JobManager', () => { const taskId = taskEntity.id; prisma.task.findUnique.mockResolvedValue(taskEntity); - const task = await taskManager.getTaskById(taskId as TaskId); + const task = await taskManager.getTaskById(taskId); const { creationTime, updateTime, xstate, startTime, endTime, ...rest } = taskEntity; const expectedTask = { ...rest, tracestate: undefined, creationTime: creationTime.toISOString(), updateTime: updateTime.toISOString() }; @@ -174,7 +174,7 @@ describe('JobManager', () => { describe('#HappyPath', () => { it('should return task object by provided stage id', async function () { const stageEntity = createStageEntity({}); - const taskEntity = createTaskEntity({ stageId: stageEntity.id }); + const taskEntity = createTaskEntity({ stageId: stageEntity.id as StageId }); prisma.stage.findUnique.mockResolvedValue(stageEntity); prisma.task.findMany.mockResolvedValue([taskEntity]); prisma.task.count.mockResolvedValue(1); @@ -213,7 +213,7 @@ describe('JobManager', () => { prisma.task.update.mockResolvedValue(taskEntity); - await expect(taskManager.updateUserMetadata(taskEntity.id as TaskId, { newData: 'test' })).toResolve(); + await expect(taskManager.updateUserMetadata(taskEntity.id, { newData: 'test' })).toResolve(); }); }); @@ -240,10 +240,10 @@ describe('JobManager', () => { describe('#HappyPath', () => { it("should add new tasks to existing stage's tasks", async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); - const taskEntity = createTaskEntity({ stageId: stageId, id: faker.string.uuid(), userMetadata: {} }); + const taskEntity = createTaskEntity({ stageId: stageId, id: faker.string.uuid() as TaskId, userMetadata: {} }); prisma.stage.findUnique.mockResolvedValue(stageEntity); prisma.job.findUnique.mockResolvedValue(jobEntity); @@ -265,7 +265,7 @@ describe('JobManager', () => { userMetadata: { someData: '123' }, } satisfies TaskCreateModel; - const tasksResponse = await taskManager.addTasks(stageId as StageId, [taskPayload]); + const tasksResponse = await taskManager.addTasks(stageId, [taskPayload]); // Extract unnecessary fields from the job object and assemble the expected result const { creationTime, updateTime, xstate, startTime, endTime, ...rest } = taskEntity; @@ -285,7 +285,7 @@ describe('JobManager', () => { it('should reject adding tasks to job with IN_PROGRESS stage', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const stageEntity = createStageEntity({ jobId, @@ -321,7 +321,7 @@ describe('JobManager', () => { describe('#SadPath', () => { it('should fail with a database error when adding tasks', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); @@ -348,13 +348,13 @@ describe('JobManager', () => { describe('#HappyPath', () => { it('should update task status by provided ID', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.IN_PROGRESS, xstate: inProgressStageXstatePersistentSnapshot, @@ -375,22 +375,24 @@ describe('JobManager', () => { return callback(mockTx); }); - vi.spyOn(stageManager, 'getStageEntityById').mockResolvedValue(stageEntity); + vi.spyOn(stageManager, 'getStageEntityById').mockResolvedValue( + stageEntity as unknown as Awaited> + ); vi.spyOn(stageManager, 'updateStatus').mockResolvedValue(undefined); vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.COMPLETED)).toResolve(); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.COMPLETED)).toResolve(); }); it('should update task status to RETRIED', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.IN_PROGRESS, xstate: inProgressStageXstatePersistentSnapshot, @@ -415,18 +417,18 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.FAILED)).toResolve(); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.FAILED)).toResolve(); }); it('should update task status to IN_PROGRESS and add startTime', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.PENDING, xstate: pendingStageXstatePersistentSnapshot, @@ -451,13 +453,13 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); }); it('should update task status to FAILED and add endTime', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId, status: JobOperationStatus.IN_PROGRESS, xstate: inProgressStageXstatePersistentSnapshot }); const stageEntity = createStageEntity({ @@ -467,7 +469,7 @@ describe('JobManager', () => { xstate: inProgressStageXstatePersistentSnapshot, }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.IN_PROGRESS, xstate: inProgressStageXstatePersistentSnapshot, @@ -498,18 +500,18 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.FAILED)).toResolve(); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.FAILED)).toResolve(); }); it('should update task status to IN_PROGRESS', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.PENDING, xstate: pendingStageXstatePersistentSnapshot, @@ -534,7 +536,7 @@ describe('JobManager', () => { vi.spyOn(stageManager, 'updateStageProgressFromTaskChanges').mockResolvedValue(undefined); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.IN_PROGRESS)).toResolve(); }); }); @@ -555,13 +557,13 @@ describe('JobManager', () => { it("should reject update invalid task's status [from IN_PROGRESS to CREATED]", async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.IN_PROGRESS, xstate: inProgressStageXstatePersistentSnapshot, @@ -577,7 +579,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.updateStatus(taskId as TaskId, TaskOperationStatus.CREATED)).rejects.toThrow(IllegalTaskStatusTransitionError); + await expect(taskManager.updateStatus(taskId, TaskOperationStatus.CREATED)).rejects.toThrow(IllegalTaskStatusTransitionError); }); }); @@ -602,13 +604,13 @@ describe('JobManager', () => { describe('#HappyPath', () => { it('should update task status by provided ID', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId, type: 'SOME_DEQUEUE_STAGE_TYPE' }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.PENDING, xstate: pendingStageXstatePersistentSnapshot, @@ -665,13 +667,13 @@ describe('JobManager', () => { it('should fail with bad race conditions (task already pulled)', async function () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); - const taskId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; + const taskId = faker.string.uuid() as TaskId; const jobEntity = createJobEntity({ id: jobId }); const stageEntity = createStageEntity({ jobId: jobEntity.id, id: stageId }); const taskEntity = createTaskEntity({ - stageId: stageEntity.id, + stageId: stageEntity.id as StageId, id: taskId, status: TaskOperationStatus.PENDING, xstate: pendingStageXstatePersistentSnapshot, @@ -700,8 +702,8 @@ describe('JobManager', () => { it('should successfully clean stale tasks and update them to FAILED status', async function () { prisma.task.findMany.mockResolvedValue([staleTaskOneHour, staleTaskFortyFiveMinutes]); const updateStatusMock = vi.spyOn(taskManager, 'updateStatus').mockResolvedValue({ - id: staleTaskOneHour.id as TaskId, - stageId: staleTaskOneHour.stageId as StageId, + id: staleTaskOneHour.id, + stageId: staleTaskOneHour.stageId, status: TaskOperationStatus.FAILED, attempts: 0, maxAttempts: 2, @@ -734,8 +736,8 @@ describe('JobManager', () => { const updateStatusMock = vi .spyOn(taskManager, 'updateStatus') .mockResolvedValueOnce({ - id: staleTaskOneHour.id as TaskId, - stageId: staleTaskOneHour.stageId as StageId, + id: staleTaskOneHour.id, + stageId: staleTaskOneHour.stageId, status: TaskOperationStatus.FAILED, attempts: 0, maxAttempts: 2, From 9044e0c7d67dd3f82822e56389e658278d066edd Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:35:42 +0300 Subject: [PATCH 11/16] refactor(jobnik-sdk): merge copy:openapi and copy:errors into one copy:generated step copySpec.mts and copyErrors.mts each did the same thing (copy a file resolved through jobnik-openapi's exports into src/, since jobnik-openapi is private and unpublished so jobnik-sdk must vendor these at build time). Collapse both into a single copyGenerated.mts and one prebuild step. Co-Authored-By: Claude Sonnet 5 --- packages/jobnik-sdk/package.json | 5 ++--- packages/jobnik-sdk/scripts/copyErrors.mts | 13 ------------- packages/jobnik-sdk/scripts/copyGenerated.mts | 18 ++++++++++++++++++ packages/jobnik-sdk/scripts/copySpec.mts | 10 ---------- packages/jobnik-sdk/scripts/tsconfig.json | 2 +- 5 files changed, 21 insertions(+), 27 deletions(-) delete mode 100644 packages/jobnik-sdk/scripts/copyErrors.mts create mode 100644 packages/jobnik-sdk/scripts/copyGenerated.mts delete mode 100644 packages/jobnik-sdk/scripts/copySpec.mts diff --git a/packages/jobnik-sdk/package.json b/packages/jobnik-sdk/package.json index e5c3aca..e9e0a12 100644 --- a/packages/jobnik-sdk/package.json +++ b/packages/jobnik-sdk/package.json @@ -19,12 +19,11 @@ "prelint": "npm run format", "lint": "eslint .", "lint:fix": "eslint --fix .", - "prebuild": "npm run clean && npm run copy:openapi && npm run copy:errors", + "prebuild": "npm run clean && npm run copy:generated", "build": "tsc --project tsconfig.build.json && copyfiles -f src/openapi3.yaml dist", "start": "npm run build && cd dist && node ./index.js", "clean": "rimraf dist", - "copy:openapi": "node scripts/copySpec.mts", - "copy:errors": "node scripts/copyErrors.mts", + "copy:generated": "node scripts/copyGenerated.mts", "prepack": "npm run build", "type-check": "tsc --noEmit -p tsconfig.lint.json", "check-pack": "publint && attw --pack . && node scripts/checkPack.mts" diff --git a/packages/jobnik-sdk/scripts/copyErrors.mts b/packages/jobnik-sdk/scripts/copyErrors.mts deleted file mode 100644 index 6ad4fc9..0000000 --- a/packages/jobnik-sdk/scripts/copyErrors.mts +++ /dev/null @@ -1,13 +0,0 @@ -import { copyFileSync, mkdirSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { errorsFilePath } from 'jobnik-openapi'; - -// Stages a local copy of the generated error classes/mapping, resolved through jobnik-openapi, -// next to the SDK's own source. The SDK ships this copy in its build output and imports it at -// runtime, so a physical file has to exist here — jobnik-openapi is private and never published, -// so the SDK can't depend on it at runtime once installed by a real consumer. -const destinationDir = fileURLToPath(new URL('../src/generated', import.meta.url)); -const destination = fileURLToPath(new URL('../src/generated/openapi-errors.ts', import.meta.url)); - -mkdirSync(destinationDir, { recursive: true }); -copyFileSync(errorsFilePath, destination); diff --git a/packages/jobnik-sdk/scripts/copyGenerated.mts b/packages/jobnik-sdk/scripts/copyGenerated.mts new file mode 100644 index 0000000..5c5ecea --- /dev/null +++ b/packages/jobnik-sdk/scripts/copyGenerated.mts @@ -0,0 +1,18 @@ +import { copyFileSync, mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { errorsFilePath, openapiFilePath } from 'jobnik-openapi'; + +// Stages local copies of artifacts resolved through jobnik-openapi, next to the SDK's own +// source. The SDK ships these copies in its build output and reads them at runtime — +// jobnik-openapi is private and never published, so the SDK can't depend on it at runtime +// once installed by a real consumer. +const files = [ + { from: openapiFilePath, to: fileURLToPath(new URL('../src/openapi3.yaml', import.meta.url)) }, + { from: errorsFilePath, to: fileURLToPath(new URL('../src/generated/openapi-errors.ts', import.meta.url)) }, +]; + +for (const { from, to } of files) { + mkdirSync(dirname(to), { recursive: true }); + copyFileSync(from, to); +} diff --git a/packages/jobnik-sdk/scripts/copySpec.mts b/packages/jobnik-sdk/scripts/copySpec.mts deleted file mode 100644 index 17e08a3..0000000 --- a/packages/jobnik-sdk/scripts/copySpec.mts +++ /dev/null @@ -1,10 +0,0 @@ -import { copyFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { openapiFilePath } from 'jobnik-openapi'; - -// Stages a local copy of the specification, resolved through jobnik-openapi, next to the -// SDK's own source. The SDK ships this copy in its build output and parses it at runtime, -// so a physical file has to exist here for both `build` and `test` to find it. -const destination = fileURLToPath(new URL('../src/openapi3.yaml', import.meta.url)); - -copyFileSync(openapiFilePath, destination); diff --git a/packages/jobnik-sdk/scripts/tsconfig.json b/packages/jobnik-sdk/scripts/tsconfig.json index 0938ccb..bd98909 100644 --- a/packages/jobnik-sdk/scripts/tsconfig.json +++ b/packages/jobnik-sdk/scripts/tsconfig.json @@ -4,5 +4,5 @@ "erasableSyntaxOnly": true, "lib": ["ESNext"] }, - "include": ["copySpec.mts", "copyErrors.mts", "checkPack.mts"] + "include": ["copyGenerated.mts", "checkPack.mts"] } From acd799019131864cbad0723eb26fadc062694a6f Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:42:20 +0300 Subject: [PATCH 12/16] fix(jobnik-sdk): mark generated openapi-errors import as unresolved in knip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/generated/openapi-errors.ts is staged by the copy:generated prebuild step and gitignored, so it doesn't exist on a fresh checkout — knip's static analysis (which doesn't run build scripts) flagged the import in sdkErrors.ts as unresolved, failing CI. Same pattern already used for jobnik-manager's generated ./instrumentation.mjs. Also drop the now-stale ignore entry for src/types/openapi.ts, removed when the SDK started consuming jobnik-openapi types directly. Co-Authored-By: Claude Sonnet 5 --- knip.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knip.json b/knip.json index b56d6d7..130055a 100644 --- a/knip.json +++ b/knip.json @@ -10,7 +10,7 @@ "packages/jobnik-openapi": {}, "packages/jobnik-sdk": { "entry": ["tests/**/*.test-d.ts"], - "ignore": ["src/types/openapi.ts"] + "ignoreUnresolved": ["../generated/openapi-errors"] }, "e2e": { "ignoreDependencies": ["jobnik-manager"] From 8ce5f3203610aa0f3ed780534bfb1d09e04c542f Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:21:21 +0300 Subject: [PATCH 13/16] refactor(jobnik-openapi): rename parameter in generateTypes transform function for clarity --- packages/jobnik-openapi/scripts/generateTypes.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jobnik-openapi/scripts/generateTypes.mts b/packages/jobnik-openapi/scripts/generateTypes.mts index 4b70863..36fa032 100644 --- a/packages/jobnik-openapi/scripts/generateTypes.mts +++ b/packages/jobnik-openapi/scripts/generateTypes.mts @@ -8,7 +8,7 @@ await generateTypes(OPENAPI_PATH, TYPES_DESTINATION_PATH, { addTypedRequestHandler: true, shouldFormat: true, inject: 'import type { JobId, StageId, TaskId } from "./brands";', - transform(schemaObject, metadata) { + transform(_schemaObject, metadata) { if (metadata.path === '#/components/schemas/taskId') { return factory.createTypeReferenceNode('TaskId', undefined); } From 1b3a06250e7a1814bc9b66c857cf974f8c0e0109 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:24:20 +0300 Subject: [PATCH 14/16] chore(global): knip fixes --- commitlint.config.js | 2 +- knip.json | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index b39cd40..d0c57fc 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -2,7 +2,7 @@ // discovered dynamically from pnpm-workspace.yaml so the allow-list can never drift from the // real workspaces. The release-please and dependency-bump commits get two extra scopes that // name no workspace. -const EXTRA_SCOPES = ['release', 'deps']; +const EXTRA_SCOPES = ['release', 'deps', 'global']; /** @type {import('@commitlint/types').UserConfig} */ module.exports = { diff --git a/knip.json b/knip.json index 130055a..9063d83 100644 --- a/knip.json +++ b/knip.json @@ -5,12 +5,11 @@ "entry": ["src/instrumentation.mts"], "ignore": ["src/db/prisma/generated/**"], "ignoreUnresolved": ["./instrumentation.mjs"], - "ignoreDependencies": ["@types/express-serve-static-core", "@types/multer", "@types/swagger-ui-express", "@prismaClient"] + "ignoreDependencies": ["@types/express-serve-static-core", "@types/multer", "@types/swagger-ui-express"] }, "packages/jobnik-openapi": {}, "packages/jobnik-sdk": { - "entry": ["tests/**/*.test-d.ts"], - "ignoreUnresolved": ["../generated/openapi-errors"] + "entry": ["tests/**/*.test-d.ts"] }, "e2e": { "ignoreDependencies": ["jobnik-manager"] From 4b8a2ddb1b85237701a943928fe2b1f945581f77 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:05:18 +0300 Subject: [PATCH 15/16] fix(jobnik-sdk): restore ignoreUnresolved for generated openapi-errors import 1b3a062 dropped this entry while cleaning up the unrelated @prismaClient ignoreDependencies hint, regressing the CI failure fixed in acd7990. src/generated/openapi-errors.ts is still only staged by the gitignored copy:generated prebuild step, so it still doesn't exist on a fresh checkout and knip's static analysis still can't resolve the import in sdkErrors.ts without this entry. Reproduced and verified locally by deleting the staged file before running knip. Co-Authored-By: Claude Sonnet 5 --- knip.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/knip.json b/knip.json index 9063d83..482ed1d 100644 --- a/knip.json +++ b/knip.json @@ -9,7 +9,8 @@ }, "packages/jobnik-openapi": {}, "packages/jobnik-sdk": { - "entry": ["tests/**/*.test-d.ts"] + "entry": ["tests/**/*.test-d.ts"], + "ignoreUnresolved": ["../generated/openapi-errors"] }, "e2e": { "ignoreDependencies": ["jobnik-manager"] From ac454922186eb298a3ff509b8f0ba18d060e7ac0 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:11:45 +0300 Subject: [PATCH 16/16] fix(jobnik-manager): restore @prismaClient in knip ignoreDependencies 1b3a062 dropped @prismaClient from ignoreDependencies based on a knip "unused config" hint, but that hint only held locally where the generated Prisma client was already built. Only src/db/prisma/generated/client/sql is committed to git (see apps/jobnik-manager/.gitignore); the rest of the client, including its index, is gitignored and generated at build time. On a fresh checkout knip's static analysis can't resolve @prismaClient and flags it as an unlisted dependency in ~35 files, failing CI. Verified by simulating a true fresh checkout (git checkout -- the generated dir, which restores only the tracked sql/ subfolder) and running knip against it: fails without this entry, passes with it. Co-Authored-By: Claude Sonnet 5 --- knip.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knip.json b/knip.json index 482ed1d..130055a 100644 --- a/knip.json +++ b/knip.json @@ -5,7 +5,7 @@ "entry": ["src/instrumentation.mts"], "ignore": ["src/db/prisma/generated/**"], "ignoreUnresolved": ["./instrumentation.mjs"], - "ignoreDependencies": ["@types/express-serve-static-core", "@types/multer", "@types/swagger-ui-express"] + "ignoreDependencies": ["@types/express-serve-static-core", "@types/multer", "@types/swagger-ui-express", "@prismaClient"] }, "packages/jobnik-openapi": {}, "packages/jobnik-sdk": {