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

Adding Greenhouse job openings to syncs and unified api #48

Merged
merged 4 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
})
Loading