import contact, { Channel, Employee, EmployeeAccount, Person } from '@hcengineering/contact' import core, { AttachedData, Data, generateId, MeasureContext, MeasureMetricsContext, metricsToString, MixinUpdate, Ref, TxOperations } from '@hcengineering/core' import recruit from '@hcengineering/model-recruit' import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit' import { genRanks, State } from '@hcengineering/task' import faker from 'faker' import jpeg, { BufferRet } from 'jpeg-js' import { Client } from 'minio' import { addAttachments, AttachmentOptions } from './attachments' import { addComments, CommentOptions } from './comments' import { connect } from './connect' import { createUpdateSpaceKanban } from './kanban' import { findOrUpdate, findOrUpdateAttached } from './utils' export interface RecruitOptions { random: boolean // random id prefix. contacts: number // how many contacts to add vacancy: number // Will add number of vacancies with applications. // Comment generation control comments: CommentOptions // Attachment generation control attachments: AttachmentOptions applicants: { min: number max: number applicantUpdateFactor: number } lite: boolean } export async function generateContacts ( transactorUrl: string, dbName: string, options: RecruitOptions, minio: Client ): Promise { const connection = await connect(transactorUrl, dbName) const accounts = await connection.findAll(contact.class.EmployeeAccount, {}) const accountIds = accounts.map((a) => a._id) const emoloyeeIds = accounts.map((a) => a.employee) const account = faker.random.arrayElement(accounts) const client = new TxOperations(connection, account._id) const candidates: Ref[] = [] const ctx = new MeasureMetricsContext('recruit', { contacts: options.contacts }) for (let i = 0; i < options.contacts; i++) { await ctx.with('candidate', {}, (ctx) => genCandidate(ctx, i, minio, dbName, options, candidates, client)) } // Work on Vacancy/Applications. for (let i = 0; i < options.vacancy; i++) { await ctx.with('vacancy', {}, (ctx) => genVacansyApplicants(ctx, accountIds, options, i, client, minio, dbName, candidates, emoloyeeIds) ) } await connection.close() ctx.end() console.info(metricsToString(ctx.metrics, 'Client')) } async function genVacansyApplicants ( ctx: MeasureContext, accountIds: Ref[], options: RecruitOptions, i: number, client: TxOperations, minio: Client, dbName: string, candidates: Ref[], emoloyeeIds: Ref[] ): Promise { const vacancy: Data = { name: faker.company.companyName(), description: faker.lorem.sentences(2), fullDescription: faker.lorem.sentences(10), location: faker.address.city(), members: accountIds, private: false, archived: false } const vacancyId = (options.random ? `vacancy-${generateId()}-${i}` : `vacancy-genid-${i}`) as Ref console.log('Creating vacandy', vacancy.name) // Update or create candidate await ctx.with('update', {}, (ctx) => findOrUpdate(ctx, client, core.space.Space, recruit.class.Vacancy, vacancyId, vacancy) ) console.log('Vacandy generated', vacancy.name) if (!options.lite) { await ctx.with('add-attachments', {}, () => addAttachments( options.attachments, client, minio, dbName, vacancyId, vacancyId, recruit.class.Vacancy, 'attachments' ) ) } console.log('Vacandy attachments generated', vacancy.name) const states = await ctx.with('create-kanbad', {}, (ctx) => createUpdateSpaceKanban(ctx, vacancyId, client)) console.log('States generated', vacancy.name) const applicantsForCount = options.applicants.min + faker.datatype.number(options.applicants.max) const applicantsFor = faker.random.arrayElements(candidates, applicantsForCount) const rankGen = genRanks(candidates.length) for (const candidateId of applicantsFor) { await ctx.with('applicant', {}, (ctx) => genApplicant(ctx, vacancyId, candidateId, emoloyeeIds, states, client, options, minio, dbName, rankGen) ) } } async function genApplicant ( ctx: MeasureContext, vacancyId: Ref, candidateId: Ref, emoloyeeIds: Ref[], states: Ref[], client: TxOperations, options: RecruitOptions, minio: Client, dbName: string, rankGen: Generator ): Promise { const applicantId = `vacancy-${vacancyId}-${candidateId}` as Ref const rank = rankGen.next().value const applicant: AttachedData = { number: faker.datatype.number(), assignee: faker.random.arrayElement(emoloyeeIds), state: faker.random.arrayElement(states), doneState: null, rank: rank as string, startDate: null, dueDate: null } // Update or create candidate await findOrUpdateAttached(ctx, client, vacancyId, recruit.class.Applicant, applicantId, applicant, { attachedTo: candidateId, attachedClass: recruit.mixin.Candidate, collection: 'applications' }) await ctx.with('add-comment', {}, () => addComments(options.comments, client, vacancyId, applicantId, recruit.class.Vacancy, 'comments') ) if (!options.lite) { await ctx.with('add-attachment', {}, () => addAttachments( options.attachments, client, minio, dbName, vacancyId, applicantId, recruit.class.Applicant, 'attachments' ) ) } if (faker.datatype.number(100) > options.applicants.applicantUpdateFactor) { await ctx.with('update-collection', {}, () => client.updateCollection( recruit.class.Applicant, vacancyId, applicantId, candidateId, recruit.class.Applicant, 'applications', { state: faker.random.arrayElement(states) } ) ) } } const liteAvatar = generateAvatar(0) // @measure('Candidate') async function genCandidate ( ctx: MeasureContext, i: number, minio: Client, dbName: string, options: RecruitOptions, candidates: Ref[], client: TxOperations ): Promise { const fName = faker.name.firstName() const lName = faker.name.lastName() const { imgId, jpegImageData } = options.lite ? liteAvatar : generateAvatar(i) if (!options.lite) { await ctx.with('avatar', {}, () => minio.putObject(dbName, imgId, jpegImageData.data, jpegImageData.data.length, { 'Content-Type': 'image/jpeg' }) ) } const candidate: Data = { name: fName + ',' + lName, city: faker.address.city(), avatar: imgId } const candidateMixin: MixinUpdate = { title: faker.name.title(), onsite: faker.datatype.boolean(), remote: faker.datatype.boolean(), source: faker.lorem.lines(1) } const candidateId = (options.random ? `candidate-${generateId()}-${i}` : `candidate-genid-${i}`) as Ref candidates.push(candidateId) const channelId = (options.random ? `channel-${generateId()}-${i}` : `channel-genid-${i}`) as Ref // Update or create candidate await ctx.with('find-update', {}, async () => { await findOrUpdate(ctx, client, recruit.space.CandidatesPublic, contact.class.Person, candidateId, candidate) await findOrUpdateAttached( ctx, client, recruit.space.CandidatesPublic, contact.class.Channel, channelId, { provider: contact.channelProvider.Email, value: faker.internet.email(fName, lName) }, { attachedTo: candidateId, attachedClass: contact.class.Person, collection: 'channels' } ) await client.updateMixin( candidateId, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, candidateMixin ) }) await ctx.with('add-comment', {}, () => addComments(options.comments, client, recruit.space.CandidatesPublic, candidateId, contact.class.Person, 'comments') ) if (!options.lite) { await ctx.with('add-attachment', {}, () => addAttachments( options.attachments, client, minio, dbName, recruit.space.CandidatesPublic, candidateId, contact.class.Person, 'attachments' ) ) } console.log('Candidate', candidates.length, fName, lName, ' generated') } function generateAvatar (pos: number): { imgId: string, jpegImageData: BufferRet } { const imgId = generateId() const width = 128 const height = 128 const frameData = Buffer.alloc(width * height * 4) let i = 0 const baseR = faker.datatype.number(255) const baseG = faker.datatype.number(255) const baseB = faker.datatype.number(255) while (i < frameData.length) { frameData[i++] = (baseR + faker.datatype.number(100)) % 255 // red frameData[i++] = (baseG + faker.datatype.number(100)) % 255 // green frameData[i++] = (baseB + faker.datatype.number(100)) % 255 // blue frameData[i++] = 0xff // alpha - ignored in JPEGs } const rawImageData = { data: frameData, width, height } const jpegImageData = jpeg.encode(rawImageData, 50) return { imgId, jpegImageData } }