Skip to content

Commit

Permalink
refactor: redefine branded id types
Browse files Browse the repository at this point in the history
  • Loading branch information
solufa committed Jun 1, 2024
1 parent d93cc9e commit d33eb19
Show file tree
Hide file tree
Showing 16 changed files with 56 additions and 46 deletions.
2 changes: 2 additions & 0 deletions server/api/@constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const APP_NAME = 'next-frourio-starter';

export const BRANDED_ID_NAMES = ['user', 'task'] as const;
10 changes: 6 additions & 4 deletions server/api/@types/brandedId.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { BRANDED_ID_NAMES } from 'api/@constants';
import type { z } from 'zod';

type Branded<T extends string> = string & z.BRAND<T>;
export type IdName = (typeof BRANDED_ID_NAMES)[number];

export type Maybe<T> = T | Branded<'Maybe'>;
type Branded<T extends string> = string & z.BRAND<T>;
type Entity<T extends IdName> = string & z.BRAND<`${T}EntityId`>;

export type UserId = Branded<'UserId'>;
export type TaskId = Branded<'TaskId'>;
export type EntityId = { [T in IdName]: Entity<T> };
export type MaybeId = { [T in IdName]: Entity<T> | Branded<'maybe'> };
8 changes: 4 additions & 4 deletions server/api/@types/task.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Maybe, TaskId, UserId } from './brandedId';
import type { EntityId, MaybeId } from './brandedId';

export type TaskEntity = {
id: TaskId;
id: EntityId['task'];
label: string;
done: boolean;
createdTime: number;
image: { url: string; s3Key: string } | undefined;
author: { id: UserId; displayName: string | undefined };
author: { id: EntityId['user']; displayName: string | undefined };
};

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

export type TaskUpdateVal = { taskId: Maybe<TaskId>; label?: string; done?: boolean };
export type TaskUpdateVal = { taskId: MaybeId['task']; label?: string; done?: boolean };
4 changes: 2 additions & 2 deletions server/api/@types/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UserId } from './brandedId';
import type { EntityId } from './brandedId';

