Skip to content

Commit

Permalink
Major refactor to move several key tables from TABLE_ROWS (pre-fetche…
Browse files Browse the repository at this point in the history
…d in code) to live DB fetches, allowing the team to immediately see changes to these tables take effect
  • Loading branch information
brundonsmith committed Apr 5, 2024
1 parent 9d3f16d commit 975d32d
Show file tree
Hide file tree
Showing 21 changed files with 281 additions and 232 deletions.
5 changes: 0 additions & 5 deletions back-end/generate-db-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,8 @@ type PostgresColumnType = keyof typeof TYPE_MAP
* relatively small, and don't change very often!
*/
const TABLES_TO_DUMP: readonly string[] = [
'purchase_type',
'discount',
'volunteer_type',
'diet',
'festival',
'festival_site',
'event_site',
'event_type',
'age_range'
]
Expand Down
2 changes: 2 additions & 0 deletions back-end/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import v1_account from './v1/account.ts'
import v1_event from './v1/event.ts'
import v1_purchase from './v1/purchase.ts'
import v1_misc from './v1/misc.ts'
import v1_tables from './v1/tables.ts'

export const router = new Router()

Expand All @@ -15,6 +16,7 @@ v1_account(router)
v1_event(router)
v1_misc(router)
v1_purchase(router)
v1_tables(router)

