From bcf7277a5fb083fcc5c31e1a1da7b38a26285788 Mon Sep 17 00:00:00 2001 From: hyojinaaa Date: Sun, 5 Nov 2023 16:20:47 +0900 Subject: [PATCH] find runn id of the person using slack username --- package.json | 1 + pnpm-lock.yaml | 17 +++++++++++ src/api/index.ts | 26 ++++++++++++++++ src/api/people.ts | 40 ++++++++++++++++++++++++ src/constants.ts | 4 +++ src/lib/sync/people.test.ts | 61 +++++++++++++++++++++++++++++++++++++ src/lib/sync/people.ts | 52 +++++++++++++++++++++++++++++++ 7 files changed, 201 insertions(+) create mode 100644 src/api/index.ts create mode 100644 src/api/people.ts create mode 100644 src/lib/sync/people.test.ts create mode 100644 src/lib/sync/people.ts diff --git a/package.json b/package.json index 0179f19..0f980af 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@slack/bolt": "3.13.2", "@slack/web-api": "6.8.1", "@stayradiated/error-boundary": "4.1.0", + "axios": "^1.6.0", "cilly": "1.0.25", "date-fns": "2.30.0", "date-fns-tz": "2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7712976..41fa1c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@stayradiated/error-boundary': specifier: 4.1.0 version: 4.1.0 + axios: + specifier: ^1.6.0 + version: 1.6.0 cilly: specifier: 1.0.25 version: 1.0.25 @@ -1254,6 +1257,16 @@ packages: - debug dev: false + /axios@1.6.0: + resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -4107,6 +4120,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..67faf38 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,26 @@ +import axios from 'axios' + +import { RUNN_API_BASE_URL, RUNN_API_TOKEN } from 'src/constants.js' + +export const API_CONFIG = { + headers: { + accept: 'application/json', + 'accept-version': '1.0.0', + Authorization: `Bearer ${RUNN_API_TOKEN}`, + }, +} + +const apiClient = axios.create({ + baseURL: RUNN_API_BASE_URL, + headers: API_CONFIG.headers, +}) + +export const api = async (endpoint: string, params = {}) => { + try { + const response = await apiClient.get(endpoint, { params }) + return response.data + } catch (error) { + console.error(`Error fetching data from ${endpoint}:`, error) + throw error + } +} diff --git a/src/api/people.ts b/src/api/people.ts new file mode 100644 index 0000000..6687590 --- /dev/null +++ b/src/api/people.ts @@ -0,0 +1,40 @@ +import { api } from './index.js' + +export interface Person { + id: number + firstName: string + lastName: string + email: string + archived: boolean + references: Reference[] + teamId: number | null + tags: Tag[] + holidaysGroupId: number | null +} + +interface Reference { + referenceName: string + externalId: string +} + +interface Tag { + id: number + name: string +} + +export interface People { + values: Person[] + nextCursor: string +} + +export const getPeople = async (cursor?: string) => { + const endpoint = '/people/' + const params = { limit: 50, cursor } + + try { + const data = await api(endpoint, params) + return data + } catch (error) { + console.error('Error fetching people:', error) + } +} diff --git a/src/constants.ts b/src/constants.ts index d946a2b..b3d5291 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,8 @@ const schema = z.object({ .regex(/^\d\d:\d\d$/) .default('17:00'), OPENAI_API_KEY: z.optional(z.string()), + RUNN_API_BASE_URL: z.string(), + RUNN_API_TOKEN: z.string(), }) const config = schema.parse(process.env) @@ -28,3 +30,5 @@ export const HANDOVER_CHANNEL = config.HANDOVER_CHANNEL export const HANDOVER_TITLE = config.HANDOVER_TITLE export const HANDOVER_DAILY_REMINDER_TIME = config.HANDOVER_DAILY_REMINDER_TIME export const OPENAI_API_KEY = config.OPENAI_API_KEY +export const RUNN_API_BASE_URL = config.RUNN_API_BASE_URL +export const RUNN_API_TOKEN = config.RUNN_API_TOKEN diff --git a/src/lib/sync/people.test.ts b/src/lib/sync/people.test.ts new file mode 100644 index 0000000..04ce09f --- /dev/null +++ b/src/lib/sync/people.test.ts @@ -0,0 +1,61 @@ +import { test, expect, describe } from 'vitest' +import { findPerson } from './people.js' +import { Person } from 'src/api/people.js' + +const peopleResponse: Person[] = [ + { + id: 2832, + firstName: 'Andria', + lastName: 'Hibe', + email: 'andria+runn@runn.io', + archived: false, + references: [], + teamId: null, + tags: [], + holidaysGroupId: null, + }, + { + id: 25032, + firstName: 'Zehavit', + lastName: 'Zaslansky', + email: 'zehavit+runn@runn.io', + archived: false, + references: [], + teamId: 63, + tags: [ + { + id: 21, + name: 'Powlowskicester', + }, + ], + holidaysGroupId: null, + }, + { + id: 19859, + firstName: 'Aaron', + lastName: 'Carlino', + email: 'aaron+runn@runn.io', + archived: false, + references: [], + teamId: 63, + tags: [], + holidaysGroupId: null, + }, +] + +describe('findPerson', () => { + test('finds a person by full name', async () => { + const person = findPerson('Aaron Carlino', peopleResponse) + expect(person?.firstName).toEqual('Aaron') + }) + + test('finds a person by first name in lowercase', async () => { + const person = findPerson('andria', peopleResponse) + expect(person?.firstName).toEqual('Andria') + }) + + test('finds a person by first name in uppercase', async () => { + const person = findPerson('Zehavit', peopleResponse) + expect(person?.firstName).toEqual('Zehavit') + }) +}) diff --git a/src/lib/sync/people.ts b/src/lib/sync/people.ts new file mode 100644 index 0000000..5bfc9c0 --- /dev/null +++ b/src/lib/sync/people.ts @@ -0,0 +1,52 @@ +import { Person, getPeople } from 'src/api/people.js' + +interface PersonFound { + id: number + firstName?: string +} + +export const findPerson = ( + userName: string, + people: Person[], +): PersonFound | null => { + const [firstName, _] = userName.split(' ') + const person = people.find((person) => { + return person.firstName.toLowerCase() === firstName?.toLowerCase() + }) + return person + ? { + id: person.id, + firstName: person.firstName, + } + : null +} + +export const findPeopleFromList = async ( + userName: string, + cursor?: string, + count: number = 1, +): Promise => { + console.log(`findPeopleFromList called ${count} times`) + + const response = await getPeople(cursor) + + if (!response || !response.values) { + console.error('Failed to fetch people or response is malformed.') + return null + } + + const personFound = findPerson(userName, response.values) + + if (personFound) { + return personFound + } else if (response.nextCursor) { + return findPeopleFromList(userName, response.nextCursor, count + 1) + } + + // the person is not found and there is no next cursor + return null +} + +// 1. findPeopleFromList(userName) +// 2-a. could not find person -> enter Runn ID manually +// 2-b. found person -> fetch timeOffs with the person ID