From 6db1b2600f9394187a96b24676796e272e85037d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 11 Mar 2026 14:41:04 +0100 Subject: [PATCH 1/3] chore: implemented patch HTTP method for member project affiliations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- backend/src/api/public/v1/members/index.ts | 7 ++ .../patchProjectAffiliation.ts | 104 ++++++++++++++++++ .../src/members/projectAffiliations.ts | 89 ++++++++++++++- 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts diff --git a/backend/src/api/public/v1/members/index.ts b/backend/src/api/public/v1/members/index.ts index b9f108de5a..6aa6117c9f 100644 --- a/backend/src/api/public/v1/members/index.ts +++ b/backend/src/api/public/v1/members/index.ts @@ -8,6 +8,7 @@ import { getMemberIdentities } from './identities/getMemberIdentities' import { verifyMemberIdentity } from './identities/verifyMemberIdentity' import { getMemberMaintainerRoles } from './maintainer-roles/getMemberMaintainerRoles' import { getProjectAffiliations } from './project-affiliations/getProjectAffiliations' +import { patchProjectAffiliation } from './project-affiliations/patchProjectAffiliation' import { resolveMemberByIdentities } from './resolveMember' import { createMemberWorkExperience } from './work-experiences/createMemberWorkExperience' import { deleteMemberWorkExperience } from './work-experiences/deleteMemberWorkExperience' @@ -44,6 +45,12 @@ export function membersRouter(): Router { safeWrap(getProjectAffiliations), ) + router.patch( + '/:memberId/project-affiliations/:projectId', + requireScopes([SCOPES.WRITE_PROJECT_AFFILIATIONS]), + safeWrap(patchProjectAffiliation), + ) + router.post( '/:memberId/work-experiences', requireScopes([SCOPES.WRITE_WORK_EXPERIENCES]), diff --git a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts new file mode 100644 index 0000000000..7bec535329 --- /dev/null +++ b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts @@ -0,0 +1,104 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditAffiliationsAction } from '@crowd/audit-logs' +import { NotFoundError } from '@crowd/common' +import { CommonMemberService } from '@crowd/common_services' +import { + MemberField, + fetchMemberSegmentAffiliationForProject, + findMemberById, + optionsQx, + updateMemberSegmentAffiliation, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), + projectId: z.uuid(), +}) + +const bodySchema = z + .object({ + organizationId: z.uuid().optional(), + dateStart: z.coerce.date().nullable().optional(), + dateEnd: z.coerce.date().nullable().optional(), + verified: z.boolean().optional(), + verifiedBy: z.string().nullable().optional(), + }) + .refine((data) => Object.keys(data).length > 0, { + message: 'At least one field must be provided', + }) + .refine( + (data) => { + const { dateStart, dateEnd } = data + if (dateStart != null && dateEnd != null) { + return dateEnd >= dateStart + } + return true + }, + { message: 'dateEnd must be greater than or equal to dateStart' }, + ) + +export async function patchProjectAffiliation(req: Request, res: Response): Promise { + const { memberId, projectId } = validateOrThrow(paramsSchema, req.params) + const data = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const existing = await fetchMemberSegmentAffiliationForProject(qx, memberId, projectId) + + if (!existing) { + throw new NotFoundError('Project affiliation not found') + } + + let updated = existing + + await captureApiChange( + req, + memberEditAffiliationsAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(existing) + + await qx.tx(async (tx) => { + await updateMemberSegmentAffiliation(tx, memberId, projectId, { + ...(data.organizationId !== undefined && { organizationId: data.organizationId }), + ...(data.dateStart !== undefined && { dateStart: data.dateStart?.toISOString() ?? null }), + ...(data.dateEnd !== undefined && { dateEnd: data.dateEnd?.toISOString() ?? null }), + ...(data.verified !== undefined && { verified: data.verified }), + ...(data.verifiedBy !== undefined && { verifiedBy: data.verifiedBy }), + }) + + const organizationId = data.organizationId ?? existing.organizationId + if (organizationId) { + const service = new CommonMemberService(tx, req.temporal, req.log) + await service.startAffiliationRecalculation(memberId, [organizationId]) + } + }) + + updated = await fetchMemberSegmentAffiliationForProject(qx, memberId, projectId) + if (!updated) { + throw new Error('Failed to re-fetch project affiliation after update') + } + captureNewState(updated) + }), + ) + + ok(res, { + id: updated.id, + organizationId: updated.organizationId, + organizationName: updated.organizationName, + organizationLogo: updated.organizationLogo ?? null, + verified: updated.verified, + verifiedBy: updated.verifiedBy ?? null, + startDate: updated.dateStart ?? null, + endDate: updated.dateEnd ?? null, + }) +} diff --git a/services/libs/data-access-layer/src/members/projectAffiliations.ts b/services/libs/data-access-layer/src/members/projectAffiliations.ts index e3423a5b37..882fee873f 100644 --- a/services/libs/data-access-layer/src/members/projectAffiliations.ts +++ b/services/libs/data-access-layer/src/members/projectAffiliations.ts @@ -11,8 +11,8 @@ export interface IProjectAffiliationSegment { export interface ISegmentAffiliationWithOrg { id: string segmentId: string - organizationId: string - organizationName: string + organizationId: string | null + organizationName: string | null organizationLogo: string | null verified: boolean verifiedBy: string | null @@ -88,6 +88,91 @@ export async function fetchMemberSegmentAffiliationsWithOrg( ) } +/** + * Fetch a single segment affiliation for a member + project (segment) combination. + */ +export async function fetchMemberSegmentAffiliationForProject( + qx: QueryExecutor, + memberId: string, + segmentId: string, +): Promise { + const rows = await qx.select( + ` + SELECT + msa.id, + msa."segmentId", + msa."organizationId", + o."displayName" AS "organizationName", + o.logo AS "organizationLogo", + msa.verified, + msa."verifiedBy", + msa."dateStart", + msa."dateEnd" + FROM "memberSegmentAffiliations" msa + LEFT JOIN organizations o ON msa."organizationId" = o.id + WHERE msa."memberId" = $(memberId) + AND msa."segmentId" = $(segmentId) + `, + { memberId, segmentId }, + ) + return rows[0] ?? null +} + +export interface ISegmentAffiliationUpdate { + organizationId?: string + dateStart?: string | null + dateEnd?: string | null + verified?: boolean + verifiedBy?: string | null +} + +/** + * Partially update a member's segment affiliation for a given project (segment). + * Only fields present in `data` are updated. + */ +export async function updateMemberSegmentAffiliation( + qx: QueryExecutor, + memberId: string, + segmentId: string, + data: ISegmentAffiliationUpdate, +): Promise { + const sets: string[] = [] + const params: Record = { memberId, segmentId } + + if ('organizationId' in data) { + sets.push('"organizationId" = $(organizationId)') + params.organizationId = data.organizationId + } + if ('dateStart' in data) { + sets.push('"dateStart" = $(dateStart)') + params.dateStart = data.dateStart + } + if ('dateEnd' in data) { + sets.push('"dateEnd" = $(dateEnd)') + params.dateEnd = data.dateEnd + } + if ('verified' in data) { + sets.push('"verified" = $(verified)') + params.verified = data.verified + } + if ('verifiedBy' in data) { + sets.push('"verifiedBy" = $(verifiedBy)') + params.verifiedBy = data.verifiedBy + } + + if (sets.length === 0) return + + await qx.result( + ` + UPDATE "memberSegmentAffiliations" + SET ${sets.join(', ')} + WHERE "memberId" = $(memberId) + AND "segmentId" = $(segmentId) + `, + params, + ) +} + /** * Fetch work experiences for a member with organization details. * Used as fallback affiliations when no segment affiliations exist for a project. From 9a5affdabbc49c04636a1b8b23e95266a3df753d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 11 Mar 2026 20:12:17 +0100 Subject: [PATCH 2/3] fix: fixes according to the spec and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../patchProjectAffiliation.ts | 175 ++++++++++++------ .../src/members/projectAffiliations.ts | 122 +++++++----- 2 files changed, 193 insertions(+), 104 deletions(-) diff --git a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts index 7bec535329..d38f2f269f 100644 --- a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts +++ b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts @@ -6,10 +6,18 @@ import { NotFoundError } from '@crowd/common' import { CommonMemberService } from '@crowd/common_services' import { MemberField, - fetchMemberSegmentAffiliationForProject, + deleteAllMemberSegmentAffiliationsForProject, + fetchMemberProjectSegment, + fetchMemberSegmentAffiliationsForProject, + fetchMemberWorkExperienceAffiliations, + findMaintainerRoles, findMemberById, + insertMemberSegmentAffiliations, optionsQx, - updateMemberSegmentAffiliation, +} from '@crowd/data-access-layer' +import type { + ISegmentAffiliationWithOrg, + IWorkExperienceAffiliation, } from '@crowd/data-access-layer' import { ok } from '@/utils/api' @@ -20,85 +28,138 @@ const paramsSchema = z.object({ projectId: z.uuid(), }) -const bodySchema = z - .object({ - organizationId: z.uuid().optional(), - dateStart: z.coerce.date().nullable().optional(), - dateEnd: z.coerce.date().nullable().optional(), - verified: z.boolean().optional(), - verifiedBy: z.string().nullable().optional(), - }) - .refine((data) => Object.keys(data).length > 0, { - message: 'At least one field must be provided', - }) - .refine( - (data) => { - const { dateStart, dateEnd } = data - if (dateStart != null && dateEnd != null) { - return dateEnd >= dateStart - } - return true - }, - { message: 'dateEnd must be greater than or equal to dateStart' }, - ) +const bodySchema = z.object({ + affiliations: z + .array( + z + .object({ + organizationId: z.uuid(), + dateStart: z.coerce.date(), + dateEnd: z.coerce.date().nullable().optional(), + }) + .refine((a) => a.dateEnd == null || a.dateEnd >= a.dateStart, { + message: 'dateEnd must be greater than or equal to dateStart', + }), + ) + .min(1), + verifiedBy: z.string().max(255), +}) + +function mapSegmentAffiliation(a: ISegmentAffiliationWithOrg) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified, + verifiedBy: a.verifiedBy ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + } +} + +function mapWorkExperienceAffiliation(a: IWorkExperienceAffiliation) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified ?? false, + verifiedBy: a.verifiedBy ?? null, + source: a.source ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + } +} export async function patchProjectAffiliation(req: Request, res: Response): Promise { const { memberId, projectId } = validateOrThrow(paramsSchema, req.params) - const data = validateOrThrow(bodySchema, req.body) + const { affiliations, verifiedBy } = validateOrThrow(bodySchema, req.body) const qx = optionsQx(req) const member = await findMemberById(qx, memberId, [MemberField.ID]) - if (!member) { throw new NotFoundError('Member not found') } - const existing = await fetchMemberSegmentAffiliationForProject(qx, memberId, projectId) - - if (!existing) { - throw new NotFoundError('Project affiliation not found') + const segment = await fetchMemberProjectSegment(qx, memberId, projectId) + if (!segment) { + throw new NotFoundError('Project not found') } - let updated = existing + const existingAffiliations = await fetchMemberSegmentAffiliationsForProject( + qx, + memberId, + projectId, + ) await captureApiChange( req, memberEditAffiliationsAction(memberId, async (captureOldState, captureNewState) => { - captureOldState(existing) + captureOldState(existingAffiliations) await qx.tx(async (tx) => { - await updateMemberSegmentAffiliation(tx, memberId, projectId, { - ...(data.organizationId !== undefined && { organizationId: data.organizationId }), - ...(data.dateStart !== undefined && { dateStart: data.dateStart?.toISOString() ?? null }), - ...(data.dateEnd !== undefined && { dateEnd: data.dateEnd?.toISOString() ?? null }), - ...(data.verified !== undefined && { verified: data.verified }), - ...(data.verifiedBy !== undefined && { verifiedBy: data.verifiedBy }), - }) - - const organizationId = data.organizationId ?? existing.organizationId - if (organizationId) { - const service = new CommonMemberService(tx, req.temporal, req.log) - await service.startAffiliationRecalculation(memberId, [organizationId]) - } + await deleteAllMemberSegmentAffiliationsForProject(tx, memberId, projectId) + + await insertMemberSegmentAffiliations( + tx, + memberId, + projectId, + affiliations.map((a) => ({ + organizationId: a.organizationId, + dateStart: a.dateStart.toISOString(), + dateEnd: a.dateEnd?.toISOString() ?? null, + verifiedBy, + })), + ) + + const oldOrgIds = existingAffiliations.map((a) => a.organizationId) + const newOrgIds = affiliations.map((a) => a.organizationId) + const orgIdsToRecalculate = [...new Set([...oldOrgIds, ...newOrgIds])] + + const service = new CommonMemberService(tx, req.temporal, req.log) + await service.startAffiliationRecalculation(memberId, orgIdsToRecalculate) }) - updated = await fetchMemberSegmentAffiliationForProject(qx, memberId, projectId) - if (!updated) { - throw new Error('Failed to re-fetch project affiliation after update') - } - captureNewState(updated) + const updatedAffiliations = await fetchMemberSegmentAffiliationsForProject( + qx, + memberId, + projectId, + ) + captureNewState(updatedAffiliations) }), ) + const [updatedAffiliations, maintainerRoles, workExperiences] = await Promise.all([ + fetchMemberSegmentAffiliationsForProject(qx, memberId, projectId), + findMaintainerRoles(qx, [memberId]), + fetchMemberWorkExperienceAffiliations(qx, memberId), + ]) + + const roles = maintainerRoles + .filter((r) => r.segmentId === projectId) + .map((r) => ({ + id: r.id, + role: r.role, + startDate: r.dateStart ?? null, + endDate: r.dateEnd ?? null, + repoUrl: r.url ?? null, + repoFileUrl: r.maintainerFile ?? null, + })) + + const mappedAffiliations = + updatedAffiliations.length > 0 + ? updatedAffiliations.map(mapSegmentAffiliation) + : workExperiences.map(mapWorkExperienceAffiliation) + ok(res, { - id: updated.id, - organizationId: updated.organizationId, - organizationName: updated.organizationName, - organizationLogo: updated.organizationLogo ?? null, - verified: updated.verified, - verifiedBy: updated.verifiedBy ?? null, - startDate: updated.dateStart ?? null, - endDate: updated.dateEnd ?? null, + id: segment.id, + projectSlug: segment.slug, + projectName: segment.name, + projectLogo: segment.projectLogo ?? null, + contributionCount: Number(segment.activityCount), + roles, + affiliations: mappedAffiliations, }) } diff --git a/services/libs/data-access-layer/src/members/projectAffiliations.ts b/services/libs/data-access-layer/src/members/projectAffiliations.ts index 882fee873f..bc183ec24d 100644 --- a/services/libs/data-access-layer/src/members/projectAffiliations.ts +++ b/services/libs/data-access-layer/src/members/projectAffiliations.ts @@ -11,8 +11,8 @@ export interface IProjectAffiliationSegment { export interface ISegmentAffiliationWithOrg { id: string segmentId: string - organizationId: string | null - organizationName: string | null + organizationId: string + organizationName: string organizationLogo: string | null verified: boolean verifiedBy: string | null @@ -60,6 +60,35 @@ export async function fetchMemberProjectSegments( ) } +/** + * Fetch a single project-level segment for a member + segment combination. + */ +export async function fetchMemberProjectSegment( + qx: QueryExecutor, + memberId: string, + segmentId: string, +): Promise { + const rows = await qx.select( + ` + SELECT + s.id, + s.slug, + s.name, + msa."activityCount", + ip."logoUrl" AS "projectLogo" + FROM "memberSegmentsAgg" msa + JOIN segments s ON msa."segmentId" = s.id + LEFT JOIN "insightsProjects" ip ON ip."segmentId" = s.id AND ip."deletedAt" IS NULL + WHERE msa."memberId" = $(memberId) + AND s.id = $(segmentId) + AND s."parentSlug" IS NOT NULL + AND s."grandparentSlug" IS NULL + `, + { memberId, segmentId }, + ) + return rows[0] ?? null +} + /** * Fetch segment affiliations for a member with organization details. * These are manual per-project overrides. @@ -89,14 +118,14 @@ export async function fetchMemberSegmentAffiliationsWithOrg( } /** - * Fetch a single segment affiliation for a member + project (segment) combination. + * Fetch all segment affiliations for a member + project (segment) combination. */ -export async function fetchMemberSegmentAffiliationForProject( +export async function fetchMemberSegmentAffiliationsForProject( qx: QueryExecutor, memberId: string, segmentId: string, -): Promise { - const rows = await qx.select( +): Promise { + return qx.select( ` SELECT msa.id, @@ -109,70 +138,69 @@ export async function fetchMemberSegmentAffiliationForProject( msa."dateStart", msa."dateEnd" FROM "memberSegmentAffiliations" msa - LEFT JOIN organizations o ON msa."organizationId" = o.id + JOIN organizations o ON msa."organizationId" = o.id WHERE msa."memberId" = $(memberId) AND msa."segmentId" = $(segmentId) `, { memberId, segmentId }, ) - return rows[0] ?? null } -export interface ISegmentAffiliationUpdate { - organizationId?: string - dateStart?: string | null - dateEnd?: string | null - verified?: boolean - verifiedBy?: string | null +export interface ISegmentAffiliationInsert { + organizationId: string + dateStart: string | null + dateEnd: string | null + verifiedBy: string } /** - * Partially update a member's segment affiliation for a given project (segment). - * Only fields present in `data` are updated. + * Delete all segment affiliations for a member + project (segment) combination. */ -export async function updateMemberSegmentAffiliation( +export async function deleteAllMemberSegmentAffiliationsForProject( qx: QueryExecutor, memberId: string, segmentId: string, - data: ISegmentAffiliationUpdate, ): Promise { - const sets: string[] = [] - const params: Record = { memberId, segmentId } - - if ('organizationId' in data) { - sets.push('"organizationId" = $(organizationId)') - params.organizationId = data.organizationId - } - if ('dateStart' in data) { - sets.push('"dateStart" = $(dateStart)') - params.dateStart = data.dateStart - } - if ('dateEnd' in data) { - sets.push('"dateEnd" = $(dateEnd)') - params.dateEnd = data.dateEnd - } - if ('verified' in data) { - sets.push('"verified" = $(verified)') - params.verified = data.verified - } - if ('verifiedBy' in data) { - sets.push('"verifiedBy" = $(verifiedBy)') - params.verifiedBy = data.verifiedBy - } - - if (sets.length === 0) return - await qx.result( ` - UPDATE "memberSegmentAffiliations" - SET ${sets.join(', ')} + DELETE FROM "memberSegmentAffiliations" WHERE "memberId" = $(memberId) AND "segmentId" = $(segmentId) `, - params, + { memberId, segmentId }, ) } +/** + * Insert multiple segment affiliations for a member + project (segment) combination. + * All inserted affiliations are marked as verified. + */ +export async function insertMemberSegmentAffiliations( + qx: QueryExecutor, + memberId: string, + segmentId: string, + affiliations: ISegmentAffiliationInsert[], +): Promise { + for (const aff of affiliations) { + await qx.result( + ` + INSERT INTO "memberSegmentAffiliations" + (id, "memberId", "segmentId", "organizationId", "dateStart", "dateEnd", verified, "verifiedBy") + VALUES + (gen_random_uuid(), $(memberId), $(segmentId), $(organizationId), $(dateStart), $(dateEnd), true, $(verifiedBy)) + `, + { + memberId, + segmentId, + organizationId: aff.organizationId, + dateStart: aff.dateStart, + dateEnd: aff.dateEnd, + verifiedBy: aff.verifiedBy, + }, + ) + } +} + /** * Fetch work experiences for a member with organization details. * Used as fallback affiliations when no segment affiliations exist for a project. From c113bb052b0befbf64cd834ecf2d362bac77ddaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 11 Mar 2026 20:51:35 +0100 Subject: [PATCH 3/3] fix: small refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../getProjectAffiliations.ts | 33 ++----------------- .../members/project-affiliations/mappers.ts | 31 +++++++++++++++++ .../patchProjectAffiliation.ts | 33 ++----------------- 3 files changed, 35 insertions(+), 62 deletions(-) create mode 100644 backend/src/api/public/v1/members/project-affiliations/mappers.ts diff --git a/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts index e890a8826f..db3e18de43 100644 --- a/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts +++ b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts @@ -11,45 +11,16 @@ import { findMemberById, optionsQx, } from '@crowd/data-access-layer' -import type { - ISegmentAffiliationWithOrg, - IWorkExperienceAffiliation, -} from '@crowd/data-access-layer' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' +import { mapSegmentAffiliation, mapWorkExperienceAffiliation } from './mappers' + const paramsSchema = z.object({ memberId: z.uuid(), }) -function mapSegmentAffiliation(a: ISegmentAffiliationWithOrg) { - return { - id: a.id, - organizationId: a.organizationId, - organizationName: a.organizationName, - organizationLogo: a.organizationLogo ?? null, - verified: a.verified, - verifiedBy: a.verifiedBy ?? null, - startDate: a.dateStart ?? null, - endDate: a.dateEnd ?? null, - } -} - -function mapWorkExperienceAffiliation(a: IWorkExperienceAffiliation) { - return { - id: a.id, - organizationId: a.organizationId, - organizationName: a.organizationName, - organizationLogo: a.organizationLogo ?? null, - verified: a.verified ?? false, - verifiedBy: a.verifiedBy ?? null, - source: a.source ?? null, - startDate: a.dateStart ?? null, - endDate: a.dateEnd ?? null, - } -} - export async function getProjectAffiliations(req: Request, res: Response): Promise { const { memberId } = validateOrThrow(paramsSchema, req.params) const qx = optionsQx(req) diff --git a/backend/src/api/public/v1/members/project-affiliations/mappers.ts b/backend/src/api/public/v1/members/project-affiliations/mappers.ts new file mode 100644 index 0000000000..eadea60090 --- /dev/null +++ b/backend/src/api/public/v1/members/project-affiliations/mappers.ts @@ -0,0 +1,31 @@ +import type { + ISegmentAffiliationWithOrg, + IWorkExperienceAffiliation, +} from '@crowd/data-access-layer' + +export function mapSegmentAffiliation(a: ISegmentAffiliationWithOrg) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified, + verifiedBy: a.verifiedBy ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + } +} + +export function mapWorkExperienceAffiliation(a: IWorkExperienceAffiliation) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified ?? false, + verifiedBy: a.verifiedBy ?? null, + source: a.source ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + } +} diff --git a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts index d38f2f269f..88e49e29ad 100644 --- a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts +++ b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts @@ -15,14 +15,12 @@ import { insertMemberSegmentAffiliations, optionsQx, } from '@crowd/data-access-layer' -import type { - ISegmentAffiliationWithOrg, - IWorkExperienceAffiliation, -} from '@crowd/data-access-layer' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' +import { mapSegmentAffiliation, mapWorkExperienceAffiliation } from './mappers' + const paramsSchema = z.object({ memberId: z.uuid(), projectId: z.uuid(), @@ -45,33 +43,6 @@ const bodySchema = z.object({ verifiedBy: z.string().max(255), }) -function mapSegmentAffiliation(a: ISegmentAffiliationWithOrg) { - return { - id: a.id, - organizationId: a.organizationId, - organizationName: a.organizationName, - organizationLogo: a.organizationLogo ?? null, - verified: a.verified, - verifiedBy: a.verifiedBy ?? null, - startDate: a.dateStart ?? null, - endDate: a.dateEnd ?? null, - } -} - -function mapWorkExperienceAffiliation(a: IWorkExperienceAffiliation) { - return { - id: a.id, - organizationId: a.organizationId, - organizationName: a.organizationName, - organizationLogo: a.organizationLogo ?? null, - verified: a.verified ?? false, - verifiedBy: a.verifiedBy ?? null, - source: a.source ?? null, - startDate: a.dateStart ?? null, - endDate: a.dateEnd ?? null, - } -} - export async function patchProjectAffiliation(req: Request, res: Response): Promise { const { memberId, projectId } = validateOrThrow(paramsSchema, req.params) const { affiliations, verifiedBy } = validateOrThrow(bodySchema, req.body)