chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { ActionDefinition } from '@botpress/sdk'
|
||||
import {
|
||||
getCandidateInputSchema,
|
||||
getCandidateOutputSchema,
|
||||
listCandidatesInputSchema,
|
||||
listCandidatesOutputSchema,
|
||||
postCandidateInJobInputSchema,
|
||||
postCandidateInJobOutputSchema,
|
||||
postCandidateInTalentPoolInputSchema,
|
||||
postCandidateInTalentPoolOutputSchema,
|
||||
updateCandidateInputSchema,
|
||||
updateCandidateOutputSchema,
|
||||
} from 'definitions/models/candidates'
|
||||
|
||||
export const listCandidates = {
|
||||
title: 'List candidates',
|
||||
description: 'List the candidates for a job',
|
||||
input: {
|
||||
schema: listCandidatesInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: listCandidatesOutputSchema,
|
||||
},
|
||||
} satisfies ActionDefinition
|
||||
|
||||
export const getCandidate = {
|
||||
title: 'Get candidate',
|
||||
description: 'Get a candidate by ID',
|
||||
input: {
|
||||
schema: getCandidateInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: getCandidateOutputSchema,
|
||||
},
|
||||
} satisfies ActionDefinition
|
||||
|
||||
export const createCandidateInJob = {
|
||||
title: 'Create candidate in job',
|
||||
description: 'Create a candidate in the specified job',
|
||||
input: {
|
||||
schema: postCandidateInJobInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: postCandidateInJobOutputSchema,
|
||||
},
|
||||
} satisfies ActionDefinition
|
||||
|
||||
export const createCandidateInTalentPool = {
|
||||
title: 'Create candidate in talent pool',
|
||||
description: 'Create a candidate in the talent pool',
|
||||
input: {
|
||||
schema: postCandidateInTalentPoolInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: postCandidateInTalentPoolOutputSchema,
|
||||
},
|
||||
} satisfies ActionDefinition
|
||||
|
||||
export const updateCandidate = {
|
||||
title: 'Update candidate',
|
||||
description: 'Update an existing candidate. Omitted fields will remain unchanged.',
|
||||
input: {
|
||||
schema: updateCandidateInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: updateCandidateOutputSchema,
|
||||
},
|
||||
} satisfies ActionDefinition
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getJobQuestionsInputSchema, getJobQuestionsOutputSchema } from 'definitions/models/jobs'
|
||||
|
||||
export const getJobQuestions = {
|
||||
title: 'Get job questions',
|
||||
description: 'Get the questions associated with a job offer',
|
||||
input: {
|
||||
schema: getJobQuestionsInputSchema,
|
||||
},
|
||||
output: {
|
||||
schema: getJobQuestionsOutputSchema,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { EventDefinition, z } from '@botpress/sdk'
|
||||
import { candidateSchema } from 'definitions/models/candidates'
|
||||
import { webhookEvent } from './common'
|
||||
|
||||
export const eventTypes = z.enum(['candidate_created', 'candidate_moved'])
|
||||
|
||||
export const candidateCreatedSchema = webhookEvent
|
||||
.extend({
|
||||
data: candidateSchema.title('Data').describe('The candidate that was created'),
|
||||
eventType: z.literal(eventTypes.Enum.candidate_created).title('Event Type').describe('The type of event'),
|
||||
})
|
||||
.title('Data')
|
||||
.describe('Event data')
|
||||
|
||||
export const candidateMovedSchema = webhookEvent
|
||||
.extend({
|
||||
data: candidateSchema.title('Data').describe('The candidate that was moved'),
|
||||
eventType: z.literal(eventTypes.Enum.candidate_moved).title('Event Type').describe('The type of event'),
|
||||
})
|
||||
.title('Data')
|
||||
.describe('Event data')
|
||||
|
||||
export const candidateCreated = {
|
||||
title: 'Candidate Created',
|
||||
description: 'A candidate was created on a job.',
|
||||
schema: candidateCreatedSchema,
|
||||
} satisfies EventDefinition
|
||||
|
||||
export const candidateMoved = {
|
||||
title: 'Candidate Moved',
|
||||
description: 'A candidate was moved on a job.',
|
||||
schema: candidateMovedSchema,
|
||||
} satisfies EventDefinition
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
export const webhookEvent = z.object({
|
||||
firedAt: z.string().title('Fired At').describe('The date and time the event was triggered'),
|
||||
id: z.string().title('Id').describe('The event identifier'),
|
||||
resourceType: z.string().title('Resource Type').describe('The type of the payload resource'),
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
const freeTextAnswerSchema = z.object({
|
||||
body: z.string().title('Body').describe("The candidate's response"),
|
||||
})
|
||||
|
||||
const shortTextAnswerSchema = z.object({
|
||||
body: z.string().max(128).title('Body').describe("The candidate's response"),
|
||||
})
|
||||
|
||||
const booleanAnswerSchema = z.object({
|
||||
checked: z.boolean().title('Checked').describe("The candidate's response"),
|
||||
})
|
||||
|
||||
const multipleChoiceAnswerSchema = z.object({
|
||||
choices: z.array(z.string()).title('Choices').describe('The IDs of the choice(s) selected'),
|
||||
})
|
||||
|
||||
const dropdownAnswerSchema = z.object({
|
||||
choice: z.string().title('Choice').describe('The ID of the choice selected'),
|
||||
})
|
||||
|
||||
const dateAnswerSchema = z.object({
|
||||
date: z.string().title('Date').describe('The date in ISO 8601 format'),
|
||||
})
|
||||
|
||||
const numericAnswerSchema = z.object({
|
||||
number: z.number().title('Number').describe('The value may be an integer or a decimal number'),
|
||||
})
|
||||
|
||||
const fileAnswerUrlSchema = z.object({
|
||||
fileUrl: z.string().title('File Url').describe("A url pointing to the candidate's answer"),
|
||||
})
|
||||
const fileAnswerBase64Schema = z.object({
|
||||
data: z.string().title('Data').describe("The candidate's answer encoded in base64"),
|
||||
})
|
||||
|
||||
export const answerSchema = z.object({
|
||||
question: z.object({
|
||||
body: z.string().nullable().title('Question').describe('The question'),
|
||||
}),
|
||||
answer: z
|
||||
.union([
|
||||
freeTextAnswerSchema,
|
||||
shortTextAnswerSchema,
|
||||
booleanAnswerSchema,
|
||||
multipleChoiceAnswerSchema,
|
||||
dropdownAnswerSchema,
|
||||
dateAnswerSchema,
|
||||
numericAnswerSchema,
|
||||
fileAnswerUrlSchema,
|
||||
fileAnswerBase64Schema,
|
||||
])
|
||||
.nullable()
|
||||
.title('Answer')
|
||||
.describe('The answer'),
|
||||
})
|
||||
|
||||
export const postAnswerSchema = z.object({
|
||||
questionKey: z.string().title('Question Key').describe('The question key'),
|
||||
body: z.string().optional().title('Body').describe("The candidate's response"),
|
||||
checked: z.boolean().optional().title('Checked').describe("The candidate's response"),
|
||||
choices: z.array(z.string()).optional().title('Choices').describe('The IDs of the choice(s) selected'),
|
||||
date: z.string().optional().title('Date').describe('The date in ISO 8601 format'),
|
||||
value: z.number().optional().title('Value').describe('The value may be an integer or a decimal number'),
|
||||
fileUrl: z.string().optional().title('File Url').describe("A url pointing to the candidate's answer"),
|
||||
data: z.string().optional().title('Data').describe("The candidate's answer encoded in base64"),
|
||||
})
|
||||
@@ -0,0 +1,470 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { answerSchema, postAnswerSchema } from './answers'
|
||||
|
||||
export const socialProfileTypesSchema = z.enum([
|
||||
'academiaedu',
|
||||
'angellist',
|
||||
'behance',
|
||||
'bitbucket',
|
||||
'blogger',
|
||||
'crunchbase',
|
||||
'dandyid',
|
||||
'delicious',
|
||||
'deviantart',
|
||||
'digg',
|
||||
'doyoubuzz',
|
||||
'dribble',
|
||||
'dribbble',
|
||||
'econsultancy',
|
||||
'facebook',
|
||||
'flavorsme',
|
||||
'flickr',
|
||||
'fullcontact',
|
||||
'getglue',
|
||||
'gist',
|
||||
'github',
|
||||
'goodreads',
|
||||
'googleplus',
|
||||
'gravatar',
|
||||
'hackernews',
|
||||
'hiim',
|
||||
'klout',
|
||||
'lanyrd',
|
||||
'linkedin',
|
||||
'myspace',
|
||||
'ohloh',
|
||||
'orkut',
|
||||
'pinterest',
|
||||
'quora',
|
||||
'reddit',
|
||||
'scribd',
|
||||
'slideshare',
|
||||
'stackexchange',
|
||||
'stackoverflow',
|
||||
'tumblr',
|
||||
'twitter',
|
||||
'typepad',
|
||||
'vk',
|
||||
'wordpress',
|
||||
'xing',
|
||||
])
|
||||
|
||||
export const baseCandidateSchema = z
|
||||
.object({
|
||||
id: z.string().title('Id').describe('The candidate identifier'),
|
||||
name: z.string().title('Name').describe("The candidate's full name"),
|
||||
firstName: z.string().title('First Name').describe("The candidate's first name"),
|
||||
lastName: z.string().title('Last Name').describe("The candidate's last name"),
|
||||
email: z.string().nullable().title('Email').describe("The candidate's email address"),
|
||||
headline: z.string().nullable().title('Headline').describe("The candidate's headline"),
|
||||
account: z
|
||||
.object({
|
||||
subdomain: z.string().nullable().title('Subdomain').describe('The account subdomain'),
|
||||
name: z.string().nullable().title('Account Name').describe('The account name'),
|
||||
})
|
||||
.partial()
|
||||
.nullable()
|
||||
.describe('The account details'),
|
||||
stage: z.string().nullable().title('Stage').describe("The candidate's current stage slug"),
|
||||
stageKind: z.string().nullable().title('Stage').describe("The candidate's current stage kind"),
|
||||
disqualified: z.boolean().title('Disqualified').describe('Flag indicating whether the candidate is disqualified'),
|
||||
withdrew: z.boolean().title('Withdrew').describe('Flag indicating whether the candidate withdrew'),
|
||||
disqualificationReason: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Disqualification Reason')
|
||||
.describe('Reason for disqualification, if applicable'),
|
||||
sourced: z.boolean().title('Sourced').describe('Flag indicating whether the candidate has been sourced'),
|
||||
profileUrl: z.string().nullable().title('Profile URL').describe("The URL to the candidate's profile in Workable"),
|
||||
domain: z.string().nullable().title('Domain').describe('Where the candidate came from'),
|
||||
createdAt: z.string().title('Created At').describe('The creation timestamp of the candidate record'),
|
||||
updatedAt: z.string().title('Updated At').describe('The last update timestamp of the candidate record'),
|
||||
hiredAt: z.string().nullable().title('Hired At').describe('The date the candidate was moved to the hired stage'),
|
||||
address: z.string().nullable().title('Address').describe("The candidate's address"),
|
||||
phone: z.string().nullable().title('Phone Number').describe("The candidate's phone number"),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const candidateSchema = baseCandidateSchema
|
||||
.extend({
|
||||
job: z
|
||||
.object({
|
||||
shortCode: z.string().title('Shortcode').describe("The job's system generated code"),
|
||||
title: z.string().title('Job Title').describe('The job title'),
|
||||
})
|
||||
.partial()
|
||||
.describe('The job details'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const listCandidatesOutputSchema = z.object({
|
||||
candidates: z.array(candidateSchema).title('Candidates').describe('The array of candidates'),
|
||||
nextToken: z.string().optional().title('Next Token').describe('The token for the next page of results'),
|
||||
})
|
||||
|
||||
export const listCandidatesInputSchema = z
|
||||
.object({
|
||||
email: z.string().title('Email').describe('The email of the candidate to filter by'),
|
||||
shortCode: z.string().title('Shortcode').describe("The job's system generated code"),
|
||||
stage: z.string().title('Stage').describe("The job's stage slug, can be retrieved from the '/stages' endpoint"),
|
||||
limit: z.number().title('Limit').describe('Specifies the number of candidates to try and retrieve per page'),
|
||||
nextToken: z
|
||||
.string()
|
||||
.title('Next Token')
|
||||
.describe('Returns results with an ID greater than or equal to the specified ID'),
|
||||
createdAfter: z.string().title('Created After').describe('Returns results created after the specified timestamp'),
|
||||
updatedAfter: z.string().title('Updated After').describe('Returns results updated after the specified timestamp'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const educationEntrySchema = z
|
||||
.object({
|
||||
id: z.string().title('Id').describe('The education entry identifier'),
|
||||
degree: z.string().nullable().title('Degree').describe('The graduation degree'),
|
||||
school: z.string().title('School').describe('The name of the school graduated'),
|
||||
fieldOfStudy: z.string().nullable().title('Field Of Study').describe('The field of study'),
|
||||
startDate: z.string().nullable().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().nullable().title('End Date').describe('The date ended'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const socialProfileSchema = z
|
||||
.object({
|
||||
type: z.string().title('Type').describe('The slug name of the social profile'),
|
||||
name: z.string().nullable().title('Name').describe('The full name of the social profile'),
|
||||
username: z.string().nullable().title('Username').describe('The username of the social profile'),
|
||||
url: z.string().title('Url').describe("Url to the candidate's social profile page"),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const experienceEntrySchema = z
|
||||
.object({
|
||||
id: z.string().title('Id').describe('The experience entry identifier'),
|
||||
title: z.string().title('Title').describe('The title of the experience entry'),
|
||||
summary: z.string().nullable().title('Summary').describe('The summary of the experience entry'),
|
||||
startDate: z.string().nullable().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().nullable().title('End Date').describe('The date ended'),
|
||||
company: z.string().nullable().title('Company').describe('The company name'),
|
||||
industry: z.string().nullable().title('Industry').describe('The industry of the company'),
|
||||
current: z.boolean().nullable().title('Current').describe('Indicates if currently works there'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const updateEducationEntrySchema = z.object({
|
||||
id: z.string().optional().title('Id').describe('The education entry identifier'),
|
||||
degree: z.string().optional().title('Degree').describe('The graduation degree'),
|
||||
school: z.string().title('School').describe('The name of the school graduated'),
|
||||
fieldOfStudy: z.string().optional().title('Field Of Study').describe('The field of study'),
|
||||
startDate: z.string().optional().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().optional().title('End Date').describe('The date ended'),
|
||||
})
|
||||
|
||||
export const updateSocialProfileSchema = z.object({
|
||||
type: socialProfileTypesSchema.title('Type').describe('The slug name of the social profile'),
|
||||
name: z.string().optional().title('Name').describe('The full name of the social profile'),
|
||||
username: z.string().optional().title('Username').describe('The username of the social profile'),
|
||||
url: z.string().title('Url').describe("Url to the candidate's social profile page"),
|
||||
})
|
||||
|
||||
export const updateExperienceEntrySchema = z.object({
|
||||
id: z.string().optional().title('Id').describe('The experience entry identifier'),
|
||||
title: z.string().title('Title').describe('The title of the experience entry'),
|
||||
summary: z.string().optional().title('Summary').describe('The summary of the experience entry'),
|
||||
startDate: z.string().optional().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().optional().title('End Date').describe('The date ended'),
|
||||
company: z.string().optional().title('Company').describe('The company name'),
|
||||
industry: z.string().optional().title('Industry').describe('The industry of the company'),
|
||||
current: z.boolean().optional().title('Current').describe('Indicates if currently works there'),
|
||||
})
|
||||
|
||||
export const locationSchema = z
|
||||
.object({
|
||||
locationString: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Location String')
|
||||
.describe('The string representation of the location'),
|
||||
country: z.string().nullable().title('Country').describe('The country full name'),
|
||||
countryCode: z.string().nullable().title('CountryCode').describe('The 2-letter ISO code of the country'),
|
||||
region: z.string().nullable().title('Region').describe('The region of the candidate'),
|
||||
regionCode: z.string().nullable().title('Region Code').describe('The code of the region of the candidate'),
|
||||
city: z.string().nullable().title('City').describe('The city of the candidate'),
|
||||
zipCode: z.string().nullable().title('Zip Code').describe('The ZIP code of the candidate'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const detailedCandidateSchemaExtraFields = {
|
||||
imageUrl: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Image Url')
|
||||
.describe("Url of candidate's avatar. Available only if provided by the candidate"),
|
||||
disqualifiedAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Disqualified At')
|
||||
.describe('The timestamp the candidate was disqualified'),
|
||||
outboundMailbox: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Outbound Mailbox')
|
||||
.describe(
|
||||
'Mailbox that can be used to communicate with the candidate and inform the recruitment team of the job as well'
|
||||
),
|
||||
uploaderId: z.string().nullable().title('Uploader Id').describe('The ID of the member who uploaded the candidate'),
|
||||
coverLetter: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Cover Letter')
|
||||
.describe('The cover letter provided when the candidate applied'),
|
||||
summary: z.string().nullable().title('Summary').describe('The summary of the candidate'),
|
||||
educationEntries: z
|
||||
.array(educationEntrySchema)
|
||||
.title('Education Entries')
|
||||
.describe('A collection with education entries'),
|
||||
experienceEntries: z
|
||||
.array(experienceEntrySchema)
|
||||
.title('Experience Entries')
|
||||
.describe('A collection with experience entries'),
|
||||
skills: z
|
||||
.array(z.object({ name: z.string().title('Name') }).partial())
|
||||
.title('Skills')
|
||||
.describe('A collection of skills with names'),
|
||||
answers: z.array(answerSchema).title('Answers').describe('A collection with answers provided'),
|
||||
resumeUrl: z.string().nullable().title('Resume Url').describe('Url to the candidate resume'),
|
||||
socialProfiles: z
|
||||
.array(socialProfileSchema)
|
||||
.title('Social Profiles')
|
||||
.describe('A collection with social profiles of candidates'),
|
||||
tags: z.array(z.string()).title('Tags').describe('A collection of tags'),
|
||||
location: locationSchema.title('Location').nullable().describe('The location of the candidate'),
|
||||
originatingCandidateId: z
|
||||
.string()
|
||||
.nullable()
|
||||
.title('Originating Candidate Id')
|
||||
.describe('The ID this candidate originated from'),
|
||||
}
|
||||
|
||||
export const baseDetailedCandidateSchema = baseCandidateSchema.extend(detailedCandidateSchemaExtraFields).partial()
|
||||
|
||||
export const detailedCandidateSchema = candidateSchema.extend(detailedCandidateSchemaExtraFields).partial()
|
||||
|
||||
export const getCandidateOutputSchema = z
|
||||
.object({
|
||||
candidate: detailedCandidateSchema.title('Candidate').describe('The candidate found'),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const getCandidateInputSchema = z.object({
|
||||
id: z.string().title('ID').describe("The candidate's ID"),
|
||||
})
|
||||
|
||||
export const postEducationEntrySchema = z.object({
|
||||
degree: z.string().optional().title('Degree').describe('The graduation degree'),
|
||||
school: z.string().title('School').describe('The name of the school graduated'),
|
||||
fieldOfStudy: z.string().optional().title('Field Of Study').describe('The field of study'),
|
||||
startDate: z.string().optional().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().optional().title('End Date').describe('The date ended'),
|
||||
})
|
||||
|
||||
export const postSocialProfileSchema = z.object({
|
||||
type: socialProfileTypesSchema.title('Type').describe('The slug name of the social profile'),
|
||||
username: z.string().optional().title('Username').describe('The username of the social profile'),
|
||||
url: z.string().title('Url').describe("Url to the candidate's social profile page"),
|
||||
})
|
||||
|
||||
export const postExperienceEntrySchema = z.object({
|
||||
title: z.string().title('Title').describe('The title of the experience entry'),
|
||||
summary: z.string().optional().title('Summary').describe('The summary of the experience entry'),
|
||||
startDate: z.string().optional().title('Start Date').describe('The date started'),
|
||||
endDate: z.string().optional().title('End Date').describe('The date ended'),
|
||||
company: z.string().optional().title('Company').describe('The company name'),
|
||||
current: z.boolean().optional().title('Current').describe('Indicates if currently works there'),
|
||||
industry: z.string().optional().title('Industry').describe('The industry of the company'),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolSchema = z.object({
|
||||
firstName: z.string().title('First Name').describe("The candidate's first name"),
|
||||
lastName: z.string().title('Last Name').describe("The candidate's last name"),
|
||||
email: z.string().title('Email').describe("The candidate's email address"),
|
||||
headline: z.string().optional().title('Headline').describe("The candidate's headline"),
|
||||
summary: z.string().optional().title('Summary').describe('The summary of the candidate'),
|
||||
address: z.string().optional().title('Address').describe("The candidate's address"),
|
||||
phone: z.string().optional().title('Phone Number').describe("The candidate's phone number"),
|
||||
coverLetter: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Cover Letter')
|
||||
.describe('The cover letter provided when the candidate applied'),
|
||||
educationEntries: z
|
||||
.array(postEducationEntrySchema)
|
||||
.optional()
|
||||
.title('Education Entries')
|
||||
.describe('A collection with education entries'),
|
||||
experienceEntries: z
|
||||
.array(postExperienceEntrySchema)
|
||||
.optional()
|
||||
.title('Experience Entries')
|
||||
.describe('A collection with experience entries'),
|
||||
skills: z.array(z.string()).optional().title('Skills').describe('A collection of skills with names'),
|
||||
tags: z.array(z.string()).optional().title('Tags').describe('A collection of tags'),
|
||||
disqualified: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.title('Disqualified')
|
||||
.describe('Flag indicating whether the candidate is disqualified'),
|
||||
disqualificationReason: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Disqualification Reason')
|
||||
.describe('Reason for disqualification, if applicable'),
|
||||
disqualifiedAt: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Disqualified At')
|
||||
.describe('The timestamp the candidate was disqualified'),
|
||||
socialProfiles: z
|
||||
.array(postSocialProfileSchema)
|
||||
.optional()
|
||||
.title('Social Profiles')
|
||||
.describe('A collection with social profiles of candidates'),
|
||||
domain: z.string().optional().title('Domain').describe('Where the candidate came from'),
|
||||
recruiterKey: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Recruiter Key')
|
||||
.describe('The key corresponding to the recruiter who sourced the candidate'),
|
||||
resumeUrl: z.string().optional().title('Resume Url').describe('Url to the candidate resume'),
|
||||
resume: z
|
||||
.object({
|
||||
name: z.string().title('Name').describe('The name of the file'),
|
||||
data: z.string().title('Data').describe('The base64 encoded data'),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const postCandidateInJobSchema = postCandidateInTalentPoolSchema.extend({
|
||||
answers: z.array(postAnswerSchema).optional().title('Answers').describe('A collection with answers provided'),
|
||||
})
|
||||
|
||||
export const postCandidateInJobOutputSchema = z.object({
|
||||
status: z.string().title('Status').describe('The status of the candidate'),
|
||||
candidate: detailedCandidateSchema.title('Candidate').describe('The candidate created'),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolOutputSchema = z.object({
|
||||
status: z.string().title('Status').describe('The status of the candidate'),
|
||||
candidate: baseDetailedCandidateSchema
|
||||
.extend({
|
||||
talentPool: z
|
||||
.object({
|
||||
talentPoolId: z.number().title('Talent Pool ID').describe('The ID of the candidate in the talent pool'),
|
||||
})
|
||||
.title('Talent Pool')
|
||||
.describe('The talent pool fields'),
|
||||
})
|
||||
.title('Candidate')
|
||||
.describe('The candidate created'),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolInputSchema = z.object({
|
||||
sourced: z.boolean().optional().title('Sourced').describe('Indicates if the candidate is sourced or applied'),
|
||||
candidate: postCandidateInTalentPoolSchema.title('Candidate').describe('The candidate to create'),
|
||||
})
|
||||
|
||||
export const postCandidateInJobInputSchema = z.object({
|
||||
sourced: z.boolean().optional().title('Sourced').describe('Indicates if the candidate is sourced or applied'),
|
||||
candidate: postCandidateInJobSchema.title('Candidate').describe('The candidate to create'),
|
||||
shortCode: z.string().title('Short Code').describe('The shortcode of the job the candidate is applying to'),
|
||||
})
|
||||
|
||||
export const imageSource = z.enum([
|
||||
'academiaedu',
|
||||
'angellist',
|
||||
'behance',
|
||||
'bitbucket',
|
||||
'blogger',
|
||||
'crunchbase',
|
||||
'dandyid',
|
||||
'delicious',
|
||||
'deviantart',
|
||||
'digg',
|
||||
'doyoubuzz',
|
||||
'dribble',
|
||||
'dribbble',
|
||||
'econsultancy',
|
||||
'facebook',
|
||||
'flavorsme',
|
||||
'flickr',
|
||||
'fullcontact',
|
||||
'getglue',
|
||||
'gist',
|
||||
'github',
|
||||
'goodreads',
|
||||
'googleplus',
|
||||
'gravatar',
|
||||
'hackernews',
|
||||
'hiim',
|
||||
'klout',
|
||||
'lanyrd',
|
||||
'linkedin',
|
||||
'myspace',
|
||||
'ohloh',
|
||||
'orkut',
|
||||
'pinterest',
|
||||
'quora',
|
||||
'reddit',
|
||||
'scribd',
|
||||
'skype',
|
||||
'slideshare',
|
||||
'stackexchange',
|
||||
'stackoverflow',
|
||||
'tumblr',
|
||||
'twitter',
|
||||
'typepad',
|
||||
'vk',
|
||||
'wordpress',
|
||||
'xing',
|
||||
])
|
||||
|
||||
export const updateCandidateInputSchema = z.object({
|
||||
id: z.string().title('ID').describe("The candidate to update's id"),
|
||||
candidate: postCandidateInTalentPoolSchema
|
||||
.extend({
|
||||
textingConsent: z.enum(['forced', 'declined']).title('Texting Consent'),
|
||||
imageUrl: z.string().title('Image Url').describe("A url pointing to the candidate's image"),
|
||||
imageSource: imageSource
|
||||
.title('Image Source')
|
||||
.describe('The source of the image (if not provided by the candidate)'),
|
||||
image: z
|
||||
.object({
|
||||
name: z.string().title('Name').describe("The candidate's image name"),
|
||||
data: z.string().title('Data').describe("The candidate's image encodede in base64"),
|
||||
source: imageSource.title('Image Source').describe('The image source'),
|
||||
})
|
||||
.partial(),
|
||||
educationEntries: z
|
||||
.array(updateEducationEntrySchema)
|
||||
.describe('Existing entries will be deleted if not included in the new array.'),
|
||||
experienceEntries: z
|
||||
.array(updateExperienceEntrySchema)
|
||||
.describe('Existing entries will be deleted if not included in the new array.'),
|
||||
socialProfiles: z
|
||||
.array(updateSocialProfileSchema)
|
||||
.describe('Existing profiles will be deleted if not included in the new array.'),
|
||||
})
|
||||
.omit({
|
||||
disqualified: true,
|
||||
disqualificationReason: true,
|
||||
disqualifiedAt: true,
|
||||
domain: true,
|
||||
recruiterKey: true,
|
||||
})
|
||||
.partial()
|
||||
.title('Candidate')
|
||||
.describe('The candidate to update'),
|
||||
})
|
||||
|
||||
export const updateCandidateOutputSchema = z.object({
|
||||
candidate: detailedCandidateSchema.title('Candidate').describe('The candidate updated'),
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
export const getJobQuestionsInputSchema = z.object({
|
||||
shortCode: z.string().title('Shortcode').describe("The job's system generated code"),
|
||||
})
|
||||
|
||||
export const questionType = z.enum(['free_text', 'multiple_choice', 'boolean', 'dropdown', 'numeric', 'date', 'file'])
|
||||
|
||||
export const questionSchema = z.object({
|
||||
id: z.string().title('id').describe("The question's id"),
|
||||
body: z.string().title('Body').describe("The question's body"),
|
||||
type: questionType.title('Question Type').describe("The question's type"),
|
||||
required: z.boolean().title('Required').describe('specifies wether providing an answer to the question is required'),
|
||||
singleAnswer: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.title('Single Answer')
|
||||
.describe('Wether the question allows for multiple answers. Defined only for type multiple_choice.'),
|
||||
choices: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().title('ID').describe("The choice's id"),
|
||||
body: z.string().title('Body').describe("The choice's text"),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.title('Choices')
|
||||
.describe('An array specifying the possible answers. Defined only for types multiple_choice and dropdown.'),
|
||||
supportedFileTypes: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.title('Supported File Types')
|
||||
.describe('An array specifying the accepted file types. Defined only if question type is file.'),
|
||||
maxFileSize: z
|
||||
.number()
|
||||
.optional()
|
||||
.title('Max File Size')
|
||||
.describe('The maximum file size in bytes. Defined only if question is file'),
|
||||
})
|
||||
|
||||
export const getJobQuestionsOutputSchema = z.object({
|
||||
questions: z.array(questionSchema).title('questions').describe('The questions'),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
# Workable
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="477" height="477">
|
||||
<path d="M0 0 C0.74378906 0.53109375 1.48757813 1.0621875 2.25390625 1.609375 C13.37262324 10.39990663 21.39962712 25.12204871 23.71875 38.9921875 C26.18225509 65.33800579 18.42950121 89.05195767 6 112 C5.61440918 112.72429199 5.22881836 113.44858398 4.83154297 114.19482422 C-12.61152252 146.90672879 -35.93884075 176.97499224 -66 199 C-66.9178125 199.70125 -67.835625 200.4025 -68.78125 201.125 C-94.25500682 220.30524043 -124.7968889 233.73109055 -157.20556641 229.72753906 C-161.22269903 229.09836142 -165.09560416 228.12002776 -169 227 C-169.83789062 226.80664062 -170.67578125 226.61328125 -171.5390625 226.4140625 C-202.38759394 218.67680869 -226.34325905 189.8337829 -242.1796875 163.875 C-251.37679119 148.35660881 -258.97180648 132.00405532 -265 115 C-265.26522461 114.25733887 -265.53044922 113.51467773 -265.80371094 112.74951172 C-273.09174756 92.18132362 -277.74213942 71.2296279 -281.23217773 49.71508789 C-281.56039188 47.70044007 -281.89633215 45.68717496 -282.23242188 43.67382812 C-285.04568501 26.585306 -285 10.62016131 -285 -7 C-266.85 -7 -248.7 -7 -230 -7 C-229.2575 -0.1525 -228.515 6.695 -227.75 13.75 C-226.94860159 20.94275672 -226.13237295 28.13367159 -225.30664062 35.32366943 C-225.04026268 37.64859107 -224.77749578 39.97388432 -224.515625 42.29931641 C-222.85021504 56.7386521 -220.07776296 70.72523376 -216.5625 84.8125 C-216.37522339 85.56395294 -216.18794678 86.31540588 -215.99499512 87.08963013 C-211.17885632 106.30421126 -211.17885632 106.30421126 -203.36328125 124.42578125 C-202.35220182 126.33495415 -201.40000875 128.27555981 -200.48046875 130.23046875 C-189.55755449 153.27858525 -173.1928697 170.88071819 -148.82519531 179.7956543 C-142.40828911 181.94033202 -136.61064679 182.42927612 -129.875 182.3125 C-128.46927734 182.31733398 -128.46927734 182.31733398 -127.03515625 182.32226562 C-101.60126173 182.12662028 -80.76540733 165.50666671 -63 149 C-62.27167969 148.34902344 -61.54335938 147.69804687 -60.79296875 147.02734375 C-43.37378682 130.7380547 -28.49172524 104.93742917 -22 82 C-21.72414063 81.21367187 -21.44828125 80.42734375 -21.1640625 79.6171875 C-18.14266574 70.33124332 -16.84139219 58.19215176 -20.5625 48.9375 C-24.41775638 41.98186261 -29.13596881 36.49793563 -36.6796875 33.62109375 C-43.76936501 31.72335179 -50.41802596 32.91431085 -57 36 C-63.15379312 40.14197614 -66.53385135 45.43935329 -69.3125 52.1875 C-72.80496411 71.55480097 -65.36847679 91.78902123 -54.74267578 107.90722656 C-53.02220735 110.27678397 -51.22453017 112.53845178 -49.30078125 114.74609375 C-48 117 -48 117 -48.1484375 119.96875 C-51.22927998 130.93541865 -58.8821249 139.33897501 -67 147 C-67.99 147.33 -68.98 147.66 -70 148 C-78.33115778 137.01583407 -86.28140554 126.04756273 -93 114 C-93.56992676 112.99936523 -93.56992676 112.99936523 -94.15136719 111.97851562 C-100.00532701 101.66048916 -104.42272144 91.30251825 -108 80 C-108.32871094 78.99582031 -108.65742187 77.99164063 -108.99609375 76.95703125 C-114.05294619 60.96382807 -115.20453245 39.40279351 -108 24 C-107.59910156 23.07960937 -107.19820312 22.15921875 -106.78515625 21.2109375 C-100.98705901 9.27862145 -91.61700206 0.19020253 -80 -6 C-79.13890625 -6.47050781 -78.2778125 -6.94101563 -77.390625 -7.42578125 C-52.53813382 -19.88879088 -22.08063761 -16.09320503 0 0 Z " fill="#02756A" transform="translate(285,131)"/>
|
||||
<path d="M0 0 C12.87 0.66 25.74 1.32 39 2 C39 8.08003431 38.87592291 13.57185028 37.9375 19.5 C37.74885376 20.6919397 37.74885376 20.6919397 37.55639648 21.90795898 C36.32488128 29.07730792 34.51533228 36.05909619 32.5625 43.0625 C32.27431396 44.09632813 31.98612793 45.13015625 31.68920898 46.1953125 C29.83134401 52.63937563 27.63655349 58.83469706 25 65 C24.67668701 65.75845215 24.35337402 66.5169043 24.02026367 67.29833984 C7.70139675 104.75986416 -17.77902312 134.70548139 -56.0703125 150.24609375 C-66.62607151 154.25963443 -76.01170669 155.31972212 -87.3125 155.25 C-88.55451172 155.25773438 -89.79652344 155.26546875 -91.07617188 155.2734375 C-106.98172414 155.24007504 -121.29269569 152.02458534 -135.5625 145 C-136.24151367 144.66790527 -136.92052734 144.33581055 -137.62011719 143.99365234 C-141.34035596 142.09433325 -144.24044602 140.1457511 -147 137 C-146.58226318 136.61779297 -146.16452637 136.23558594 -145.73413086 135.84179688 C-133.38503727 124.80982964 -133.38503727 124.80982964 -123 112 C-119.07891308 112.63915153 -115.75921808 113.84693962 -112.125 115.4375 C-103.52873643 119.19696253 -96.37289678 120.31858487 -87.0625 120.25 C-86.01642578 120.25773437 -84.97035156 120.26546875 -83.89257812 120.2734375 C-68.30563234 120.2342497 -54.30062042 114.09433687 -43.09130859 103.26635742 C-15.53582139 75.49817778 -5.9407713 37.54567458 0 0 Z " fill="#02756A" transform="translate(438,188)"/></svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,58 @@
|
||||
import { z, IntegrationDefinition } from '@botpress/sdk'
|
||||
import {
|
||||
createCandidateInJob,
|
||||
createCandidateInTalentPool,
|
||||
getCandidate,
|
||||
listCandidates,
|
||||
updateCandidate,
|
||||
} from 'definitions/actions/candidates'
|
||||
import { getJobQuestions } from 'definitions/actions/jobs'
|
||||
import { candidateCreated, candidateMoved } from 'definitions/events/candidates'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'workable',
|
||||
title: 'Workable',
|
||||
description: 'Integration with Workable for Botpress',
|
||||
version: '0.1.2',
|
||||
readme: 'hub.md',
|
||||
icon: 'icon.svg',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
apiToken: z
|
||||
.string()
|
||||
.min(1, 'API token is required')
|
||||
.describe('Your Workable API access token')
|
||||
.title('API access token'),
|
||||
subDomain: z
|
||||
.string()
|
||||
.min(1, 'Account subdomain is required')
|
||||
.describe('Your account subdomain is required')
|
||||
.title('Account subdomain'),
|
||||
}),
|
||||
},
|
||||
states: {
|
||||
webhooks: {
|
||||
type: 'integration',
|
||||
schema: z.object({
|
||||
ids: z.array(z.number()).title('ID').describe('The ID of the webhook'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
listCandidates,
|
||||
getCandidate,
|
||||
createCandidateInJob,
|
||||
createCandidateInTalentPool,
|
||||
updateCandidate,
|
||||
getJobQuestions,
|
||||
},
|
||||
events: {
|
||||
candidateCreated,
|
||||
candidateMoved,
|
||||
},
|
||||
attributes: {
|
||||
category: 'Business Operations',
|
||||
guideSlug: 'workable',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@botpresshub/workable",
|
||||
"scripts": {
|
||||
"build": "bp add -y && bp build",
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Request, z } from '@botpress/sdk'
|
||||
import crypto from 'crypto'
|
||||
import { eventTypes } from 'definitions/events/candidates'
|
||||
import { toCandidateCreatedEventModel, toCandidateMovedEventModel } from './mapping/candidate-mapper'
|
||||
import { webhookRequestSchema } from './workable-schemas/events'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const isEventTypeHandled = (request: z.infer<typeof webhookRequestSchema>) => {
|
||||
return eventTypes.options.includes(request.event_type)
|
||||
}
|
||||
|
||||
type VerifyWebhookSignatureReturn =
|
||||
| {
|
||||
isSignatureValid: true
|
||||
signatureError: null
|
||||
}
|
||||
| {
|
||||
isSignatureValid: false
|
||||
signatureError: string
|
||||
}
|
||||
|
||||
const _verifyWebhookSignature = (encryptionKey: string, request: Request): VerifyWebhookSignatureReturn => {
|
||||
const signature = request.headers['x-workable-signature']
|
||||
|
||||
if (!signature) {
|
||||
return {
|
||||
isSignatureValid: false,
|
||||
signatureError: 'Missing signature headers',
|
||||
}
|
||||
}
|
||||
|
||||
const expected = crypto
|
||||
.createHmac('sha256', encryptionKey)
|
||||
.update(request.body ?? '')
|
||||
.digest('hex')
|
||||
|
||||
if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))) {
|
||||
return {
|
||||
isSignatureValid: false,
|
||||
signatureError: 'Signature invalid',
|
||||
}
|
||||
}
|
||||
return {
|
||||
isSignatureValid: true,
|
||||
signatureError: null,
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: bp.IntegrationProps['handler'] = async (props) => {
|
||||
const { isSignatureValid, signatureError } = _verifyWebhookSignature(props.ctx.configuration.apiToken, props.req)
|
||||
if (!isSignatureValid) {
|
||||
props.logger.forBot().error(`Webhook Signature Verification: ${signatureError}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.req.body) {
|
||||
props.logger.forBot().error('Handler received an empty body')
|
||||
return
|
||||
}
|
||||
|
||||
let json: unknown | null = null
|
||||
try {
|
||||
json = JSON.parse(props.req.body)
|
||||
} catch {
|
||||
props.logger.forBot().error('Failed to parse request body as JSON')
|
||||
return
|
||||
}
|
||||
|
||||
const webhookInfoResult = webhookRequestSchema.safeParse(json)
|
||||
|
||||
if (!webhookInfoResult.success) {
|
||||
props.logger.forBot().error(`Failed to validate request body: ${webhookInfoResult.error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEventTypeHandled(webhookInfoResult.data)) {
|
||||
props.logger.forBot().info(`Event ${webhookInfoResult.data} filtered out`)
|
||||
return
|
||||
}
|
||||
|
||||
const { success, error, data: webhookRequestPayload } = webhookRequestSchema.safeParse(json)
|
||||
|
||||
if (!success) {
|
||||
props.logger.forBot().error(`Failed to validate request body: ${error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
switch (webhookRequestPayload.event_type) {
|
||||
case 'candidate_created':
|
||||
await props.client.createEvent({
|
||||
type: 'candidateCreated',
|
||||
payload: toCandidateCreatedEventModel(webhookRequestPayload),
|
||||
})
|
||||
break
|
||||
case 'candidate_moved':
|
||||
await props.client.createEvent({
|
||||
type: 'candidateMoved',
|
||||
payload: toCandidateMovedEventModel(webhookRequestPayload),
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { RuntimeError, z } from '@botpress/sdk'
|
||||
import { eventTypes } from 'definitions/events/candidates'
|
||||
import { handler } from './handler'
|
||||
import {
|
||||
fromGetCandidateInputModel,
|
||||
fromListCandidatesInputModel,
|
||||
fromPostCandidateInJobInputModel,
|
||||
fromPostCandidateInTalentPoolInputModel,
|
||||
fromUpdateCandidateInputModel,
|
||||
toGetCandidateModel,
|
||||
toListCandidatesOutputModel,
|
||||
toPostCandidateInJobOutputModel,
|
||||
toPostCandidateInTalentPoolOutputModel,
|
||||
toUpdateCandidateOutputModel,
|
||||
} from './mapping/candidate-mapper'
|
||||
import { fromGetJobQuestionsInputModel, toGetJobQuestionsOutputModel } from './mapping/jobs-mapper'
|
||||
import { WorkableClient } from './workable-api/client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
async function _registerWebhook(
|
||||
client: WorkableClient,
|
||||
url: string,
|
||||
eventType: z.infer<typeof eventTypes>,
|
||||
subDomain: string
|
||||
): Promise<number> {
|
||||
const response = await client.registerWebhook({
|
||||
// The query param is a workaround to allow registering the same url for each event.
|
||||
// The Workable API will not allow registering the same url string twice, even for different events.
|
||||
target: `${url}?event_type=${eventType}`,
|
||||
event: eventType,
|
||||
args: {
|
||||
account_id: subDomain,
|
||||
job_shortcode: '',
|
||||
stage_slug: '',
|
||||
},
|
||||
})
|
||||
return response.id
|
||||
}
|
||||
|
||||
export default new bp.Integration({
|
||||
register: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
try {
|
||||
const {
|
||||
state: {
|
||||
payload: { ids },
|
||||
},
|
||||
} = await props.client.getOrSetState({
|
||||
id: props.ctx.integrationId,
|
||||
name: 'webhooks',
|
||||
type: 'integration',
|
||||
payload: {
|
||||
ids: [],
|
||||
},
|
||||
})
|
||||
|
||||
if (ids !== undefined) {
|
||||
for (const id of ids) {
|
||||
await client.unregisterWebhook(id).catch((thrown) => {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
props.logger.forBot().warn(`Failed to unregister webhook: ${msg}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const newIds: number[] = []
|
||||
|
||||
for (const eventType of eventTypes.options) {
|
||||
const id = await _registerWebhook(client, props.webhookUrl, eventType, props.ctx.configuration.subDomain)
|
||||
newIds.push(id)
|
||||
}
|
||||
|
||||
await props.client.setState({
|
||||
id: props.ctx.integrationId,
|
||||
name: 'webhooks',
|
||||
type: 'integration',
|
||||
payload: {
|
||||
ids: newIds,
|
||||
},
|
||||
})
|
||||
} catch (thrown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to register the integration: ${msg}`)
|
||||
}
|
||||
},
|
||||
unregister: async (props) => {
|
||||
const {
|
||||
state: {
|
||||
payload: { ids },
|
||||
},
|
||||
} = await props.client.getState({
|
||||
name: 'webhooks',
|
||||
id: props.ctx.integrationId,
|
||||
type: 'integration',
|
||||
})
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
const errors: string[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await client.unregisterWebhook(id)
|
||||
} catch (thrown) {
|
||||
errors.push(thrown instanceof Error ? thrown.message : String(thrown))
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new RuntimeError(`Failed to unregister the integration: ${errors.join(', ')}`)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
listCandidates: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.listCandidates(fromListCandidatesInputModel(props.input))
|
||||
return toListCandidatesOutputModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to list candidates: ${msg}`)
|
||||
}
|
||||
},
|
||||
getCandidate: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.getCandidate(fromGetCandidateInputModel(props.input))
|
||||
return toGetCandidateModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to get candidate with id ${props.input.id}: ${msg}`)
|
||||
}
|
||||
},
|
||||
createCandidateInJob: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.postCandidateInJob(fromPostCandidateInJobInputModel(props.input))
|
||||
return toPostCandidateInJobOutputModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to create candidate: ${msg}`)
|
||||
}
|
||||
},
|
||||
createCandidateInTalentPool: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.postCandidateInTalentPool(fromPostCandidateInTalentPoolInputModel(props.input))
|
||||
return toPostCandidateInTalentPoolOutputModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to create candidate: ${msg}`)
|
||||
}
|
||||
},
|
||||
updateCandidate: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.updateCandidate(fromUpdateCandidateInputModel(props.input))
|
||||
return toUpdateCandidateOutputModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to update candidate: ${msg}`)
|
||||
}
|
||||
},
|
||||
getJobQuestions: async (props) => {
|
||||
const client = new WorkableClient(props.ctx.configuration.apiToken, props.ctx.configuration.subDomain)
|
||||
|
||||
try {
|
||||
const raw = await client.getJobQuestions(fromGetJobQuestionsInputModel(props.input))
|
||||
return toGetJobQuestionsOutputModel(raw)
|
||||
} catch (thrown: unknown) {
|
||||
const msg = thrown instanceof Error ? thrown.message : String(thrown)
|
||||
throw new RuntimeError(`Failed to get questions: ${msg}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
channels: {},
|
||||
handler,
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { RuntimeError, z } from '@botpress/sdk'
|
||||
import * as defs from 'definitions/models/answers'
|
||||
import * as workable from 'src/workable-schemas/answers'
|
||||
|
||||
export function fromPostAnswerModel(
|
||||
schema: z.infer<typeof defs.postAnswerSchema>
|
||||
): z.infer<typeof workable.postAnswerSchema> {
|
||||
const { questionKey, ...answers } = schema
|
||||
|
||||
const definedValues = Object.entries(answers).filter(([, value]) => {
|
||||
if (value === undefined) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(value) || typeof value === 'string') {
|
||||
return value.length > 0
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (definedValues.length !== 1) {
|
||||
throw new RuntimeError(
|
||||
`One single answer must be provided for each question. Question '${questionKey}' has ${definedValues.length} answers.`
|
||||
)
|
||||
}
|
||||
|
||||
const [rawKey, value] = definedValues[0]!
|
||||
|
||||
const key = rawKey === 'fileUrl' ? 'file_url' : rawKey
|
||||
|
||||
return { question_key: questionKey, [key]: value } as unknown as z.infer<typeof workable.postAnswerSchema>
|
||||
}
|
||||
|
||||
export function toAnswerModel(schema: z.infer<typeof workable.answerSchema>): z.infer<typeof defs.answerSchema> {
|
||||
const { answer, ...rest } = schema
|
||||
if (answer === null) {
|
||||
return {
|
||||
...rest,
|
||||
answer,
|
||||
}
|
||||
}
|
||||
if ('file_url' in answer) {
|
||||
const { file_url, ...answerRest } = answer
|
||||
return {
|
||||
...rest,
|
||||
answer: {
|
||||
...answerRest,
|
||||
fileUrl: file_url,
|
||||
},
|
||||
}
|
||||
} else if ('choices' in answer) {
|
||||
const { choices, ...answerRest } = answer
|
||||
return {
|
||||
...rest,
|
||||
answer: {
|
||||
...answerRest,
|
||||
choices: choices.map((choice) => choice.body),
|
||||
},
|
||||
}
|
||||
}
|
||||
return { ...rest, answer }
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import * as defEvents from 'definitions/events/candidates'
|
||||
import * as def from 'definitions/models/candidates'
|
||||
import * as workable from 'src/workable-schemas/candidates'
|
||||
import * as workableEvents from 'src/workable-schemas/events'
|
||||
import { fromPostAnswerModel, toAnswerModel } from './answers-mapper'
|
||||
import { parseNextToken } from './pagination'
|
||||
|
||||
export function fromListCandidatesInputModel(
|
||||
model: z.infer<typeof def.listCandidatesInputSchema>
|
||||
): z.infer<typeof workable.listCandidatesInputSchema> {
|
||||
const { createdAfter, nextToken, shortCode, updatedAfter, ...rest } = model
|
||||
return {
|
||||
...rest,
|
||||
shortcode: shortCode,
|
||||
since_id: nextToken,
|
||||
created_after: createdAfter,
|
||||
updated_after: updatedAfter,
|
||||
}
|
||||
}
|
||||
|
||||
export function toListCandidatesOutputModel(
|
||||
schema: z.infer<typeof workable.listCandidatesOutputSchema>
|
||||
): z.infer<typeof def.listCandidatesOutputSchema> {
|
||||
return {
|
||||
nextToken: schema.paging?.next === undefined ? undefined : parseNextToken(schema.paging?.next),
|
||||
candidates: schema.candidates?.map(toCandidateModel),
|
||||
}
|
||||
}
|
||||
|
||||
export function toCandidateModel(
|
||||
schema: z.infer<typeof workable.candidateSchema>
|
||||
): z.infer<typeof def.candidateSchema> {
|
||||
const {
|
||||
firstname,
|
||||
lastname,
|
||||
job,
|
||||
stage_kind,
|
||||
disqualification_reason,
|
||||
profile_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
hired_at,
|
||||
...rest
|
||||
} = schema
|
||||
|
||||
return {
|
||||
...rest,
|
||||
firstName: firstname,
|
||||
lastName: lastname,
|
||||
job: {
|
||||
title: job?.title,
|
||||
shortCode: job?.shortcode,
|
||||
},
|
||||
stageKind: stage_kind,
|
||||
disqualificationReason: disqualification_reason,
|
||||
profileUrl: profile_url,
|
||||
createdAt: created_at,
|
||||
updatedAt: updated_at,
|
||||
hiredAt: hired_at,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromGetCandidateInputModel(
|
||||
model: z.infer<typeof def.getCandidateInputSchema>
|
||||
): z.infer<typeof workable.getCandidateInputSchema> {
|
||||
return model
|
||||
}
|
||||
|
||||
export function toBaseDetailedCandidateModel(
|
||||
schema: z.infer<typeof workable.baseDetailedCandidateSchema>
|
||||
): z.infer<typeof def.baseDetailedCandidateSchema> {
|
||||
const {
|
||||
firstname,
|
||||
lastname,
|
||||
stage_kind,
|
||||
disqualification_reason,
|
||||
profile_url,
|
||||
created_at,
|
||||
updated_at,
|
||||
hired_at,
|
||||
image_url,
|
||||
disqualified_at,
|
||||
outbound_mailbox,
|
||||
uploader_id,
|
||||
cover_letter,
|
||||
education_entries,
|
||||
experience_entries,
|
||||
resume_url,
|
||||
social_profiles,
|
||||
location,
|
||||
originating_candidate_id,
|
||||
answers,
|
||||
...rest
|
||||
} = schema
|
||||
|
||||
return {
|
||||
...rest,
|
||||
firstName: firstname,
|
||||
lastName: lastname,
|
||||
stageKind: stage_kind,
|
||||
disqualificationReason: disqualification_reason,
|
||||
profileUrl: profile_url,
|
||||
createdAt: created_at,
|
||||
updatedAt: updated_at,
|
||||
hiredAt: hired_at,
|
||||
imageUrl: image_url,
|
||||
disqualifiedAt: disqualified_at,
|
||||
outboundMailbox: outbound_mailbox,
|
||||
uploaderId: uploader_id,
|
||||
coverLetter: cover_letter,
|
||||
educationEntries: education_entries?.map(toEducationEntryModel),
|
||||
experienceEntries: experience_entries?.map(toExperienceEntryModel),
|
||||
resumeUrl: resume_url,
|
||||
socialProfiles: social_profiles,
|
||||
location: location === null || location === undefined ? undefined : toLocationModel(location),
|
||||
originatingCandidateId: originating_candidate_id,
|
||||
answers: answers?.map((answer) => toAnswerModel(answer)),
|
||||
}
|
||||
}
|
||||
|
||||
export function toDetailedCandidateModel(
|
||||
schema: z.infer<typeof workable.detailedCandidateSchema>
|
||||
): z.infer<typeof def.detailedCandidateSchema> {
|
||||
const { job, ...rest } = schema
|
||||
|
||||
return {
|
||||
...toBaseDetailedCandidateModel(rest),
|
||||
job: {
|
||||
title: job?.title,
|
||||
shortCode: job?.shortcode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function toEducationEntryModel(
|
||||
schema: z.infer<typeof workable.educationEntrySchema>
|
||||
): z.infer<typeof def.educationEntrySchema> {
|
||||
const { end_date, field_of_study, start_date, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
fieldOfStudy: field_of_study,
|
||||
startDate: start_date,
|
||||
endDate: end_date,
|
||||
}
|
||||
}
|
||||
|
||||
export function toExperienceEntryModel(
|
||||
schema: z.infer<typeof workable.experienceEntrySchema>
|
||||
): z.infer<typeof def.experienceEntrySchema> {
|
||||
const { start_date, end_date, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
startDate: start_date,
|
||||
endDate: end_date,
|
||||
}
|
||||
}
|
||||
|
||||
export function toLocationModel(schema: z.infer<typeof workable.locationSchema>): z.infer<typeof def.locationSchema> {
|
||||
const { location_str, country_code, region_code, zip_code, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
locationString: location_str,
|
||||
countryCode: country_code,
|
||||
regionCode: region_code,
|
||||
zipCode: zip_code,
|
||||
}
|
||||
}
|
||||
|
||||
export function toGetCandidateModel(
|
||||
schema: z.infer<typeof workable.getCandidateOutputSchema>
|
||||
): z.infer<typeof def.getCandidateOutputSchema> {
|
||||
return {
|
||||
candidate: schema.candidate === undefined ? undefined : toDetailedCandidateModel(schema.candidate),
|
||||
}
|
||||
}
|
||||
|
||||
export function toCandidateCreatedEventModel(
|
||||
schema: z.infer<typeof workableEvents.candidateCreatedSchema>
|
||||
): z.infer<typeof defEvents.candidateCreatedSchema> {
|
||||
const candidateModel = toDetailedCandidateModel(schema.data)
|
||||
const { event_type, fired_at, resource_type, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
eventType: event_type,
|
||||
firedAt: fired_at,
|
||||
resourceType: resource_type,
|
||||
data: candidateModel,
|
||||
}
|
||||
}
|
||||
|
||||
export function toCandidateMovedEventModel(
|
||||
schema: z.infer<typeof workableEvents.candidateMovedSchema>
|
||||
): z.infer<typeof defEvents.candidateMovedSchema> {
|
||||
const candidateModel = toDetailedCandidateModel(schema.data)
|
||||
const { event_type, fired_at, resource_type, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
eventType: event_type,
|
||||
firedAt: fired_at,
|
||||
resourceType: resource_type,
|
||||
data: candidateModel,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromPostEducationEntryModel(
|
||||
schema: z.infer<typeof def.postEducationEntrySchema>
|
||||
): z.infer<typeof workable.postEducationEntrySchema> {
|
||||
const { endDate, startDate, fieldOfStudy, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
end_date: endDate,
|
||||
start_date: startDate,
|
||||
field_of_study: fieldOfStudy,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromEducationEntryModel(
|
||||
schema: z.infer<typeof def.updateEducationEntrySchema>
|
||||
): z.infer<typeof workable.updateEducationEntrySchema> {
|
||||
const { endDate, startDate, fieldOfStudy, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
end_date: endDate,
|
||||
start_date: startDate,
|
||||
field_of_study: fieldOfStudy,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromPostExperienceEntryModel(
|
||||
schema: z.infer<typeof def.postExperienceEntrySchema>
|
||||
): z.infer<typeof workable.postExperienceEntrySchema> {
|
||||
const { startDate, endDate, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromExperienceEntryModel(
|
||||
schema: z.infer<typeof def.updateExperienceEntrySchema>
|
||||
): z.infer<typeof workable.updateExperienceEntrySchema> {
|
||||
const { startDate, endDate, ...rest } = schema
|
||||
return {
|
||||
...rest,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}
|
||||
}
|
||||
|
||||
export function toPostCandidateInJobOutputModel(
|
||||
schema: z.infer<typeof workable.postCandidateInJobOutputSchema>
|
||||
): z.infer<typeof def.postCandidateInJobOutputSchema> {
|
||||
const { candidate, ...rest } = schema
|
||||
|
||||
return {
|
||||
candidate: toDetailedCandidateModel(candidate),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function toPostCandidateInTalentPoolOutputModel(
|
||||
schema: z.infer<typeof workable.postCandidateInTalentPoolOutputSchema>
|
||||
): z.infer<typeof def.postCandidateInTalentPoolOutputSchema> {
|
||||
const { candidate, ...rest } = schema
|
||||
const { talent_pool, ...candidateRest } = candidate
|
||||
|
||||
return {
|
||||
candidate: {
|
||||
...toBaseDetailedCandidateModel(candidateRest),
|
||||
talentPool: {
|
||||
talentPoolId: talent_pool.talent_pool_id,
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromPostCandidateInTalentPoolModel(
|
||||
schema: z.infer<typeof def.postCandidateInTalentPoolSchema>
|
||||
): z.infer<typeof workable.postCandidateInTalentPoolSchema> {
|
||||
const {
|
||||
educationEntries,
|
||||
experienceEntries,
|
||||
firstName,
|
||||
lastName,
|
||||
socialProfiles,
|
||||
coverLetter,
|
||||
disqualificationReason,
|
||||
disqualifiedAt,
|
||||
recruiterKey,
|
||||
resumeUrl,
|
||||
resume,
|
||||
...rest
|
||||
} = schema
|
||||
|
||||
const result = {
|
||||
...rest,
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
education_entries:
|
||||
educationEntries === undefined ? [] : educationEntries.map((entry) => fromPostEducationEntryModel(entry)),
|
||||
experience_entries:
|
||||
experienceEntries === undefined ? [] : experienceEntries.map((entry) => fromPostExperienceEntryModel(entry)),
|
||||
social_profiles: socialProfiles === undefined ? [] : socialProfiles,
|
||||
cover_letter: coverLetter,
|
||||
disqualification_reason: disqualificationReason,
|
||||
disqualified_at: disqualifiedAt,
|
||||
recruiter_key: recruiterKey,
|
||||
resume_url: resumeUrl,
|
||||
}
|
||||
|
||||
if (resume?.name && resume?.data) {
|
||||
return { ...result, resume }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function fromPostCandidateInTalentPoolInputModel(
|
||||
schema: z.infer<typeof def.postCandidateInTalentPoolInputSchema>
|
||||
): z.infer<typeof workable.postCandidateInTalentPoolInputSchema> {
|
||||
const { candidate, ...rest } = schema
|
||||
|
||||
return {
|
||||
...rest,
|
||||
candidate: fromPostCandidateInTalentPoolModel(candidate),
|
||||
}
|
||||
}
|
||||
|
||||
export function fromPostCandidateInJobInputModel(
|
||||
schema: z.infer<typeof def.postCandidateInJobInputSchema>
|
||||
): z.infer<typeof workable.postCandidateInJobInputSchema> {
|
||||
const { shortCode, candidate, ...rest } = schema
|
||||
const { answers, ...restCandidate } = candidate
|
||||
|
||||
return {
|
||||
body: {
|
||||
...rest,
|
||||
candidate: {
|
||||
...fromPostCandidateInTalentPoolModel(restCandidate),
|
||||
answers: answers?.map((answer) => fromPostAnswerModel(answer)),
|
||||
},
|
||||
},
|
||||
shortCode,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromUpdateCandidateInputModel(
|
||||
schema: z.infer<typeof def.updateCandidateInputSchema>
|
||||
): z.infer<typeof workable.updateCandidateInputSchema> {
|
||||
const { candidate, ...rest } = schema
|
||||
const {
|
||||
textingConsent,
|
||||
imageUrl,
|
||||
imageSource,
|
||||
educationEntries,
|
||||
experienceEntries,
|
||||
firstName,
|
||||
lastName,
|
||||
socialProfiles,
|
||||
coverLetter,
|
||||
resumeUrl,
|
||||
resume,
|
||||
image,
|
||||
...restCandidate
|
||||
} = candidate
|
||||
|
||||
return {
|
||||
...rest,
|
||||
body: {
|
||||
candidate: {
|
||||
..._omitEmptyStrings({
|
||||
...restCandidate,
|
||||
image_source: imageSource,
|
||||
image_url: imageUrl,
|
||||
texting_consent: textingConsent,
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
education_entries:
|
||||
educationEntries === undefined
|
||||
? undefined
|
||||
: educationEntries.map((entry) => fromEducationEntryModel(entry)),
|
||||
experience_entries:
|
||||
experienceEntries === undefined
|
||||
? undefined
|
||||
: experienceEntries.map((entry) => fromExperienceEntryModel(entry)),
|
||||
social_profiles: socialProfiles === undefined ? undefined : socialProfiles,
|
||||
cover_letter: coverLetter,
|
||||
resume_url: resumeUrl,
|
||||
...(resume?.data && resume.name ? { resume } : {}),
|
||||
...(image?.data && image.name ? { resume } : {}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function toUpdateCandidateOutputModel(
|
||||
schema: z.infer<typeof workable.updateCandidateOutputSchema>
|
||||
): z.infer<typeof def.updateCandidateOutputSchema> {
|
||||
const { candidate, ...rest } = schema
|
||||
|
||||
return {
|
||||
...rest,
|
||||
candidate: {
|
||||
...toBaseDetailedCandidateModel(candidate),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function _omitEmptyStrings<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([, value]) => !(typeof value === 'string' && value.trim() === ''))
|
||||
) as Partial<T>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import * as def from 'definitions/models/jobs'
|
||||
import * as workable from 'src/workable-schemas/jobs'
|
||||
|
||||
export function toGetJobQuestionsOutputModel(
|
||||
schema: z.infer<typeof workable.getJobQuestionsOutputSchema>
|
||||
): z.infer<typeof def.getJobQuestionsOutputSchema> {
|
||||
const { questions, ...rest } = schema
|
||||
const mappedQuestions = questions.map(toQuestionModel)
|
||||
|
||||
return {
|
||||
...rest,
|
||||
questions: mappedQuestions,
|
||||
}
|
||||
}
|
||||
|
||||
function toQuestionModel(schema: z.infer<typeof workable.questionSchema>): z.infer<typeof def.questionSchema> {
|
||||
const { max_file_size, single_answer, supported_file_types, ...rest } = schema
|
||||
|
||||
return {
|
||||
...rest,
|
||||
maxFileSize: max_file_size,
|
||||
singleAnswer: single_answer,
|
||||
supportedFileTypes: supported_file_types,
|
||||
}
|
||||
}
|
||||
|
||||
export function fromGetJobQuestionsInputModel(
|
||||
schema: z.infer<typeof def.getJobQuestionsInputSchema>
|
||||
): z.infer<typeof workable.getJobQuestionsInputSchema> {
|
||||
return schema
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export function parseNextToken(url: string): string | undefined {
|
||||
const parsedUrl = new URL(url)
|
||||
return parsedUrl.searchParams.get('since_id') ?? undefined
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import axios, { Axios, AxiosResponse } from 'axios'
|
||||
import {
|
||||
getCandidateInputSchema,
|
||||
getCandidateOutputSchema,
|
||||
listCandidatesInputSchema,
|
||||
listCandidatesOutputSchema,
|
||||
postCandidateInJobInputSchema,
|
||||
postCandidateInJobOutputSchema,
|
||||
postCandidateInTalentPoolInputSchema,
|
||||
postCandidateInTalentPoolOutputSchema,
|
||||
updateCandidateInputSchema,
|
||||
updateCandidateOutputSchema,
|
||||
} from 'src/workable-schemas/candidates'
|
||||
import {
|
||||
getWebhooksOutputSchema,
|
||||
registerWebhookInputSchema,
|
||||
registerWebhookOutputSchema,
|
||||
} from 'src/workable-schemas/events'
|
||||
import { getJobQuestionsInputSchema, getJobQuestionsOutputSchema } from 'src/workable-schemas/jobs'
|
||||
|
||||
export type ErrorResponse = {
|
||||
error: string | undefined
|
||||
}
|
||||
|
||||
type ApiOutput<K extends object> = K | ErrorResponse
|
||||
|
||||
export class WorkableClient {
|
||||
private _client: Axios
|
||||
|
||||
public constructor(apiToken: string, subDomain: string) {
|
||||
this._client = axios.create({
|
||||
baseURL: `https://${subDomain}.workable.com/spi/v3`,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + apiToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private _unwrapResponse<K extends object>(response: ApiOutput<K>): K {
|
||||
if ('error' in response) {
|
||||
throw new Error(response.error)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private _handleAxiosError(thrown: unknown): never {
|
||||
if (axios.isAxiosError(thrown)) {
|
||||
throw new Error(JSON.stringify(thrown.response?.data?.error, null, 2) || thrown.message)
|
||||
}
|
||||
throw thrown
|
||||
}
|
||||
|
||||
public async unregisterWebhook(id: number): Promise<void> {
|
||||
await this._client.delete(`/subscriptions/${id}`).catch(this._handleAxiosError)
|
||||
}
|
||||
|
||||
public async registerWebhook(
|
||||
params: z.infer<typeof registerWebhookInputSchema>
|
||||
): Promise<z.infer<typeof registerWebhookOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof registerWebhookOutputSchema>> = await this._client
|
||||
.post('/subscriptions', params)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async getWebhooks(): Promise<z.infer<typeof getWebhooksOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof getWebhooksOutputSchema>> = await this._client
|
||||
.get('/subscriptions')
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async listCandidates(
|
||||
params?: z.infer<typeof listCandidatesInputSchema>
|
||||
): Promise<z.infer<typeof listCandidatesOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof listCandidatesOutputSchema>> = await this._client
|
||||
.get('/candidates', { params })
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async getCandidate(
|
||||
params: z.infer<typeof getCandidateInputSchema>
|
||||
): Promise<z.infer<typeof getCandidateOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof getCandidateOutputSchema>> = await this._client
|
||||
.get(`/candidates/${params.id}`)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async postCandidateInJob(
|
||||
params: z.infer<typeof postCandidateInJobInputSchema>
|
||||
): Promise<z.infer<typeof postCandidateInJobOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof postCandidateInJobOutputSchema>> = await this._client
|
||||
.post(`/jobs/${params.shortCode}/candidates`, params.body)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async postCandidateInTalentPool(
|
||||
params: z.infer<typeof postCandidateInTalentPoolInputSchema>
|
||||
): Promise<z.infer<typeof postCandidateInTalentPoolOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof postCandidateInTalentPoolOutputSchema>> = await this._client
|
||||
.post('/talent_pool/candidates', params)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async updateCandidate(
|
||||
params: z.infer<typeof updateCandidateInputSchema>
|
||||
): Promise<z.infer<typeof updateCandidateOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof updateCandidateOutputSchema>> = await this._client
|
||||
.patch(`/candidates/${params.id}`, params.body)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
|
||||
public async getJobQuestions(
|
||||
params: z.infer<typeof getJobQuestionsInputSchema>
|
||||
): Promise<z.infer<typeof getJobQuestionsOutputSchema>> {
|
||||
const response: AxiosResponse<z.infer<typeof getJobQuestionsOutputSchema>> = await this._client
|
||||
.get(`/jobs/${params.shortCode}/questions`)
|
||||
.catch(this._handleAxiosError)
|
||||
return this._unwrapResponse(response.data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
const basePostAnswerSchema = z.object({
|
||||
question_key: z.string(),
|
||||
})
|
||||
|
||||
const freeTextAnswerSchema = z.object({
|
||||
body: z.string(),
|
||||
})
|
||||
|
||||
const shortTextAnswerSchema = z.object({
|
||||
body: z.string().max(128),
|
||||
})
|
||||
|
||||
const booleanAnswerSchema = z.object({
|
||||
checked: z.boolean(),
|
||||
})
|
||||
|
||||
const multipleChoiceAnswerSchema = z.object({
|
||||
choices: z.array(z.object({ body: z.string() })),
|
||||
})
|
||||
|
||||
const postMultipleChoiceAnswerSchema = z.object({
|
||||
choices: z.array(z.string()),
|
||||
})
|
||||
|
||||
const dateAnswerSchema = z.object({
|
||||
date: z.string(),
|
||||
})
|
||||
|
||||
const numericAnswerSchema = z.object({
|
||||
number: z.number(),
|
||||
})
|
||||
|
||||
const postNumericAnswerSchema = z.object({
|
||||
value: z.number(),
|
||||
})
|
||||
|
||||
const fileAnswerUrlSchema = z.object({
|
||||
file_url: z.string(),
|
||||
})
|
||||
const fileAnswerBase64Schema = z.object({
|
||||
data: z.string(),
|
||||
})
|
||||
|
||||
export const postAnswerSchema = z.union([
|
||||
basePostAnswerSchema.merge(freeTextAnswerSchema),
|
||||
basePostAnswerSchema.merge(shortTextAnswerSchema),
|
||||
basePostAnswerSchema.merge(booleanAnswerSchema),
|
||||
basePostAnswerSchema.merge(postMultipleChoiceAnswerSchema),
|
||||
basePostAnswerSchema.merge(dateAnswerSchema),
|
||||
basePostAnswerSchema.merge(postNumericAnswerSchema),
|
||||
basePostAnswerSchema.merge(fileAnswerUrlSchema),
|
||||
basePostAnswerSchema.merge(fileAnswerBase64Schema),
|
||||
])
|
||||
|
||||
export const answerSchema = z.object({
|
||||
question: z.object({
|
||||
body: z.string().nullable(),
|
||||
}),
|
||||
answer: z
|
||||
.union([
|
||||
freeTextAnswerSchema,
|
||||
shortTextAnswerSchema,
|
||||
booleanAnswerSchema,
|
||||
multipleChoiceAnswerSchema,
|
||||
dateAnswerSchema,
|
||||
numericAnswerSchema,
|
||||
fileAnswerUrlSchema,
|
||||
fileAnswerBase64Schema,
|
||||
])
|
||||
.nullable(),
|
||||
})
|
||||
@@ -0,0 +1,279 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { imageSource, socialProfileTypesSchema } from 'definitions/models/candidates'
|
||||
import { answerSchema, postAnswerSchema } from './answers'
|
||||
|
||||
export const baseCandidateSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
headline: z.string().nullable(),
|
||||
account: z
|
||||
.object({
|
||||
subdomain: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
})
|
||||
.partial(),
|
||||
stage: z.string().nullable(),
|
||||
stage_kind: z.string().nullable(),
|
||||
disqualified: z.boolean(),
|
||||
withdrew: z.boolean(),
|
||||
disqualification_reason: z.string().nullable(),
|
||||
sourced: z.boolean(),
|
||||
profile_url: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
domain: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
hired_at: z.string().nullable(),
|
||||
address: z.string().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const candidateSchema = baseCandidateSchema.extend({
|
||||
job: z
|
||||
.object({
|
||||
shortcode: z.string(),
|
||||
title: z.string(),
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
|
||||
export const listCandidatesOutputSchema = z.object({
|
||||
candidates: z.array(candidateSchema),
|
||||
paging: z.object({ next: z.string() }).partial().optional(),
|
||||
})
|
||||
|
||||
export const listCandidatesInputSchema = z
|
||||
.object({
|
||||
email: z.string().nullable(),
|
||||
shortcode: z.string(),
|
||||
stage: z.string(),
|
||||
limit: z.number(),
|
||||
since_id: z.string(),
|
||||
created_after: z.string(),
|
||||
updated_after: z.string(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const educationEntrySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
degree: z.string().nullable(),
|
||||
school: z.string(),
|
||||
field_of_study: z.string().nullable(),
|
||||
start_date: z.string().nullable(),
|
||||
end_date: z.string().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const socialProfileSchema = z
|
||||
.object({
|
||||
type: z.string(),
|
||||
name: z.string().nullable(),
|
||||
username: z.string().nullable(),
|
||||
url: z.string(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const experienceEntrySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
summary: z.string().nullable(),
|
||||
start_date: z.string().nullable(),
|
||||
end_date: z.string().nullable(),
|
||||
company: z.string().nullable(),
|
||||
industry: z.string().nullable(),
|
||||
current: z.boolean().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const updateEducationEntrySchema = z.object({
|
||||
id: z.string().optional(),
|
||||
degree: z.string().optional(),
|
||||
school: z.string(),
|
||||
field_of_study: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
})
|
||||
|
||||
export const updateSocialProfileSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
|
||||
export const updateExperienceEntrySchema = z.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string(),
|
||||
summary: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
industry: z.string().optional(),
|
||||
current: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const locationSchema = z
|
||||
.object({
|
||||
location_str: z.string().nullable(),
|
||||
country: z.string().nullable(),
|
||||
country_code: z.string().nullable(),
|
||||
region: z.string().nullable(),
|
||||
region_code: z.string().nullable(),
|
||||
city: z.string().nullable(),
|
||||
zip_code: z.string().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
const detailedCandidateSchemaExtraFields = {
|
||||
image_url: z.string().nullable(),
|
||||
disqualified_at: z.string().nullable(),
|
||||
outbound_mailbox: z.string().nullable(),
|
||||
uploader_id: z.string().nullable(),
|
||||
cover_letter: z.string().nullable(),
|
||||
summary: z.string().nullable(),
|
||||
education_entries: z.array(educationEntrySchema),
|
||||
experience_entries: z.array(experienceEntrySchema),
|
||||
skills: z.array(z.object({ name: z.string() }).partial()),
|
||||
answers: z.array(answerSchema),
|
||||
resume_url: z.string().nullable(),
|
||||
social_profiles: z.array(socialProfileSchema),
|
||||
tags: z.array(z.string()),
|
||||
location: locationSchema.nullable(),
|
||||
originating_candidate_id: z.string().nullable(),
|
||||
}
|
||||
|
||||
export const baseDetailedCandidateSchema = baseCandidateSchema.extend(detailedCandidateSchemaExtraFields).partial()
|
||||
|
||||
export const detailedCandidateSchema = candidateSchema.extend(detailedCandidateSchemaExtraFields).partial()
|
||||
|
||||
export const getCandidateOutputSchema = z
|
||||
.object({
|
||||
candidate: detailedCandidateSchema,
|
||||
})
|
||||
.partial()
|
||||
|
||||
export const getCandidateInputSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export const postEducationEntrySchema = z.object({
|
||||
degree: z.string().optional(),
|
||||
school: z.string(),
|
||||
field_of_study: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
})
|
||||
|
||||
export const postSocialProfileSchema = z.object({
|
||||
type: socialProfileTypesSchema,
|
||||
username: z.string().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
|
||||
export const postExperienceEntrySchema = z.object({
|
||||
title: z.string(),
|
||||
summary: z.string().optional(),
|
||||
start_date: z.string().optional(),
|
||||
end_date: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
current: z.boolean().optional(),
|
||||
industry: z.string().optional(),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolSchema = z.object({
|
||||
firstname: z.string(),
|
||||
lastname: z.string(),
|
||||
email: z.string(),
|
||||
headline: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
cover_letter: z.string().optional(),
|
||||
education_entries: z.array(postEducationEntrySchema),
|
||||
experience_entries: z.array(postExperienceEntrySchema),
|
||||
skills: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
disqualified: z.boolean().optional(),
|
||||
disqualification_reason: z.string().optional(),
|
||||
disqualified_at: z.string().optional(),
|
||||
social_profiles: z.array(postSocialProfileSchema),
|
||||
domain: z.string().optional(),
|
||||
recruiter_key: z.string().optional(),
|
||||
resume_url: z.string().optional(),
|
||||
resume: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
data: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const postCandidateInJobSchema = postCandidateInTalentPoolSchema.extend({
|
||||
answers: z.array(postAnswerSchema).optional(),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolInputSchema = z.object({
|
||||
sourced: z.boolean().optional(),
|
||||
candidate: postCandidateInTalentPoolSchema,
|
||||
})
|
||||
|
||||
export const postCandidateInJobInputSchema = z.object({
|
||||
body: z.object({
|
||||
sourced: z.boolean().optional(),
|
||||
candidate: postCandidateInJobSchema,
|
||||
}),
|
||||
shortCode: z.string(),
|
||||
})
|
||||
|
||||
export const postCandidateInTalentPoolOutputSchema = z.object({
|
||||
status: z.string(),
|
||||
candidate: baseDetailedCandidateSchema.extend({
|
||||
talent_pool: z.object({
|
||||
talent_pool_id: z.number(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const postCandidateInJobOutputSchema = z.object({
|
||||
status: z.string(),
|
||||
candidate: detailedCandidateSchema,
|
||||
})
|
||||
|
||||
export const updateCandidateInputSchema = z.object({
|
||||
id: z.string(),
|
||||
body: z.object({
|
||||
candidate: postCandidateInTalentPoolSchema
|
||||
.extend({
|
||||
texting_consent: z.enum(['forced', 'declined']),
|
||||
image_url: z.string(),
|
||||
image_source: imageSource,
|
||||
image: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
data: z.string(),
|
||||
source: imageSource,
|
||||
})
|
||||
.partial(),
|
||||
education_entries: z.array(updateEducationEntrySchema),
|
||||
experience_entries: z.array(updateExperienceEntrySchema),
|
||||
social_profiles: z.array(updateSocialProfileSchema),
|
||||
})
|
||||
.omit({
|
||||
disqualified: true,
|
||||
disqualification_reason: true,
|
||||
disqualified_at: true,
|
||||
domain: true,
|
||||
recruiter_key: true,
|
||||
})
|
||||
.partial(),
|
||||
}),
|
||||
})
|
||||
export const updateCandidateOutputSchema = z.object({
|
||||
candidate: detailedCandidateSchema,
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { eventTypes } from 'definitions/events/candidates'
|
||||
import { candidateSchema } from './candidates'
|
||||
|
||||
export const webhookEvent = z.object({
|
||||
fired_at: z.string(),
|
||||
id: z.string(),
|
||||
resource_type: z.string(),
|
||||
})
|
||||
|
||||
export const candidateCreatedSchema = webhookEvent.extend({
|
||||
data: candidateSchema,
|
||||
event_type: z.literal(eventTypes.Values.candidate_created),
|
||||
})
|
||||
|
||||
export const candidateMovedSchema = webhookEvent.extend({
|
||||
data: candidateSchema,
|
||||
event_type: z.literal(eventTypes.Values.candidate_moved),
|
||||
})
|
||||
|
||||
export const registerWebhookInputSchema = z.object({
|
||||
target: z.string(),
|
||||
event: eventTypes,
|
||||
args: z.object({
|
||||
account_id: z.string(),
|
||||
job_shortcode: z.string(),
|
||||
stage_slug: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const registerWebhookOutputSchema = z.object({
|
||||
id: z.number(),
|
||||
})
|
||||
|
||||
export const getWebhooksOutputSchema = z.object({
|
||||
subscriptions: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
event: z.string(),
|
||||
target: z.string(),
|
||||
valid_util: z.string().nullable(),
|
||||
created_at: z.string().nullable(),
|
||||
stage_slug: z.string().nullable(),
|
||||
job_shortcode: z.string().nullable(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const webhookRequestSchema = z.union([candidateCreatedSchema, candidateMovedSchema])
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { questionType } from 'definitions/models/jobs'
|
||||
|
||||
export const getJobQuestionsInputSchema = z.object({
|
||||
shortCode: z.string(),
|
||||
})
|
||||
|
||||
export const questionSchema = z.object({
|
||||
id: z.string(),
|
||||
body: z.string(),
|
||||
type: questionType,
|
||||
required: z.boolean(),
|
||||
single_answer: z.boolean().optional(),
|
||||
choices: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
body: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
supported_file_types: z.array(z.string()).optional(),
|
||||
max_file_size: z.number().optional(),
|
||||
})
|
||||
|
||||
export const getJobQuestionsOutputSchema = z.object({
|
||||
questions: z.array(questionSchema),
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user