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

Feat: new API route to overwrite Sessions & speakers #139

Merged
merged 10 commits into from
Sep 22, 2024
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ jobs:
run: |
npm ci
npm run build
- name: Test serverless functions
working-directory: ./functions
run: npm run test
11,527 changes: 4,433 additions & 7,094 deletions functions/package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"logs": "firebase functions:log",
"apiOnly": "NODE_ENV=development nodemon src/api/index.ts"
"apiOnly": "NODE_ENV=development nodemon src/api/index.ts",
"test": "vitest --dir src"
},
"engines": {
"node": "20"
Expand All @@ -27,6 +28,7 @@
"file-type": "^16.5.4",
"firebase-admin": "^11.11.0",
"firebase-functions": "^4.5.0",
"luxon": "^3.4.4",
"ts-custom-error": "^3.3.1",
"uuid": "^9.0.1"
},
Expand All @@ -36,10 +38,12 @@
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^3.1.0",
"light-my-request": "^6.0.0",
"nodemon": "^3.0.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"tsx": "^4.19.0",
"typescript": "^5.2.2",
"vitest": "^2.0.5"
},
"private": true
}
62 changes: 60 additions & 2 deletions functions/src/api/dao/eventDao.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import firebase from 'firebase-admin'
import { Event } from '../../types'
import firebase, { firestore } from 'firebase-admin'
import { Category, Event, Format, Track } from '../../types'
import FieldValue = firestore.FieldValue
import { randomColor } from '../other/randomColor'

export class EventDao {
public static async getEvent(firebaseApp: firebase.app.App, eventId: string): Promise<Event> {
Expand All @@ -12,4 +14,60 @@ export class EventDao {
}
return data as Event
}

public static async createCategory(
firebaseApp: firebase.app.App,
eventId: string,
category: Category
): Promise<any> {
const db = firebaseApp.firestore()

db.collection(`events`)
.doc(eventId)
.update({
categories: FieldValue.arrayUnion({
id: category.id,
name: category.name,
color: category.color || randomColor(),
}),
})
.catch((error) => {
console.error('error creating category', error)
throw new Error('Error creating category ' + error)
})
}

public static async createTrack(firebaseApp: firebase.app.App, eventId: string, track: Track): Promise<any> {
const db = firebaseApp.firestore()

db.collection(`events`)
.doc(eventId)
.update({
tracks: FieldValue.arrayUnion({
id: track.id,
name: track.name,
}),
})
.catch((error) => {
console.error('error creating track', error)
throw new Error('Error creating track ' + error)
})
}
public static async createFormat(firebaseApp: firebase.app.App, eventId: string, format: Format): Promise<any> {
const db = firebaseApp.firestore()

db.collection(`events`)
.doc(eventId)
.update({
formats: FieldValue.arrayUnion({
id: format.id,
name: format.name,
durationMinutes: format.durationMinutes,
}),
})
.catch((error) => {
console.error('error creating format', error)
throw new Error('Error creating format ' + error)
})
}
}
6 changes: 3 additions & 3 deletions functions/src/api/dao/firebasePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fb, { credential } from 'firebase-admin'
import { FastifyInstance } from 'fastify'
import { defineString } from 'firebase-functions/params'

