Slide 1

Slide 1 text

Build a web service 3. Implement the server side with Next.js, Firebase, GraphQL, and MySQL

Slide 2

Slide 2 text

Summary 1. Setup GraphQL and integrate MySQL with Prisma and PlanetScale 2. On a server side, use Firebase Auth and Cloud Storage with Firebase Admin 3. Update the logic of uploading and watching a video with a server side code

Slide 3

Slide 3 text

Upload a video flow User Browser GraphQL Cloud Storage MySQL Upload a video Upload a video Upload the video Upload the video Submit Submit Send its metadata Send its metadata Fetch its metadata Fetch its metadata Metadata Metadata Update its metadata Update its metadata Store its metadata Store its metadata Respond with its video id Respond with its video id Show the video detail page Show the video detail page User Browser GraphQL Cloud Storage MySQL

Slide 4

Slide 4 text

Watch a video flow User Browser GraphQL Cloud Storage MySQL Go to a video page Go to a video page Get video data Get video data Get video metadata by its id Get video metadata by its id Video metadata Video metadata Fetch the metadata of the video Fetch the metadata of the video Metadata Metadata Video metadata Video metadata Fetch a video file Fetch a video file Video Video Show a video detail page Show a video detail page User Browser GraphQL Cloud Storage MySQL

Slide 5

Slide 5 text

Setup GraphQL, Prisma, and MySQL There are great examples in a Next.js official repository to set up GraphQL and Prisma GraphQL GraphQL Yoda TypeGraphQL MySQL integration by Prisma and PlanetScale Prisma PlanetScale

Slide 6

Slide 6 text

Set up GraphQL in Next.js We just need to define a server in the /pages/api/{url} import { createServer } from "@graphql-yoga/node"; const server = createServer({ schema: { typeDefs, resolvers, }, endpoint: "/api/graphql", }); export default server;

Slide 7

Slide 7 text

Setup GraphQL with TypeScript Use TypeGraphQL to define schema definitions and resolvers with TypeScript. The code like next for data types. @ObjectType() class Video { @Field((type) => ID) id: number; // ... } @InputType() class VideoInput { @Field() title: string; // ... }

Slide 8

Slide 8 text

Setup GraphQL with TypeScript Resolver definition with TypeScript @Resolver(Video) class VideoResolver { @Query((returns) => Video) async video(@Arg("id", (type) => ID) id: string) { // ... } @Authorized() @Mutation((returns) => String) async submitVideo( @Arg("input") input: VideoInput, @Ctx("currentUser") currentUser: CurrentUser ): Promise { // ... } }

Slide 9

Slide 9 text

Setup GraphQL with TypeScript Use buildSchema for TypeGraphQL resolvers. import { buildSchema, } from "type-graphql"; const server = createServer<{ req: NextApiRequest; res: NextApiResponse; }>({ schema: await buildSchema({ resolvers: [VideoResolver], }), endpoint: "/api", });

Slide 10

Slide 10 text

Set up Prisma and MySQL with Next.js There is an example of setting up Prisma with PlanetScale in Next.js repository MySQL integration by Prisma and PlanetScale Prisma PlanetScale Followed a few steps for the tutorial of PlanetScale to migrate a database.

Slide 11

Slide 11 text

Set up Prisma and MySQL with Next.js There are a few things I needed to be careful to use Prisma With PlanetScale, the step to apply migration is different Just push a database and pull it to the branch When I tried to use prisma migrate dev --name init , there is an error which shadow DB cannot be created. Error: P3014 Prisma Migrate could not create the shadow database. // Original error: create database is not supported 0: migration_core::state::DevDiagnostic at migration-engine/core/src/state.rs:250

Slide 12

Slide 12 text

Set up Prisma and MySQL with Next.js The table schemas are created with the definitions of Prisma models without any conversions In some cases, DB tables schemas are defined with plural and snake cases To change naming conventions of table schemas with models, use @@map and @map attribute descried here It seems there is no way to convert them automaticallly without defining the attributes

Slide 13

Slide 13 text

Prisma definition enum VideoStatus { UNPUBLISHED @map("unpublished") PUBLISHED @map("published") } model Video { id Int @id @default(autoincrement()) userId String @map("user_id") @db.VarChar(64) status VideoStatus contentType String @map("content_type") @db.VarChar(64) filename String @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("videos") }

Slide 14

Slide 14 text

Use Firebase Auth on the server side Send a token by an Authorization header with a Bearer authentication Run a verification check when const server = createServer<{ req: NextApiRequest; res: NextApiResponse; }>({ authChecker: async ({ root, args, context, info }) => { const token = context.request.headers.get("authorization"); if (token == null) { return false; } await auth().verifyIdToken(token.replace("Bearer ", "")); return true; },

Slide 15

Slide 15 text

Update uploading a video 1. Upload a video file from a frontend to a Cloud Storage directly without the server 2. Send the metadata of the video to the server side and update it 1. It's required to verify whether it's the same between the file a user uploads originally and a filepath set by a user later 1. Use md5hash of the metadata of a GCS object 3. Store filename, user id and other metadata in MySQL

Slide 16

Slide 16 text

Update uploading a video @Authorized() @Mutation((returns) => String) async submitVideo( @Arg("input") input: VideoInput, @Ctx("currentUser") currentUser: CurrentUser ): Promise { const gcsFile = storage().bucket().file(path); const [metadata] = await gcsFile.getMetadata(); if (metadata.md5Hash != file.md5Hash) { throw new GraphQLYogaError(INTERNAL_SYSTEM_ERROR); } const video = await prisma.video.create({ data: { userId, filename: file.name, }, }); return String(video.id); }

Slide 17

Slide 17 text

Watch a video @Query((returns) => Video) async video(@Arg("id", (type) => ID) id: string) { const video = await prisma.video.findUnique({ where: { id: parsedId, }, }); // skip const storageRef = ref( getStorage(), `videos/${video.userId}/${video.filename}` ); const url = await getDownloadURL(storageRef) return { id: video.id, // }; }

Slide 18

Slide 18 text

Next 1. Show the timeline of the videos 2. Update the video on a video edit page