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/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 3486586..aac83b0 100644 --- a/apps/jobnik-manager/src/jobs/models/manager.ts +++ b/apps/jobnik-manager/src/jobs/models/manager.ts @@ -5,6 +5,8 @@ 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 { IllegalJobStatusTransitionError, JobNotInFiniteStateError, JobNotFoundError } from 'jobnik-openapi'; import type { PrismaClient, Priority } from '@prismaClient'; import { Prisma, JobOperationStatus } from '@prismaClient'; import { SERVICES } from '@common/constants'; @@ -12,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'; @@ -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) { @@ -279,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/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 ae087a2..acb16b3 100644 --- a/apps/jobnik-manager/src/stages/models/manager.ts +++ b/apps/jobnik-manager/src/stages/models/manager.ts @@ -4,6 +4,8 @@ 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, 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'; @@ -12,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'; @@ -37,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( @@ -55,7 +65,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, @@ -152,7 +162,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, @@ -172,7 +182,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, @@ -199,7 +209,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, }); @@ -212,7 +222,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, }); @@ -237,7 +247,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, @@ -265,7 +275,7 @@ export class StageManager { */ @withSpanAsyncV4 public async getStageEntityById( - stageId: string, + stageId: StageId, options: T = {} as T ): Promise> { trace.getActiveSpan()?.setAttributes({ @@ -294,7 +304,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, @@ -319,13 +329,15 @@ 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) { throw new StageNotFoundError(stagesErrorMessages.stageNotFound); } + const jobId = stage.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) @@ -384,38 +396,40 @@ 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, StageOperationStatus.PENDING, tx); 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(jobId, tx); if (completedStages === totalStages) { - await this.jobManager.updateStatus(stage.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, 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, 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 @@ -462,7 +476,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 +500,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/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/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 e0fcd37..68eefeb 100644 --- a/apps/jobnik-manager/src/tasks/models/manager.ts +++ b/apps/jobnik-manager/src/tasks/models/manager.ts @@ -5,7 +5,15 @@ 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 { Prisma, StageOperationStatus, Task, TaskOperationStatus, type PrismaClient } from '@prismaClient'; +import type { StageId, TaskId } from 'jobnik-openapi'; +import { + NotAllowedToAddTasksToInProgressStageError, + StageInFiniteStateError, + StageNotFoundError, + TaskNotFoundError, + TaskStatusUpdateFailedError, +} from 'jobnik-openapi'; +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'; @@ -16,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'; @@ -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, @@ -139,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, @@ -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, @@ -175,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, @@ -200,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, @@ -247,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, }); @@ -259,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; } /** @@ -279,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: { @@ -291,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' }); @@ -352,7 +357,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) { @@ -428,7 +433,9 @@ export class TaskManager { 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({ @@ -510,12 +517,12 @@ 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 }; } private async updateStageSummary( - stageId: string, + stageId: StageId, previousStatus: TaskOperationStatus, nextStatus: TaskOperationStatus, tx: PrismaTransaction @@ -533,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; }> { 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/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/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/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/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/integration/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/integration/tasks/tasks.spec.ts index 12d91c5..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 } 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); @@ -969,7 +975,7 @@ describe('task', function () { ); const secondStage = await requestSender.addStageV1({ - pathParams: { jobId: stage.jobId }, + pathParams: { jobId: stage.jobId as JobId }, requestBody: { type: 'Second Stage', data: {}, @@ -983,14 +989,14 @@ 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 } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId as JobId } }); expect(updateStatusResponse).toSatisfyApiSpec(); expect(getTaskResponse.body).toMatchObject(expectedTaskStatus); @@ -1017,13 +1023,13 @@ 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 getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: stage.jobId } }); + 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(); expect(getTaskResponse.body).toMatchObject(expectedTaskStatus); @@ -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,8 +1244,8 @@ describe('task', function () { pathParams: { stageType: 'SOME_TEST_TYPE_3' }, }); - const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId } }); - const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + const getStageResponse = await requestSender.getStageByIdV1({ pathParams: { stageId: stageId as StageId } }); + const getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId: jobId as JobId } }); expect(dequeueResponse).toSatisfyApiSpec(); expect(dequeueResponse).toMatchObject({ @@ -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,9 +1600,9 @@ 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 getJobResponse = await requestSender.getJobByIdV1({ pathParams: { jobId } }); + 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(); expect(dequeueResponse).toMatchObject({ @@ -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/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/jobs/jobs.spec.ts b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts index 961ffdd..2aa2790 100644 --- a/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts +++ b/apps/jobnik-manager/tests/unit/jobs/jobs.spec.ts @@ -3,6 +3,7 @@ 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'; @@ -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/repository.spec.ts b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts index 730f852..4abe80c 100644 --- a/apps/jobnik-manager/tests/unit/stages/repository.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/repository.spec.ts @@ -2,6 +2,7 @@ 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'; @@ -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 24a4000..f427d37 100644 --- a/apps/jobnik-manager/tests/unit/stages/stages.spec.ts +++ b/apps/jobnik-manager/tests/unit/stages/stages.spec.ts @@ -5,6 +5,8 @@ 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 { JobInFiniteStateError } from 'jobnik-openapi'; import type { PrismaClient } from '@prismaClient'; import { Prisma, StageOperationStatus, JobOperationStatus } from '@prismaClient'; import { StageManager } from '@src/stages/models/manager'; @@ -17,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, @@ -77,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]); @@ -126,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; @@ -137,7 +138,7 @@ 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] }); @@ -158,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); }); }); @@ -166,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'); }); }); }); @@ -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]; @@ -187,14 +188,14 @@ 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] }); 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'); }); }); }); @@ -248,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({}); }); @@ -258,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); }); }); @@ -266,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'); }); }); }); @@ -278,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); }); @@ -288,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 + ); }); }); @@ -296,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'); }); }); }); @@ -327,7 +330,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 +363,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 +395,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 +427,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 +476,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 +494,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 +513,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'); }); }); }); @@ -532,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 () { @@ -556,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 () { @@ -606,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 () { @@ -645,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 () { @@ -691,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 () { @@ -721,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(); }); }); @@ -737,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 () { @@ -762,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 () { @@ -776,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) ); }); @@ -794,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'); }); }); }); @@ -828,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 () { @@ -865,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(); }); }); }); diff --git a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts index 9f707d2..90f6ee8 100644 --- a/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts +++ b/apps/jobnik-manager/tests/unit/tasks/tasks.spec.ts @@ -5,6 +5,8 @@ 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 { 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'; @@ -17,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'; @@ -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'); }); }); }); @@ -173,12 +174,12 @@ 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); - 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'); }); }); }); @@ -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'); }); }); }); @@ -237,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); @@ -277,12 +280,12 @@ 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 () { const jobId = faker.string.uuid(); - const stageId = faker.string.uuid(); + const stageId = faker.string.uuid() as StageId; const stageEntity = createStageEntity({ jobId, @@ -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) ); }); @@ -318,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 }); @@ -336,7 +339,7 @@ describe('JobManager', () => { return callback(mockTx); }); - await expect(taskManager.addTasks(jobEntity.id, [])).rejects.toThrow('db connection error'); + await expect(taskManager.addTasks(stageEntity.id as StageId, [])).rejects.toThrow('db connection error'); }); }); }); @@ -345,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, @@ -372,7 +375,9 @@ 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); @@ -381,13 +386,13 @@ describe('JobManager', () => { 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, @@ -417,13 +422,13 @@ describe('JobManager', () => { 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, @@ -453,8 +458,8 @@ describe('JobManager', () => { 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({ @@ -464,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, @@ -500,13 +505,13 @@ describe('JobManager', () => { 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, @@ -547,18 +552,18 @@ 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 () { 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, @@ -590,7 +595,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'); }); }); }); @@ -599,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, @@ -662,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, 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 3b4dd89..130055a 100644 --- a/knip.json +++ b/knip.json @@ -3,14 +3,14 @@ "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"] }, "packages/jobnik-openapi": {}, "packages/jobnik-sdk": { "entry": ["tests/**/*.test-d.ts"], - "ignore": ["src/types/openapi.ts"] + "ignoreUnresolved": ["../generated/openapi-errors"] }, "e2e": { "ignoreDependencies": ["jobnik-manager"] 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/packages/jobnik-openapi/scripts/generateTypes.mts b/packages/jobnik-openapi/scripts/generateTypes.mts index 3cc93b0..36fa032 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-sdk/src/types/brands.ts b/packages/jobnik-openapi/src/brands.ts similarity index 91% rename from packages/jobnik-sdk/src/types/brands.ts rename to packages/jobnik-openapi/src/brands.ts index 7d36021..30882cd 100644 --- a/packages/jobnik-sdk/src/types/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/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 c78af31..4069a1b 100644 --- a/packages/jobnik-openapi/src/index.ts +++ b/packages/jobnik-openapi/src/index.ts @@ -1,7 +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-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 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 1efbfac..e9e0a12 100644 --- a/packages/jobnik-sdk/package.json +++ b/packages/jobnik-sdk/package.json @@ -19,13 +19,11 @@ "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: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", - "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", + "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" @@ -53,7 +51,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/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/openapi.mts b/packages/jobnik-sdk/scripts/openapi.mts deleted file mode 100644 index 48016a4..0000000 --- a/packages/jobnik-sdk/scripts/openapi.mts +++ /dev/null @@ -1,26 +0,0 @@ -import { generateErrors, generateTypes } from '@map-colonies/openapi-helpers/generators'; -import { factory } from 'typescript'; -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/scripts/tsconfig.json b/packages/jobnik-sdk/scripts/tsconfig.json index dd58a29..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": ["openapi.mts", "copySpec.mts", "checkPack.mts"] + "include": ["copyGenerated.mts", "checkPack.mts"] } diff --git a/packages/jobnik-sdk/src/clients/consumer.ts b/packages/jobnik-sdk/src/clients/consumer.ts index 1b325b8..4f315c1 100644 --- a/packages/jobnik-sdk/src/clients/consumer.ts +++ b/packages/jobnik-sdk/src/clients/consumer.ts @@ -1,11 +1,10 @@ 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 { TaskId, components } from 'jobnik-openapi'; import type { ApiClient } from '../api'; -import type { TaskId } from '../types/brands'; 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..a0d0551 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 { components, JobId, StageId } from 'jobnik-openapi'; import type { ApiClient } from '../api'; -import type { components } from '../types/openapi'; -import type { JobId, StageId } from '../types/brands'; 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..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 '../types/brands'; 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/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/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/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..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 './brands'; 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..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 '../src/types/brands'; /* 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..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 '../../src/types/brands'; 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..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 '../../src/types/brands'; 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..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 '../../src/types/brands'; 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..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 '../../src/types/brands'; 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..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 '../../src/types/brands'; import type { NoopLogger } from '../../src/telemetry/noopLogger'; import type { TaskHandler, WorkerOptions } from '../../src/types/worker'; import type { IProducer } from '../../src/types/producer'; 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 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": {