router.get('/', (ctx) => {
ctx.response.body = 'OK'
Expand Down
16 changes: 6 additions & 10 deletions back-end/routes/v1/_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,17 @@ export const rateLimited = <TContext extends AnyRouteContext, TReturn extends [u
}
}

export const cached = <TContext extends AnyRouteContext, TReturn>(ms: number, fn: (context: TContext) => Promise<TReturn>): (context: TContext) => Promise<TReturn> => {
const cache = new Map<string, { timestamp: number, value: TReturn }>()
export const cached = <TReturn>(ms: number, fn: (context: AnyRouteContext) => Promise<TReturn>): (context: AnyRouteContext) => Promise<TReturn> => {
let cache: { timestamp: number, value: TReturn } | undefined

return async (context: TContext): Promise<TReturn> => {
const { ctx: _, ...contextWithoutCtx } = context
const cacheKey = JSON.stringify(contextWithoutCtx)

const cached = cache.get(cacheKey)
if (cached != null && Date.now() - cached.timestamp < ms) {
return cached.value
return async (context: AnyRouteContext): Promise<TReturn> => {
if (cache != null && Date.now() - cache.timestamp < ms) {
return cache.value
}

const value = await fn(context)

cache.set(cacheKey, { timestamp: Date.now(), value })
cache = { timestamp: Date.now(), value }

return value
}
Expand Down
26 changes: 3 additions & 23 deletions back-end/routes/v1/event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Router, Status } from 'oak'
import { defineRoute } from './_common.ts'
import { accountReferralStatus, withDBConnection } from '../../utils/db.ts'
import { TABLE_ROWS, Tables } from '../../types/db-types.ts'
import { Tables } from '../../types/db-types.ts'
import { EventJson } from '../../types/route-types.ts'
import { PURCHASE_TYPES_BY_TYPE } from '../../types/misc.ts'

const eventToJson = (event: Tables['event']): EventJson => ({
...event,
Expand Down Expand Up @@ -81,26 +80,6 @@ export default function register(router: Router) {
},
})

defineRoute(router, {
endpoint: '/event-sites',
method: 'post',
requireAuth: true,
handler: async ({ jwt: { account_id }, body: { festival_id } }) => {
return await withDBConnection(async db => {

// only referred accounts can view events schedule
const { allowedToPurchase } = await accountReferralStatus(db, account_id)
if (!allowedToPurchase) {
return [null, Status.Unauthorized]
}

const festival = TABLE_ROWS.festival.find(f => f.festival_id === festival_id)
return [{ eventSites: TABLE_ROWS.event_site.filter(s => s.festival_site_id === festival?.festival_site_id).toSorted((a, b) => a.name.localeCompare(b.name)) }, Status.OK]
})
}
})


defineRoute(router, {
endpoint: '/event/save',
method: 'post',
Expand All @@ -126,7 +105,8 @@ export default function register(router: Router) {
// TODO: only allow on-site event creation for users with a ticket to
// that specific festival, as opposed to just any festival
const accountPurchases = await db.queryTable('purchase', { where: ['owned_by_account_id', '=', account_id] })
if (!accountPurchases.some(p => PURCHASE_TYPES_BY_TYPE[p.purchase_type_id].is_attendance_ticket)) {
const purchaseTypes = await db.queryTable('purchase_type')
if (!accountPurchases.some(p => purchaseTypes.find(t => t.purchase_type_id === p.purchase_type_id)?.is_attendance_ticket)) {
return [null, Status.Unauthorized]
}

Expand Down
29 changes: 18 additions & 11 deletions back-end/routes/v1/purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import {
withDBTransaction,
} from '../../utils/db.ts'
import { exists, objectEntries, objectFromEntries, purchaseBreakdown, purchaseTypeAvailableNow, sum } from '../../utils/misc.ts'
import { TABLE_ROWS, Tables } from "../../types/db-types.ts"
import { Tables } from "../../types/db-types.ts"
import { Purchases, Routes } from '../../types/route-types.ts'
import { PURCHASE_TYPES_BY_TYPE } from '../../types/misc.ts'
import { sendMail, receiptEmail } from '../../utils/mailgun.ts'
import env from '../../env.ts'

Expand Down Expand Up @@ -39,14 +38,17 @@ export default function register(router: Router) {
count: Number(row.count)
})))

const purchaseTypes = await withDBConnection(db => db.queryTable('purchase_type'))

for (const [purchaseTypeId, toPurchaseCount] of objectEntries(purchases)) {
const purchaseType = PURCHASE_TYPES_BY_TYPE[purchaseTypeId]
const purchaseType = purchaseTypes.find(p => p.purchase_type_id === purchaseTypeId)!
if (purchaseType == null) {
throw Error(`Can't create purchase intent with invalid purchase type: ${purchaseTypeId}`)
}

const { festival_id, max_available, max_per_account } = purchaseType
const festival = TABLE_ROWS.festival.find(f => f.festival_id === festival_id)!
const festivals = await withDBConnection(db => db.queryTable('festival'))
const festival = festivals.find(f => f.festival_id === festival_id)!

// if festival hasn't started sales, don't allow purchases
if (!festival.sales_are_open) {
Expand All @@ -72,7 +74,9 @@ export default function register(router: Router) {
}
}

const discounts = Array.from(new Set(discount_codes.map(c => c.toLocaleUpperCase()))).map(code => TABLE_ROWS.discount.filter(d => d.discount_code.toLocaleUpperCase() === code)).flat()
const allDiscounts = await withDBConnection(db => db.queryTable('discount'))
const discounts = Array.from(new Set(discount_codes.map(c => c.toLocaleUpperCase()))).map(code =>
allDiscounts.filter(d => d.discount_code.toLocaleUpperCase() === code)).flat()

const sanitizedPurchases = objectFromEntries(objectEntries(purchases).map(
([purchaseType, count]) => {
Expand All @@ -82,7 +86,7 @@ export default function register(router: Router) {
return [purchaseType, integerCount]
}))

const purchaseInfo = purchaseBreakdown(sanitizedPurchases, discounts)
const purchaseInfo = await purchaseBreakdown(sanitizedPurchases, discounts, purchaseTypes)

const amount = purchaseInfo
.map(({ discountedPrice }) => discountedPrice)
Expand Down Expand Up @@ -117,7 +121,7 @@ export default function register(router: Router) {

type PurchaseMetadata =
& { accountId: string, discount_ids?: string }
& Record<(typeof TABLE_ROWS)['purchase_type'][number]['purchase_type_id'], string> // stripe converts numbers to strings for some reason
& Record<Tables['purchase_type']['purchase_type_id'], string> // stripe converts numbers to strings for some reason

router.post('/purchase/record', async ctx => {
const rawBody = await ctx.request.body({ type: 'bytes' }).value
Expand Down Expand Up @@ -148,12 +152,14 @@ export default function register(router: Router) {
: event.data.object.payment_intent
)

await withDBTransaction(async (db) => {
await withDBTransaction(async db => {
let festival_id: Tables['festival']['festival_id']

const purchaseTypes = await db.queryTable('purchase_type')

for (const [purchaseType, count] of objectEntries(purchases)) {
for (let i = 0; i < count!; i++) {
festival_id = PURCHASE_TYPES_BY_TYPE[purchaseType].festival_id
festival_id = purchaseTypes.find(p => p.purchase_type_id === purchaseType)!.festival_id

await db.insertTable('purchase', {
owned_by_account_id: accountId,
Expand All @@ -173,8 +179,9 @@ export default function register(router: Router) {

const account = (await db.queryTable('account', { where: ['account_id', '=', accountId] }))[0]!

const discountsArray = discount_ids?.split(',').map(id => TABLE_ROWS.discount.find(d => d.discount_id === id)).filter(exists) ?? []
await sendMail(receiptEmail(account, purchases, discountsArray))
const discounts = await db.queryTable('discount')
const discountsArray = discount_ids?.split(',').map(id => discounts.find(d => d.discount_id === id)).filter(exists) ?? []
await sendMail(await receiptEmail(account, purchases, discountsArray))
})

ctx.response.status = Status.OK
Expand Down
22 changes: 22 additions & 0 deletions back-end/routes/v1/tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Router } from 'https://deno.land/x/[email protected]/router.ts'
import { cached, defineRoute } from './_common.ts'
import { PUBLIC_TABLES, Routes } from '../../types/route-types.ts'
import { Status } from 'https://deno.land/[email protected]/http/http_status.ts'
import { withDBConnection } from '../../utils/db.ts'
import { ONE_MINUTE_MS } from '../../utils/constants.ts'

export default function register(router: Router) {

for (const table of PUBLIC_TABLES) {
const endpoint = `/tables/${table}` as const

defineRoute(router, {
endpoint,
method: 'get',
handler: cached(5 * ONE_MINUTE_MS, async () => {
const rows = await withDBConnection(db => db.queryTable(table)) as Routes[typeof endpoint]['response']
return [rows, Status.OK]
})
})
}
}
125 changes: 46 additions & 79 deletions back-end/types/db-types.ts

Large diffs are not rendered by default.

9 changes: 1 addition & 8 deletions back-end/types/misc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { TABLE_ROWS, Tables } from "./db-types.ts"
import { objectFromEntries } from '../utils/misc.ts'
import { Tables } from "./db-types.ts"

export type VibeJWTPayload = {
iss?: string,
Expand Down Expand Up @@ -30,9 +29,3 @@ export type AttendeeInfo = Omit<Tables['attendee'], 'attendee_id' | 'notes' | 'a
export type UnknownObject = Record<string | number | symbol, unknown>

export type Maybe<T> = T | null | undefined

export const PURCHASE_TYPES_BY_TYPE = objectFromEntries(
TABLE_ROWS.purchase_type.map(r => [r.purchase_type_id, r])
)

export type PurchaseType = keyof typeof PURCHASE_TYPES_BY_TYPE
36 changes: 24 additions & 12 deletions back-end/types/route-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TABLE_ROWS, Tables } from "./db-types.ts"
import { Tables } from "./db-types.ts"
import { AttendeeInfo, FullAccountInfo } from "./misc.ts"

export type Routes = {
Expand Down Expand Up @@ -80,15 +80,6 @@ export type Routes = {
events: (EventJson & { creator_name: string | null, bookmarks: number })[]
}
},
'/event-sites': {
method: 'post',
body: {
festival_id: Tables['festival']['festival_id']
},
response: {
eventSites: Tables['event_site'][]
}
},
'/event/save': {
method: 'post',
body: {
Expand Down Expand Up @@ -133,7 +124,7 @@ export type Routes = {
},
response: { stripe_client_secret: string }
}
}
} & PublicTablesRoutes

export type NewApplication = Omit<Tables['application'], 'application_id' | 'submitted_on' | 'is_accepted'>

Expand All @@ -142,5 +133,26 @@ export type EventJson = Omit<Tables['event'], 'start_datetime' | 'end_datetime'>
end_datetime: string | null
}

export type Purchases = Partial<Record<(typeof TABLE_ROWS)['purchase_type'][number]['purchase_type_id'], number>>
export type Purchases = Partial<Record<Tables['purchase_type']['purchase_type_id'], number>>

/**
* The **entire contents** of these tables will be **public** to all users.
* Don't include any that contain user-specific info!
*/
export const PUBLIC_TABLES = [
'purchase_type',
'discount',
'festival',
'festival_site',
'event_site'
] as const satisfies Readonly<Array<keyof Tables>>

type PublicTables = typeof PUBLIC_TABLES

type PublicTablesRoutes = {
[Table in PublicTables[number]as `/tables/${Table}`]: {
method: 'get',
body: undefined,
response: Tables[Table][]
}
}
4 changes: 0 additions & 4 deletions back-end/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { TABLE_ROWS } from '../types/db-types.ts'

export const REFERRAL_MAXES = [
5, // seed people
3 // 1 degrees of separation
Expand All @@ -10,6 +8,4 @@ export const ONE_MINUTE_MS = 60 * ONE_SECOND_MS
export const ONE_HOUR_MS = 60 * ONE_MINUTE_MS
export const ONE_DAY_MS = 24 * ONE_HOUR_MS

export const FESTIVALS_WITH_SALES_OPEN = TABLE_ROWS.festival.filter(f => f.sales_are_open)

export const PASSWORD_RESET_SECRET_KEY = 'passwordResetSecret'
19 changes: 12 additions & 7 deletions back-end/utils/mailgun.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import env from '../env.ts'
import { encode } from 'std/encoding/base64.ts'
import { Purchases } from '../types/route-types.ts'
import { TABLE_ROWS, Tables } from '../types/db-types.ts'
import { Tables } from '../types/db-types.ts'
import { objectEntries, purchaseBreakdown, sum } from './misc.ts'
import { PURCHASE_TYPES_BY_TYPE } from '../types/misc.ts'
import { PASSWORD_RESET_SECRET_KEY } from './constants.ts'
import { withDBConnection } from './db.ts'

const MAILGUN_DOMAIN = 'mail.vibe.camp'
const FROM = `Vibecamp <support@${MAILGUN_DOMAIN}>`
Expand Down Expand Up @@ -45,16 +45,21 @@ export async function sendMail(email: Email) {
}
}

export const receiptEmail = (account: Pick<Tables['account'], 'email_address' | 'account_id'>, purchases: Purchases, discounts: readonly Tables['discount'][]): Email => {
const purchaseTypes = objectEntries(purchases).filter(([type, count]) => count != null && count > 0).map(([type]) => PURCHASE_TYPES_BY_TYPE[type])
const festival = TABLE_ROWS.festival.find(f => f.festival_id === purchaseTypes[0]?.festival_id)
export const receiptEmail = async (account: Pick<Tables['account'], 'email_address' | 'account_id'>, purchases: Purchases, discounts: readonly Tables['discount'][]): Promise<Email> => {
const allPurchaseTypes = await withDBConnection(db => db.queryTable('purchase_type'))
const allFestivals = await withDBConnection(db => db.queryTable('festival'))

const purchaseTypes = objectEntries(purchases)
.filter(([_, count]) => count != null && count > 0)
.map(([type, _]) => allPurchaseTypes.find(p => p.purchase_type_id === type))
const festival = allFestivals.find(f => f.festival_id === purchaseTypes[0]?.festival_id)
const now = new Date()

const purchasesInfo = purchaseBreakdown(purchases, discounts)
const purchasesInfo = await purchaseBreakdown(purchases, discounts, allPurchaseTypes)

const purchaseRows = purchasesInfo.map(({ purchaseType, count, basePrice, discountMultiplier, discountedPrice }) =>
`<tr>
<td>${PURCHASE_TYPES_BY_TYPE[purchaseType].description} x${count}</td>
<td>${allPurchaseTypes.find(p => p.purchase_type_id === purchaseType)!.description} x${count}</td>
<td>
${discountMultiplier != null
? `<s>$${(basePrice / 100).toFixed(2)}<s> $${(discountedPrice / 100).toFixed(2)}`
Expand Down
Loading

0 comments on commit 975d32d

Please sign in to comment.