From 177aa19a61e42ad07ab80dff5b797c2f09af7549 Mon Sep 17 00:00:00 2001 From: Ido Shamun Date: Mon, 5 Jan 2026 11:33:58 +0200 Subject: [PATCH 1/6] feat: simplify reimportOpportunity mutation to update all fields - Remove sections parameter from reimportOpportunity mutation - Update all opportunity fields (title, tldr, content, keywords) on reimport - Simplify updateOpportunityFromParsedData to not require section selection - Add API tests for reimportOpportunity mutation --- __tests__/schema/opportunity.ts | 159 +++++++++++++++++++++++++++++ src/common/opportunity/parse.ts | 87 ++++++++++++++++ src/common/schema/opportunities.ts | 31 ++++++ src/schema/opportunity.ts | 135 +++++++++++++++++++++++- 4 files changed, 411 insertions(+), 1 deletion(-) diff --git a/__tests__/schema/opportunity.ts b/__tests__/schema/opportunity.ts index f4fdfcb8ce..7a5b06b5dd 100644 --- a/__tests__/schema/opportunity.ts +++ b/__tests__/schema/opportunity.ts @@ -6399,3 +6399,162 @@ describe('query opportunityStats', () => { expect(res.errors[0].extensions.code).toBe('FORBIDDEN'); }); }); + +describe('mutation reimportOpportunity', () => { + const MUTATION = /* GraphQL */ ` + mutation ReimportOpportunity($payload: ReimportOpportunityInput!) { + reimportOpportunity(payload: $payload) { + id + title + tldr + content { + overview { + content + } + requirements { + content + } + responsibilities { + content + } + } + keywords { + keyword + } + } + } + `; + + beforeEach(async () => { + jest.resetAllMocks(); + + const transport = createMockBrokkrTransport(); + const serviceClient = { + instance: createClient(BrokkrService, transport), + garmr: createGarmrMock(), + }; + + jest + .spyOn(brokkrCommon, 'getBrokkrClient') + .mockImplementation((): ServiceClient => { + return serviceClient; + }); + }); + + it('should require authentication', async () => { + loggedUser = null; + + await testMutationErrorCode( + client, + { + mutation: MUTATION, + variables: { + payload: { + opportunityId: opportunitiesFixture[0].id, + url: 'https://example.com/job', + }, + }, + }, + 'UNAUTHENTICATED', + ); + }); + + it('should require recruiter permission', async () => { + loggedUser = '3'; // User 3 is not a recruiter for opportunity 3 + + await testMutationErrorCode( + client, + { + mutation: MUTATION, + variables: { + payload: { + opportunityId: opportunitiesFixture[3].id, + url: 'https://example.com/job', + }, + }, + }, + 'FORBIDDEN', + ); + }); + + it('should fail when neither file nor URL is provided', async () => { + loggedUser = '2'; // User 2 is a recruiter for opportunity 3 + + const res = await client.mutate(MUTATION, { + variables: { + payload: { + opportunityId: opportunitiesFixture[3].id, + }, + }, + }); + + expect(res.errors).toBeTruthy(); + }); + + it('should reimport opportunity from URL and update all fields', async () => { + loggedUser = '2'; // User 2 is a recruiter for opportunity 3 (which is in DRAFT state) + + const fetchSpy = jest.spyOn(globalThis, 'fetch'); + const pdfResponse = new Response('Mocked PDF content', { + status: 200, + headers: { 'Content-Type': 'application/pdf' }, + }); + jest.spyOn(pdfResponse, 'arrayBuffer').mockResolvedValue(new ArrayBuffer(0)); + fetchSpy.mockResolvedValueOnce(pdfResponse); + + fileTypeFromBuffer.mockResolvedValue({ + ext: 'pdf', + mime: 'application/pdf', + }); + + const uploadResumeFromBufferSpy = jest.spyOn( + googleCloud, + 'uploadResumeFromBuffer', + ); + uploadResumeFromBufferSpy.mockResolvedValue( + `https://storage.cloud.google.com/${RESUME_BUCKET_NAME}/file`, + ); + + const deleteFileFromBucketSpy = jest.spyOn( + googleCloud, + 'deleteFileFromBucket', + ); + deleteFileFromBucketSpy.mockResolvedValue(true); + + // Get original opportunity state + const originalOpportunity = await con + .getRepository(OpportunityJob) + .findOneByOrFail({ id: opportunitiesFixture[3].id }); + + const res = await client.mutate(MUTATION, { + variables: { + payload: { + opportunityId: opportunitiesFixture[3].id, + url: 'https://example.com/updated-job', + }, + }, + }); + + expect(res.errors).toBeFalsy(); + expect(res.data.reimportOpportunity.id).toBe(opportunitiesFixture[3].id); + + // Verify fields were updated with mocked Brokkr response + expect(res.data.reimportOpportunity.title).toBe('Mocked Opportunity Title'); + expect(res.data.reimportOpportunity.tldr).toBe( + 'This is a mocked TL;DR of the opportunity.', + ); + expect(res.data.reimportOpportunity.keywords).toEqual([ + { keyword: 'mock' }, + { keyword: 'opportunity' }, + { keyword: 'test' }, + ]); + + // Verify opportunity still exists and was updated + const updatedOpportunity = await con + .getRepository(OpportunityJob) + .findOneByOrFail({ id: opportunitiesFixture[3].id }); + + expect(updatedOpportunity.title).toBe('Mocked Opportunity Title'); + expect(updatedOpportunity.state).toBe(originalOpportunity.state); // State should be preserved + }); +}); diff --git a/src/common/opportunity/parse.ts b/src/common/opportunity/parse.ts index f55de6a69a..87ef4da7e0 100644 --- a/src/common/opportunity/parse.ts +++ b/src/common/opportunity/parse.ts @@ -351,3 +351,90 @@ export async function createOpportunityFromParsedData( return opportunity; }); } + +export interface UpdateOpportunityContext { + con: DataSource; + log: FastifyBaseLogger; +} + +/** + * Updates an existing opportunity with all parsed data. + * + * @param ctx - Context with database connection and logger + * @param opportunityId - ID of the opportunity to update + * @param parsedData - The parsed opportunity data from Brokkr + * @returns The updated opportunity + */ +export async function updateOpportunityFromParsedData( + ctx: UpdateOpportunityContext, + opportunityId: string, + parsedData: ParsedOpportunityResult, +): Promise { + const { opportunity: parsedOpportunity, content } = parsedData; + + return ctx.con.transaction(async (entityManager) => { + // Fetch the existing opportunity + const existingOpportunity = await entityManager + .getRepository(OpportunityJob) + .findOne({ + where: { id: opportunityId }, + }); + + if (!existingOpportunity) { + throw new ValidationError('Opportunity not found'); + } + + // Build update object with all parsed data + const updateData: Partial = {}; + + if (parsedOpportunity.title) { + updateData.title = parsedOpportunity.title; + } + + if (parsedOpportunity.tldr) { + updateData.tldr = parsedOpportunity.tldr; + } + + // Update content - merge with existing to preserve any sections not in parsed data + updateData.content = { + ...existingOpportunity.content, + ...(content.overview && { overview: content.overview }), + ...(content.responsibilities && { + responsibilities: content.responsibilities, + }), + ...(content.requirements && { requirements: content.requirements }), + ...(content.whatYoullDo && { whatYoullDo: content.whatYoullDo }), + ...(content.interviewProcess && { + interviewProcess: content.interviewProcess, + }), + } as OpportunityContent; + + // Update the opportunity + if (Object.keys(updateData).length > 0) { + await entityManager + .getRepository(OpportunityJob) + .update({ id: opportunityId }, updateData); + } + + // Update keywords if present in parsed data + if (parsedOpportunity.keywords?.length) { + // Delete existing keywords + await entityManager.getRepository(OpportunityKeyword).delete({ + opportunityId, + }); + + // Insert new keywords + await entityManager.getRepository(OpportunityKeyword).insert( + parsedOpportunity.keywords.map((keyword) => ({ + opportunityId, + keyword: keyword.keyword, + })), + ); + } + + // Fetch and return the updated opportunity + return entityManager.getRepository(OpportunityJob).findOneOrFail({ + where: { id: opportunityId }, + }); + }); +} diff --git a/src/common/schema/opportunities.ts b/src/common/schema/opportunities.ts index aa8303f92d..3e2b7c32a9 100644 --- a/src/common/schema/opportunities.ts +++ b/src/common/schema/opportunities.ts @@ -242,6 +242,37 @@ export const parseOpportunitySchema = z }, ); +export const reimportOpportunitySchema = z + .object({ + opportunityId: z.uuid(), + url: urlParseSchema.optional(), + file: fileUploadSchema.optional(), + }) + .refine( + (data) => { + if (!data.url && !data.file) { + return false; + } + + return true; + }, + { + error: 'Either url or file must be provided.', + }, + ) + .refine( + (data) => { + if (data.url && data.file) { + return false; + } + + return true; + }, + { + error: 'Only one of url or file can be provided.', + }, + ); + export const createSharedSlackChannelSchema = z.object({ organizationId: z.string().uuid('Organization ID must be a valid UUID'), email: z.string().email('Email must be a valid email address'), diff --git a/src/schema/opportunity.ts b/src/schema/opportunity.ts index 592a119504..7bd24b7a1c 100644 --- a/src/schema/opportunity.ts +++ b/src/schema/opportunity.ts @@ -37,7 +37,11 @@ import { } from '../common/schema/opportunityMatch'; import { OpportunityJob } from '../entity/opportunities/OpportunityJob'; import { OpportunityUserRecruiter } from '../entity/opportunities/user/OpportunityUserRecruiter'; -import { ForbiddenError, ValidationError } from 'apollo-server-errors'; +import { + AuthenticationError, + ForbiddenError, + ValidationError, +} from 'apollo-server-errors'; import { ConflictError, NotFoundError, PaymentRequiredError } from '../errors'; import { UserCandidateKeyword } from '../entity/user/UserCandidateKeyword'; import { User } from '../entity/user/User'; @@ -53,6 +57,7 @@ import { opportunityUpdateStateSchema, createSharedSlackChannelSchema, parseOpportunitySchema, + reimportOpportunitySchema, opportunityMatchesQuerySchema, addOpportunitySeatsSchema, } from '../common/schema/opportunities'; @@ -97,6 +102,7 @@ import { validateOpportunityFileType, parseOpportunityWithBrokkr, createOpportunityFromParsedData, + updateOpportunityFromParsedData, } from '../common/opportunity/parse'; import { isMockEnabled, @@ -740,6 +746,23 @@ export const typeDefs = /* GraphQL */ ` url: String } + input ReimportOpportunityInput { + """ + ID of the opportunity to update + """ + opportunityId: ID! + + """ + PDF, Word file to parse + """ + file: Upload + + """ + URL to scrape and parse + """ + url: String + } + input OpportunitySeatInput { priceId: String! quantity: Int! @@ -878,6 +901,13 @@ export const typeDefs = /* GraphQL */ ` parseOpportunity(payload: ParseOpportunityInput!): Opportunity! @rateLimit(limit: 10, duration: 3600) + """ + Re-import and update an existing opportunity from a URL or file upload + """ + reimportOpportunity(payload: ReimportOpportunityInput!): Opportunity! + @auth + @rateLimit(limit: 10, duration: 3600) + """ Create a shared Slack channel and invite a user by email """ @@ -2647,6 +2677,109 @@ export const resolvers: IResolvers = traceResolvers< throw error; } }, + reimportOpportunity: async ( + _, + { + payload, + }: { + payload: unknown; + }, + ctx: Context, + info, + ): Promise => { + if (!ctx.userId) { + throw new AuthenticationError('User must be authenticated'); + } + + try { + const startTime = Date.now(); + let stepStart = startTime; + + const parsedPayload = + await reimportOpportunitySchema.parseAsync(payload); + ctx.log.info( + { + durationMs: Date.now() - stepStart, + opportunityId: parsedPayload.opportunityId, + }, + 'reimportOpportunity: payload schema validated', + ); + + // Check user has permission to edit this opportunity + stepStart = Date.now(); + await ensureOpportunityPermissions({ + con: ctx.con.manager, + userId: ctx.userId, + opportunityId: parsedPayload.opportunityId, + permission: OpportunityPermissions.Edit, + isTeamMember: ctx.isTeamMember, + }); + ctx.log.info( + { durationMs: Date.now() - stepStart }, + 'reimportOpportunity: permissions verified', + ); + + stepStart = Date.now(); + const { buffer, extension } = + await getOpportunityFileBuffer(parsedPayload); + ctx.log.info( + { durationMs: Date.now() - stepStart, bufferSize: buffer.length }, + 'reimportOpportunity: file buffer acquired', + ); + + stepStart = Date.now(); + const { mime } = await validateOpportunityFileType(buffer, extension); + ctx.log.info( + { durationMs: Date.now() - stepStart, mime }, + 'reimportOpportunity: file type validated', + ); + + stepStart = Date.now(); + const parsedData = await parseOpportunityWithBrokkr( + buffer, + mime, + ctx.log, + ); + ctx.log.info( + { + durationMs: Date.now() - stepStart, + title: parsedData.opportunity.title, + }, + 'reimportOpportunity: Brokkr parsing completed', + ); + + stepStart = Date.now(); + const opportunity = await updateOpportunityFromParsedData( + { + con: ctx.con, + log: ctx.log, + }, + parsedPayload.opportunityId, + parsedData, + ); + ctx.log.info( + { durationMs: Date.now() - stepStart, opportunityId: opportunity.id }, + 'reimportOpportunity: database records updated', + ); + + const totalDurationMs = Date.now() - startTime; + ctx.log.info( + { totalDurationMs, opportunityId: opportunity.id }, + 'reimportOpportunity: completed successfully', + ); + + return graphorm.queryOneOrFail(ctx, info, (builder) => { + builder.queryBuilder.where({ id: opportunity.id }); + return builder; + }); + } catch (error) { + ctx.log.error( + { error }, + 'reimportOpportunity: failed to reimport opportunity', + ); + throw error; + } + }, }, OpportunityMatch: { engagementProfile: async ( From 376025e1a0cc2f3bd65d42d29bc81565c85dc9a4 Mon Sep 17 00:00:00 2001 From: Ido Shamun Date: Mon, 5 Jan 2026 11:36:48 +0200 Subject: [PATCH 2/6] fix: lint issue --- __tests__/schema/opportunity.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/schema/opportunity.ts b/__tests__/schema/opportunity.ts index 7a5b06b5dd..86f11d2a3c 100644 --- a/__tests__/schema/opportunity.ts +++ b/__tests__/schema/opportunity.ts @@ -6499,7 +6499,9 @@ describe('mutation reimportOpportunity', () => { status: 200, headers: { 'Content-Type': 'application/pdf' }, }); - jest.spyOn(pdfResponse, 'arrayBuffer').mockResolvedValue(new ArrayBuffer(0)); + jest + .spyOn(pdfResponse, 'arrayBuffer') + .mockResolvedValue(new ArrayBuffer(0)); fetchSpy.mockResolvedValueOnce(pdfResponse); fileTypeFromBuffer.mockResolvedValue({ From 93b4885058da99aed26ce3a16a8bd3282ef04619 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:28:09 +0000 Subject: [PATCH 3/6] refactor: address code review feedback for reimport opportunity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Object.keys() for dynamic content field handling instead of manual listing - Reuse handleOpportunityKeywordsUpdate helper function for keyword updates - Return only opportunity ID from updateOpportunityFromParsedData to avoid wasteful fetch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Ido Shamun --- src/common/opportunity/parse.ts | 68 +++++++++++++++++++++------------ src/schema/opportunity.ts | 8 ++-- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/common/opportunity/parse.ts b/src/common/opportunity/parse.ts index 87ef4da7e0..214e6cc8f8 100644 --- a/src/common/opportunity/parse.ts +++ b/src/common/opportunity/parse.ts @@ -30,6 +30,7 @@ import { OpportunityUserRecruiter } from '../../entity/opportunities/user/Opport import { findDatasetLocation } from '../../entity/dataset/utils'; import { addOpportunityDefaultQuestionFeedback } from './question'; import type { Opportunity } from '../../entity/opportunities/Opportunity'; +import { EntityManager } from 'typeorm'; interface FileUpload { filename: string; @@ -357,19 +358,44 @@ export interface UpdateOpportunityContext { log: FastifyBaseLogger; } +/** + * Handles opportunity keywords updates + * Replaces all existing keywords with the new set + */ +async function handleOpportunityKeywordsUpdate( + entityManager: EntityManager, + opportunityId: string, + keywords: Array<{ keyword: string }> | undefined, +): Promise { + if (!Array.isArray(keywords)) { + return; + } + + await entityManager.getRepository(OpportunityKeyword).delete({ + opportunityId, + }); + + await entityManager.getRepository(OpportunityKeyword).insert( + keywords.map((keyword) => ({ + opportunityId, + keyword: keyword.keyword, + })), + ); +} + /** * Updates an existing opportunity with all parsed data. * * @param ctx - Context with database connection and logger * @param opportunityId - ID of the opportunity to update * @param parsedData - The parsed opportunity data from Brokkr - * @returns The updated opportunity + * @returns The opportunity ID */ export async function updateOpportunityFromParsedData( ctx: UpdateOpportunityContext, opportunityId: string, parsedData: ParsedOpportunityResult, -): Promise { +): Promise { const { opportunity: parsedOpportunity, content } = parsedData; return ctx.con.transaction(async (entityManager) => { @@ -398,15 +424,16 @@ export async function updateOpportunityFromParsedData( // Update content - merge with existing to preserve any sections not in parsed data updateData.content = { ...existingOpportunity.content, - ...(content.overview && { overview: content.overview }), - ...(content.responsibilities && { - responsibilities: content.responsibilities, - }), - ...(content.requirements && { requirements: content.requirements }), - ...(content.whatYoullDo && { whatYoullDo: content.whatYoullDo }), - ...(content.interviewProcess && { - interviewProcess: content.interviewProcess, - }), + ...Object.keys(content).reduce( + (acc, key) => { + const contentKey = key as keyof OpportunityContent; + if (content[contentKey]) { + acc[contentKey] = content[contentKey]; + } + return acc; + }, + {} as Partial, + ), } as OpportunityContent; // Update the opportunity @@ -418,23 +445,14 @@ export async function updateOpportunityFromParsedData( // Update keywords if present in parsed data if (parsedOpportunity.keywords?.length) { - // Delete existing keywords - await entityManager.getRepository(OpportunityKeyword).delete({ + await handleOpportunityKeywordsUpdate( + entityManager, opportunityId, - }); - - // Insert new keywords - await entityManager.getRepository(OpportunityKeyword).insert( - parsedOpportunity.keywords.map((keyword) => ({ - opportunityId, - keyword: keyword.keyword, - })), + parsedOpportunity.keywords, ); } - // Fetch and return the updated opportunity - return entityManager.getRepository(OpportunityJob).findOneOrFail({ - where: { id: opportunityId }, - }); + // Return the opportunity ID + return opportunityId; }); } diff --git a/src/schema/opportunity.ts b/src/schema/opportunity.ts index 7bd24b7a1c..ebd005a1fb 100644 --- a/src/schema/opportunity.ts +++ b/src/schema/opportunity.ts @@ -2749,7 +2749,7 @@ export const resolvers: IResolvers = traceResolvers< ); stepStart = Date.now(); - const opportunity = await updateOpportunityFromParsedData( + const opportunityId = await updateOpportunityFromParsedData( { con: ctx.con, log: ctx.log, @@ -2758,18 +2758,18 @@ export const resolvers: IResolvers = traceResolvers< parsedData, ); ctx.log.info( - { durationMs: Date.now() - stepStart, opportunityId: opportunity.id }, + { durationMs: Date.now() - stepStart, opportunityId }, 'reimportOpportunity: database records updated', ); const totalDurationMs = Date.now() - startTime; ctx.log.info( - { totalDurationMs, opportunityId: opportunity.id }, + { totalDurationMs, opportunityId }, 'reimportOpportunity: completed successfully', ); return graphorm.queryOneOrFail(ctx, info, (builder) => { - builder.queryBuilder.where({ id: opportunity.id }); + builder.queryBuilder.where({ id: opportunityId }); return builder; }); } catch (error) { From a18fa840906a078039bdf5f8e014aba7bc69c158 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:52:00 +0000 Subject: [PATCH 4/6] fix: remove duplicate handleOpportunityKeywordsUpdate function - Export handleOpportunityKeywordsUpdate from parse.ts - Remove duplicate implementation from opportunity.ts - Add best practices section to AGENTS.md about avoiding duplication Co-authored-by: Ido Shamun --- AGENTS.md | 8 ++++++++ src/common/opportunity/parse.ts | 2 +- src/schema/opportunity.ts | 26 +------------------------- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ada10cfa52..dfd293b4a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,14 @@ This file provides guidance to coding agents when working with code in this repo - `.infra/common.ts` - Worker subscription definitions - `.infra/index.ts` - Main Pulumi deployment configuration +## Best Practices & Lessons Learned + +**Avoiding Code Duplication:** +- **Always check for existing implementations** before creating new helper functions. Use Grep or Glob tools to search for similar function names or logic patterns across the codebase. +- **Prefer extracting to common utilities** when logic needs to be shared. Place shared helpers in appropriate `src/common/` subdirectories (e.g., `src/common/opportunity/` for opportunity-related helpers). +- **Export and import, don't duplicate**: When you need the same logic in multiple places, export the function from its original location and import it where needed. This ensures a single source of truth and prevents maintenance issues. +- **Example lesson**: When implementing `handleOpportunityKeywordsUpdate`, the function was duplicated in both `src/common/opportunity/parse.ts` and `src/schema/opportunity.ts`. This caused lint failures and maintenance burden. The correct approach was to export it from `parse.ts` and import it in `opportunity.ts`. + ## Pull Requests Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations. diff --git a/src/common/opportunity/parse.ts b/src/common/opportunity/parse.ts index d397abfd48..2a8960df6e 100644 --- a/src/common/opportunity/parse.ts +++ b/src/common/opportunity/parse.ts @@ -368,7 +368,7 @@ export interface UpdateOpportunityContext { * Handles opportunity keywords updates * Replaces all existing keywords with the new set */ -async function handleOpportunityKeywordsUpdate( +export async function handleOpportunityKeywordsUpdate( entityManager: EntityManager, opportunityId: string, keywords: Array<{ keyword: string }> | undefined, diff --git a/src/schema/opportunity.ts b/src/schema/opportunity.ts index ebd005a1fb..5cd78cff7a 100644 --- a/src/schema/opportunity.ts +++ b/src/schema/opportunity.ts @@ -103,6 +103,7 @@ import { parseOpportunityWithBrokkr, createOpportunityFromParsedData, updateOpportunityFromParsedData, + handleOpportunityKeywordsUpdate, } from '../common/opportunity/parse'; import { isMockEnabled, @@ -1115,31 +1116,6 @@ async function handleOpportunityLocationUpdate( } } -/** - * Handles opportunity keywords updates - * Replaces all existing keywords with the new set - */ -async function handleOpportunityKeywordsUpdate( - entityManager: EntityManager, - opportunityId: string, - keywords: Array<{ keyword: string }> | undefined, -): Promise { - if (!Array.isArray(keywords)) { - return; - } - - await entityManager.getRepository(OpportunityKeyword).delete({ - opportunityId, - }); - - await entityManager.getRepository(OpportunityKeyword).insert( - keywords.map((keyword) => ({ - opportunityId, - keyword: keyword.keyword, - })), - ); -} - /** * Handles opportunity screening questions updates * Validates questions ownership and upserts them with proper ordering From b4e0d80f3a08f2d976952b948a7acfbf2b96d243 Mon Sep 17 00:00:00 2001 From: Ido Shamun Date: Mon, 5 Jan 2026 13:54:32 +0200 Subject: [PATCH 5/6] fix: lint issue --- src/common/opportunity/parse.ts | 17 +++++++---------- src/schema/opportunity.ts | 1 - 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/common/opportunity/parse.ts b/src/common/opportunity/parse.ts index 2a8960df6e..aeece3403f 100644 --- a/src/common/opportunity/parse.ts +++ b/src/common/opportunity/parse.ts @@ -430,16 +430,13 @@ export async function updateOpportunityFromParsedData( // Update content - merge with existing to preserve any sections not in parsed data updateData.content = { ...existingOpportunity.content, - ...Object.keys(content).reduce( - (acc, key) => { - const contentKey = key as keyof OpportunityContent; - if (content[contentKey]) { - acc[contentKey] = content[contentKey]; - } - return acc; - }, - {} as Partial, - ), + ...Object.keys(content).reduce((acc, key) => { + const contentKey = key as keyof OpportunityContent; + if (content[contentKey]) { + acc[contentKey] = content[contentKey]; + } + return acc; + }, {} as Partial), } as OpportunityContent; // Update the opportunity diff --git a/src/schema/opportunity.ts b/src/schema/opportunity.ts index 5cd78cff7a..46f33e1964 100644 --- a/src/schema/opportunity.ts +++ b/src/schema/opportunity.ts @@ -61,7 +61,6 @@ import { opportunityMatchesQuerySchema, addOpportunitySeatsSchema, } from '../common/schema/opportunities'; -import { OpportunityKeyword } from '../entity/OpportunityKeyword'; import { ensureOpportunityPermissions, OpportunityPermissions, From 0a7d51a482ea96d0a05ff7f9027eee372e39b777 Mon Sep 17 00:00:00 2001 From: Ido Shamun Date: Mon, 5 Jan 2026 14:08:06 +0200 Subject: [PATCH 6/6] fix: lint issue --- src/common/opportunity/parse.ts | 22 +++++++++++++++------- src/workers/cdc/common.ts | 18 ------------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/common/opportunity/parse.ts b/src/common/opportunity/parse.ts index aeece3403f..159cd8baf6 100644 --- a/src/common/opportunity/parse.ts +++ b/src/common/opportunity/parse.ts @@ -428,15 +428,23 @@ export async function updateOpportunityFromParsedData( } // Update content - merge with existing to preserve any sections not in parsed data + // Explicitly list content block keys to avoid iterating over protobuf methods + const contentBlockKeys = [ + 'overview', + 'responsibilities', + 'requirements', + 'whatYoullDo', + 'interviewProcess', + ] as const; + const mergedContent: Partial = {}; + for (const key of contentBlockKeys) { + if (content[key]) { + mergedContent[key] = content[key]; + } + } updateData.content = { ...existingOpportunity.content, - ...Object.keys(content).reduce((acc, key) => { - const contentKey = key as keyof OpportunityContent; - if (content[contentKey]) { - acc[contentKey] = content[contentKey]; - } - return acc; - }, {} as Partial), + ...mergedContent, } as OpportunityContent; // Update the opportunity diff --git a/src/workers/cdc/common.ts b/src/workers/cdc/common.ts index c9f22354aa..7b9d66f08a 100644 --- a/src/workers/cdc/common.ts +++ b/src/workers/cdc/common.ts @@ -2,7 +2,6 @@ import { ContentMeta, ContentQuality, ContentUpdatedMessage, - Translation, } from '@dailydotdev/schema'; import { DataSource, ObjectLiteral } from 'typeorm'; import { EntityTarget } from 'typeorm/common/EntityTarget'; @@ -139,23 +138,6 @@ export const notifyPostContentUpdated = async ({ }), deleted: articlePost.deleted, sharedPostId: sharePost.sharedPostId || undefined, - translation: post.translation - ? Object.entries( - typeof post.translation === 'string' - ? JSON.parse(post.translation) - : post.translation, - ).reduce( - (acc, [key, value]) => { - acc[key] = decodeJsonField({ - value: value as JsonValue, - decoder: new Translation(), - }); - - return acc; - }, - {} as { [key: string]: Translation }, - ) - : undefined, }); await triggerTypedEvent(