export type UserEntity = {
id: UserId;
id: EntityId['user'];
email: string;
displayName: string | undefined;
photoURL: string | undefined;
Expand Down
6 changes: 3 additions & 3 deletions server/api/private/tasks/_taskId@string/controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { taskUseCase } from 'domain/task/useCase/taskUseCase';
import { taskIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { z } from 'zod';
import { defineController } from './$relay';

Expand All @@ -9,14 +9,14 @@ export default defineController(() => ({
handler: async ({ user, body, params }) => {
const task = await taskUseCase.update(user, {
...body,
taskId: taskIdParser.parse(params.taskId),
taskId: brandedId.task.entity.parse(params.taskId),
});

return { status: 204, body: task };
},
},
delete: async ({ user, params }) => {
const task = await taskUseCase.delete(user, taskIdParser.parse(params.taskId));
const task = await taskUseCase.delete(user, brandedId.task.entity.parse(params.taskId));

return { status: 204, body: task };
},
Expand Down
4 changes: 2 additions & 2 deletions server/api/private/tasks/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { taskQuery } from 'domain/task/repository/taskQuery';
import { taskValidator } from 'domain/task/service/taskValidator';
import { taskUseCase } from 'domain/task/useCase/taskUseCase';
import { taskIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { prismaClient } from 'service/prismaClient';
import { z } from 'zod';
import { defineController } from './$relay';
Expand All @@ -27,7 +27,7 @@ export default defineController(() => ({
},
},
delete: {
validators: { body: z.object({ taskId: taskIdParser }) },
validators: { body: z.object({ taskId: brandedId.task.maybe }) },
handler: async ({ user, body }) => {
const task = await taskUseCase.delete(user, body.taskId);

Expand Down
4 changes: 2 additions & 2 deletions server/api/private/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Maybe, TaskId } from 'api/@types/brandedId';
import type { MaybeId } from 'api/@types/brandedId';
import type { TaskCreateVal, TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { DefineMethods } from 'aspida';

Expand All @@ -24,7 +24,7 @@ export type Methods = DefineMethods<{

delete: {
reqBody: {
taskId: Maybe<TaskId>;
taskId: MaybeId['task'];
};
status: 204;
resBody: TaskEntity;
Expand Down
4 changes: 2 additions & 2 deletions server/domain/task/model/taskMethod.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { UserEntity } from 'api/@types/user';
import assert from 'assert';
import { taskIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { s3 } from 'service/s3Client';
import { ulid } from 'ulid';
import type { TaskCreateServerVal, TaskDeleteVal, TaskSaveVal } from './taskEntity';

export const taskMethod = {
create: async (user: UserEntity, val: TaskCreateServerVal): Promise<TaskSaveVal> => {
const task: TaskEntity = {
id: taskIdParser.parse(ulid()),
id: brandedId.task.entity.parse(ulid()),
done: false,
label: val.label,
image: undefined,
Expand Down
14 changes: 7 additions & 7 deletions server/domain/task/repository/taskQuery.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import type { Prisma, Task, User } from '@prisma/client';
import type { Maybe, TaskId, UserId } from 'api/@types/brandedId';
import type { EntityId, MaybeId } from 'api/@types/brandedId';
import type { TaskEntity } from 'api/@types/task';
import { taskIdParser, userIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { s3 } from 'service/s3Client';
import { depend } from 'velona';

const toModel = async (prismaTask: Task & { Author: User }): Promise<TaskEntity> => ({
id: taskIdParser.parse(prismaTask.id),
id: brandedId.task.entity.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),
id: brandedId.user.entity.parse(prismaTask.authorId),
displayName: prismaTask.Author.displayName ?? undefined,
},
createdTime: prismaTask.createdAt.getTime(),
});

const listByAuthorId = async (
tx: Prisma.TransactionClient,
authorId: UserId,
authorId: EntityId['user'],
limit?: number,
): Promise<TaskEntity[]> => {
const prismaTasks = await tx.task.findMany({
Expand All @@ -39,9 +39,9 @@ export const taskQuery = {
listByAuthorId,
findManyWithDI: depend(
{ listByAuthorId },
(deps, tx: Prisma.TransactionClient, userId: UserId): Promise<TaskEntity[]> =>
(deps, tx: Prisma.TransactionClient, userId: EntityId['user']): Promise<TaskEntity[]> =>
deps.listByAuthorId(tx, userId),
),
findById: async (tx: Prisma.TransactionClient, taskId: Maybe<TaskId>): Promise<TaskEntity> =>
findById: async (tx: Prisma.TransactionClient, taskId: MaybeId['task']): Promise<TaskEntity> =>
tx.task.findUniqueOrThrow({ where: { id: taskId }, include: { Author: true } }).then(toModel),
};
4 changes: 2 additions & 2 deletions server/domain/task/service/taskValidator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { multipartFileValidator } from 'api/$relay';
import type { TaskUpdateVal } from 'api/@types/task';
import { taskIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { z } from 'zod';
import type { TaskCreateServerVal } from '../model/taskEntity';

Expand All @@ -10,7 +10,7 @@ export const taskValidator = {
image: multipartFileValidator().optional(),
}) satisfies z.ZodType<TaskCreateServerVal>,
taskUpdate: z.object({
taskId: taskIdParser,
taskId: brandedId.task.maybe,
label: z.string().optional(),
done: z.boolean().optional(),
}) satisfies z.ZodType<TaskUpdateVal>,
Expand Down
4 changes: 2 additions & 2 deletions server/domain/task/useCase/taskUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Maybe, TaskId } from 'api/@types/brandedId';
import type { MaybeId } from 'api/@types/brandedId';
import type { TaskEntity, TaskUpdateVal } from 'api/@types/task';
import type { UserEntity } from 'api/@types/user';
import { transaction } from 'service/prismaClient';
Expand All @@ -25,7 +25,7 @@ export const taskUseCase = {

return updated.task;
}),
delete: (user: UserEntity, taskId: Maybe<TaskId>): Promise<TaskEntity> =>
delete: (user: UserEntity, taskId: MaybeId['task']): Promise<TaskEntity> =>
transaction('RepeatableRead', async (tx) => {
const task = await taskQuery.findById(tx, taskId);
const deleted = taskMethod.delete(user, task);
Expand Down
4 changes: 2 additions & 2 deletions server/domain/user/model/userMethod.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { UserEntity } from 'api/@types/user';
import assert from 'assert';
import type { UserRecord } from 'firebase-admin/lib/auth/user-record';
import { userIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';

export const userMethod = {
create: (record: UserRecord): UserEntity => {
assert(record.email);

return {
id: userIdParser.parse(record.uid),
id: brandedId.user.entity.parse(record.uid),
email: record.email,
displayName: record.displayName,
photoURL: record.photoURL,
Expand Down
4 changes: 2 additions & 2 deletions server/domain/user/repository/userQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Prisma, User } from '@prisma/client';
import type { UserEntity } from 'api/@types/user';
import { userIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';

const toUserEntity = (user: User): UserEntity => ({
id: userIdParser.parse(user.id),
id: brandedId.user.entity.parse(user.id),
email: user.email,
displayName: user.displayName ?? undefined,
photoURL: user.photoURL ?? undefined,
Expand Down
13 changes: 13 additions & 0 deletions server/service/brandedId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BRANDED_ID_NAMES } from 'api/@constants';
import type { EntityId, IdName, MaybeId } from 'api/@types/brandedId';
import { z } from 'zod';

export const brandedId = BRANDED_ID_NAMES.reduce(
(dict, current) => ({
...dict,
[current]: { entity: z.string(), maybe: z.string() },
}),
{} as {
[Name in IdName]: { entity: z.ZodType<EntityId[Name]>; maybe: z.ZodType<MaybeId[Name]> };
},
);
8 changes: 0 additions & 8 deletions server/service/idParsers.ts

This file was deleted.

9 changes: 5 additions & 4 deletions server/tests/api/private/di.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Prisma } from '@prisma/client';
import type { UserId } from 'api/@types/brandedId';
import type { EntityId } from 'api/@types/brandedId';
import type { TaskEntity } from 'api/@types/task';
import type { UserEntity } from 'api/@types/user';
import controller from 'api/private/tasks/di/controller';
import fastify from 'fastify';
import { taskIdParser } from 'service/idParsers';
import { brandedId } from 'service/brandedId';
import { ulid } from 'ulid';
import { expect, test } from 'vitest';

test('Dependency Injection', async () => {
Expand All @@ -16,10 +17,10 @@ test('Dependency Injection', async () => {

const mockedFindManyTask = async (
_: Prisma.TransactionClient,
authorId: UserId,
authorId: EntityId['user'],
): Promise<TaskEntity[]> => [
{
id: taskIdParser.parse('foo'),
id: brandedId.task.entity.parse(ulid()),
label: 'baz',
done: false,
image: undefined,
Expand Down

0 comments on commit d33eb19

Please sign in to comment.