Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tasklist): add multitenant support to tasklist #85

Merged
merged 2 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/saas.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: SaaS Integration Tests

on:
push:
branches:
- main
on: [push]

jobs:
integration:
Expand Down
16 changes: 8 additions & 8 deletions src/__tests__/tasklist/tasklist.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ 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)
})

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)
Expand All @@ -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
Expand All @@ -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({
Expand All @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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, {
Expand Down
17 changes: 14 additions & 3 deletions src/lib/LosslessJsonParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,9 +69,15 @@ export function parseWithAnnotations<T>(
} 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
}
Expand Down
2 changes: 1 addition & 1 deletion src/tasklist/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { TasklistApiClient } from './lib/TasklistApiClient'

export * as TasklistDto from './lib/Types'
export * as TasklistDto from './lib/TasklistDto'
122 changes: 95 additions & 27 deletions src/tasklist/lib/TasklistApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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))
Expand All @@ -92,28 +130,34 @@ export class TasklistApiClient {
* ```
* @param query
*/
public async getTasks(query: Partial<TaskQuery>): Promise<Task[]> {
public async searchTasks(
query:
| TaskSearchAfterRequest
| TaskSearchAfterOrEqualRequest
| TaskSearchBeforeRequest
| TaskSearchBeforeOrEqualRequest
| Partial<TaskSearchRequestBase>
): Promise<TaskSearchResponse[]> {
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<Task[]> {
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<Task> {
public async getTask(taskId: string): Promise<TaskResponse> {
const headers = await this.getHeaders()
return this.rest
.get(`tasks/${taskId}`, {
Expand All @@ -127,33 +171,42 @@ export class TasklistApiClient {
*/
public async getForm(
formId: string,
processDefinitionKey: string
processDefinitionKey: string,
version?: string | number
): Promise<Form> {
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<Variable[]> {
includeVariables?: { name: string; alwaysReturnFullValue: boolean }[]
}): Promise<VariableSearchResponse[]> {
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()
Expand All @@ -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,
Expand All @@ -184,7 +240,7 @@ export class TasklistApiClient {
taskId: string
assignee?: string
allowOverrideAssignment?: boolean
}): Promise<Task> {
}): Promise<TaskResponse> {
const headers = await this.getHeaders()
return this.rest
.patch(`tasks/${taskId}/assign`, {
Expand All @@ -193,36 +249,48 @@ 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<Task> {
): Promise<TaskResponse> {
const headers = await this.getHeaders()
return this.rest
.patch(`tasks/${taskId}/complete`, {
headers,
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<Task> {
public async unassignTask(taskId: string): Promise<TaskResponse> {
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()
}
}
Loading
Loading