From b36936815777f0a9ba8540f13e247246397b2c5d Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Wed, 16 Oct 2024 16:56:09 -0700 Subject: [PATCH 1/5] adding opening to connector-greenhouse --- connectors/connector-greenhouse/def.ts | 1 + connectors/connector-greenhouse/server.ts | 21 +++++++++---- .../adapters/greenhouse-adapter/index.ts | 30 +++++++++++++++++++ .../adapters/greenhouse-adapter/mappers.ts | 11 +++++++ unified/unified-ats/router.ts | 5 ++++ unified/unified-ats/unifiedModels.ts | 15 ++++++++++ 6 files changed, 77 insertions(+), 6 deletions(-) diff --git a/connectors/connector-greenhouse/def.ts b/connectors/connector-greenhouse/def.ts index 9a6a95f8b..9011fe8e5 100644 --- a/connectors/connector-greenhouse/def.ts +++ b/connectors/connector-greenhouse/def.ts @@ -7,6 +7,7 @@ export const GREENHOUSE_ENTITY_NAMES = [ 'application', 'offer', 'candidate', + 'opening', ] as const export const greenhouseSchema = { diff --git a/connectors/connector-greenhouse/server.ts b/connectors/connector-greenhouse/server.ts index 61535c751..0df1c0b2c 100644 --- a/connectors/connector-greenhouse/server.ts +++ b/connectors/connector-greenhouse/server.ts @@ -71,23 +71,32 @@ const NextPageCursor: CursorParser<{next_page: number}> = { // TODO2: Implement low-code connector spec function greenhouseSource({sdk}: {sdk: GreenhouseSDK}): EtlSource<{ job: GreenhouseObjectType['job'] - // candidate: GreenhouseObjectType['candidate'] - // application: GreenhouseObjectType['application'] - // opening: GreenhouseObjectType['opening'] - // offer: GreenhouseObjectType['offer'] + candidate: GreenhouseObjectType['candidate'] + application: GreenhouseObjectType['application'] + opening: GreenhouseObjectType['opening'] + offer: GreenhouseObjectType['offer'] }> { return { // Perhaps allow cursor implementation to be passed in as a parameter + // @ts-expect-error ile greenhouse sdk is updated async listEntities(type, {cursor}) { const {next_page: page} = NextPageCursor.fromString(cursor) + + const isOpening = type === 'opening' + if(isOpening) { + console.debug('[greenhouse] opening type detected, using job type instead') + type = 'job' as typeof type + } const res = await sdk.GET(`/v1/${type as 'job'}s`, { params: {query: {per_page: 50, page}}, }) return { - entities: res.data.map((j) => ({id: `${j.id}`, data: j})), + entities: isOpening ? + res.data.flatMap((j) => j.openings.map((o) => ({id: `${o.id}`, data: {job_id: j.id, ...o}}))) : + res.data.map((j) => ({id: `${j.id}`, data: j})), next_cursor: NextPageCursor.toString({next_page: page + 1}), - // TODO: instead check for count / from respnose header + // TODO: instead check for count / from response header has_next_page: res.data.length === 0, } }, diff --git a/unified/unified-ats/adapters/greenhouse-adapter/index.ts b/unified/unified-ats/adapters/greenhouse-adapter/index.ts index 83c5391ad..36ad18e34 100644 --- a/unified/unified-ats/adapters/greenhouse-adapter/index.ts +++ b/unified/unified-ats/adapters/greenhouse-adapter/index.ts @@ -27,6 +27,36 @@ export const greenhouseAdapter = { items: res.data?.map((d) => applyMapper(mappers.job, d)) ?? [], } }, + listJobOpenings: async ({instance, input}) => { + const cursor = + input?.cursor && Number(input?.cursor) > 0 + ? Number(input?.cursor) + : undefined + const jobId = input?.jobId; + if (!jobId) { + throw new Error('jobId is required'); + } + // @ts-expect-error while greenhouse sdk is updated + const res = await instance.GET(`/v1/jobs/${jobId}/openings`, { + params: { + query: { + per_page: input?.page_size, + page: cursor, + }, + }, + }) + let nextCursor = undefined + // @ts-expect-error while greenhouse sdk is updated + if (input?.page_size && res.data?.length === input?.page_size) { + nextCursor = (cursor || 0) + input.page_size + } + return { + has_next_page: !!nextCursor, + next_cursor: nextCursor ? String(nextCursor) : undefined, + // @ts-expect-error while greenhouse sdk is updated + items: res.data?.map((d) => applyMapper(mappers.jobOpening, d)) ?? [], + } + }, listOffers: async ({instance, input}) => { const cursor = input?.cursor && Number(input?.cursor) > 0 diff --git a/unified/unified-ats/adapters/greenhouse-adapter/mappers.ts b/unified/unified-ats/adapters/greenhouse-adapter/mappers.ts index 88c67734f..b8aca733f 100644 --- a/unified/unified-ats/adapters/greenhouse-adapter/mappers.ts +++ b/unified/unified-ats/adapters/greenhouse-adapter/mappers.ts @@ -65,9 +65,20 @@ const offer = mapper(zCast(), unified.offer, { status: 'status', }) +const opening = mapper(zCast(), unified.opening, { + id: (record) => String(record.id), + created_at: 'opened_at', + // Greenhouse doesn't provide a separate 'updated_at' field for job openings so we can used the greater of created or closed at. + modified_at: (record) => record.closed_at || record.opened_at, + status: 'status', + job_id: 'job_id', +}) + + export const mappers = { candidate, department, job, offer, + opening } diff --git a/unified/unified-ats/router.ts b/unified/unified-ats/router.ts index 8a40b87ef..c8cc15b7c 100644 --- a/unified/unified-ats/router.ts +++ b/unified/unified-ats/router.ts @@ -24,6 +24,11 @@ export const atsRouter = trpc.router({ .input(zPaginationParams.nullish()) .output(zPaginatedResult.extend({items: z.array(unified.job)})) .query(async ({input, ctx}) => proxyCallAdapter({input, ctx})), + listJobOpenings: procedure + .meta(oapi({method: 'GET', path: '/job/{jobId}/opening'})) + .input(z.object({jobId: z.string()}).extend(zPaginationParams.shape).nullish()) + .output(zPaginatedResult.extend({items: z.array(unified.opening)})) + .query(async ({input, ctx}) => proxyCallAdapter({input, ctx})), listOffers: procedure .meta(oapi({method: 'GET', path: '/offer'})) .input(zPaginationParams.nullish()) diff --git a/unified/unified-ats/unifiedModels.ts b/unified/unified-ats/unifiedModels.ts index 676a9b90c..7c46ccf39 100644 --- a/unified/unified-ats/unifiedModels.ts +++ b/unified/unified-ats/unifiedModels.ts @@ -87,3 +87,18 @@ export const candidate = z ref: 'ats.candidate', description: 'A candidate for a job', }) + + + export const opening = z + .object({ + id: z.string(), + created_at: z.string(), + modified_at: z.string(), + status: z.string(), + job_id: z.string(), + raw_data: z.record(z.unknown()).optional(), + }) + .openapi({ + ref: 'ats.opening', + description: 'An opening for a job', + }) From d182d0b4c41ed1d2026d7b072139cf38cd94f9f8 Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Wed, 16 Oct 2024 16:56:36 -0700 Subject: [PATCH 2/5] adding opening to ats vertical --- kits/cdk/verticals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kits/cdk/verticals.ts b/kits/cdk/verticals.ts index 4291c231b..1a25bfaac 100644 --- a/kits/cdk/verticals.ts +++ b/kits/cdk/verticals.ts @@ -38,7 +38,7 @@ const _VERTICAL_BY_KEY = { integrating with your payroll. Only users who are invited to the platform can access this information, and the integration is one-way with no impact on original data.`, - objects: ['job', 'offer', 'candidate'], + objects: ['job', 'offer', 'candidate', 'opening'], }, } satisfies Record From c41b5827ccd48b19e7b11a071e600855739a3caf Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Wed, 16 Oct 2024 16:57:16 -0700 Subject: [PATCH 3/5] adding opening to ats links and improving ag insertion --- connectors/connector-postgres/server.ts | 12 +++++------- kits/cdk/base-links.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/connectors/connector-postgres/server.ts b/connectors/connector-postgres/server.ts index dd0b5bee2..d1a073ed0 100644 --- a/connectors/connector-postgres/server.ts +++ b/connectors/connector-postgres/server.ts @@ -16,7 +16,8 @@ const agTableMappings = [ {from: 'integration_ats_candidate', to: 'IntegrationATSCandidate'}, {from: 'integration_ats_job_opening', to: 'IntegrationATSJobOpening'}, {from: 'integration_ats_offer', to: 'IntegrationATSOffer'}, - {from: 'integration_connection', to: 'IntegrationConnection'} + {from: 'integration_connection', to: 'IntegrationConnection'}, + {from: 'integration_ats_opening', to: 'IntegrationATSOpening'}, ] async function setupTable({ @@ -245,7 +246,7 @@ export const postgresServer = { isOpenInt: true, } - const isAgInsert = + const isAgInsert = endUser?.orgId === 'org_2lcCCimyICKI8cpPNQt195h5zrP' || endUser?.orgId === 'org_2ms9FdeczlbrDIHJLcwGdpv3dTx' @@ -258,11 +259,8 @@ export const postgresServer = { rowToInsert['opening_external_id'] = data.entity?.raw?.id || ''; rowToInsert['candidate_name'] = data.entity?.raw?.name + ' ' + data.entity?.raw?.last_name || ''; } else if (tableName === 'IntegrationAtsJobOpening') { - rowToInsert['opening_external_id'] = data.entity?.raw?.id || ''; - // NOTE Job openings are nested within Jobs and that o bject does not contain an id of the parent (job id) - // Depends on the implementation we may have to change this, leaving empty for now - // https://developers.greenhouse.io/harvest.html#the-job-object - rowToInsert['job_id'] = ''; + rowToInsert['opening_external_id'] = data.entity?.raw?.opening_id || ''; + rowToInsert['job_id'] = data.entity?.raw?.job_id || ''; } else if (tableName === 'IntegrationAtsOffer') { // Note: These fields seemed duplicated from the nested objects rowToInsert['opening_external_id'] = data.entity?.raw?.opening?.id || ''; diff --git a/kits/cdk/base-links.ts b/kits/cdk/base-links.ts index dd949c8c0..d0d1e9c5a 100644 --- a/kits/cdk/base-links.ts +++ b/kits/cdk/base-links.ts @@ -147,7 +147,7 @@ export function agColumnRenameLink(_ctx: { const entityMappings = { job: 'IntegrationAtsJob', candidate: 'IntegrationAtsCandidate', - job_opening: 'IntegrationAtsJobOpening', + opening: 'IntegrationAtsJobOpening', offer: 'IntegrationAtsOffer', } From f38e4135a7104f866174249c2a8621c2fce5075b Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Thu, 17 Oct 2024 12:21:36 -0700 Subject: [PATCH 4/5] updating to latest SDK version --- connectors/connector-greenhouse/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/connectors/connector-greenhouse/package.json b/connectors/connector-greenhouse/package.json index 36cc66813..4dc7e1b28 100644 --- a/connectors/connector-greenhouse/package.json +++ b/connectors/connector-greenhouse/package.json @@ -7,7 +7,7 @@ "@openint/cdk": "workspace:*", "@openint/util": "workspace:*", "@opensdks/runtime": "^0.0.19", - "@opensdks/sdk-greenhouse": "^0.0.6" + "@opensdks/sdk-greenhouse": "^0.0.7" }, "devDependencies": {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94d6d9316..c9c42d06c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -857,8 +857,8 @@ importers: specifier: ^0.0.19 version: 0.0.19 '@opensdks/sdk-greenhouse': - specifier: ^0.0.6 - version: 0.0.6 + specifier: ^0.0.7 + version: 0.0.7 connectors/connector-heron: dependencies: @@ -3606,8 +3606,8 @@ packages: '@opensdks/sdk-finch@0.0.4': resolution: {integrity: sha512-7CxSxCIE8POPBFrEGlovRdkziXNfbrh6GD7fPwIk6GPR2f2CKmLqPxOpLhBhzH/5Ol0nCyzCq8NDsvLbI5OvEQ==} - '@opensdks/sdk-greenhouse@0.0.6': - resolution: {integrity: sha512-+ZpAp8Gca0Hta3ligCL49tbfiUwCEVZhTD4isVZRYOhvapORvuarApZROxWTDq8oJ0XNDDQrpPh0/Rq0QPM/XA==} + '@opensdks/sdk-greenhouse@0.0.7': + resolution: {integrity: sha512-WgmMjMO8DXyjnSJ1fvnK1TrNabgUatUOIKHRhUu6WSFJFUzfryoMJfPR8zSWIUzMNrtNS5lthK2x+kY+w4NtBQ==} '@opensdks/sdk-heron@0.0.1': resolution: {integrity: sha512-7DOFFlq5C8u060L9GJ6euwE/g8aqIClyZysio4jqYQ1W9rcYX/pZUsZ7NNPt6LQTX+coAz+8nieEi35ejiKbdg==} @@ -14868,7 +14868,7 @@ snapshots: '@opensdks/sdk-finch@0.0.4': {} - '@opensdks/sdk-greenhouse@0.0.6': + '@opensdks/sdk-greenhouse@0.0.7': dependencies: '@opensdks/runtime': 0.0.20 From 032269bde2fa07a07ad18cb968f414900e4d76e5 Mon Sep 17 00:00:00 2001 From: Amadeo Pellicce Date: Fri, 18 Oct 2024 18:25:32 -0700 Subject: [PATCH 5/5] fixing job opening unified API --- connectors/connector-greenhouse/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- .../unified-ats/adapters/greenhouse-adapter/index.ts | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/connectors/connector-greenhouse/package.json b/connectors/connector-greenhouse/package.json index 4dc7e1b28..8f48a57ca 100644 --- a/connectors/connector-greenhouse/package.json +++ b/connectors/connector-greenhouse/package.json @@ -7,7 +7,7 @@ "@openint/cdk": "workspace:*", "@openint/util": "workspace:*", "@opensdks/runtime": "^0.0.19", - "@opensdks/sdk-greenhouse": "^0.0.7" + "@opensdks/sdk-greenhouse": "^0.0.8" }, "devDependencies": {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9c42d06c..c4d86cc6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -857,8 +857,8 @@ importers: specifier: ^0.0.19 version: 0.0.19 '@opensdks/sdk-greenhouse': - specifier: ^0.0.7 - version: 0.0.7 + specifier: ^0.0.8 + version: 0.0.8 connectors/connector-heron: dependencies: @@ -3606,8 +3606,8 @@ packages: '@opensdks/sdk-finch@0.0.4': resolution: {integrity: sha512-7CxSxCIE8POPBFrEGlovRdkziXNfbrh6GD7fPwIk6GPR2f2CKmLqPxOpLhBhzH/5Ol0nCyzCq8NDsvLbI5OvEQ==} - '@opensdks/sdk-greenhouse@0.0.7': - resolution: {integrity: sha512-WgmMjMO8DXyjnSJ1fvnK1TrNabgUatUOIKHRhUu6WSFJFUzfryoMJfPR8zSWIUzMNrtNS5lthK2x+kY+w4NtBQ==} + '@opensdks/sdk-greenhouse@0.0.8': + resolution: {integrity: sha512-kIN0ckK36QmWUfUNbl7B6Fi8G63JQ/iCWKnY5Ozd+lTRQYQV+ub30FOZk0djiepNOcUkOFv3QSy9K1Sk8B4ADA==} '@opensdks/sdk-heron@0.0.1': resolution: {integrity: sha512-7DOFFlq5C8u060L9GJ6euwE/g8aqIClyZysio4jqYQ1W9rcYX/pZUsZ7NNPt6LQTX+coAz+8nieEi35ejiKbdg==} @@ -14868,7 +14868,7 @@ snapshots: '@opensdks/sdk-finch@0.0.4': {} - '@opensdks/sdk-greenhouse@0.0.7': + '@opensdks/sdk-greenhouse@0.0.8': dependencies: '@opensdks/runtime': 0.0.20 diff --git a/unified/unified-ats/adapters/greenhouse-adapter/index.ts b/unified/unified-ats/adapters/greenhouse-adapter/index.ts index 36ad18e34..bd2590db9 100644 --- a/unified/unified-ats/adapters/greenhouse-adapter/index.ts +++ b/unified/unified-ats/adapters/greenhouse-adapter/index.ts @@ -36,25 +36,25 @@ export const greenhouseAdapter = { if (!jobId) { throw new Error('jobId is required'); } - // @ts-expect-error while greenhouse sdk is updated - const res = await instance.GET(`/v1/jobs/${jobId}/openings`, { + const res = await instance.GET(`/v1/jobs/{id}/openings`, { params: { query: { per_page: input?.page_size, page: cursor, }, + path: { + id: jobId, + } }, }) let nextCursor = undefined - // @ts-expect-error while greenhouse sdk is updated if (input?.page_size && res.data?.length === input?.page_size) { nextCursor = (cursor || 0) + input.page_size } return { has_next_page: !!nextCursor, next_cursor: nextCursor ? String(nextCursor) : undefined, - // @ts-expect-error while greenhouse sdk is updated - items: res.data?.map((d) => applyMapper(mappers.jobOpening, d)) ?? [], + items: res.data?.map((d) => applyMapper(mappers.opening, {job_id: jobId, ...d})) ?? [], } }, listOffers: async ({instance, input}) => {