Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

Commit

Permalink
Adding Greenhouse job openings to syncs and unified api (#48)
Browse files Browse the repository at this point in the history
* adding opening to connector-greenhouse

* adding opening to ats vertical

* adding opening to ats links and improving ag insertion

* updating to latest SDK version
  • Loading branch information
pellicceama authored Oct 17, 2024
1 parent ef95c8a commit 3e9d1b1
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 21 deletions.
1 change: 1 addition & 0 deletions connectors/connector-greenhouse/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const GREENHOUSE_ENTITY_NAMES = [
'application',
'offer',
'candidate',
'opening',
] as const

export const greenhouseSchema = {
Expand Down
2 changes: 1 addition & 1 deletion connectors/connector-greenhouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
21 changes: 15 additions & 6 deletions connectors/connector-greenhouse/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
},
Expand Down
12 changes: 5 additions & 7 deletions connectors/connector-postgres/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -245,7 +246,7 @@ export const postgresServer = {
isOpenInt: true,
}

const isAgInsert =
const isAgInsert =
endUser?.orgId === 'org_2lcCCimyICKI8cpPNQt195h5zrP' ||
endUser?.orgId === 'org_2ms9FdeczlbrDIHJLcwGdpv3dTx'

Expand All @@ -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 || '';
Expand Down
2 changes: 1 addition & 1 deletion kits/cdk/base-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function agColumnRenameLink(_ctx: {
const entityMappings = {
job: 'IntegrationAtsJob',
candidate: 'IntegrationAtsCandidate',
job_opening: 'IntegrationAtsJobOpening',
opening: 'IntegrationAtsJobOpening',
offer: 'IntegrationAtsOffer',
}

Expand Down
2 changes: 1 addition & 1 deletion kits/cdk/verticals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, VerticalInfo>

Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions unified/unified-ats/adapters/greenhouse-adapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions unified/unified-ats/adapters/greenhouse-adapter/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,20 @@ const offer = mapper(zCast<GreenhouseObjectType['offer']>(), unified.offer, {
status: 'status',
})

const opening = mapper(zCast<GreenhouseObjectType['opening'] & {job_id: string}>(), 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
}
5 changes: 5 additions & 0 deletions unified/unified-ats/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
15 changes: 15 additions & 0 deletions unified/unified-ats/unifiedModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})

0 comments on commit 3e9d1b1

Please sign in to comment.