Skip to content

Commit

Permalink
Finish adding Libcal endpoints for app
Browse files Browse the repository at this point in the history
  • Loading branch information
shu8 committed May 14, 2023
1 parent 372908d commit 6d9248f
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 49 deletions.
5 changes: 4 additions & 1 deletion src/constants/apiRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default {
ROOMBOOKINGS_FREEROOMS_URL: `${ROOMBOOKINGS_BASE_URL}/freerooms`,
LIBCAL_LOCATIONS_URL: `${LIBCAL_BASE_URL}/space/locations`,
LIBCAL_CATEGORIES_URL: `${LIBCAL_BASE_URL}/space/categories`,
LIBCAL_CATEGORY_URL: `${LIBCAL_BASE_URL}/space/category`,
LIBCAL_SEATS_URL: `${LIBCAL_BASE_URL}/space/seats`,
LIBCAL_BOOKINGS_URL: `${LIBCAL_BASE_URL}/space/personal_bookings`,
LIBCAL_PERSONAL_BOOKINGS_URL: `${LIBCAL_BASE_URL}/space/personal_bookings`,
LIBCAL_RESERVE_URL: `${LIBCAL_BASE_URL}/space/reserve`,
LIBCAL_CANCEL_URL: `${LIBCAL_BASE_URL}/space/cancel`,
}
1 change: 1 addition & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const jwtVerifyDev = async (ctx: Context, next: Next) => {

export const genToken = user => jsonwebtoken.sign(user, Environment.SECRET)

// Note: You need the JWT for timetable & libcal, so comment out if needed
const shouldBypassAuthentication = (
Environment.DEVELOPMENT_MODE ||
Environment.TEST_MODE
Expand Down
5 changes: 5 additions & 0 deletions src/redis/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const PEOPLE_PATH = `/people`
const ROOMS_PATH = `/rooms`
const TIMETABLE_PATH = `/timetable`
const SITES_PATH = `/sites`
const LIBCAL_PATH = `/libcal`
export default {
TIMETABLE_PERSONAL_PATH: `${TIMETABLE_PATH}/personal`,
TIMETABLE_MODULE_PATH: `${TIMETABLE_PATH}/module`,
Expand All @@ -13,4 +14,8 @@ export default {
PEOPLE_SEARCH_PATH: `${PEOPLE_PATH}/search`,
ROOMS_SEARCH_PATH: `${ROOMS_PATH}/rooms`,
SITES_SEARCH_PATH: `${SITES_PATH}/sites`,
LIBCAL_LOCATIONS_PATH: `${LIBCAL_PATH}/locations`,
LIBCAL_BOOKINGS_PATH: `${LIBCAL_PATH}/bookings`,
LIBCAL_CATEGORIES_PATH: `${LIBCAL_PATH}/categories`,
LIBCAL_CATEGORY_PATH: `${LIBCAL_PATH}/category`,
}
20 changes: 14 additions & 6 deletions src/redis/redis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { Context } from "koa"
import Environment from "../lib/Environment"

const SKIP_CACHE = (
Environment.TEST_MODE || Environment.DEVELOPMENT_MODE
)

const load = async (ctx: Context, key: string) => {
if (SKIP_CACHE) return null

const cacheData = await ctx.redisGet(key)
return JSON.parse(cacheData)
}

/**
* Loads data from redis using the given key, or fetches
* new data if there is a cache miss, populating the cache
Expand All @@ -15,18 +26,14 @@ import Environment from "../lib/Environment"
* @returns The new or cached data.
*/
const loadOrFetch = async (ctx: Context, key, fetchNewData, ttl) => {

const skipCache = (
Environment.TEST_MODE || Environment.DEVELOPMENT_MODE
)
if (!skipCache) {
if (!SKIP_CACHE) {
const cacheData = await ctx.redisGet(key)
if (cacheData) {
return JSON.parse(cacheData)
}
}
const newData = await fetchNewData()
if (!skipCache) {
if (!SKIP_CACHE) {
if (ttl) {
await ctx.redisSet(key, JSON.stringify(newData), `EX`, ttl)
} else {
Expand All @@ -38,4 +45,5 @@ const loadOrFetch = async (ctx: Context, key, fetchNewData, ttl) => {

export default {
loadOrFetch,
load,
}
3 changes: 3 additions & 0 deletions src/redis/ttl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ export default {
WORKSPACE_EQUIPMENT_TTL: DAY,
PEOPLE_SEARCH_TTL: DAY,
ROOMS_SEARCH_TTL: DAY,
LIBCAL_CATEGORIES_TTL: DAY,
LIBCAL_LOCATIONS_TTL: DAY,
LIBCAL_BOOKINGS_TTL: MINUTE,
}
104 changes: 91 additions & 13 deletions src/uclapi/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Koa from 'koa'
import Koa, { Context } from 'koa'
import Router from 'koa-router'
import { jwt } from '../middleware/auth'
import redis from '../redis'
Expand All @@ -12,6 +12,14 @@ import {
} from './timetable'
import { getUserData } from './user'
import Workspaces from './workspaces'
import {
cancelBooking,
getGroupSpaces,
getLocations,
getPersonalBookingsSevenDays,
getSeats,
reserveSpace,
} from "./libcal"

const app = new Koa()
const router = new Router()
Expand Down Expand Up @@ -50,7 +58,7 @@ router.get(`/timetable/week`, jwt, async ctx => {
ctx.set(`Last-Modified`, lastModified)
})

router.get(`/timetable/:module`, jwt, async ctx => {
router.get(`/timetable/:module`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.module, 400)
const { module: timetableModule } = ctx.params
const date = ctx.query.date || null
Expand All @@ -70,7 +78,7 @@ router.get(`/timetable/:module`, jwt, async ctx => {
ctx.set(`Last-Modified`, lastModified)
})

router.get(`/search/people`, jwt, async ctx => {
router.get(`/search/people`, jwt, async (ctx: Context) => {
ctx.assert(
(ctx.query.query || ``).length >= 3,
400,
Expand All @@ -85,7 +93,7 @@ router.get(`/search/people`, jwt, async ctx => {
ctx.body = data
})

router.get(`/search/rooms`, jwt, async ctx => {
router.get(`/search/rooms`, jwt, async (ctx: Context) => {
ctx.assert(
(ctx.query.query || ``).length >= 3,
400,
Expand All @@ -110,29 +118,27 @@ router.get(`/sites`, jwt, async ctx => {
ctx.body = rooms
})

router.get(`/equipment`, jwt, async ctx => {
router.get(`/equipment`, jwt, async (ctx: Context) => {
ctx.assert(ctx.query.roomid, 400, `Must specify roomid`)
ctx.assert(ctx.query.siteid, 400, `Must specify siteid`)
const data = await redis.loadOrFetch(
ctx,
`${
redis.keys.WORKSPACE_EQUIPMENT_PATH
}/${ctx.query.roomid}/${ctx.query.siteid}`,
`${redis.keys.WORKSPACE_EQUIPMENT_PATH}/${ctx.query.roomid}/${ctx.query.siteid}`,
async () => getEquipment(ctx.query.roomid, ctx.query.siteid),
redis.ttl.WORKSPACE_EQUIPMENT_TTL,
)
ctx.body = data
})

router.get(`/workspaces/getimage/:id.png`, jwt, async ctx => {
router.get(`/workspaces/getimage/:id.png`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.id, 400, `Must specify id`)
ctx.set({ "Content-Type": `image/png` })
ctx.state.jsonify = false
const res = await Workspaces.getImage(ctx.params.id)
ctx.body = res
})

router.get(`/workspaces/getliveimage/map.svg`, jwt, async ctx => {
router.get(`/workspaces/getliveimage/map.svg`, jwt, async (ctx: Context) => {
ctx.assert(ctx.query.survey_id)
ctx.assert(ctx.query.map_id)
ctx.set({ "Content-Type": `image/svg+xml` })
Expand All @@ -159,7 +165,7 @@ router.get(`/workspaces/summary`, jwt, async ctx => {
ctx.set(`Last-Modified`, lastModified)
})

router.get(`/workspaces/historic`, jwt, async ctx => {
router.get(`/workspaces/historic`, jwt, async (ctx: Context) => {
ctx.assert(ctx.query.id, 400, `Need to include a survey id.`)
const data = await redis.loadOrFetch(
ctx,
Expand All @@ -170,7 +176,7 @@ router.get(`/workspaces/historic`, jwt, async ctx => {
ctx.body = data
})

router.get(`/workspaces/:id/seatinfo`, jwt, async ctx => {
router.get(`/workspaces/:id/seatinfo`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.id, 400)
ctx.body = await Workspaces.getSeatingInfo(ctx.params.id)
})
Expand All @@ -187,7 +193,7 @@ router.get(`/workspaces`, jwt, async ctx => {
)
})

router.get(`/roombookings`, jwt, async ctx => {
router.get(`/roombookings`, jwt, async (ctx: Context) => {
ctx.assert(ctx.query.roomid, 400, `Please include a roomid`)
ctx.assert(ctx.query.siteid, 400, `Please include a siteid`)
ctx.assert(ctx.query.date, 400, `Please include a date`)
Expand All @@ -206,6 +212,78 @@ router.get(`/freerooms`, jwt, async ctx => {
ctx.body = await getFreeRooms(<string>startDateTime, <string>endDateTime)
})

router.get(`/libcal/locations`, jwt, async (ctx: Context) => {
ctx.body = await redis.loadOrFetch(
ctx,
redis.keys.LIBCAL_LOCATIONS_PATH,
async () => getLocations(ctx.state.user.apiToken),
redis.ttl.LIBCAL_LOCATIONS_TTL,
)
})

// Note: don't cache this endpoint, because ?availability query param
// implies always needing fresh data
router.get(`/libcal/locations/:id/seats`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.id, 400)
ctx.assert(ctx.query.date, 400)
ctx.body = await getSeats(
ctx,
ctx.state.user.apiToken,
ctx.params.id,
ctx.query.date as string,
)
})

// Note: there is more nuanced cache implementation within the implementation
// Do not cache the entire endpoint
router.get(`/libcal/locations/:id/groupSpaces`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.id, 400)
ctx.assert(ctx.query.date, 400)
ctx.body = await getGroupSpaces(
ctx,
ctx.state.user.apiToken,
ctx.params.id,
ctx.query.date as string,
)
})

router.post(`/libcal/spaces/:id/reserve`, jwt, async (ctx: Context) => {
ctx.assert(ctx.params.id, 400)

const body = ctx.request.body as { [key: string]: string | number }
ctx.assert(body.from, 400)
ctx.assert(body.to, 400)

ctx.body = await reserveSpace(
ctx.state.user.apiToken,
parseInt(ctx.params.id, 10),
body.from as string,
body.to as string,
body.seat_id
? parseInt(body.seat_id as string, 10)
: null,
)
})

router.get(`/libcal/bookings`, jwt, async (ctx: Context) => {
ctx.body = await redis.loadOrFetch(
ctx,
redis.keys.LIBCAL_BOOKINGS_PATH,
async () => await getPersonalBookingsSevenDays(ctx.state.user.apiToken),
redis.ttl.LIBCAL_BOOKINGS_TTL,
)
})

router.post(`/libcal/bookings/cancel`, jwt, async (ctx: Context) => {
const body = ctx.request.body as { [key: string]: string | number }
ctx.assert(body.booking_id, 400)

ctx.body = await cancelBooking(
ctx.state.user.apiToken,
body.booking_id as string,
)
})

app.use(router.routes())
app.use(router.allowedMethods())

Expand Down
Loading

0 comments on commit 6d9248f

Please sign in to comment.