Skip to content
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
18 changes: 11 additions & 7 deletions src/app/api/tasks/public/public.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { subtaskTemplateBatchSize } from '@/constants/tasks'
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
import { runInBatches } from '@/utils/array'
import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNotifications } from '@/jobs/notifications'
import { CreateTaskRequest, UpdateTaskRequest, Associations, AssociationsSchema } from '@/types/dto/tasks.dto'
import { DISPATCHABLE_EVENT } from '@/types/webhook'
Expand Down Expand Up @@ -270,13 +272,15 @@ export class PublicTasksService extends TasksSharedService {
}

if (template.subTaskTemplates.length) {
await Promise.all(
template.subTaskTemplates.map(async (sub, index) => {
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp)
}),
)
await runInBatches(template.subTaskTemplates, subtaskTemplateBatchSize, async (sub, index) => {
const manualTimestamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
await this.createSubtasksFromTemplate({
subTemplateId: sub.id,
parentTask: newTask,
manualTimestamp,
templateService,
})
})
Comment thread
arpandhakal marked this conversation as resolved.
}
}

Expand Down
18 changes: 11 additions & 7 deletions src/app/api/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ClientResponse, CompanyResponse, InternalUsers } from '@/types/common'
import { TaskWithWorkflowState } from '@/types/db'
import { AncestorTaskResponse, CreateTaskRequest, UpdateTaskRequest, Associations } from '@/types/dto/tasks.dto'
import { DISPATCHABLE_EVENT } from '@/types/webhook'
import { runInBatches } from '@/utils/array'
import { subtaskTemplateBatchSize } from '@/constants/tasks'
import { UserIdsType } from '@/utils/assignee'
import { isPastDateString } from '@/utils/dateHelper'
import { getIdsFromLtreePath } from '@/utils/ltree'
Expand Down Expand Up @@ -251,13 +253,15 @@ export class TasksService extends TasksSharedService {
}

if (template.subTaskTemplates.length) {
await Promise.all(
template.subTaskTemplates.map(async (sub, index) => {
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp)
}),
)
await runInBatches(template.subTaskTemplates, subtaskTemplateBatchSize, async (sub, index) => {
const manualTimestamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
await this.createSubtasksFromTemplate({
subTemplateId: sub.id,
parentTask: newTask,
manualTimestamp,
templateService,
})
})
Comment thread
arpandhakal marked this conversation as resolved.
}
}

Expand Down
28 changes: 23 additions & 5 deletions src/app/api/tasks/tasksShared.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { SupabaseActions } from '@/utils/SupabaseActions'
import APIError from '@api/core/exceptions/api'
import { BaseService } from '@api/core/services/base.service'
import { UserRole } from '@api/core/types/user'
import { AssigneeType, Prisma, PrismaClient, StateType, Task, TaskTemplate } from '@prisma/client'
import { AssigneeType, Prisma, PrismaClient, StateType, Task } from '@prisma/client'
import httpStatus from 'http-status'
import z from 'zod'
import { AttachmentsService } from '@api/attachments/attachments.service'
import type { TemplatesService } from '@api/tasks/templates/templates.service'

//Base class with shared permission logic and methods that both tasks.service.ts and public.service.ts could use
export abstract class TasksSharedService extends BaseService {
Expand Down Expand Up @@ -589,11 +590,22 @@ export abstract class TasksSharedService extends BaseService {
}
}

protected async createSubtasksFromTemplate(data: TaskTemplate, parentTask: Task, manualTimestamp: Date) {
const { workspaceId, title, body, workflowStateId } = data
protected async createSubtasksFromTemplate({
subTemplateId,
parentTask,
manualTimestamp,
templateService,
}: {
subTemplateId: string
parentTask: Task
manualTimestamp: Date
templateService: TemplatesService
}) {
const { id: parentId, internalUserId, clientId, companyId, associations, isShared } = parentTask

try {
const { workspaceId, title, body, workflowStateId } =
await templateService.getAppliedTemplateDescription(subTemplateId)
Comment thread
arpandhakal marked this conversation as resolved.
const createTaskPayload = CreateTaskRequestSchema.parse({
title: resolveDynamicFields(title),
body: body ? resolveAutofillTags(body) : body,
Expand All @@ -608,8 +620,10 @@ export abstract class TasksSharedService extends BaseService {
isShared,
})

await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp: manualTimestamp })
await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp })
} catch (e) {
// All-or-nothing: if any subtask fails to apply, roll back the parent task
// rather than leaving a partially-applied template, and surface the error.
const deleteTask = this.db.task.delete({ where: { id: parentId } })
const deleteActivityLogs = this.db.activityLog.deleteMany({ where: { taskId: parentId } })

Expand All @@ -620,7 +634,11 @@ export abstract class TasksSharedService extends BaseService {
this.unsetTransaction()
})

console.error('TasksService#createTask | Rolling back task creation', e)
console.error('TasksSharedService#createSubtasksFromTemplate | Rolling back task creation', {
parentId,
subTemplateId,
e,
})
throw new APIError(
httpStatus.INTERNAL_SERVER_ERROR,
'Failed to create subtask from template, new task was not created.',
Expand Down
4 changes: 4 additions & 0 deletions src/constants/tasks.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export const maxSubTaskDepth = 1

// Bounds how many subtasks we create concurrently when applying a template, so a
// template with many sub-templates can't exhaust the DB connection pool.
export const subtaskTemplateBatchSize = 5
21 changes: 21 additions & 0 deletions src/utils/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,24 @@ export const groupBy = <T, K extends keyof T>(arr: T[], key: K): Grouped<T> => {
return acc
}, {} as Grouped<T>)
}

export const chunk = <T>(arr: T[], size: number): T[][] =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size))

/**
* Runs `handler` over `items` with bounded concurrency: at most `size` run at once,
* one batch after another. Prevents unbounded fan-out from exhausting the DB connection
* pool (and downstream rate limits) when the input can be large.
*/
export const runInBatches = async <T, R>(
items: T[],
size: number,
handler: (item: T, index: number) => Promise<R>,
): Promise<R[]> => {
const indexed = items.map((item, index) => ({ item, index }))
const results: R[] = []
for (const batch of chunk(indexed, size)) {
results.push(...(await Promise.all(batch.map(({ item, index }) => handler(item, index)))))
}
return results
}
Loading