From e7758169aa07cdb135756c26cb1094ec56bfa892 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 20 Feb 2026 15:30:16 +0545 Subject: [PATCH 1/3] fix(OUT-3179): Resetting association from public API on task update does not work --- src/app/api/tasks/public/public.service.ts | 24 ++++++++++++++++++---- src/app/api/tasks/tasks.service.ts | 18 ---------------- src/app/api/tasks/tasksShared.service.ts | 24 +++++++++++++++++++++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/app/api/tasks/public/public.service.ts b/src/app/api/tasks/public/public.service.ts index dc0eb8405..05e872d6d 100644 --- a/src/app/api/tasks/public/public.service.ts +++ b/src/app/api/tasks/public/public.service.ts @@ -332,10 +332,25 @@ export class PublicTasksService extends TasksSharedService { companyId: validatedIds?.companyId ?? null, }) - const associations: Associations = await this.getValidatedAssociations({ - prevAssociations: prevTask.associations, - associationsResetCondition: shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId, - }) + // const associations: Associations = await this.getValidatedAssociations({ + // prevAssociations: prevTask.associations, + // associationsResetCondition: shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId, + // }) + + let associations: Associations = AssociationsSchema.parse(prevTask.associations) + // check if current or previous assignee is a client or company + const associationsResetCondition = shouldUpdateUserIds + ? !!clientId || !!companyId + : prevTask.clientId || prevTask.companyId + + if (data.associations) { + // only update of associations attribute is available. No associations in payload attribute means the data remains as it is in DB. + if (associationsResetCondition || !data.associations?.length) { + associations = [] // reset associations to [] if task is not reassigned to IU. + } else if (data.associations?.length) { + associations = await this.validateAssociations(data.associations) + } + } const userAssignmentFields = shouldUpdateUserIds ? { @@ -383,6 +398,7 @@ export class PublicTasksService extends TasksSharedService { completedBy, completedByUserType, associations, + isShared: this.validateTaskShare(prevTask, data), ...userAssignmentFields, ...(await getTaskTimestamps('update', this.user, data, prevTask)), }, diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index e09b0e145..6c7bc8c70 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -313,24 +313,6 @@ export class TasksService extends TasksSharedService { } } - private validateTaskShare(prevTask: Task, data: UpdateTaskRequest): boolean | undefined { - const isTaskShared = data.isShared - - if (isTaskShared === undefined) return undefined - - if (isTaskShared) { - const isEligibleForShare = !!( - (prevTask.associations.length && prevTask.internalUserId) || - (data.associations?.length && data.internalUserId) - ) - if (!isEligibleForShare) { - throw new APIError(httpStatus.BAD_REQUEST, 'Cannot share task with assocations') - } - return true - } - return false - } - async updateOneTask(id: string, data: UpdateTaskRequest) { const policyGate = new PoliciesService(this.user) policyGate.authorize(UserAction.Update, Resource.Tasks) diff --git a/src/app/api/tasks/tasksShared.service.ts b/src/app/api/tasks/tasksShared.service.ts index 417cec180..5b4be7e29 100644 --- a/src/app/api/tasks/tasksShared.service.ts +++ b/src/app/api/tasks/tasksShared.service.ts @@ -2,7 +2,7 @@ import { maxSubTaskDepth } from '@/constants/tasks' import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users' import { InternalUsers, TempClientFilter, Uuid } from '@/types/common' import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto' -import { CreateTaskRequest, CreateTaskRequestSchema, Associations } from '@/types/dto/tasks.dto' +import { CreateTaskRequest, CreateTaskRequestSchema, Associations, UpdateTaskRequest } from '@/types/dto/tasks.dto' import { getFileNameFromPath } from '@/utils/attachmentUtils' import { buildLtree, buildLtreeNodeString } from '@/utils/ltree' import { getFilePathFromUrl } from '@/utils/signedUrlReplacer' @@ -536,4 +536,26 @@ export abstract class TasksSharedService extends BaseService { ) } } + + protected validateTaskShare(prevTask: Task, data: UpdateTaskRequest): boolean | undefined { + const finalIsShared = data.isShared !== undefined ? data.isShared : prevTask.isShared + + const finalInternalUser = data.internalUserId !== undefined ? data.internalUserId : prevTask.internalUserId + + const finalAssociations = data.associations !== undefined ? data.associations : prevTask.associations + + if (!finalIsShared) return false + + const hasInternalUser = !!finalInternalUser + const hasAssociations = !!finalAssociations?.length + + if (!hasInternalUser || !hasAssociations) { + throw new APIError( + httpStatus.BAD_REQUEST, + 'Cannot share task. A task must have an internal user and at least one association to be shared.', + ) + } + + return true + } } From 6773836ba45811de80eb6e35bbfdc5fa6bbd84e4 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Mon, 23 Feb 2026 12:59:27 +0545 Subject: [PATCH 2/3] fix(OUT-3179): separate utility for handling association update and its reset condition --- src/app/api/tasks/public/public.service.ts | 26 +++-------- src/app/api/tasks/tasks.service.ts | 21 +++------ src/app/api/tasks/tasksShared.service.ts | 51 +++++++++++++++++++++- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/app/api/tasks/public/public.service.ts b/src/app/api/tasks/public/public.service.ts index 05e872d6d..18bca794c 100644 --- a/src/app/api/tasks/public/public.service.ts +++ b/src/app/api/tasks/public/public.service.ts @@ -332,25 +332,13 @@ export class PublicTasksService extends TasksSharedService { companyId: validatedIds?.companyId ?? null, }) - // const associations: Associations = await this.getValidatedAssociations({ - // prevAssociations: prevTask.associations, - // associationsResetCondition: shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId, - // }) - - let associations: Associations = AssociationsSchema.parse(prevTask.associations) - // check if current or previous assignee is a client or company - const associationsResetCondition = shouldUpdateUserIds - ? !!clientId || !!companyId - : prevTask.clientId || prevTask.companyId - - if (data.associations) { - // only update of associations attribute is available. No associations in payload attribute means the data remains as it is in DB. - if (associationsResetCondition || !data.associations?.length) { - associations = [] // reset associations to [] if task is not reassigned to IU. - } else if (data.associations?.length) { - associations = await this.validateAssociations(data.associations) - } - } + const associations = await this.resolveAssociations({ + prevTask, + data, + shouldUpdateUserIds, + clientId, + companyId, + }) const userAssignmentFields = shouldUpdateUserIds ? { diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index 6c7bc8c70..a3e456137 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -347,20 +347,13 @@ export class TasksService extends TasksSharedService { companyId: validatedIds?.companyId ?? null, }) - let associations: Associations = AssociationsSchema.parse(prevTask.associations) - - // check if current or previous assignee is a client or company - const associationsResetCondition = shouldUpdateUserIds - ? !!clientId || !!companyId - : prevTask.clientId || prevTask.companyId - if (data.associations) { - // only update of associations attribute is available. No associations in payload attribute means the data remains as it is in DB. - if (associationsResetCondition || !data.associations?.length) { - associations = [] // reset associations to [] if task is not reassigned to IU. - } else if (data.associations?.length) { - associations = await this.validateAssociations(data.associations) - } - } + const associations = await this.resolveAssociations({ + prevTask, + data, + shouldUpdateUserIds, + clientId, + companyId, + }) const userAssignmentFields = shouldUpdateUserIds ? { diff --git a/src/app/api/tasks/tasksShared.service.ts b/src/app/api/tasks/tasksShared.service.ts index 5b4be7e29..105173ffc 100644 --- a/src/app/api/tasks/tasksShared.service.ts +++ b/src/app/api/tasks/tasksShared.service.ts @@ -2,7 +2,13 @@ import { maxSubTaskDepth } from '@/constants/tasks' import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users' import { InternalUsers, TempClientFilter, Uuid } from '@/types/common' import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto' -import { CreateTaskRequest, CreateTaskRequestSchema, Associations, UpdateTaskRequest } from '@/types/dto/tasks.dto' +import { + CreateTaskRequest, + CreateTaskRequestSchema, + Associations, + UpdateTaskRequest, + AssociationsSchema, +} from '@/types/dto/tasks.dto' import { getFileNameFromPath } from '@/utils/attachmentUtils' import { buildLtree, buildLtreeNodeString } from '@/utils/ltree' import { getFilePathFromUrl } from '@/utils/signedUrlReplacer' @@ -558,4 +564,47 @@ export abstract class TasksSharedService extends BaseService { return true } + + protected async resolveAssociations(params: { + prevTask: Task + data: UpdateTaskRequest + shouldUpdateUserIds: boolean + clientId?: string | null + companyId?: string | null + }): Promise { + const { prevTask, data, shouldUpdateUserIds, clientId, companyId } = params + if (!data.associations) { + return AssociationsSchema.parse(prevTask.associations) + } + + const shouldReset = this.shouldResetAssociations({ + shouldUpdateUserIds, + prevTask, + clientId, + companyId, + }) + + if (shouldReset) return [] + + const parsed = AssociationsSchema.parse(data.associations) + + if (!parsed?.length) return [] + + return this.validateAssociations(parsed) + } + + private shouldResetAssociations(params: { + shouldUpdateUserIds: boolean + prevTask: Task + clientId?: string | null + companyId?: string | null + }): boolean { + const { shouldUpdateUserIds, prevTask, clientId, companyId } = params + + if (shouldUpdateUserIds) { + return !!clientId || !!companyId + } + + return !!prevTask.clientId || !!prevTask.companyId + } } From 79f59ccae07dad4df9a4509782ed922e73ad25a4 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Mon, 23 Feb 2026 12:37:22 +0545 Subject: [PATCH 3/3] fix(OUT-3186): added viewers on task response on public api --- src/app/api/tasks/public/public.dto.ts | 1 + src/app/api/tasks/public/public.serializer.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/app/api/tasks/public/public.dto.ts b/src/app/api/tasks/public/public.dto.ts index e2b590452..ff7ef956f 100644 --- a/src/app/api/tasks/public/public.dto.ts +++ b/src/app/api/tasks/public/public.dto.ts @@ -42,6 +42,7 @@ export const PublicTaskDtoSchema = z.object({ clientId: z.string().uuid().nullable(), companyId: z.string().uuid().nullable(), association: AssociationsSchema, + viewers: AssociationsSchema, attachments: z.array(PublicAttachmentDtoSchema), isShared: z.boolean().optional(), }) diff --git a/src/app/api/tasks/public/public.serializer.ts b/src/app/api/tasks/public/public.serializer.ts index 9f7391774..6de8e1d0d 100644 --- a/src/app/api/tasks/public/public.serializer.ts +++ b/src/app/api/tasks/public/public.serializer.ts @@ -64,6 +64,7 @@ export class PublicTaskSerializer { clientId: task.clientId, companyId: task.companyId, association: AssociationsSchema.parse(task.associations), + viewers: task.isShared ? AssociationsSchema.parse(task.associations) : [], attachments: await PublicAttachmentSerializer.serializeAttachments({ attachments: task.attachments, uploadedByUserType: 'internalUser', // task creator is always IU