Skip to content

Commit

Permalink
#2 - WIP part deux
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikpyt committed Sep 27, 2023
1 parent f5eee44 commit 2454fcc
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 15 deletions.
35 changes: 35 additions & 0 deletions prisma/migrations/20230927083830_item/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "Item" (
"id" SERIAL NOT NULL,
"name" VARCHAR(50) NOT NULL,
"mimeType" VARCHAR(255) NOT NULL,
"blobUrl" VARCHAR(1024) NOT NULL,
"ownerId" INTEGER NOT NULL,
"parentId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),

CONSTRAINT "Item_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Item_blobUrl_idx" ON "Item"("blobUrl");

-- CreateIndex
CREATE INDEX "Item_ownerId_idx" ON "Item"("ownerId");

-- CreateIndex
CREATE INDEX "Item_parentId_idx" ON "Item"("parentId");

-- CreateIndex
CREATE INDEX "Item_ownerId_parentId_idx" ON "Item"("ownerId", "parentId");

-- CreateIndex
CREATE INDEX "Item_deletedAt_idx" ON "Item"("deletedAt");

-- AddForeignKey
ALTER TABLE "Item" ADD CONSTRAINT "Item_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Item" ADD CONSTRAINT "Item_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
58 changes: 44 additions & 14 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,55 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
name String @db.VarChar(50)
email String @unique @db.VarChar(255)
password String @db.Text
sessions UserSession[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
name String @db.VarChar(50)
email String @unique @db.VarChar(255)
password String @db.Text
sessions UserSession[]
Item Item[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model UserSession {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
refreshToken String @db.VarChar(1024)
tokenFamily String @db.VarChar(36)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
refreshToken String @db.VarChar(1024)
tokenFamily String @db.VarChar(36)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tokenFamily])
@@index([userId])
@@index([userId, tokenFamily])
}

model Item {
id Int @id @default(autoincrement())
name String @db.VarChar(50)
mimeType String @db.VarChar(255)
blobUrl String @db.VarChar(1024)
ownerId Int
parentId Int?
owner User @relation(fields: [ownerId], references: [id])
parentItem Item? @relation("ItemToItem", fields: [parentId], references: [id])
Items Item[] @relation("ItemToItem")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([blobUrl])
@@index([ownerId])
@@index([parentId])
@@index([ownerId, parentId])
@@index([deletedAt])
}
47 changes: 46 additions & 1 deletion src/modules/blob/blob.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { FastifyReply, FastifyRequest } from 'fastify';
import { UploadInput } from './blob.schema';
import BlobService from './blob.service';
import { HandleUploadBody } from '@vercel/blob/client';
import ItemService from '../item/item.service';

export default class BlobController {
private blobService: BlobService;
private itemService: ItemService;

constructor(blobService: BlobService) {
this.blobService = blobService;
this.itemService = new ItemService();
}

public async uploadHandler(
Expand All @@ -22,7 +25,49 @@ export default class BlobController {
request.raw,
['text/plain'],
async ({ blob, tokenPayload }) => {
console.log('File uploaded!', blob, tokenPayload);
request.log.info('bloby blob', blob);

if (!tokenPayload) {
request.log.error(
"Vercel blob storage didn't pass a token payload!",
blob,
tokenPayload,
);
throw new Error('Unauthorized');
}

const tokenPayloadObject = JSON.parse(tokenPayload);
if (!tokenPayloadObject.ownerId) {
request.log.error(
"Vercel blob storage didn't pass a valid token payload! ownerId is missing!",
blob,
tokenPayload,
);
throw new Error('Unauthorized');
}

await this.itemService.createItem({
name: blob.pathname,
mimeType: blob.contentType,
blobUrl: blob.url,
ownerId: tokenPayloadObject.ownerId,
parentId: tokenPayloadObject.parentId ?? null,
});
},
async (clientPayload, accessTokenPayload) => {
if (!clientPayload) {
throw new Error('No clientPayload was passed!');
}

const clientPayloadObject = JSON.parse(clientPayload);
if ('parentId' in clientPayloadObject === false) {
throw new Error('clientPayload.parentId is required!');
}

return JSON.stringify({
parentId: clientPayloadObject.parentId,
ownerId: accessTokenPayload.sub,
});
},
);

Expand Down
2 changes: 2 additions & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FastifyInstance, FastifyPluginOptions } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import auth from './auth';
import blob from './blob';
import item from './item';

const getOptionsWithPrefix = (options: FastifyPluginOptions, prefix: string) => {
return {
Expand All @@ -19,5 +20,6 @@ export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPl
await Promise.all([
fastify.register(auth, getOptionsWithPrefix(options, '/auth')),
fastify.register(blob, getOptionsWithPrefix(options, '/blob')),
fastify.register(item, getOptionsWithPrefix(options, '/item')),
]);
});
13 changes: 13 additions & 0 deletions src/modules/item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FastifyInstance, FastifyPluginOptions } from 'fastify';

import fastifyPlugin from 'fastify-plugin';
import itemRoute from './item.route';
import { itemSchemas } from './item.schema';

export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => {
for (const schema of itemSchemas) {
fastify.addSchema(schema);
}

await fastify.register(itemRoute, options);
});
89 changes: 89 additions & 0 deletions src/modules/item/item.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { UploadInput } from './item.schema';
import ItemService from './item.service';
import { HandleUploadBody } from '@vercel/blob/client';
import BlobService from '../blob/blob.service';