function firebase(fastify: FastifyInstance, options: any, next: () => void) {
export function setupFirebase(fastify: FastifyInstance, options: any, next: () => void) {
const cert = process.env.FIREBASE_SERVICE_ACCOUNT
? JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT as string)
: undefined
Expand All @@ -22,14 +22,14 @@ function firebase(fastify: FastifyInstance, options: any, next: () => void) {
next()
}

export const firebasePlugin = fp(firebase, {
export const firebasePlugin = fp(setupFirebase, {
fastify: '>=1.1.0',
name: 'fastify-firebase',
})

export const getStorageBucketName = () => {
const storageBucketParam = defineString('BUCKET', {
input: { resource: { resource: { type: 'storage.googleapis.com/Bucket' } } },
input: { resource: { type: 'storage.googleapis.com/Bucket' } },
description:
'This will automatically populate the selector field with the deploying Cloud Project’s storage buckets',
})
Expand Down
50 changes: 48 additions & 2 deletions functions/src/api/dao/sessionDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ import { Session } from '../../types'
const { FieldValue } = firebase.firestore

export class SessionDao {
public static async doesSessionExist(
firebaseApp: firebase.app.App,
eventId: string,
sessionId: string
): Promise<boolean | Session> {
const db = firebaseApp.firestore()

// 1. Check if the session exist
const snapshot = await db.collection(`events/${eventId}/sessions`).doc(sessionId).get()
const existingSessionData = snapshot.data()
if (!existingSessionData) {
return false
}
return existingSessionData as Session
}

public static async createSession(
firebaseApp: firebase.app.App,
eventId: string,
session: Partial<Session> & {
id: string
}
): Promise<any> {
const db = firebaseApp.firestore()

await db
.collection(`events/${eventId}/sessions`)
.doc(session.id)
.set({
...session,
updatedAt: FieldValue.serverTimestamp(),
})
}

public static async updateSession(
firebaseApp: firebase.app.App,
eventId: string,
Expand All @@ -12,8 +46,7 @@ export class SessionDao {
const db = firebaseApp.firestore()

// 1. Check if the session exist
const snapshot = await db.collection(`events/${eventId}/sessions`).doc(partialSession.id).get()
const existingSessionData = snapshot.data()
const existingSessionData = await SessionDao.doesSessionExist(firebaseApp, eventId, partialSession.id)
if (!existingSessionData) {
throw new Error('Session not found')
}
Expand All @@ -31,4 +64,17 @@ export class SessionDao {
const snapshot2 = await db.collection(`events/${eventId}/sessions`).doc(partialSession.id).get()
return snapshot2.data() as Session
}

public static async updateOrCreateSession(
firebaseApp: firebase.app.App,
eventId: string,
session: Partial<Session> & { id: string }
): Promise<any> {
const existingSessionData = await SessionDao.doesSessionExist(firebaseApp, eventId, session.id)
if (!existingSessionData) {
await SessionDao.createSession(firebaseApp, eventId, session)
} else {
await SessionDao.updateSession(firebaseApp, eventId, session)
}
}
}
78 changes: 78 additions & 0 deletions functions/src/api/dao/speakerDao.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Speaker } from '../../types'
import firebase from 'firebase-admin'

const { FieldValue } = firebase.firestore

export class SpeakerDao {
public static async doesSpeakerExist(
firebaseApp: firebase.app.App,
eventId: string,
speakerId: string
): Promise<boolean | Speaker> {
const db = firebaseApp.firestore()

// 1. Check if the session exist
const snapshot = await db.collection(`events/${eventId}/speakers`).doc(speakerId).get()
const existingSessionData = snapshot.data()
if (!existingSessionData) {
return false
}
return existingSessionData as Speaker
}

public static async createSpeaker(
firebaseApp: firebase.app.App,
eventId: string,
speaker: Partial<Speaker> & {
id: string
}
): Promise<any> {
const db = firebaseApp.firestore()

await db
.collection(`events/${eventId}/speakers`)
.doc(speaker.id)
.set({
...speaker,
updatedAt: FieldValue.serverTimestamp(),
})
return speaker
}

public static async updateSpeaker(
firebaseApp: firebase.app.App,
eventId: string,
speaker: Partial<Speaker> & {
id: string
}
): Promise<any> {
const db = firebaseApp.firestore()

// 1. Check if the session exist
const existingSpeakerData = await SpeakerDao.doesSpeakerExist(firebaseApp, eventId, speaker.id)
if (!existingSpeakerData) {
throw new Error('Speaker not found')
}

await db
.collection(`events/${eventId}/speakers`)
.doc(speaker.id)
.set({
...speaker,
updatedAt: FieldValue.serverTimestamp(),
})
}

public static async updateOrCreateSpeaker(
firebaseApp: firebase.app.App,
eventId: string,
speaker: Partial<Speaker> & { id: string }
): Promise<any> {
const existingSpeakerData = await SpeakerDao.doesSpeakerExist(firebaseApp, eventId, speaker.id)
if (!existingSpeakerData) {
await SpeakerDao.createSpeaker(firebaseApp, eventId, speaker)
} else {
await SpeakerDao.updateSpeaker(firebaseApp, eventId, speaker)
}
}
}
22 changes: 22 additions & 0 deletions functions/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect, test, describe } from 'vitest'
import { setupFastify } from './setupFastify'

describe('API base 404 + swagger', () => {
let fastify: any = setupFastify()

test('Swagger is present on API root', async () => {
const res = await fastify.inject({ method: 'get', url: '/' })
expect(res.statusCode).to.equal(302)
expect(res.headers.location).to.equal('./static/index.html')
})
test('404 on non existing route', async () => {
const res = await fastify.inject({ method: 'get', url: '/non-existing-route' })
expect(res.statusCode).to.equal(404)
const body = JSON.parse(res.body)
expect(body).toMatchObject({
message: 'Route GET:/non-existing-route not found',
error: 'Not Found',
statusCode: 404,
})
})
})
76 changes: 3 additions & 73 deletions functions/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,8 @@
import './other/typeBoxAdditionalsFormats'
import { onRequest } from 'firebase-functions/v2/https'
import Fastify from 'fastify'
import { fastifyAuth, FastifyAuthFunction } from '@fastify/auth'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { registerSwagger } from './swagger'
import { app as firebaseApp } from 'firebase-admin'
import cors from '@fastify/cors'
import { firebasePlugin } from './dao/firebasePlugin'
import { setupFastify } from './setupFastify'

import { fastifyErrorHandler } from './other/fastifyErrorHandler'
import { addContentTypeParserForServerless } from './other/addContentTypeParserForServerless'
import { apiKeyPlugin } from './apiKeyPlugin'

import { sponsorsRoutes } from './routes/sponsors/sponsors'
import { filesRoutes } from './routes/file/files'
import { faqRoutes } from './routes/faq/faq'
import { helloRoute } from './routes/hello/hello'
import { sessionsRoutes } from './routes/sessions/sessions'
import { transcriptionRoutes } from './routes/transcription/transcription'

type Firebase = firebaseApp.App

declare module 'fastify' {
interface FastifyInstance {
firebase: Firebase
verifyApiKey: FastifyAuthFunction
}
}

const isDev = !!(process.env.FUNCTIONS_EMULATOR && process.env.FUNCTIONS_EMULATOR === 'true')
const isNodeEnvDev = process.env.NODE_ENV === 'development'

const fastify = Fastify({
logger: isDev,
}).withTypeProvider<TypeBoxTypeProvider>()

if (!isNodeEnvDev) {
addContentTypeParserForServerless(fastify)
}

fastify.register(fastifyAuth)
fastify.register(firebasePlugin)
fastify.register(apiKeyPlugin)
fastify.register(cors, {
origin: '*',
})

fastify.addHook('onSend', (_, reply, _2, done: () => void) => {
reply.header('Cache-Control', 'must-revalidate,no-cache,no-store')
done()
})
registerSwagger(fastify)

fastify.register(sponsorsRoutes)
fastify.register(sessionsRoutes)
fastify.register(faqRoutes)
fastify.register(transcriptionRoutes)
fastify.register(filesRoutes)
fastify.register(helloRoute)

fastify.setErrorHandler(fastifyErrorHandler)

if (isNodeEnvDev) {
fastify.listen({ port: 3000 }, function (err) {
if (err) {
fastify.log.error(err)
console.error('error starting fastify server', err)
process.exit(1)
}
// Server is now listening on ${address}
console.log('listening :3000')
})
} else {
console.log("Running in production mode, don't listen")
}
export const fastify = setupFastify()

export const fastifyFunction = onRequest({ timeoutSeconds: 300 }, async (request, reply) => {
fastify.ready((error) => {
Expand Down
6 changes: 6 additions & 0 deletions functions/src/api/other/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ export class NotFoundError extends HttpError {
super(404, message || 'Not Found')
}
}

export class FormatError extends CustomError {
public constructor(public statusCode: number, public code: string, public reason: string) {
super(reason)
}
}
Loading
Loading