diff --git a/.github/workflows/saas.yml b/.github/workflows/saas.yml index a624afc9..b525cce2 100644 --- a/.github/workflows/saas.yml +++ b/.github/workflows/saas.yml @@ -1,9 +1,6 @@ name: SaaS Integration Tests -on: - push: - branches: - - main +on: [push] jobs: integration: diff --git a/src/__tests__/tasklist/tasklist.integration.spec.ts b/src/__tests__/tasklist/tasklist.integration.spec.ts index 2963c34a..9a3a7935 100644 --- a/src/__tests__/tasklist/tasklist.integration.spec.ts +++ b/src/__tests__/tasklist/tasklist.integration.spec.ts @@ -56,13 +56,13 @@ describe('TasklistApiClient', () => { describe('Read operations', () => { it('can request all tasks', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getAllTasks() + const tasks = await tasklist.searchTasks({}) expect(tasks.length).toBeGreaterThan(0) }) it('can request a task with parameters', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getTasks({ + const tasks = await tasklist.searchTasks({ state: 'CREATED', }) expect(tasks.length).toBeGreaterThan(0) @@ -70,7 +70,7 @@ describe('TasklistApiClient', () => { it('gets all fields for a task', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getTasks({ + const tasks = await tasklist.searchTasks({ state: 'CREATED', }) expect(tasks.length).toBeGreaterThan(0) @@ -80,7 +80,7 @@ describe('TasklistApiClient', () => { it('can request a specific task', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getTasks({ + const tasks = await tasklist.searchTasks({ state: 'CREATED', }) const id = tasks[0].id @@ -101,7 +101,7 @@ describe('TasklistApiClient', () => { describe('Write operations', () => { it('can claim a task', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getTasks({ state: 'CREATED' }) + const tasks = await tasklist.searchTasks({ state: 'CREATED' }) const taskid = tasks[0].id expect(tasks.length).toBeGreaterThan(0) const claimTask = await tasklist.assignTask({ @@ -114,7 +114,7 @@ describe('TasklistApiClient', () => { it('will not allow a task to be claimed twice', async () => { const tasklist = new TasklistApiClient() - const tasks = await tasklist.getTasks({ state: 'CREATED' }) + const tasks = await tasklist.searchTasks({ state: 'CREATED' }) const task = await tasklist.assignTask({ taskId: tasks[0].id, assignee: 'jwulf', @@ -135,7 +135,7 @@ describe('TasklistApiClient', () => { it('can unclaim task', async () => { const tasklist = new TasklistApiClient(/*{ oauthProvider: oAuth }*/) - const tasks = await tasklist.getTasks({ state: 'CREATED' }) + const tasks = await tasklist.searchTasks({ state: 'CREATED' }) const taskId = tasks[0].id const task = await tasklist.assignTask({ taskId: taskId, @@ -166,7 +166,7 @@ describe('TasklistApiClient', () => { it('can complete a Task', async () => { const tasklist = new TasklistApiClient(/*{ oauthProvider: oAuth }*/) - const tasks = await tasklist.getTasks({ state: 'CREATED' }) + const tasks = await tasklist.searchTasks({ state: 'CREATED' }) const taskid = tasks[0].id expect(tasks.length).toBeGreaterThan(0) const completeTask = await tasklist.completeTask(taskid, { diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 93d437f8..e6a2d260 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -9,7 +9,12 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { LosslessNumber, parse, stringify } from 'lossless-json' +import { + LosslessNumber, + isLosslessNumber, + parse, + stringify, +} from 'lossless-json' import 'reflect-metadata' // Custom Decorators @@ -64,9 +69,15 @@ export function parseWithAnnotations( } else { // Existing logic for int32 and int64... if (Reflect.hasMetadata('type:int32', dto.prototype, key)) { - instance[key] = (value as LosslessNumber).valueOf() // Assuming value is already the correct type + instance[key] = + value && isLosslessNumber(value) + ? (value as LosslessNumber).valueOf() + : value // Assuming value is already the correct type } else if (Reflect.hasMetadata('type:int64', dto.prototype, key)) { - instance[key] = (value as LosslessNumber).toString() // Assuming value is string + instance[key] = + value && isLosslessNumber(value) + ? (value as LosslessNumber).toString() + : value // Assuming value is string } else { instance[key] = value // Assign directly for other types } diff --git a/src/tasklist/index.ts b/src/tasklist/index.ts index 4a0f90b3..7087b9f0 100644 --- a/src/tasklist/index.ts +++ b/src/tasklist/index.ts @@ -1,3 +1,3 @@ export { TasklistApiClient } from './lib/TasklistApiClient' -export * as TasklistDto from './lib/Types' +export * as TasklistDto from './lib/TasklistDto' diff --git a/src/tasklist/lib/TasklistApiClient.ts b/src/tasklist/lib/TasklistApiClient.ts index d6d54dda..f0331f8c 100644 --- a/src/tasklist/lib/TasklistApiClient.ts +++ b/src/tasklist/lib/TasklistApiClient.ts @@ -8,11 +8,25 @@ import { RequireConfiguration, constructOAuthProvider, packageVersion, + parseArrayWithAnnotations, + parseWithAnnotations, } from 'lib' import { IOAuthProvider } from '../../oauth' -import { Form, Task, TaskQuery, Variable } from './Types' +import { + DateFilter, + Form, + TaskResponse, + TaskSearchAfterOrEqualRequest, + TaskSearchAfterRequest, + TaskSearchBeforeOrEqualRequest, + TaskSearchBeforeRequest, + TaskSearchRequestBase, + TaskSearchResponse, + Variable, + VariableSearchResponse, +} from './TasklistDto' import { JSONDoc, encodeTaskVariablesForAPIRequest } from './utils' const trace = debug('tasklist:rest') @@ -76,14 +90,38 @@ export class TasklistApiClient { accept: '*/*', } } + + private replaceDatesWithString< + T extends { followUpDate?: DateFilter; dueDate?: DateFilter }, + >(query: T) { + if (query.followUpDate) { + if (typeof query.followUpDate.from === 'object') { + query.followUpDate.from = query.followUpDate.from.toISOString() + } + if (typeof query.followUpDate.to === 'object') { + query.followUpDate.to = query.followUpDate.to.toISOString() + } + } + if (query.dueDate) { + if (typeof query.dueDate.from === 'object') { + query.dueDate.from = query.dueDate.from.toISOString() + } + if (typeof query.dueDate.to === 'object') { + query.dueDate.to = query.dueDate.to.toISOString() + } + } + return query + } + /** * @description Query Tasklist for a list of tasks. See the [API documentation](https://docs.camunda.io/docs/apis-clients/tasklist-api/queries/tasks/). + * @throws Status 400 - An error is returned when more than one search parameters among [`searchAfter`, `searchAfterOrEqual`, `searchBefore`, `searchBeforeOrEqual`] are present in request * @example * ``` * const tasklist = new TasklistApiClient() * * async function getTasks() { - * const res = await tasklist.getTasks({ + * const res = await tasklist.searchTasks({ * state: TaskState.CREATED * }) * console.log(res ? 'Nothing' : JSON.stringify(res, null, 2)) @@ -92,28 +130,34 @@ export class TasklistApiClient { * ``` * @param query */ - public async getTasks(query: Partial): Promise { + public async searchTasks( + query: + | TaskSearchAfterRequest + | TaskSearchAfterOrEqualRequest + | TaskSearchBeforeRequest + | TaskSearchBeforeOrEqualRequest + | Partial + ): Promise { const headers = await this.getHeaders() const url = 'tasks/search' + trace(`Requesting ${url}`) return this.rest .post(url, { - json: query, + json: this.replaceDatesWithString(query), headers, + parseJson: (text) => + parseArrayWithAnnotations(text, TaskSearchResponse), }) .json() } - public async getAllTasks(): Promise { - return this.getTasks({}) - } - /** * @description Return a task by id, or throw if not found. * @throws Will throw if no task of the given taskId exists * @returns */ - public async getTask(taskId: string): Promise { + public async getTask(taskId: string): Promise { const headers = await this.getHeaders() return this.rest .get(`tasks/${taskId}`, { @@ -127,33 +171,42 @@ export class TasklistApiClient { */ public async getForm( formId: string, - processDefinitionKey: string + processDefinitionKey: string, + version?: string | number ): Promise
{ const headers = await this.getHeaders() return this.rest .get(`forms/${formId}`, { searchParams: { processDefinitionKey, + version, }, + parseJson: (text) => parseWithAnnotations(text, Form), headers, }) .json() } /** - * @description Returns a list of variables - * @param taskId - * @param variableNames - * @throws Throws 404 if no task of taskId is found + * @description This method returns a list of task variables for the specified taskId and variableNames. If the variableNames parameter is empty, all variables associated with the task will be returned. + * @throws Status 404 - An error is returned when the task with the taskId is not found. */ - public async getVariables( - taskId: string, + public async getVariables({ + taskId, + variableNames, + includeVariables, + }: { + taskId: string variableNames?: string[] - ): Promise { + includeVariables?: { name: string; alwaysReturnFullValue: boolean }[] + }): Promise { const headers = await this.getHeaders() return this.rest .post(`tasks/${taskId}/variables/search`, { - body: JSON.stringify(variableNames || []), + body: JSON.stringify({ + variableNames: variableNames || [], + includeVariables: includeVariables || {}, + }), headers, }) .json() @@ -174,7 +227,10 @@ export class TasklistApiClient { /** * @description Assign a task with taskId to assignee or the active user. - * @throws 400 - task not active, or already assigned. 403 - no permission to reassign task. 404 - no task for taskId. + * @throws Status 400 - An error is returned when the task is not active (not in the CREATED state). + * @throws Status 400 - An error is returned when task was already assigned, except the case when JWT authentication token used and allowOverrideAssignment = true. + * @throws Status 403 - An error is returned when user doesn't have the permission to assign another user to this task. + * @throws Status 404 - An error is returned when the task with the taskId is not found. */ public async assignTask({ taskId, @@ -184,7 +240,7 @@ export class TasklistApiClient { taskId: string assignee?: string allowOverrideAssignment?: boolean - }): Promise { + }): Promise { const headers = await this.getHeaders() return this.rest .patch(`tasks/${taskId}/assign`, { @@ -193,19 +249,23 @@ export class TasklistApiClient { allowOverrideAssignment, }), headers, + parseJson: (text) => parseWithAnnotations(text, TaskResponse), }) .json() } /** * @description Complete a task with taskId and optional variables - * @param taskId - * @param variables + * @throws Status 400 An error is returned when the task is not active (not in the CREATED state). + * @throws Status 400 An error is returned if the task was not claimed (assigned) before. + * @throws Status 400 An error is returned if the task is not assigned to the current user. + * @throws Status 403 User has no permission to access the task (Self-managed only). + * @throws Status 404 An error is returned when the task with the taskId is not found. */ public async completeTask( taskId: string, variables?: JSONDoc - ): Promise { + ): Promise { const headers = await this.getHeaders() return this.rest .patch(`tasks/${taskId}/complete`, { @@ -213,16 +273,24 @@ export class TasklistApiClient { body: JSON.stringify({ variables: encodeTaskVariablesForAPIRequest(variables || {}), }), + parseJson: (text) => parseWithAnnotations(text, TaskResponse), }) .json() } /** - * @description Unassign a task with taskI - * @param taskId + * @description Unassign a task with taskId + * @throws Status 400 An error is returned when the task is not active (not in the CREATED state). + * @throws Status 400 An error is returned if the task was not claimed (assigned) before. + * @throws Status 404 An error is returned when the task with the taskId is not found. */ - public async unassignTask(taskId: string): Promise { + public async unassignTask(taskId: string): Promise { const headers = await this.getHeaders() - return this.rest.patch(`tasks/${taskId}/unassign`, { headers }).json() + return this.rest + .patch(`tasks/${taskId}/unassign`, { + headers, + parseJson: (text) => parseWithAnnotations(text, TaskResponse), + }) + .json() } } diff --git a/src/tasklist/lib/TasklistDto.ts b/src/tasklist/lib/TasklistDto.ts new file mode 100644 index 00000000..65e909e3 --- /dev/null +++ b/src/tasklist/lib/TasklistDto.ts @@ -0,0 +1,226 @@ +import { Int64, LosslessDto } from 'lib' + +export type TaskState = 'COMPLETED' | 'CREATED' | 'CANCELED' | 'FAILED' + +export interface DraftSearchVariableValue { + /* The value of the variable. */ + value: string + /* Does the previewValue contain the truncated value or full value? */ + isValueTruncated: boolean + /* A preview of the variable's value. Limited in size. */ + previewValue: string +} + +export interface VariableSearchResponseWithoutDraft { + /* The unique identifier of the variable. */ + id: string + /* The name of the variable. */ + name: string + /* The value of the variable. */ + value: string + /* A preview of the variable's value. Limited in size. */ + previewValue: string +} + +export interface VariableSearchResponse + extends VariableSearchResponseWithoutDraft { + draft: DraftSearchVariableValue +} + +export interface Variable { + /* The unique identifier of the variable. */ + id: string + /* The name of the variable. */ + name: string + /* The value of the variable. */ + value: string + tenantId: string + draft: DraftSearchVariableValue +} + +export interface DateFilter { + /* Start date range to search from. Pass a Date object, or string date-time format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard. */ + from: Date | string + /* End date range to search to. Pass a Date object, or string date-time format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard. */ + to: Date | string +} + +export interface TaskByVariables { + /* The name of the variable. */ + name: string + /* The value of the variable. When specifying the variable value, it's crucial to maintain consistency with JSON values (serialization for the complex objects such as list) and ensure that strings remain appropriately formatted. */ + value: string + /* The comparison operator to use for the variable. */ + operator: 'eq' +} + +interface TaskOrderBy { + field: 'completionTime' | 'creationTime' | 'followUpDate' | 'dueDate' + order: 'ASC' | 'DESC' +} + +interface IncludeVariable { + /* The name of the variable. */ + name: string + /* Always return the full value of the variable? Default: false. */ + alwaysReturnFullValue?: boolean +} + +export interface TaskSearchRequestBase { + /* The state of the tasks. */ + state: TaskState + /* Are the tasks assigned? */ + assigned: boolean + /* Who is assigned to the tasks? */ + assignee: string + /* The assignee is one of the given assignees. */ + assignees: string[] + /* What's the BPMN flow node? */ + taskDefinitionId: string + /* Given group is in candidate groups list. */ + candidateGroup: string + /* At least one of the given groups is in candidate groups list. */ + candidateGroups: string[] + /* Given user is in candidate user list. */ + candidateUser: string + /* At least one of the given users is in candidate user list. */ + candidateUsers: string[] + /* Reference to process definition. */ + processDefinitionKey: string + /* Reference to process instance. */ + processInstanceKey: string + /* Size of tasks page (default = 50). */ + pageSize: number + /* A range of followup dates for the tasks to search for. */ + followUpDate: DateFilter + /* A range of due dates for the tasks to search for. */ + dueDate: DateFilter + /** An array of filter clauses specifying the variables to filter for. + * If defined, the query returns only tasks to which all clauses apply. + * However, it's important to note that this filtering mechanism is + * designed to work exclusively with truncated variables. This means + * variables of a larger size are not compatible with this filter, and + * attempts to use them may result in inaccurate or incomplete query results. + */ + taskVariables: TaskByVariables[] + /* An array of Tenant IDs to filter tasks. When multi-tenancy is enabled, tasks associated with the specified tenant IDs are returned; if disabled, this parameter is ignored. */ + tenantIds: string[] + /* An array of objects specifying the fields to sort the results by. */ + sort: TaskOrderBy[] + /** + * An array used to specify a list of variable names that should be included in the response when querying tasks. + * This field allows users to selectively retrieve specific variables associated with the tasks returned in the search results. + */ + includeVariables: IncludeVariable[] + implementation: 'JOB_WORKER' | 'ZEEBE_USER_TASK' +} + +export interface TaskSearchAfterRequest extends Partial { + /** + * Used to return a paginated result. Array of values that should be copied from sortValues of one of the tasks from the current search results page. + * It enables the API to return a page of tasks that directly follow the task identified by the provided values, with respect to the sorting order. + */ + searchAfter: string[] +} + +export interface TaskSearchAfterOrEqualRequest + extends Partial { + /** + * Used to return a paginated result. Array of values that should be copied from sortValues of one of the tasks from the current search results page. + * It enables the API to return a page of tasks that directly follow or are equal to the task identified by the provided values, with respect to the sorting order. + */ + searchAfterOrEqual: string[] +} + +export interface TaskSearchBeforeRequest + extends Partial { + /** + * Used to return a paginated result. Array of values that should be copied from sortValues of one of the tasks from the current search results page. + * It enables the API to return a page of tasks that directly precede the task identified by the provided values, with respect to the sorting order. + */ + searchBefore: string[] +} + +export interface TaskSearchBeforeOrEqualRequest + extends Partial { + /** + * Used to return a paginated result. Array of values that should be copied from sortValues of one of the tasks from the current search results page. + * It enables the API to return a page of tasks that directly precede or are equal to the task identified by the provided values, with respect to the sorting order. + */ + searchBeforeOrEqual: string[] +} + +export class TaskResponse extends LosslessDto { + /* The unique identifier of the task. */ + id!: string + /* The name of the task. */ + name!: string + /* User Task ID from the BPMN definition. */ + taskDefinitionId!: string + /* The name of the process. */ + processName!: string + /* When was the task created (renamed equivalent of Task.creationTime field). */ + creationDate!: string + /* When was the task completed (renamed equivalent of Task.completionTime field). */ + completionDate!: string + /* The username/id of who is assigned to the task. */ + assignee!: string + /* The state of the task. */ + taskState!: 'COMPLETED' | 'CREATED' | 'CANCELED' | 'FAILED' + /* Reference to the task form. */ + formKey!: string + /* Reference to the ID of a deployed form. If the form is not deployed, this property is null. */ + formId!: string + /* Reference to the version of a deployed form. If the form is not deployed, this property is null. */ + @Int64 + formVersion?: string + /* Is the form embedded for this task? If there is no form, this property is null. */ + isFormEmbedded?: boolean + /* Reference to process definition (renamed equivalent of Task.processDefinitionId field). */ + processDefinitionKey!: string + /* Reference to process instance id (renamed equivalent of Task.processInstanceId field). */ + processInstanceKey!: string + /* The tenant ID associated with the task. */ + tenantId!: string + /* The due date for the task. ($date-time) */ + dueDate!: string + /* The follow-up date for the task. ($date-time) */ + followUpDate!: string + /* The candidate groups for the task. */ + candidateGroups!: string[] + /* The candidate users for the task. */ + candidateUsers!: string[] + implementation!: 'JOB_WORKER' | 'ZEEBE_USER_TASK' +} + +export class TaskSearchResponse extends TaskResponse { + /* Array of values to be copied into TaskSearchRequest to request for next or previous page of tasks. */ + sortValues!: string[] + /* A flag to show that the task is first in the current filter. */ + isFirst!: boolean + /* An array of the task's variables. Only variables specified in TaskSearchRequest.includeVariables are returned. Note that a variable's draft value is not returned in TaskSearchResponse. */ + variables!: VariableSearchResponseWithoutDraft[] +} + +export interface VariableInput { + name: string + value: string +} + +export interface User { + userId: string + displayName: string + permissions: string[] + roles: string[] + salesPlanType: string +} + +export class Form extends LosslessDto { + id!: string + processDefinitionId!: string + schema!: string + @Int64 + version!: string + tenantId!: string + isDeleted!: boolean +} diff --git a/src/tasklist/lib/Types.ts b/src/tasklist/lib/Types.ts deleted file mode 100644 index 1a30ee5d..00000000 --- a/src/tasklist/lib/Types.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { JSONDoc } from './utils' - -export type TaskState = 'COMPLETED' | 'CREATED' | 'CANCELED' - -export interface Variable { - id: string - name: string - value: string - previewValue: string - isValueTruncated: boolean -} - -export interface TaskQuery { - state: TaskState - assigned: boolean - assignee: string - candidateGroup: string - pageSize: number - taskDefinitionId: string - searchAfter: string[] - searchAfterOrEqual: string[] - searchBefore: string[] - searchBeforeOrEqual: string[] -} - -interface TaskBase { - id: string - name: string - taskDefinitionId: string - processName: string - creationTime: string - completionTime: string - assignee: string - taskState: 'COMPLETED' | 'CREATED' | 'CANCELED' - sortValues: [string] - isFirst: boolean - formKey: string - processDefinitionId: string - processInstanceId: string - candidateGroups: string[] -} - -export interface Task extends TaskBase { - variables: Variable[] -} - -export interface TaskWithVariables extends TaskBase { - variables: T -} - -export type TaskFields = Partial[] - -export interface VariableInput { - name: string - value: string -} - -export interface User { - userId: string - displayName: string - permissions: string[] - roles: string[] - salesPlanType: string -} - -export interface Form { - id: string - processDefinitionId: string - schema: string -}