export default class ItemController {
private itemService: ItemService;
private blobService: BlobService;

constructor(itemService: ItemService, blobService: BlobService) {
this.itemService = itemService;
this.blobService = blobService;
}

public async uploadHandler(
request: FastifyRequest<{
Body: UploadInput;
}>,
reply: FastifyReply,
) {
try {
const jsonResponse = await this.blobService.handleUpload(
request.body as HandleUploadBody,
request.raw,
['text/plain'],
async ({ blob, tokenPayload }) => {
fastify.log.info('bloby blob', blob);

if (!tokenPayload) {
fastify.log.error(
"Vercel blob storage didn't pass a token payload!",
blob,
tokenPayload,
);
throw new Error('Unauthorized');
}

const tokenPayloadObject = JSON.parse(tokenPayload);
if (!tokenPayloadObject.ownerId) {
fastify.log.error(
"Vercel blob storage didn't pass a valid token payload! ownerId is missing!",
blob,
tokenPayload,
);
throw new Error('Unauthorized');
}

await this.itemService.createItem({
name: blob.pathname,
mimeType: blob.contentType,
blobUrl: blob.url,
ownerId: tokenPayloadObject.ownerId,
parentId: tokenPayloadObject.parentId ?? null,
});
},
async (clientPayload, accessTokenPayload) => {
if (!clientPayload) {
throw new Error('No clientPayload was passed!');
}

const clientPayloadObject = JSON.parse(clientPayload);
if ('parentId' in clientPayloadObject === false) {
throw new Error('clientPayload.parentId is required!');
}

return JSON.stringify({
parentId: clientPayloadObject.parentId,
ownerId: accessTokenPayload.sub,
});
},
);

return reply.code(200).send(jsonResponse);
} catch (e) {
if (e instanceof Error) {
if (e.message === 'Unauthorized') {
return reply.unauthorized();
}

console.error(e);

return reply.badRequest(e.message);
}

return reply.badRequest();
}
}
}
23 changes: 23 additions & 0 deletions src/modules/item/item.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FastifyInstance } from 'fastify';
import ItemController from './item.controller';
import { $ref } from './item.schema';
import ItemService from './item.service';
import BlobService from '../blob/blob.service';

export default async (fastify: FastifyInstance) => {
const itemController = new ItemController(new ItemService(), new BlobService());

fastify.post(
'/',
{
schema: {
headers: {
Authorization: true,
},
tags: ['Item'],
body: $ref('uploadSchema'),
},
},
itemController.uploadHandler.bind(itemController),
);
};
39 changes: 39 additions & 0 deletions src/modules/item/item.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { z } from 'zod';
import { buildJsonSchemas } from 'fastify-zod';

const uploadSchema = z.object({
type: z.enum(['blob.generate-client-token', 'blob.upload-completed']),
payload: z.object({
pathname: z.string().optional(),
callbackUrl: z.string().optional(),
clientPayload: z.string().optional(),
tokenPayload: z.string().optional(),
item: z
.object({
url: z.string(),
pathname: z.string(),
contentType: z.string(),
contentDisposition: z.string(),
})
.optional(),
}),
});

export type UploadInput = z.infer<typeof uploadSchema>;

export type CreateItem = {
name: string;
mimeType: string;
blobUrl: string;
ownerId: number;
parentId: number | null;
};

export const { schemas: itemSchemas, $ref } = buildJsonSchemas(
{
uploadSchema,
},
{
$id: 'itemSchema',
},
);
16 changes: 16 additions & 0 deletions src/modules/item/item.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { prisma } from '../../plugins/prisma';
import { CreateItem } from './item.schema';

export default class ItemService {
public async createItem(input: CreateItem) {
await prisma.item.create({
data: {
name: input.name,
mimeType: input.mimeType,
blobUrl: input.blobUrl,
ownerId: input.ownerId,
parentId: input.parentId,
},
});
}
}

0 comments on commit 2454fcc

Please sign in to comment.