Skip to content

Commit

Permalink
feat: connect s3 to task api
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed May 10, 2024
1 parent fe2bdc8 commit c9eca8e
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 36 deletions.
3 changes: 2 additions & 1 deletion server/api/@types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ export type TaskEntity = {
label: string;
done: boolean;
createdTime: number;
image: { url: string; s3Key: string } | undefined;
author: { id: UserId; displayName: string | undefined };
};

export type TaskCreateVal = { label: string };
export type TaskCreateVal = { label: string; image?: Blob };

export type TaskUpdateVal = { taskId: Maybe<TaskId>; label?: string; done?: boolean };
2 changes: 2 additions & 0 deletions server/api/health/controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prismaClient } from 'service/prismaClient';
import { s3 } from 'service/s3Client';
import { defineController } from './$relay';

export default defineController(() => ({
Expand All @@ -7,6 +8,7 @@ export default defineController(() => ({
body: {
server: 'ok',
db: await prismaClient.$queryRaw`SELECT CURRENT_TIMESTAMP;`.then(() => 'ok' as const),
storage: await s3.health().then(() => 'ok' as const),
},
}),
}));
1 change: 1 addition & 0 deletions server/api/private/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Methods = DefineMethods<{
};

post: {
reqFormat: FormData;
reqBody: TaskCreateVal;
resBody: TaskEntity;
};
Expand Down
6 changes: 6 additions & 0 deletions server/domain/task/model/taskEntity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { MultipartFile } from '@fastify/multipart';
import type { DeletableTaskId } from 'api/@types/brandedId';
import type { TaskEntity } from 'api/@types/task';
import type { S3PutParams } from 'service/s3Client';

export type TaskCreateServerVal = { label: string; image?: MultipartFile };

export type TaskSaveVal = { task: TaskEntity; s3Params?: S3PutParams };

export type TaskDeleteVal = { deletableId: DeletableTaskId; task: TaskEntity };
26 changes: 19 additions & 7 deletions server/domain/task/model/taskMethod.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import type { TaskCreateVal, TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { UserEntity } from 'api/@types/user';
import assert from 'assert';
import { randomUUID } from 'crypto';
import { deletableTaskIdParser, taskIdParser } from 'service/idParsers';
import type { TaskDeleteVal } from './taskEntity';
import { s3 } from 'service/s3Client';
import type { TaskCreateServerVal, TaskDeleteVal, TaskSaveVal } from './taskEntity';

export const taskMethod = {
create: (user: UserEntity, val: TaskCreateVal): TaskEntity => {
return {
create: async (user: UserEntity, val: TaskCreateServerVal): Promise<TaskSaveVal> => {
const task: TaskEntity = {
id: taskIdParser.parse(randomUUID()),
label: val.label,
done: false,
label: val.label,
image: undefined,
createdTime: Date.now(),
author: { id: user.id, displayName: user.displayName },
};

if (val.image === undefined) return { task };

const s3Key = `tasks/images/${randomUUID()}.${val.image.filename.split('.').at(-1)}`;
const url = await s3.getSignedUrl(s3Key);

return {
task: { ...task, image: { s3Key, url } },
s3Params: { key: s3Key, data: val.image },
};
},
update: (user: UserEntity, task: TaskEntity, val: TaskUpdateVal): TaskEntity => {
update: (user: UserEntity, task: TaskEntity, val: TaskUpdateVal): TaskSaveVal => {
assert(user.id === task.author.id);

return { ...task, ...val };
return { task: { ...task, ...val } };
},
delete: (user: UserEntity, task: TaskEntity): TaskDeleteVal => {
assert(user.id === task.author.id);
Expand Down
25 changes: 15 additions & 10 deletions server/domain/task/repository/taskCommand.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import type { Prisma } from '@prisma/client';
import type { TaskEntity } from 'api/@types/task';
import type { TaskDeleteVal } from '../model/taskEntity';
import { s3 } from 'service/s3Client';
import type { TaskDeleteVal, TaskSaveVal } from '../model/taskEntity';

export const taskCommand = {
save: async (tx: Prisma.TransactionClient, task: TaskEntity): Promise<void> => {
save: async (tx: Prisma.TransactionClient, val: TaskSaveVal): Promise<void> => {
if (val.s3Params !== undefined) await s3.put(val.s3Params);

await tx.task.upsert({
where: { id: task.id },
update: { label: task.label, done: task.done },
where: { id: val.task.id },
update: { label: val.task.label, done: val.task.done, imageKey: val.task.image?.s3Key },
create: {
id: task.id,
label: task.label,
done: task.done,
createdAt: new Date(task.createdTime),
authorId: task.author.id,
id: val.task.id,
label: val.task.label,
done: val.task.done,
imageKey: val.task.image?.s3Key,
createdAt: new Date(val.task.createdTime),
authorId: val.task.author.id,
},
});
},
delete: async (tx: Prisma.TransactionClient, val: TaskDeleteVal): Promise<void> => {
await tx.task.delete({ where: { id: val.deletableId } });

if (val.task.image !== undefined) await s3.delete(val.task.image.s3Key);
},
};
9 changes: 7 additions & 2 deletions server/domain/task/repository/taskQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import type { Prisma, Task, User } from '@prisma/client';
import type { Maybe, TaskId, UserId } from 'api/@types/brandedId';
import type { TaskEntity } from 'api/@types/task';
import { taskIdParser, userIdParser } from 'service/idParsers';
import { s3 } from 'service/s3Client';
import { depend } from 'velona';

const toModel = (prismaTask: Task & { Author: User }): TaskEntity => ({
const toModel = async (prismaTask: Task & { Author: User }): Promise<TaskEntity> => ({
id: taskIdParser.parse(prismaTask.id),
label: prismaTask.label,
done: prismaTask.done,
image:
prismaTask.imageKey === null
? undefined
: { url: await s3.getSignedUrl(prismaTask.imageKey), s3Key: prismaTask.imageKey },
author: {
id: userIdParser.parse(prismaTask.authorId),
displayName: prismaTask.Author.displayName ?? undefined,
Expand All @@ -27,7 +32,7 @@ const findManyByAuthorId = async (
include: { Author: true },
});

return prismaTasks.map(toModel);
return Promise.all(prismaTasks.map(toModel));
};

export const taskQuery = {
Expand Down
9 changes: 7 additions & 2 deletions server/domain/task/service/taskValidator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { TaskCreateVal, TaskUpdateVal } from 'api/@types/task';
import { multipartFileValidator } from 'api/$relay';
import type { TaskUpdateVal } from 'api/@types/task';
import { taskIdParser } from 'service/idParsers';
import { z } from 'zod';
import type { TaskCreateServerVal } from '../model/taskEntity';

export const taskValidator = {
taskCreate: z.object({ label: z.string() }) satisfies z.ZodType<TaskCreateVal>,
taskCreate: z.object({
label: z.string(),
image: multipartFileValidator().optional(),
}) satisfies z.ZodType<TaskCreateServerVal>,
taskUpdate: z.object({
taskId: taskIdParser,
label: z.string().optional(),
Expand Down
17 changes: 9 additions & 8 deletions server/domain/task/useCase/taskUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import type { Maybe, TaskId } from 'api/@types/brandedId';
import type { TaskCreateVal, TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { UserEntity } from 'api/@types/user';
import { transaction } from 'service/prismaClient';
import type { TaskCreateServerVal } from '../model/taskEntity';
import { taskMethod } from '../model/taskMethod';
import { taskCommand } from '../repository/taskCommand';
import { taskQuery } from '../repository/taskQuery';

export const taskUseCase = {
create: (user: UserEntity, val: TaskCreateVal): Promise<TaskEntity> =>
create: (user: UserEntity, val: TaskCreateServerVal): Promise<TaskEntity> =>
transaction('RepeatableRead', async (tx) => {
const task = await taskMethod.create(user, val);
const created = await taskMethod.create(user, val);

await taskCommand.save(tx, task);
await taskCommand.save(tx, created);

return task;
return created.task;
}),
update: (user: UserEntity, val: TaskUpdateVal): Promise<TaskEntity> =>
transaction('RepeatableRead', async (tx) => {
Expand All @@ -22,14 +23,14 @@ export const taskUseCase = {

await taskCommand.save(tx, updated);

return updated;
return updated.task;
}),
delete: (user: UserEntity, taskId: Maybe<TaskId>): Promise<TaskEntity> =>
transaction('RepeatableRead', async (tx) => {
const task = await taskQuery.findById(tx, taskId);
const deleteVal = taskMethod.delete(user, task);
const deleted = taskMethod.delete(user, task);

await taskCommand.delete(tx, deleteVal);
await taskCommand.delete(tx, deleted);

return task;
}),
Expand Down
125 changes: 125 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.569.0",
"@aws-sdk/s3-request-presigner": "^3.572.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^9.0.1",
"@fastify/helmet": "^11.1.1",
Expand Down
2 changes: 2 additions & 0 deletions server/prisma/migrations/20240510165220_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "imageKey" TEXT;
1 change: 1 addition & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ model Task {
id String @id
label String
done Boolean
imageKey String?
createdAt DateTime
Author User @relation(fields: [authorId], references: [id])
authorId String
Expand Down
Loading

0 comments on commit c9eca8e

Please sign in to comment.