Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch people from the Runn API and attempt to match them by their first name #7

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

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

26 changes: 26 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios'

Check failure on line 1 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

There should be no empty line between import groups

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 = {}) => {

Check failure on line 18 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

The variable `params` should be named `parameters`. A more descriptive name will do too.
try {
const response = await apiClient.get(endpoint, { params })
return response.data

Check failure on line 21 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Unsafe return of an `any` typed value.
} catch (error) {
console.error(`Error fetching data from ${endpoint}:`, error)
throw error
}
}
40 changes: 40 additions & 0 deletions src/api/people.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { api } from './index.js'

export interface Person {

Check failure on line 3 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Use a `type` instead of an `interface`.
id: number
firstName: string
lastName: string
email: string
archived: boolean
references: Reference[]
teamId: number | null

Check failure on line 10 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Don't use `null` as a type. Use `undefined` instead. See: https://github.com/sindresorhus/meta/issues/7
tags: Tag[]
holidaysGroupId: number | null

Check failure on line 12 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Don't use `null` as a type. Use `undefined` instead. See: https://github.com/sindresorhus/meta/issues/7
}

interface Reference {

Check failure on line 15 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Use a `type` instead of an `interface`.
referenceName: string
externalId: string
}

interface Tag {

Check failure on line 20 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Use a `type` instead of an `interface`.
id: number
name: string
}

export interface People {

Check failure on line 25 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

Use a `type` instead of an `interface`.
values: Person[]
nextCursor: string
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider using OpenAPI to auto-generate types in the future, but for now, I thought it's not necessary. These have been added manually.


export const getPeople = async (cursor?: string) => {
const endpoint = '/people/'
const params = { limit: 50, cursor }

Check failure on line 32 in src/api/people.ts

View workflow job for this annotation

GitHub Actions / Handover Bot

The variable `params` should be named `parameters`. A more descriptive name will do too.

try {
const data = await api(endpoint, params)
return data
} catch (error) {
console.error('Error fetching people:', error)
}
}
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: I need to get an API token for production and add it to the .env file of the production environment

})

const config = schema.parse(process.env)
Expand All @@ -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
61 changes: 61 additions & 0 deletions src/lib/sync/people.test.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
archived: false,
references: [],
teamId: null,
tags: [],
holidaysGroupId: null,
},
{
id: 25032,
firstName: 'Zehavit',
lastName: 'Zaslansky',
email: '[email protected]',
archived: false,
references: [],
teamId: 63,
tags: [
{
id: 21,
name: 'Powlowskicester',
},
],
holidaysGroupId: null,
},
{
id: 19859,
firstName: 'Aaron',
lastName: 'Carlino',
email: '[email protected]',
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')
})
})
52 changes: 52 additions & 0 deletions src/lib/sync/people.ts
Original file line number Diff line number Diff line change
@@ -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<PersonFound | null> => {
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ This is the plan!