From b0db96cfd16dc95240a184c81d1a7d2e7d6b4ad1 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 10:12:32 +0900 Subject: [PATCH 01/12] move shared infra to dedicated directory --- server/codegen.yml | 2 +- server/package.json | 3 +- server/src/app.ts | 77 +--------- server/src/auth/shared/createOso.ts | 2 +- server/src/context.ts | 67 --------- server/src/resolvers.ts | 58 -------- .../{ => shared/infra}/database/constants.ts | 0 .../20220418120651_init/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../prisma/migrations/migration_lock.toml | 0 .../infra/database}/prisma/schema.prisma | 0 .../shared/infra/database}/prisma/seed.ts | 2 +- server/src/shared/infra/http/app.ts | 139 ++++++++++++++++++ server/src/shared/infra/http/context.ts | 3 + .../infra/http}/generated/resolver-types.ts | 0 server/src/shared/infra/http/resolvers.ts | 80 ++++++++++ server/tests/helpers/request.ts | 2 +- 18 files changed, 230 insertions(+), 205 deletions(-) delete mode 100644 server/src/context.ts delete mode 100644 server/src/resolvers.ts rename server/src/{ => shared/infra}/database/constants.ts (100%) rename server/{ => src/shared/infra/database}/prisma/migrations/20220418120651_init/migration.sql (100%) rename server/{ => src/shared/infra/database}/prisma/migrations/20220418122916_user_user_menu_item/migration.sql (100%) rename server/{ => src/shared/infra/database}/prisma/migrations/20220419001802_user_member_not_null/migration.sql (100%) rename server/{ => src/shared/infra/database}/prisma/migrations/migration_lock.toml (100%) rename server/{ => src/shared/infra/database}/prisma/schema.prisma (100%) rename server/{ => src/shared/infra/database}/prisma/seed.ts (98%) create mode 100644 server/src/shared/infra/http/app.ts create mode 100644 server/src/shared/infra/http/context.ts rename server/src/{ => shared/infra/http}/generated/resolver-types.ts (100%) create mode 100644 server/src/shared/infra/http/resolvers.ts diff --git a/server/codegen.yml b/server/codegen.yml index b792f0f..f5d5b38 100644 --- a/server/codegen.yml +++ b/server/codegen.yml @@ -2,7 +2,7 @@ overwrite: true schema: 'schema.graphql' documents: null generates: - src/generated/resolver-types.ts: + src/shared/infra/http/generated/resolver-types.ts: plugins: - 'typescript' - 'typescript-resolvers' diff --git a/server/package.json b/server/package.json index c178b66..ae3faad 100644 --- a/server/package.json +++ b/server/package.json @@ -14,7 +14,8 @@ "db:seed": "prisma db seed" }, "prisma": { - "seed": "ts-node prisma/seed.ts" + "schema": "src/shared/infra/database/prisma/schema.prisma", + "seed": "ts-node src/shared/infra/database/prisma/seed.ts" }, "author": "", "license": "ISC", diff --git a/server/src/app.ts b/server/src/app.ts index 8a189f8..5f796ac 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,76 +1,3 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import express, { Application } from 'express'; -import { createUsersRouter } from '@/users/usersRouter'; -import { OsoDataFilter } from './auth/shared/repository/osoDataFilter'; -import { - createCoreOso, - createSqliteDataFilterOso, -} from './auth/shared/createOso'; +import { startServer } from './shared/infra/http/app'; -import 'reflect-metadata'; - -import { AuthorizeSqliteRepository } from './auth/shared/repository/authorizeSqliteRepository'; -import { createMembersRouter } from './members/membersRouter'; -import { PrismaClient } from '@prisma/client'; -import { loadSchema } from '@graphql-tools/load'; -import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; -import { createContext } from './context'; -import { createServer } from '@graphql-yoga/node'; -import { createCheckLoggedInMiddleware } from './auth/check-logged-in/checkLoggedInMiddleware'; -import { resolvers } from './resolvers'; - -export const prisma = new PrismaClient(); - -async function start() { - const oso = await createCoreOso(); - const authorizeRepository = new AuthorizeSqliteRepository(prisma); - const authorizer = new Authorizer(authorizeRepository, oso); - - const dataFilterOso = await createSqliteDataFilterOso(); - const dataFilter = new OsoDataFilter(dataFilterOso); - - // users - const usersRouter = createUsersRouter({ - dataFilter, - authorizer, - prisma, - }); - - // members - const membersRouter = createMembersRouter({ - dataFilter, - authorizer, - prisma, - }); - - // express - const app: Application = express(); - app.use(express.json()); - - app.use('/users', usersRouter); - app.use('/members', membersRouter); - - // // Build apollo-server-based graphql endpoint (trial) - const schema = await loadSchema('schema.graphql', { - loaders: [new GraphQLFileLoader()], - }); - const graphQLServer = createServer({ - schema: { - typeDefs: schema, - resolvers: resolvers, - }, - context: createContext({ dataFilter, authorizer, prisma }), - plugins: [], - }); - - app.use('/graphql', createCheckLoggedInMiddleware(authorizer)); - - app.use('/graphql', graphQLServer.requestListener); - - const port: number = 3031; - app.listen(port, function () { - console.log(`App is listening on port ${port} !`); - }); -} - -start(); +startServer(); diff --git a/server/src/auth/shared/createOso.ts b/server/src/auth/shared/createOso.ts index e2c962a..235a78f 100644 --- a/server/src/auth/shared/createOso.ts +++ b/server/src/auth/shared/createOso.ts @@ -1,4 +1,3 @@ -import { prisma } from '@/app'; import { Department } from '@/members/shared/department'; import { Member } from '@/members/shared/member'; import { @@ -11,6 +10,7 @@ import { UserMenuItem } from '@/users/shared/userMenuItem'; import { Oso } from 'oso'; import { Filter, Relation } from 'oso/dist/src/dataFiltering'; import { PrimitivePropertyNames } from '@/shared/sharedTypes'; +import { prisma } from '@/shared/infra/http/app'; // FIXME: Since prisma objects are POJOs, we need to create classes // to pass to Oso by ourselves. diff --git a/server/src/context.ts b/server/src/context.ts deleted file mode 100644 index 5727611..0000000 --- a/server/src/context.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { Authorizer } from './auth/shared/authorizer'; -import { DataFilter } from './auth/shared/dataFilter'; -import { EditMemberDetailService } from './members/edit-member-detail/editMemberDetailService'; -import { EditMemberDetailSqliteRepository } from './members/edit-member-detail/repository/editMemberDetailSqliteRepository'; -import { ListAllMembersService } from './members/list-all-members/listAllMembersService'; -import { ListAllMembersSqliteRepository } from './members/list-all-members/repository/listAllMembersSqliteRepository'; -import { ShowMemberDetailSqliteRepository } from './members/show-member-detail/repository/showMemberDetailSqliteRepository'; -import { ShowMemberDetailService } from './members/show-member-detail/showMemberDetailService'; -import { GetLoggedInUserInfoService } from './users/get-logged-in-user-info/getLoggedInUserInfoService'; -import { GetLoggedInUserInfoSqliteRepository } from './users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export const createContext = - ({ dataFilter, authorizer, prisma }: Dependencies) => - async ({ req }: any): Promise => { - const getLoggedInUserInfoRepository = - new GetLoggedInUserInfoSqliteRepository(dataFilter, prisma); - const getLoggedInUserInfoService = new GetLoggedInUserInfoService( - authorizer, - getLoggedInUserInfoRepository - ); - - const listAllMembersRepository = new ListAllMembersSqliteRepository( - dataFilter, - prisma - ); - const listAllMembersService = new ListAllMembersService( - authorizer, - listAllMembersRepository - ); - const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const showMemberDetailService = new ShowMemberDetailService( - authorizer, - showMemberDetailRepository - ); - const editMemberDetailRepository = new EditMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const editMemberDetailService = new EditMemberDetailService( - authorizer, - editMemberDetailRepository - ); - - return { - getLoggedInUserInfoService, - listAllMembersService, - showMemberDetailService, - editMemberDetailService, - }; - }; - -export interface Context { - getLoggedInUserInfoService: GetLoggedInUserInfoService; - listAllMembersService: ListAllMembersService; - showMemberDetailService: ShowMemberDetailService; - editMemberDetailService: EditMemberDetailService; -} diff --git a/server/src/resolvers.ts b/server/src/resolvers.ts deleted file mode 100644 index 5ee6e58..0000000 --- a/server/src/resolvers.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Resolvers } from './generated/resolver-types'; - -export const resolvers: Resolvers = { - Query: { - userInfo: async (_, __, context) => { - try { - const result = await context.getLoggedInUserInfoService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - listAllMembers: async (_, __, { listAllMembersService }) => { - try { - const result = await listAllMembersService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - showMemberDetail: async (_, { id }, { showMemberDetailService }) => { - try { - const result = await showMemberDetailService.execute({ memberId: id }); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - }, - Mutation: { - editMemberDetail: async (_, { input }, { editMemberDetailService }) => { - try { - const { id: memberId, ...payload } = input; - - const result = await editMemberDetailService.execute({ - memberId, - payload: { - ...(payload.age && { age: payload.age }), - ...(payload.departmentId && { departmentId: payload.departmentId }), - ...(payload.email && { email: payload.email }), - ...(payload.firstName && { firstName: payload.firstName }), - ...(payload.lastName && { lastName: payload.lastName }), - ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), - ...(payload.pr && { pr: payload.pr }), - ...(payload.salary && { salary: payload.salary }), - }, - }); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - }, -}; diff --git a/server/src/database/constants.ts b/server/src/shared/infra/database/constants.ts similarity index 100% rename from server/src/database/constants.ts rename to server/src/shared/infra/database/constants.ts diff --git a/server/prisma/migrations/20220418120651_init/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220418120651_init/migration.sql similarity index 100% rename from server/prisma/migrations/20220418120651_init/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220418120651_init/migration.sql diff --git a/server/prisma/migrations/20220418122916_user_user_menu_item/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220418122916_user_user_menu_item/migration.sql similarity index 100% rename from server/prisma/migrations/20220418122916_user_user_menu_item/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220418122916_user_user_menu_item/migration.sql diff --git a/server/prisma/migrations/20220419001802_user_member_not_null/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220419001802_user_member_not_null/migration.sql similarity index 100% rename from server/prisma/migrations/20220419001802_user_member_not_null/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220419001802_user_member_not_null/migration.sql diff --git a/server/prisma/migrations/migration_lock.toml b/server/src/shared/infra/database/prisma/migrations/migration_lock.toml similarity index 100% rename from server/prisma/migrations/migration_lock.toml rename to server/src/shared/infra/database/prisma/migrations/migration_lock.toml diff --git a/server/prisma/schema.prisma b/server/src/shared/infra/database/prisma/schema.prisma similarity index 100% rename from server/prisma/schema.prisma rename to server/src/shared/infra/database/prisma/schema.prisma diff --git a/server/prisma/seed.ts b/server/src/shared/infra/database/prisma/seed.ts similarity index 98% rename from server/prisma/seed.ts rename to server/src/shared/infra/database/prisma/seed.ts index e1fd814..3ec9518 100644 --- a/server/prisma/seed.ts +++ b/server/src/shared/infra/database/prisma/seed.ts @@ -1,4 +1,4 @@ -import { DEPARTMENT_IDS, USERS } from '../src/database/constants'; +import { DEPARTMENT_IDS, USERS } from '../constants'; import { PrismaClient, Prisma, Member, User } from '@prisma/client'; import faker from '@faker-js/faker'; const prisma = new PrismaClient(); diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts new file mode 100644 index 0000000..431a181 --- /dev/null +++ b/server/src/shared/infra/http/app.ts @@ -0,0 +1,139 @@ +import { Authorizer } from '@/auth/shared/authorizer'; +import express, { Application } from 'express'; +import { createUsersRouter } from '@/users/usersRouter'; + +import 'reflect-metadata'; + +import { PrismaClient } from '@prisma/client'; +import { loadSchema } from '@graphql-tools/load'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { createContext } from './context'; +import { createServer } from '@graphql-yoga/node'; +import { createResolvers } from './resolvers'; +import { DataFilter } from '@/auth/shared/dataFilter'; +import { GetLoggedInUserInfoSqliteRepository } from '@/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; +import { GetLoggedInUserInfoService } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; +import { ListAllMembersSqliteRepository } from '@/members/list-all-members/repository/listAllMembersSqliteRepository'; +import { ListAllMembersService } from '@/members/list-all-members/listAllMembersService'; +import { ShowMemberDetailSqliteRepository } from '@/members/show-member-detail/repository/showMemberDetailSqliteRepository'; +import { ShowMemberDetailService } from '@/members/show-member-detail/showMemberDetailService'; +import { EditMemberDetailSqliteRepository } from '@/members/edit-member-detail/repository/editMemberDetailSqliteRepository'; +import { EditMemberDetailService } from '@/members/edit-member-detail/editMemberDetailService'; +import { createMembersRouter } from '@/members/membersRouter'; +import { + createCoreOso, + createSqliteDataFilterOso, +} from '@/auth/shared/createOso'; +import { AuthorizeSqliteRepository } from '@/auth/shared/repository/authorizeSqliteRepository'; +import { OsoDataFilter } from '@/auth/shared/repository/osoDataFilter'; +import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; + +export const prisma = new PrismaClient(); + +type UseCaseDependencies = { + dataFilter: DataFilter; + authorizer: Authorizer; + prisma: PrismaClient; +}; + +const createUseCases = ({ + dataFilter, + authorizer, + prisma, +}: UseCaseDependencies) => { + const getLoggedInUserInfoRepository = new GetLoggedInUserInfoSqliteRepository( + dataFilter, + prisma + ); + const getLoggedInUserInfoService = new GetLoggedInUserInfoService( + authorizer, + getLoggedInUserInfoRepository + ); + + const listAllMembersRepository = new ListAllMembersSqliteRepository( + dataFilter, + prisma + ); + const listAllMembersService = new ListAllMembersService( + authorizer, + listAllMembersRepository + ); + const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( + dataFilter, + prisma + ); + const showMemberDetailService = new ShowMemberDetailService( + authorizer, + showMemberDetailRepository + ); + const editMemberDetailRepository = new EditMemberDetailSqliteRepository( + dataFilter, + prisma + ); + const editMemberDetailService = new EditMemberDetailService( + authorizer, + editMemberDetailRepository + ); + + return { + getLoggedInUserInfoService, + listAllMembersService, + showMemberDetailService, + editMemberDetailService, + }; +}; + +export async function startServer() { + const oso = await createCoreOso(); + const authorizeRepository = new AuthorizeSqliteRepository(prisma); + const authorizer = new Authorizer(authorizeRepository, oso); + + const dataFilterOso = await createSqliteDataFilterOso(); + const dataFilter = new OsoDataFilter(dataFilterOso); + + // users + const usersRouter = createUsersRouter({ + dataFilter, + authorizer, + prisma, + }); + + // members + const membersRouter = createMembersRouter({ + dataFilter, + authorizer, + prisma, + }); + + // express + const app: Application = express(); + app.use(express.json()); + + app.use('/users', usersRouter); + app.use('/members', membersRouter); + + // // Build apollo-server-based graphql endpoint (trial) + const schema = await loadSchema('schema.graphql', { + loaders: [new GraphQLFileLoader()], + }); + + const useCases = createUseCases({ dataFilter, authorizer, prisma }); + const resolvers = createResolvers(useCases); + const graphQLServer = createServer({ + schema: { + typeDefs: schema, + resolvers: resolvers, + }, + context: createContext(), + plugins: [], + }); + + app.use('/graphql', createCheckLoggedInMiddleware(authorizer)); + + app.use('/graphql', graphQLServer.requestListener); + + const port: number = 3031; + app.listen(port, function () { + console.log(`App is listening on port ${port} !`); + }); +} diff --git a/server/src/shared/infra/http/context.ts b/server/src/shared/infra/http/context.ts new file mode 100644 index 0000000..d075407 --- /dev/null +++ b/server/src/shared/infra/http/context.ts @@ -0,0 +1,3 @@ +export const createContext = () => ({}); + +export interface Context {} diff --git a/server/src/generated/resolver-types.ts b/server/src/shared/infra/http/generated/resolver-types.ts similarity index 100% rename from server/src/generated/resolver-types.ts rename to server/src/shared/infra/http/generated/resolver-types.ts diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts new file mode 100644 index 0000000..ec10e7c --- /dev/null +++ b/server/src/shared/infra/http/resolvers.ts @@ -0,0 +1,80 @@ +import { EditMemberDetailService } from '@/members/edit-member-detail/editMemberDetailService'; +import { ListAllMembersService } from '@/members/list-all-members/listAllMembersService'; +import { ShowMemberDetailService } from '@/members/show-member-detail/showMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; +import { Resolvers } from './generated/resolver-types'; + +type Dependencies = { + getLoggedInUserInfoService: GetLoggedInUserInfoService; + listAllMembersService: ListAllMembersService; + showMemberDetailService: ShowMemberDetailService; + editMemberDetailService: EditMemberDetailService; +}; + +export const createResolvers = ({ + editMemberDetailService, + getLoggedInUserInfoService, + listAllMembersService, + showMemberDetailService, +}: Dependencies): Resolvers => { + return { + Query: { + userInfo: async () => { + try { + const result = await getLoggedInUserInfoService.execute(); + return result; + } catch (err) { + console.error(err); + throw err; + } + }, + listAllMembers: async () => { + try { + const result = await listAllMembersService.execute(); + return result; + } catch (err) { + console.error(err); + throw err; + } + }, + showMemberDetail: async (_, { id }) => { + try { + const result = await showMemberDetailService.execute({ + memberId: id, + }); + return result; + } catch (err) { + console.error(err); + throw err; + } + }, + }, + Mutation: { + editMemberDetail: async (_, { input }) => { + try { + const { id: memberId, ...payload } = input; + + const result = await editMemberDetailService.execute({ + memberId, + payload: { + ...(payload.age && { age: payload.age }), + ...(payload.departmentId && { + departmentId: payload.departmentId, + }), + ...(payload.email && { email: payload.email }), + ...(payload.firstName && { firstName: payload.firstName }), + ...(payload.lastName && { lastName: payload.lastName }), + ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), + ...(payload.pr && { pr: payload.pr }), + ...(payload.salary && { salary: payload.salary }), + }, + }); + return result; + } catch (err) { + console.error(err); + throw err; + } + }, + }, + }; +}; diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts index 5758bbd..f333604 100644 --- a/server/tests/helpers/request.ts +++ b/server/tests/helpers/request.ts @@ -1,5 +1,5 @@ +import { USERS } from '@/shared/infra/database/constants'; import request from 'supertest'; -import { USERS } from '@/database/constants'; export function authorizeRequest( req: request.Test, From d3550da94ab31ed17681bb8a881131c0407d9508 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 11:43:28 +0900 Subject: [PATCH 02/12] update error structure --- server/src/auth/shared/authorizer.ts | 4 +- .../auth/shared/errors/userNotFoundError.ts | 7 ++ ...eRepository.ts => prismaUserRepository.ts} | 9 +-- .../editMemberDetailRepository.ts | 2 +- .../editMemberDetailService.ts | 12 ++-- .../repos/prismaMemberRepository.ts} | 35 ++++------ server/src/members/membersController.ts | 50 -------------- server/src/members/membersRouter.ts | 65 ------------------- .../showMemberDetailSqliteRepository.ts | 56 ---------------- .../showMemberDetailService.ts | 1 - .../useCases/errors/memberNotFoundError.ts | 7 ++ .../errors/memberNothingToUpdateError.ts | 7 ++ server/src/shared/apiError.ts | 44 ------------- .../src/shared/{ => core/errors}/appError.ts | 2 +- .../core/errors/invalidOperationError.ts | 7 ++ server/src/shared/core/result.ts | 47 ++++++++++++++ server/src/shared/core/useCase.ts | 3 + server/src/shared/core/useCaseError.ts | 6 ++ server/src/shared/infra/http/app.ts | 41 ++---------- 19 files changed, 118 insertions(+), 287 deletions(-) create mode 100644 server/src/auth/shared/errors/userNotFoundError.ts rename server/src/auth/shared/repository/{authorizeSqliteRepository.ts => prismaUserRepository.ts} (85%) rename server/src/members/{edit-member-detail/repository/editMemberDetailSqliteRepository.ts => infra/repos/prismaMemberRepository.ts} (65%) delete mode 100644 server/src/members/membersController.ts delete mode 100644 server/src/members/membersRouter.ts delete mode 100644 server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts create mode 100644 server/src/members/useCases/errors/memberNotFoundError.ts create mode 100644 server/src/members/useCases/errors/memberNothingToUpdateError.ts delete mode 100644 server/src/shared/apiError.ts rename server/src/shared/{ => core/errors}/appError.ts (90%) create mode 100644 server/src/shared/core/errors/invalidOperationError.ts create mode 100644 server/src/shared/core/result.ts create mode 100644 server/src/shared/core/useCase.ts create mode 100644 server/src/shared/core/useCaseError.ts diff --git a/server/src/auth/shared/authorizer.ts b/server/src/auth/shared/authorizer.ts index ae1216d..61d2e06 100644 --- a/server/src/auth/shared/authorizer.ts +++ b/server/src/auth/shared/authorizer.ts @@ -1,4 +1,4 @@ -import { AppError, ErrorCodes } from '@/shared/appError'; +import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; import { User } from '@/users/shared/user'; import { Oso } from 'oso'; import { AuthorizeRepository } from './authorizeRepository'; @@ -13,7 +13,7 @@ export class Authorizer { get currentUser() { if (!this._currentUser) { - throw new AppError('invalid operation', ErrorCodes.INVALID_OPERATION); + throw new InvalidOperationError(); } return this._currentUser; } diff --git a/server/src/auth/shared/errors/userNotFoundError.ts b/server/src/auth/shared/errors/userNotFoundError.ts new file mode 100644 index 0000000..d14e104 --- /dev/null +++ b/server/src/auth/shared/errors/userNotFoundError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class UserNotFoundError extends AppError { + constructor(userId: string) { + super(`user not found: ${userId}`, ErrorCodes.USER_NOT_FOUND); + } +} diff --git a/server/src/auth/shared/repository/authorizeSqliteRepository.ts b/server/src/auth/shared/repository/prismaUserRepository.ts similarity index 85% rename from server/src/auth/shared/repository/authorizeSqliteRepository.ts rename to server/src/auth/shared/repository/prismaUserRepository.ts index 051ea6c..52ab763 100644 --- a/server/src/auth/shared/repository/authorizeSqliteRepository.ts +++ b/server/src/auth/shared/repository/prismaUserRepository.ts @@ -1,11 +1,11 @@ import { Department } from '@/members/shared/department'; import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; import { User } from '@/users/shared/user'; import { PrismaClient } from '@prisma/client'; import { AuthorizeRepository } from '../authorizeRepository'; +import { UserNotFoundError } from '../errors/userNotFoundError'; -export class AuthorizeSqliteRepository implements AuthorizeRepository { +export class PrismaUserRepository implements AuthorizeRepository { private readonly prisma: PrismaClient; constructor(prisma: PrismaClient) { this.prisma = prisma; @@ -26,10 +26,7 @@ export class AuthorizeSqliteRepository implements AuthorizeRepository { }); if (!userRecord) { - throw new AppError( - `user id ${userId} not found`, - ErrorCodes.USER_NOT_FOUND - ); + throw new UserNotFoundError(userId); } // TODO: implement helper method diff --git a/server/src/members/edit-member-detail/editMemberDetailRepository.ts b/server/src/members/edit-member-detail/editMemberDetailRepository.ts index c00b34a..f51f519 100644 --- a/server/src/members/edit-member-detail/editMemberDetailRepository.ts +++ b/server/src/members/edit-member-detail/editMemberDetailRepository.ts @@ -13,7 +13,7 @@ export type UpdatePayload = { }; export interface EditMemberDetailRepository { - queryMember(memberId: string): Promise; + queryMember(user: User, memberId: string): Promise; updateMember( user: User, memberId: string, diff --git a/server/src/members/edit-member-detail/editMemberDetailService.ts b/server/src/members/edit-member-detail/editMemberDetailService.ts index 02f680e..515ccc8 100644 --- a/server/src/members/edit-member-detail/editMemberDetailService.ts +++ b/server/src/members/edit-member-detail/editMemberDetailService.ts @@ -1,8 +1,8 @@ import { Authorizer } from '@/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { AppError, ErrorCodes } from '@/shared/appError'; import { Member } from '../shared/member'; +import { MemberNothingToUpdateError } from '../useCases/errors/memberNothingToUpdateError'; import { EditMemberDetailRepository, UpdatePayload, @@ -27,7 +27,10 @@ export class EditMemberDetailService { payload, }: EditMemberDetailRequest): Promise { try { - const member = await this.repository.queryMember(memberId); + const member = await this.repository.queryMember( + this.authorizer.currentUser, + memberId + ); const authorizedFields = await this.authorizer.authorizedFieldsForUser( @@ -50,10 +53,7 @@ export class EditMemberDetailService { }; if (Object.keys(authorizedPayload).length === 0) { - throw new AppError( - 'nothing is able to be updated', - ErrorCodes.BAD_REQUEST - ); + throw new MemberNothingToUpdateError(memberId); } await this.repository.updateMember( diff --git a/server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts b/server/src/members/infra/repos/prismaMemberRepository.ts similarity index 65% rename from server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts rename to server/src/members/infra/repos/prismaMemberRepository.ts index e6f110e..eaa3e18 100644 --- a/server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts +++ b/server/src/members/infra/repos/prismaMemberRepository.ts @@ -1,18 +1,19 @@ import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; import { MemberOrm } from '@/auth/shared/createOso'; import { DataFilter } from '@/auth/shared/dataFilter'; +import { + EditMemberDetailRepository, + UpdatePayload, +} from '@/members/edit-member-detail/editMemberDetailRepository'; import { Department } from '@/members/shared/department'; import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; +import { ShowMemberDetailRepository } from '@/members/show-member-detail/showMemberDetailRepository'; +import { MemberNotFoundError } from '@/members/useCases/errors/memberNotFoundError'; import { User } from '@/users/shared/user'; import { PrismaClient } from '@prisma/client'; -import { - EditMemberDetailRepository, - UpdatePayload, -} from '../editMemberDetailRepository'; -export class EditMemberDetailSqliteRepository - implements EditMemberDetailRepository +export class PrismaMemberRepository + implements ShowMemberDetailRepository, EditMemberDetailRepository { private readonly prisma: PrismaClient; constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { @@ -24,31 +25,23 @@ export class EditMemberDetailSqliteRepository memberId: string, payload: UpdatePayload ): Promise { - await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); + await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); await this.prisma.member.update({ where: { id: memberId }, data: payload }); } - async queryMember(memberId: string): Promise { + async queryMember(user: User, memberId: string): Promise { + await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); + const record = await this.prisma.member.findUnique({ where: { id: memberId }, - include: { - department: true, - }, + include: { department: true }, }); if (!record) { - throw new AppError( - `member not found ${memberId}`, - ErrorCodes.MEMBER_NOT_FOUND - ); + throw new MemberNotFoundError(memberId); } - // TODO: implement helper methods return new Member( record.id, record.avatar, diff --git a/server/src/members/membersController.ts b/server/src/members/membersController.ts deleted file mode 100644 index c38cb02..0000000 --- a/server/src/members/membersController.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Request, Response } from 'express'; -import { UpdatePayload } from './edit-member-detail/editMemberDetailRepository'; -import { EditMemberDetailService } from './edit-member-detail/editMemberDetailService'; -import { ListAllMembersService } from './list-all-members/listAllMembersService'; -import { ShowMemberDetailService } from './show-member-detail/showMemberDetailService'; - -type Dependencies = { - listAllMembersService: ListAllMembersService; - showMemberDetailService: ShowMemberDetailService; - editMemberDetailService: EditMemberDetailService; -}; - -export class MembersController { - private readonly listAllMembersService: ListAllMembersService; - private readonly showMemberDetailService: ShowMemberDetailService; - private readonly editMemberDetailService: EditMemberDetailService; - - constructor(deps: Dependencies) { - this.listAllMembersService = deps.listAllMembersService; - this.showMemberDetailService = deps.showMemberDetailService; - this.editMemberDetailService = deps.editMemberDetailService; - } - - listAllMembers = async (req: Request, res: Response) => { - const result = await this.listAllMembersService.execute(); - res.json(result); - }; - - showMemberDetail = async ( - req: Request<{ memberId: string }>, - res: Response - ) => { - const result = await this.showMemberDetailService.execute({ - memberId: req.params.memberId, - }); - res.json(result); - }; - - editMemberDetail = async ( - req: Request<{ memberId: string }, any, UpdatePayload>, - res: Response - ) => { - const { memberId } = req.params; - const result = await this.editMemberDetailService.execute({ - memberId, - payload: req.body, - }); - res.json(result); - }; -} diff --git a/server/src/members/membersRouter.ts b/server/src/members/membersRouter.ts deleted file mode 100644 index 1d196e3..0000000 --- a/server/src/members/membersRouter.ts +++ /dev/null @@ -1,65 +0,0 @@ -import express from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; -import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; -import { MembersController } from './membersController'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { ListAllMembersSqliteRepository } from './list-all-members/repository/listAllMembersSqliteRepository'; -import { ListAllMembersService } from './list-all-members/listAllMembersService'; -import { ShowMemberDetailSqliteRepository } from './show-member-detail/repository/showMemberDetailSqliteRepository'; -import { ShowMemberDetailService } from './show-member-detail/showMemberDetailService'; -import { EditMemberDetailSqliteRepository } from './edit-member-detail/repository/editMemberDetailSqliteRepository'; -import { EditMemberDetailService } from './edit-member-detail/editMemberDetailService'; -import { asyncHandler } from '@/shared/asyncHandler'; -import { PrismaClient } from '@prisma/client'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export function createMembersRouter({ - dataFilter, - authorizer, - prisma, -}: Dependencies) { - const listAllMembersRepository = new ListAllMembersSqliteRepository( - dataFilter, - prisma - ); - const listAllMembersService = new ListAllMembersService( - authorizer, - listAllMembersRepository - ); - const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const showMemberDetailService = new ShowMemberDetailService( - authorizer, - showMemberDetailRepository - ); - const editMemberDetailRepository = new EditMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const editMemberDetailService = new EditMemberDetailService( - authorizer, - editMemberDetailRepository - ); - const membersController = new MembersController({ - listAllMembersService, - showMemberDetailService, - editMemberDetailService, - }); - - const router = express.Router(); - - router.use(createCheckLoggedInMiddleware(authorizer)); - - router.get('/', asyncHandler(membersController.listAllMembers)); - router.get('/:memberId', asyncHandler(membersController.showMemberDetail)); - router.patch('/:memberId', asyncHandler(membersController.editMemberDetail)); - - return router; -} diff --git a/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts b/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts deleted file mode 100644 index 590cd42..0000000 --- a/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { ShowMemberDetailRepository } from '../showMemberDetailRepository'; - -export class ShowMemberDetailSqliteRepository - implements ShowMemberDetailRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async queryMember(user: User, memberId: string): Promise { - await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - const record = await this.prisma.member.findUnique({ - where: { id: memberId }, - include: { department: true }, - }); - - if (!record) { - throw new AppError( - `member not found ${memberId}`, - ErrorCodes.MEMBER_NOT_FOUND - ); - } - - return new Member( - record.id, - record.avatar, - record.firstName, - record.lastName, - record.age, - record.salary, - new Department( - record.department.id, - record.department.name, - record.department.managerMemberId - ), - record.joinedAt, - record.phoneNumber, - record.email, - record.pr - ); - } -} diff --git a/server/src/members/show-member-detail/showMemberDetailService.ts b/server/src/members/show-member-detail/showMemberDetailService.ts index 4013765..abf4718 100644 --- a/server/src/members/show-member-detail/showMemberDetailService.ts +++ b/server/src/members/show-member-detail/showMemberDetailService.ts @@ -1,6 +1,5 @@ import { Authorizer } from '@/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; import { DisplayableMember, Member } from '../shared/member'; import { ShowMemberDetailRepository } from './showMemberDetailRepository'; diff --git a/server/src/members/useCases/errors/memberNotFoundError.ts b/server/src/members/useCases/errors/memberNotFoundError.ts new file mode 100644 index 0000000..63520f7 --- /dev/null +++ b/server/src/members/useCases/errors/memberNotFoundError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class MemberNotFoundError extends AppError { + constructor(memberId: string) { + super(`member not found: ${memberId}`, ErrorCodes.MEMBER_NOT_FOUND); + } +} diff --git a/server/src/members/useCases/errors/memberNothingToUpdateError.ts b/server/src/members/useCases/errors/memberNothingToUpdateError.ts new file mode 100644 index 0000000..ff4ca65 --- /dev/null +++ b/server/src/members/useCases/errors/memberNothingToUpdateError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class MemberNothingToUpdateError extends AppError { + constructor(memberId: string) { + super(`nothing is able to be updated: ${memberId}`, ErrorCodes.BAD_REQUEST); + } +} diff --git a/server/src/shared/apiError.ts b/server/src/shared/apiError.ts deleted file mode 100644 index 99fd028..0000000 --- a/server/src/shared/apiError.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AppError, ErrorCode, ErrorCodes } from './appError'; -import { NextFunction, Request, Response } from 'express'; - -const StatusCodeMap = { - [ErrorCodes.BAD_REQUEST]: 400, - [ErrorCodes.USER_NOT_FOUND]: 400, - [ErrorCodes.MEMBER_NOT_FOUND]: 400, -}; - -type StatusCodeMapKey = keyof typeof StatusCodeMap; -export class ApiError extends Error { - statusCode: number; - code: ErrorCode; - constructor(message: string, code: ErrorCode, statusCode: number) { - super(message); - Object.setPrototypeOf(this, ApiError.prototype); - - this.code = code; - this.statusCode = statusCode; - } - - static fromAppError(err: AppError) { - const { message, code } = err; - const errorCode = err.code as StatusCodeMapKey; - let statusCode = StatusCodeMap[errorCode]; - if (!statusCode) { - statusCode = 500; - } - return new ApiError(message, code, statusCode); - } -} - -export function apiErrorHandler( - err: Error, - req: Request, - res: Response, - next: NextFunction -) { - if (err instanceof AppError) { - const { statusCode, code, message } = ApiError.fromAppError(err); - return res.status(statusCode).json({ code: code, message: message }); - } - res.status(500).json({ message: 'something went wrong' }); -} diff --git a/server/src/shared/appError.ts b/server/src/shared/core/errors/appError.ts similarity index 90% rename from server/src/shared/appError.ts rename to server/src/shared/core/errors/appError.ts index 0f2af93..92ef5cf 100644 --- a/server/src/shared/appError.ts +++ b/server/src/shared/core/errors/appError.ts @@ -1,4 +1,4 @@ -export class AppError extends Error { +export abstract class AppError extends Error { code: ErrorCode; constructor(message: string, code: ErrorCode) { super(message); diff --git a/server/src/shared/core/errors/invalidOperationError.ts b/server/src/shared/core/errors/invalidOperationError.ts new file mode 100644 index 0000000..8e9f4ba --- /dev/null +++ b/server/src/shared/core/errors/invalidOperationError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from './appError'; + +export class InvalidOperationError extends AppError { + constructor(message = 'invalid operation') { + super(message, ErrorCodes.INVALID_OPERATION); + } +} diff --git a/server/src/shared/core/result.ts b/server/src/shared/core/result.ts new file mode 100644 index 0000000..6bc34b9 --- /dev/null +++ b/server/src/shared/core/result.ts @@ -0,0 +1,47 @@ +export class Result { + public isSuccess: boolean; + public isFailure: boolean; + public error?: Error | null; + private _value?: T; + + private constructor(isSuccess: boolean, error?: Error | null, value?: T) { + if (isSuccess && error) { + throw new Error(`InvalidOperation: A result cannot be + successful and contain an error`); + } + if (!isSuccess && !error) { + throw new Error(`InvalidOperation: A failing result + needs to contain an error message`); + } + + this.isSuccess = isSuccess; + this.isFailure = !isSuccess; + this.error = error; + this._value = value; + + Object.freeze(this); + } + + public getValue(): T { + if (!this.isSuccess) { + throw new Error(`Cant retrieve the value from a failed result.`); + } + + return this._value!; + } + + public static ok(value?: U): Result { + return new Result(true, null, value); + } + + public static fail(error: Error): Result { + return new Result(false, error); + } + + public static combine(results: Result[]): Result { + for (let result of results) { + if (result.isFailure) return result; + } + return Result.ok(); + } +} diff --git a/server/src/shared/core/useCase.ts b/server/src/shared/core/useCase.ts new file mode 100644 index 0000000..b7a7b1a --- /dev/null +++ b/server/src/shared/core/useCase.ts @@ -0,0 +1,3 @@ +export interface UseCase { + execute(request?: Req): Promise | Res; +} diff --git a/server/src/shared/core/useCaseError.ts b/server/src/shared/core/useCaseError.ts new file mode 100644 index 0000000..2d6fee4 --- /dev/null +++ b/server/src/shared/core/useCaseError.ts @@ -0,0 +1,6 @@ +export abstract class UseCaseError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, UseCaseError.prototype); + } +} diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index 431a181..5e2ca42 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -1,6 +1,5 @@ import { Authorizer } from '@/auth/shared/authorizer'; import express, { Application } from 'express'; -import { createUsersRouter } from '@/users/usersRouter'; import 'reflect-metadata'; @@ -15,18 +14,16 @@ import { GetLoggedInUserInfoSqliteRepository } from '@/users/get-logged-in-user- import { GetLoggedInUserInfoService } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; import { ListAllMembersSqliteRepository } from '@/members/list-all-members/repository/listAllMembersSqliteRepository'; import { ListAllMembersService } from '@/members/list-all-members/listAllMembersService'; -import { ShowMemberDetailSqliteRepository } from '@/members/show-member-detail/repository/showMemberDetailSqliteRepository'; import { ShowMemberDetailService } from '@/members/show-member-detail/showMemberDetailService'; -import { EditMemberDetailSqliteRepository } from '@/members/edit-member-detail/repository/editMemberDetailSqliteRepository'; import { EditMemberDetailService } from '@/members/edit-member-detail/editMemberDetailService'; -import { createMembersRouter } from '@/members/membersRouter'; import { createCoreOso, createSqliteDataFilterOso, } from '@/auth/shared/createOso'; -import { AuthorizeSqliteRepository } from '@/auth/shared/repository/authorizeSqliteRepository'; import { OsoDataFilter } from '@/auth/shared/repository/osoDataFilter'; import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; +import { PrismaMemberRepository } from '@/members/infra/repos/prismaMemberRepository'; +import { PrismaUserRepository } from '@/auth/shared/repository/prismaUserRepository'; export const prisma = new PrismaClient(); @@ -58,21 +55,14 @@ const createUseCases = ({ authorizer, listAllMembersRepository ); - const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( - dataFilter, - prisma - ); + const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); const showMemberDetailService = new ShowMemberDetailService( authorizer, - showMemberDetailRepository - ); - const editMemberDetailRepository = new EditMemberDetailSqliteRepository( - dataFilter, - prisma + prismaMemberRepository ); const editMemberDetailService = new EditMemberDetailService( authorizer, - editMemberDetailRepository + prismaMemberRepository ); return { @@ -85,33 +75,16 @@ const createUseCases = ({ export async function startServer() { const oso = await createCoreOso(); - const authorizeRepository = new AuthorizeSqliteRepository(prisma); - const authorizer = new Authorizer(authorizeRepository, oso); + const prismaUserRepository = new PrismaUserRepository(prisma); + const authorizer = new Authorizer(prismaUserRepository, oso); const dataFilterOso = await createSqliteDataFilterOso(); const dataFilter = new OsoDataFilter(dataFilterOso); - // users - const usersRouter = createUsersRouter({ - dataFilter, - authorizer, - prisma, - }); - - // members - const membersRouter = createMembersRouter({ - dataFilter, - authorizer, - prisma, - }); - // express const app: Application = express(); app.use(express.json()); - app.use('/users', usersRouter); - app.use('/members', membersRouter); - // // Build apollo-server-based graphql endpoint (trial) const schema = await loadSchema('schema.graphql', { loaders: [new GraphQLFileLoader()], From ee2b4450baa54da1309b9be4c400296b4d1bc8de Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 13:17:15 +0900 Subject: [PATCH 03/12] change directory structure --- server/src/auth/shared/authorizeRepository.ts | 5 - .../shared/repository/prismaUserRepository.ts | 58 ---------- .../infra/repos/prismaMemberRepository.ts | 63 ----------- .../listAllMembersRepository.ts | 6 -- .../listAllMembersSqliteRepository.ts | 52 --------- .../{ => modules}/auth/policies/main.polar | 0 .../{ => modules}/auth/policies/members.polar | 0 .../{ => modules}/auth/policies/users.polar | 0 .../{ => modules}/auth/shared/authorizer.ts | 6 +- .../auth/shared}/checkLoggedInMiddleware.ts | 2 +- .../auth/shared/constants/actions.ts | 0 .../{ => modules}/auth/shared/createOso.ts | 8 +- .../{ => modules}/auth/shared/dataFilter.ts | 0 .../shared/errors/not-authorized-error.ts | 0 .../auth/shared/errors/userNotFoundError.ts | 0 .../auth/shared/repository/osoDataFilter.ts | 0 .../members/domain}/department.ts | 0 .../members/domain}/member.ts | 22 +--- .../modules/members/dtos/displayableMember.ts | 6 ++ .../infra/repos/prismaMemberRepository.ts | 102 ++++++++++++++++++ .../editMemberDetailRepository.ts | 4 +- .../editMemberDetailService.ts | 10 +- .../useCases/errors/memberNotFoundError.ts | 0 .../errors/memberNothingToUpdateError.ts | 0 .../listAllMembersRepository.ts | 6 ++ .../listAllMembers}/listAllMembersService.ts | 18 ++-- .../showMemberDetailRepository.ts | 4 +- .../showMemberDetailService.ts | 14 ++- .../shared => modules/users/domain}/user.ts | 2 +- .../users/domain}/userMenuItem.ts | 0 .../users/infra/repos/prismaUserRepository.ts | 84 +++++++++++++++ .../getLoggedInUserInfoRepository.ts | 2 +- .../getLoggedInUserInfoService.ts | 4 +- .../getLoggedInUserInfoSqliteRepository.ts | 8 +- server/src/shared/infra/http/app.ts | 39 ++++--- server/src/shared/infra/http/resolvers.ts | 8 +- server/src/users/usersController.ts | 13 --- server/src/users/usersRouter.ts | 39 ------- 38 files changed, 268 insertions(+), 317 deletions(-) delete mode 100644 server/src/auth/shared/authorizeRepository.ts delete mode 100644 server/src/auth/shared/repository/prismaUserRepository.ts delete mode 100644 server/src/members/infra/repos/prismaMemberRepository.ts delete mode 100644 server/src/members/list-all-members/listAllMembersRepository.ts delete mode 100644 server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts rename server/src/{ => modules}/auth/policies/main.polar (100%) rename server/src/{ => modules}/auth/policies/members.polar (100%) rename server/src/{ => modules}/auth/policies/users.polar (100%) rename server/src/{ => modules}/auth/shared/authorizer.ts (86%) rename server/src/{auth/check-logged-in => modules/auth/shared}/checkLoggedInMiddleware.ts (90%) rename server/src/{ => modules}/auth/shared/constants/actions.ts (100%) rename server/src/{ => modules}/auth/shared/createOso.ts (94%) rename server/src/{ => modules}/auth/shared/dataFilter.ts (100%) rename server/src/{ => modules}/auth/shared/errors/not-authorized-error.ts (100%) rename server/src/{ => modules}/auth/shared/errors/userNotFoundError.ts (100%) rename server/src/{ => modules}/auth/shared/repository/osoDataFilter.ts (100%) rename server/src/{members/shared => modules/members/domain}/department.ts (100%) rename server/src/{members/shared => modules/members/domain}/member.ts (67%) create mode 100644 server/src/modules/members/dtos/displayableMember.ts create mode 100644 server/src/modules/members/infra/repos/prismaMemberRepository.ts rename server/src/{members/edit-member-detail => modules/members/useCases/editMemberDetail}/editMemberDetailRepository.ts (80%) rename server/src/{members/edit-member-detail => modules/members/useCases/editMemberDetail}/editMemberDetailService.ts (84%) rename server/src/{ => modules}/members/useCases/errors/memberNotFoundError.ts (100%) rename server/src/{ => modules}/members/useCases/errors/memberNothingToUpdateError.ts (100%) create mode 100644 server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts rename server/src/{members/list-all-members => modules/members/useCases/listAllMembers}/listAllMembersService.ts (70%) rename server/src/{members/show-member-detail => modules/members/useCases/showMemberDetail}/showMemberDetailRepository.ts (53%) rename server/src/{members/show-member-detail => modules/members/useCases/showMemberDetail}/showMemberDetailService.ts (79%) rename server/src/{users/shared => modules/users/domain}/user.ts (72%) rename server/src/{users/shared => modules/users/domain}/userMenuItem.ts (100%) create mode 100644 server/src/modules/users/infra/repos/prismaUserRepository.ts rename server/src/{users/get-logged-in-user-info => modules/users/useCases/getLoggedInUserInfo}/getLoggedInUserInfoRepository.ts (79%) rename server/src/{users/get-logged-in-user-info => modules/users/useCases/getLoggedInUserInfo}/getLoggedInUserInfoService.ts (86%) rename server/src/{users/get-logged-in-user-info => modules/users/useCases/getLoggedInUserInfo}/repository/getLoggedInUserInfoSqliteRepository.ts (74%) delete mode 100644 server/src/users/usersController.ts delete mode 100644 server/src/users/usersRouter.ts diff --git a/server/src/auth/shared/authorizeRepository.ts b/server/src/auth/shared/authorizeRepository.ts deleted file mode 100644 index b8b8e6d..0000000 --- a/server/src/auth/shared/authorizeRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { User } from '@/users/shared/user'; - -export interface AuthorizeRepository { - getUser(userId: string): Promise; -} diff --git a/server/src/auth/shared/repository/prismaUserRepository.ts b/server/src/auth/shared/repository/prismaUserRepository.ts deleted file mode 100644 index 52ab763..0000000 --- a/server/src/auth/shared/repository/prismaUserRepository.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { AuthorizeRepository } from '../authorizeRepository'; -import { UserNotFoundError } from '../errors/userNotFoundError'; - -export class PrismaUserRepository implements AuthorizeRepository { - private readonly prisma: PrismaClient; - constructor(prisma: PrismaClient) { - this.prisma = prisma; - } - - async getUser(userId: string): Promise { - const userRecord = await this.prisma.user.findUnique({ - where: { - id: userId, - }, - include: { - member: { - include: { - department: true, - }, - }, - }, - }); - - if (!userRecord) { - throw new UserNotFoundError(userId); - } - - // TODO: implement helper method - const user = new User( - userRecord.id, - userRecord.username, - new Member( - userRecord.member.id, - userRecord.member.avatar, - userRecord.member.firstName, - userRecord.member.lastName, - userRecord.member.age, - userRecord.member.salary, - new Department( - userRecord.member.department.id, - userRecord.member.department.name, - userRecord.member.department.managerMemberId - ), - userRecord.member.joinedAt, - userRecord.member.phoneNumber, - userRecord.member.email, - userRecord.member.pr - ), - userRecord.isAdmin - ); - - return user; - } -} diff --git a/server/src/members/infra/repos/prismaMemberRepository.ts b/server/src/members/infra/repos/prismaMemberRepository.ts deleted file mode 100644 index eaa3e18..0000000 --- a/server/src/members/infra/repos/prismaMemberRepository.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { - EditMemberDetailRepository, - UpdatePayload, -} from '@/members/edit-member-detail/editMemberDetailRepository'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { ShowMemberDetailRepository } from '@/members/show-member-detail/showMemberDetailRepository'; -import { MemberNotFoundError } from '@/members/useCases/errors/memberNotFoundError'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; - -export class PrismaMemberRepository - implements ShowMemberDetailRepository, EditMemberDetailRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async updateMember( - user: User, - memberId: string, - payload: UpdatePayload - ): Promise { - await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); - - await this.prisma.member.update({ where: { id: memberId }, data: payload }); - } - - async queryMember(user: User, memberId: string): Promise { - await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); - - const record = await this.prisma.member.findUnique({ - where: { id: memberId }, - include: { department: true }, - }); - - if (!record) { - throw new MemberNotFoundError(memberId); - } - - return new Member( - record.id, - record.avatar, - record.firstName, - record.lastName, - record.age, - record.salary, - new Department( - record.department.id, - record.department.name, - record.department.managerMemberId - ), - record.joinedAt, - record.phoneNumber, - record.email, - record.pr - ); - } -} diff --git a/server/src/members/list-all-members/listAllMembersRepository.ts b/server/src/members/list-all-members/listAllMembersRepository.ts deleted file mode 100644 index 56637e4..0000000 --- a/server/src/members/list-all-members/listAllMembersRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; - -export interface ListAllMembersRepository { - queryMembers(user: User): Promise; -} diff --git a/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts b/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts deleted file mode 100644 index 15e520b..0000000 --- a/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { ListAllMembersRepository } from '../listAllMembersRepository'; - -export class ListAllMembersSqliteRepository - implements ListAllMembersRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async queryMembers(user: User): Promise { - const query = await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - const records = await this.prisma.member.findMany({ - where: query, - include: { - department: true, - }, - }); - - return records.map((member) => { - return new Member( - member.id, - member.avatar, - member.firstName, - member.lastName, - member.age, - member.salary, - new Department( - member.department.id, - member.department.name, - member.department.managerMemberId - ), - member.joinedAt, - member.phoneNumber, - member.email, - member.pr - ); - }); - } -} diff --git a/server/src/auth/policies/main.polar b/server/src/modules/auth/policies/main.polar similarity index 100% rename from server/src/auth/policies/main.polar rename to server/src/modules/auth/policies/main.polar diff --git a/server/src/auth/policies/members.polar b/server/src/modules/auth/policies/members.polar similarity index 100% rename from server/src/auth/policies/members.polar rename to server/src/modules/auth/policies/members.polar diff --git a/server/src/auth/policies/users.polar b/server/src/modules/auth/policies/users.polar similarity index 100% rename from server/src/auth/policies/users.polar rename to server/src/modules/auth/policies/users.polar diff --git a/server/src/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts similarity index 86% rename from server/src/auth/shared/authorizer.ts rename to server/src/modules/auth/shared/authorizer.ts index 61d2e06..6467edc 100644 --- a/server/src/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -1,13 +1,13 @@ +import { User } from '@/modules/users/domain/user'; +import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; -import { User } from '@/users/shared/user'; import { Oso } from 'oso'; -import { AuthorizeRepository } from './authorizeRepository'; export class Authorizer { _currentUser?: User; constructor( - private readonly repository: AuthorizeRepository, + private readonly repository: PrismaUserRepository, private readonly oso: Oso ) {} diff --git a/server/src/auth/check-logged-in/checkLoggedInMiddleware.ts b/server/src/modules/auth/shared/checkLoggedInMiddleware.ts similarity index 90% rename from server/src/auth/check-logged-in/checkLoggedInMiddleware.ts rename to server/src/modules/auth/shared/checkLoggedInMiddleware.ts index d5af6a6..563270a 100644 --- a/server/src/auth/check-logged-in/checkLoggedInMiddleware.ts +++ b/server/src/modules/auth/shared/checkLoggedInMiddleware.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; +import { Authorizer } from './authorizer'; export function createCheckLoggedInMiddleware(authorizer: Authorizer) { return async function checkLoggedInMiddleware( diff --git a/server/src/auth/shared/constants/actions.ts b/server/src/modules/auth/shared/constants/actions.ts similarity index 100% rename from server/src/auth/shared/constants/actions.ts rename to server/src/modules/auth/shared/constants/actions.ts diff --git a/server/src/auth/shared/createOso.ts b/server/src/modules/auth/shared/createOso.ts similarity index 94% rename from server/src/auth/shared/createOso.ts rename to server/src/modules/auth/shared/createOso.ts index 235a78f..be50f4c 100644 --- a/server/src/auth/shared/createOso.ts +++ b/server/src/modules/auth/shared/createOso.ts @@ -1,16 +1,16 @@ -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; import { Member as PrismaMember, Department as PrismaDepartment, UserMenuItem as PrismaUserMenuItem, } from '@prisma/client'; -import { User } from '@/users/shared/user'; -import { UserMenuItem } from '@/users/shared/userMenuItem'; import { Oso } from 'oso'; import { Filter, Relation } from 'oso/dist/src/dataFiltering'; import { PrimitivePropertyNames } from '@/shared/sharedTypes'; import { prisma } from '@/shared/infra/http/app'; +import { Department } from '@/modules/members/domain/department'; +import { Member } from '@/modules/members/domain/member'; +import { UserMenuItem } from '@/modules/users/domain/userMenuItem'; +import { User } from '@/modules/users/domain/user'; // FIXME: Since prisma objects are POJOs, we need to create classes // to pass to Oso by ourselves. diff --git a/server/src/auth/shared/dataFilter.ts b/server/src/modules/auth/shared/dataFilter.ts similarity index 100% rename from server/src/auth/shared/dataFilter.ts rename to server/src/modules/auth/shared/dataFilter.ts diff --git a/server/src/auth/shared/errors/not-authorized-error.ts b/server/src/modules/auth/shared/errors/not-authorized-error.ts similarity index 100% rename from server/src/auth/shared/errors/not-authorized-error.ts rename to server/src/modules/auth/shared/errors/not-authorized-error.ts diff --git a/server/src/auth/shared/errors/userNotFoundError.ts b/server/src/modules/auth/shared/errors/userNotFoundError.ts similarity index 100% rename from server/src/auth/shared/errors/userNotFoundError.ts rename to server/src/modules/auth/shared/errors/userNotFoundError.ts diff --git a/server/src/auth/shared/repository/osoDataFilter.ts b/server/src/modules/auth/shared/repository/osoDataFilter.ts similarity index 100% rename from server/src/auth/shared/repository/osoDataFilter.ts rename to server/src/modules/auth/shared/repository/osoDataFilter.ts diff --git a/server/src/members/shared/department.ts b/server/src/modules/members/domain/department.ts similarity index 100% rename from server/src/members/shared/department.ts rename to server/src/modules/members/domain/department.ts diff --git a/server/src/members/shared/member.ts b/server/src/modules/members/domain/member.ts similarity index 67% rename from server/src/members/shared/member.ts rename to server/src/modules/members/domain/member.ts index 5b410c1..219358b 100644 --- a/server/src/members/shared/member.ts +++ b/server/src/modules/members/domain/member.ts @@ -32,29 +32,13 @@ export class Member { createObjectWithAuthorizedFields( this: any, fields: Set - ): DisplayableMember { - const authorizedMember: DisplayableMember = {}; + ): Partial { + const authorizedMember: Partial = {}; for (const field of Array.from(fields.values())) { - authorizedMember[field as keyof DisplayableMember] = this[field]; + authorizedMember[field as keyof Partial] = this[field]; } return authorizedMember; } } - -export type DisplayableMember = { - id?: string; - avatar?: string; - firstName?: string; - lastName?: string; - age?: number; - salary?: number; - department?: Department; - joinedAt?: Date; - phoneNumber?: string; - email?: string; - pr?: string; - editable?: boolean; - isLoggedInUser?: boolean; -}; diff --git a/server/src/modules/members/dtos/displayableMember.ts b/server/src/modules/members/dtos/displayableMember.ts new file mode 100644 index 0000000..b53e64b --- /dev/null +++ b/server/src/modules/members/dtos/displayableMember.ts @@ -0,0 +1,6 @@ +import { Member } from '../domain/member'; + +export type DisplayableMember = Partial & { + editable: boolean; + isLoggedInUser: boolean; +}; diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts new file mode 100644 index 0000000..1123cbd --- /dev/null +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -0,0 +1,102 @@ +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { User } from '@/modules/users/domain/user'; +import { PrismaClient } from '@prisma/client'; +import { Department } from '../../domain/department'; +import { Member } from '../../domain/member'; +import { + EditMemberDetailRepository, + UpdatePayload, +} from '../../useCases/editMemberDetail/editMemberDetailRepository'; +import { MemberNotFoundError } from '../../useCases/errors/memberNotFoundError'; +import { ListAllMembersRepository } from '../../useCases/listAllMembers/listAllMembersRepository'; +import { ShowMemberDetailRepository } from '../../useCases/showMemberDetail/showMemberDetailRepository'; + +export class PrismaMemberRepository + implements + ShowMemberDetailRepository, + EditMemberDetailRepository, + ListAllMembersRepository +{ + private readonly prisma: PrismaClient; + constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { + this.prisma = prisma; + } + + async updateMember( + user: User, + memberId: string, + payload: UpdatePayload + ): Promise { + await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); + + await this.prisma.member.update({ where: { id: memberId }, data: payload }); + } + + async queryMember(user: User, memberId: string): Promise { + await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); + + const record = await this.prisma.member.findUnique({ + where: { id: memberId }, + include: { department: true }, + }); + + if (!record) { + throw new MemberNotFoundError(memberId); + } + + return new Member( + record.id, + record.avatar, + record.firstName, + record.lastName, + record.age, + record.salary, + new Department( + record.department.id, + record.department.name, + record.department.managerMemberId + ), + record.joinedAt, + record.phoneNumber, + record.email, + record.pr + ); + } + + async queryMembers(user: User): Promise { + const query = await this.dataFilter.authorizedQuery( + user, + MEMBER_ACTIONS.READ, + MemberOrm + ); + + const records = await this.prisma.member.findMany({ + where: query, + include: { + department: true, + }, + }); + + return records.map((member) => { + return new Member( + member.id, + member.avatar, + member.firstName, + member.lastName, + member.age, + member.salary, + new Department( + member.department.id, + member.department.name, + member.department.managerMemberId + ), + member.joinedAt, + member.phoneNumber, + member.email, + member.pr + ); + }); + } +} diff --git a/server/src/members/edit-member-detail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts similarity index 80% rename from server/src/members/edit-member-detail/editMemberDetailRepository.ts rename to server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts index f51f519..0b4052b 100644 --- a/server/src/members/edit-member-detail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts @@ -1,5 +1,5 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; +import { User } from '@/modules/users/domain/user'; +import { Member } from '../../domain/member'; export type UpdatePayload = { firstName?: string; diff --git a/server/src/members/edit-member-detail/editMemberDetailService.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts similarity index 84% rename from server/src/members/edit-member-detail/editMemberDetailService.ts rename to server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts index 515ccc8..b75f9d7 100644 --- a/server/src/members/edit-member-detail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts @@ -1,8 +1,8 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { Member } from '../shared/member'; -import { MemberNothingToUpdateError } from '../useCases/errors/memberNothingToUpdateError'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { Member } from '../../domain/member'; +import { MemberNothingToUpdateError } from '../errors/memberNothingToUpdateError'; import { EditMemberDetailRepository, UpdatePayload, diff --git a/server/src/members/useCases/errors/memberNotFoundError.ts b/server/src/modules/members/useCases/errors/memberNotFoundError.ts similarity index 100% rename from server/src/members/useCases/errors/memberNotFoundError.ts rename to server/src/modules/members/useCases/errors/memberNotFoundError.ts diff --git a/server/src/members/useCases/errors/memberNothingToUpdateError.ts b/server/src/modules/members/useCases/errors/memberNothingToUpdateError.ts similarity index 100% rename from server/src/members/useCases/errors/memberNothingToUpdateError.ts rename to server/src/modules/members/useCases/errors/memberNothingToUpdateError.ts diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts new file mode 100644 index 0000000..31249e9 --- /dev/null +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts @@ -0,0 +1,6 @@ +import { User } from '@/modules/users/domain/user'; +import { Member } from '../../domain/member'; + +export interface ListAllMembersRepository { + queryMembers(user: User): Promise; +} diff --git a/server/src/members/list-all-members/listAllMembersService.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts similarity index 70% rename from server/src/members/list-all-members/listAllMembersService.ts rename to server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts index fd85327..c880496 100644 --- a/server/src/members/list-all-members/listAllMembersService.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts @@ -1,12 +1,13 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { DisplayableMember, Member } from '../shared/member'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { Member } from '../../domain/member'; +import { DisplayableMember } from '../../dtos/displayableMember'; import { ListAllMembersRepository } from './listAllMembersRepository'; export type ListAllMembersRequest = {}; export type ListAllMembersResponse = { - members: DisplayableMember[]; + members: Partial[]; }; export class ListAllMembersService { @@ -28,8 +29,11 @@ export class ListAllMembersService { member ); - const authorizedMember = - member.createObjectWithAuthorizedFields(authorizedFields); + const authorizedMember: DisplayableMember = { + ...member.createObjectWithAuthorizedFields(authorizedFields), + editable: false, + isLoggedInUser: false, + }; const allowedActions = await this.authorizer.authorizedActionsForUser( member diff --git a/server/src/members/show-member-detail/showMemberDetailRepository.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts similarity index 53% rename from server/src/members/show-member-detail/showMemberDetailRepository.ts rename to server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts index 37704a5..1eb32b9 100644 --- a/server/src/members/show-member-detail/showMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts @@ -1,5 +1,5 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; +import { User } from '@/modules/users/domain/user'; +import { Member } from '../../domain/member'; export interface ShowMemberDetailRepository { queryMember(user: User, memberId: string): Promise; diff --git a/server/src/members/show-member-detail/showMemberDetailService.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts similarity index 79% rename from server/src/members/show-member-detail/showMemberDetailService.ts rename to server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts index abf4718..e13f330 100644 --- a/server/src/members/show-member-detail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts @@ -1,6 +1,7 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { DisplayableMember, Member } from '../shared/member'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { Member } from '../../domain/member'; +import { DisplayableMember } from '../../dtos/displayableMember'; import { ShowMemberDetailRepository } from './showMemberDetailRepository'; export type ShowMemberDetailRequest = { @@ -31,8 +32,11 @@ export class ShowMemberDetailService { MEMBER_ACTIONS.READ, member ); - const authorizedMember = - member.createObjectWithAuthorizedFields(authorizedFields); + const authorizedMember: DisplayableMember = { + ...member.createObjectWithAuthorizedFields(authorizedFields), + editable: false, + isLoggedInUser: false, + }; const allowedActions = await this.authorizer.authorizedActionsForUser( member diff --git a/server/src/users/shared/user.ts b/server/src/modules/users/domain/user.ts similarity index 72% rename from server/src/users/shared/user.ts rename to server/src/modules/users/domain/user.ts index 502a659..9d14d98 100644 --- a/server/src/users/shared/user.ts +++ b/server/src/modules/users/domain/user.ts @@ -1,4 +1,4 @@ -import { Member } from '@/members/shared/member'; +import { Member } from '@/modules/members/domain/member'; export class User { constructor( diff --git a/server/src/users/shared/userMenuItem.ts b/server/src/modules/users/domain/userMenuItem.ts similarity index 100% rename from server/src/users/shared/userMenuItem.ts rename to server/src/modules/users/domain/userMenuItem.ts diff --git a/server/src/modules/users/infra/repos/prismaUserRepository.ts b/server/src/modules/users/infra/repos/prismaUserRepository.ts new file mode 100644 index 0000000..dce6a0f --- /dev/null +++ b/server/src/modules/users/infra/repos/prismaUserRepository.ts @@ -0,0 +1,84 @@ +import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { UserNotFoundError } from '@/modules/auth/shared/errors/userNotFoundError'; +import { Department } from '@/modules/members/domain/department'; +import { Member } from '@/modules/members/domain/member'; +import { PrismaClient } from '@prisma/client'; +import { User } from '../../domain/user'; +import { + GetLoggedInUserInfoRepository, + UserInfo, +} from '../../useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository'; + +export class PrismaUserRepository implements GetLoggedInUserInfoRepository { + constructor( + private readonly dataFilter: DataFilter, + private readonly prisma: PrismaClient + ) {} + + async queryUserInfo(user: User): Promise { + const query = await this.dataFilter.authorizedQuery( + user, + USER_MENU_ITEM_ACTIONS.READ, + UserMenuItemOrm + ); + + const menuItems = await this.prisma.userMenuItem.findMany({ + select: { + name: true, + }, + where: query, + orderBy: { order: 'asc' }, + }); + + return { + userMenu: menuItems, + }; + } + + async getUser(userId: string): Promise { + const userRecord = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + include: { + member: { + include: { + department: true, + }, + }, + }, + }); + + if (!userRecord) { + throw new UserNotFoundError(userId); + } + + // TODO: implement helper method + const user = new User( + userRecord.id, + userRecord.username, + new Member( + userRecord.member.id, + userRecord.member.avatar, + userRecord.member.firstName, + userRecord.member.lastName, + userRecord.member.age, + userRecord.member.salary, + new Department( + userRecord.member.department.id, + userRecord.member.department.name, + userRecord.member.department.managerMemberId + ), + userRecord.member.joinedAt, + userRecord.member.phoneNumber, + userRecord.member.email, + userRecord.member.pr + ), + userRecord.isAdmin + ); + + return user; + } +} diff --git a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts similarity index 79% rename from server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts rename to server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts index c0a686f..519cedc 100644 --- a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts @@ -1,4 +1,4 @@ -import { User } from '../shared/user'; +import { User } from '../../domain/user'; export type UserInfo = { userMenu: { name: string }[]; diff --git a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts similarity index 86% rename from server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts rename to server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts index f86aeb0..ffc150a 100644 --- a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts @@ -1,5 +1,5 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; import { GetLoggedInUserInfoRepository } from './getLoggedInUserInfoRepository'; export type GetLoggedInUserInfoRequest = {}; diff --git a/server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts similarity index 74% rename from server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts rename to server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts index 352cb08..f0607b6 100644 --- a/server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts @@ -1,7 +1,7 @@ -import { USER_MENU_ITEM_ACTIONS } from '@/auth/shared/constants/actions'; -import { UserMenuItemOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { User } from '@/users/shared/user'; +import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { User } from '@/modules/users/domain/user'; import { PrismaClient } from '@prisma/client'; import { GetLoggedInUserInfoRepository, diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index 5e2ca42..58e96e3 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -1,4 +1,3 @@ -import { Authorizer } from '@/auth/shared/authorizer'; import express, { Application } from 'express'; import 'reflect-metadata'; @@ -9,21 +8,21 @@ import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { createContext } from './context'; import { createServer } from '@graphql-yoga/node'; import { createResolvers } from './resolvers'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { GetLoggedInUserInfoSqliteRepository } from '@/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; -import { GetLoggedInUserInfoService } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; -import { ListAllMembersSqliteRepository } from '@/members/list-all-members/repository/listAllMembersSqliteRepository'; -import { ListAllMembersService } from '@/members/list-all-members/listAllMembersService'; -import { ShowMemberDetailService } from '@/members/show-member-detail/showMemberDetailService'; -import { EditMemberDetailService } from '@/members/edit-member-detail/editMemberDetailService'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { GetLoggedInUserInfoSqliteRepository } from '@/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; +import { ListAllMembersService } from '@/modules/members/useCases/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/editMemberDetail/editMemberDetailService'; import { createCoreOso, createSqliteDataFilterOso, -} from '@/auth/shared/createOso'; -import { OsoDataFilter } from '@/auth/shared/repository/osoDataFilter'; -import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; -import { PrismaMemberRepository } from '@/members/infra/repos/prismaMemberRepository'; -import { PrismaUserRepository } from '@/auth/shared/repository/prismaUserRepository'; +} from '@/modules/auth/shared/createOso'; +import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; +import { OsoDataFilter } from '@/modules/auth/shared/repository/osoDataFilter'; +import { createCheckLoggedInMiddleware } from '@/modules/auth/shared/checkLoggedInMiddleware'; export const prisma = new PrismaClient(); @@ -47,15 +46,12 @@ const createUseCases = ({ getLoggedInUserInfoRepository ); - const listAllMembersRepository = new ListAllMembersSqliteRepository( - dataFilter, - prisma - ); + const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); + const listAllMembersService = new ListAllMembersService( authorizer, - listAllMembersRepository + prismaMemberRepository ); - const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); const showMemberDetailService = new ShowMemberDetailService( authorizer, prismaMemberRepository @@ -75,12 +71,13 @@ const createUseCases = ({ export async function startServer() { const oso = await createCoreOso(); - const prismaUserRepository = new PrismaUserRepository(prisma); - const authorizer = new Authorizer(prismaUserRepository, oso); const dataFilterOso = await createSqliteDataFilterOso(); const dataFilter = new OsoDataFilter(dataFilterOso); + const prismaUserRepository = new PrismaUserRepository(dataFilter, prisma); + const authorizer = new Authorizer(prismaUserRepository, oso); + // express const app: Application = express(); app.use(express.json()); diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts index ec10e7c..81005dc 100644 --- a/server/src/shared/infra/http/resolvers.ts +++ b/server/src/shared/infra/http/resolvers.ts @@ -1,7 +1,7 @@ -import { EditMemberDetailService } from '@/members/edit-member-detail/editMemberDetailService'; -import { ListAllMembersService } from '@/members/list-all-members/listAllMembersService'; -import { ShowMemberDetailService } from '@/members/show-member-detail/showMemberDetailService'; -import { GetLoggedInUserInfoService } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; +import { EditMemberDetailService } from '@/modules/members/useCases/editMemberDetail/editMemberDetailService'; +import { ListAllMembersService } from '@/modules/members/useCases/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/showMemberDetail/showMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; import { Resolvers } from './generated/resolver-types'; type Dependencies = { diff --git a/server/src/users/usersController.ts b/server/src/users/usersController.ts deleted file mode 100644 index 83e2857..0000000 --- a/server/src/users/usersController.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express'; -import { GetLoggedInUserInfoService } from './get-logged-in-user-info/getLoggedInUserInfoService'; - -export class UsersController { - constructor( - private readonly getLoggedInUserInfoService: GetLoggedInUserInfoService - ) {} - - getLoggedInUserInfo = async (req: Request, res: Response) => { - const result = await this.getLoggedInUserInfoService.execute(); - res.json(result); - }; -} diff --git a/server/src/users/usersRouter.ts b/server/src/users/usersRouter.ts deleted file mode 100644 index 51cf627..0000000 --- a/server/src/users/usersRouter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import express from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; -import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; -import { UsersController } from './usersController'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { GetLoggedInUserInfoSqliteRepository } from './get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; -import { GetLoggedInUserInfoService } from './get-logged-in-user-info/getLoggedInUserInfoService'; -import { asyncHandler } from '@/shared/asyncHandler'; -import { PrismaClient } from '@prisma/client'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export function createUsersRouter({ - dataFilter, - authorizer, - prisma, -}: Dependencies) { - const getLoggedInUserInfoRepository = new GetLoggedInUserInfoSqliteRepository( - dataFilter, - prisma - ); - const getLoggedInUserInfoService = new GetLoggedInUserInfoService( - authorizer, - getLoggedInUserInfoRepository - ); - - const usersController = new UsersController(getLoggedInUserInfoService); - const router = express.Router(); - - router.use(createCheckLoggedInMiddleware(authorizer)); - - router.get('/info', asyncHandler(usersController.getLoggedInUserInfo)); - - return router; -} From c1cab6ac1e2b9808524547c57238ee8ce209bc25 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 14:39:38 +0900 Subject: [PATCH 04/12] use Result instead of try/catch --- server/src/modules/auth/shared/authorizer.ts | 24 ++++- .../infra/repos/prismaMemberRepository.ts | 16 ++-- .../editMemberDetailRepository.ts | 5 +- .../editMemberDetailService.ts | 88 ++++++++++--------- .../listAllMembersRepository.ts | 3 +- .../listAllMembers/listAllMembersService.ts | 80 +++++++++-------- .../showMemberDetailRepository.ts | 3 +- .../showMemberDetailService.ts | 79 ++++++++++------- .../users/infra/repos/prismaUserRepository.ts | 37 ++++---- .../getLoggedInUserInfoRepository.ts | 3 +- .../getLoggedInUserInfoService.ts | 36 ++++---- .../getLoggedInUserInfoSqliteRepository.ts | 38 -------- server/src/shared/asyncHandler.ts | 11 --- .../shared/core/{ => errors}/useCaseError.ts | 0 server/src/shared/core/result.ts | 6 +- server/src/shared/core/useCase.ts | 4 +- server/src/shared/infra/http/app.ts | 16 ++-- server/src/shared/infra/http/resolvers.ts | 83 ++++++++--------- 18 files changed, 276 insertions(+), 256 deletions(-) delete mode 100644 server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts delete mode 100644 server/src/shared/asyncHandler.ts rename server/src/shared/core/{ => errors}/useCaseError.ts (100%) diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts index 6467edc..47d5a21 100644 --- a/server/src/modules/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -1,6 +1,7 @@ import { User } from '@/modules/users/domain/user'; import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; +import { Result } from '@/shared/core/result'; import { Oso } from 'oso'; export class Authorizer { @@ -30,12 +31,27 @@ export class Authorizer { async authorizedFieldsForUser( action: any, resource: R - ): Promise> { - return this.authorizedFields(this.currentUser, action, resource); + ): Promise>> { + try { + const fields = await this.authorizedFields( + this.currentUser, + action, + resource + ); + + return Result.ok(fields); + } catch (err: any) { + return Result.fail(err); + } } - async authorizedActionsForUser(resource: R): Promise> { - return this.authorizedActions(this.currentUser, resource); + async authorizedActionsForUser(resource: R): Promise>> { + try { + const actions = await this.authorizedActions(this.currentUser, resource); + return Result.ok(actions); + } catch (err: any) { + return Result.fail(err); + } } private async authorizedFields(actor: any, action: any, resource: R) { diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index 1123cbd..f42adf1 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -2,6 +2,7 @@ import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { User } from '@/modules/users/domain/user'; +import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; import { Department } from '../../domain/department'; import { Member } from '../../domain/member'; @@ -28,13 +29,15 @@ export class PrismaMemberRepository user: User, memberId: string, payload: UpdatePayload - ): Promise { + ): Promise> { await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); await this.prisma.member.update({ where: { id: memberId }, data: payload }); + + return Result.ok(); } - async queryMember(user: User, memberId: string): Promise { + async queryMember(user: User, memberId: string): Promise> { await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); const record = await this.prisma.member.findUnique({ @@ -46,7 +49,7 @@ export class PrismaMemberRepository throw new MemberNotFoundError(memberId); } - return new Member( + const member = new Member( record.id, record.avatar, record.firstName, @@ -63,9 +66,10 @@ export class PrismaMemberRepository record.email, record.pr ); + return Result.ok(member); } - async queryMembers(user: User): Promise { + async queryMembers(user: User): Promise> { const query = await this.dataFilter.authorizedQuery( user, MEMBER_ACTIONS.READ, @@ -79,7 +83,7 @@ export class PrismaMemberRepository }, }); - return records.map((member) => { + const members = records.map((member) => { return new Member( member.id, member.avatar, @@ -98,5 +102,7 @@ export class PrismaMemberRepository member.pr ); }); + + return Result.ok(members); } } diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts index 0b4052b..498ec86 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts @@ -1,4 +1,5 @@ import { User } from '@/modules/users/domain/user'; +import { Result } from '@/shared/core/result'; import { Member } from '../../domain/member'; export type UpdatePayload = { @@ -13,10 +14,10 @@ export type UpdatePayload = { }; export interface EditMemberDetailRepository { - queryMember(user: User, memberId: string): Promise; + queryMember(user: User, memberId: string): Promise>; updateMember( user: User, memberId: string, payload: UpdatePayload - ): Promise; + ): Promise>; } diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts index b75f9d7..142f26d 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts @@ -1,6 +1,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../domain/member'; import { MemberNothingToUpdateError } from '../errors/memberNothingToUpdateError'; import { @@ -16,7 +17,9 @@ export type EditMemberDetailResponse = { result: boolean; }; -export class EditMemberDetailService { +export class EditMemberDetailService + implements UseCase> +{ constructor( private readonly authorizer: Authorizer, private readonly repository: EditMemberDetailRepository @@ -25,51 +28,52 @@ export class EditMemberDetailService { async execute({ memberId, payload, - }: EditMemberDetailRequest): Promise { - try { - const member = await this.repository.queryMember( - this.authorizer.currentUser, - memberId + }: EditMemberDetailRequest): Promise> { + const memberOrError = await this.repository.queryMember( + this.authorizer.currentUser, + memberId + ); + if (memberOrError.isFailure) { + return Result.fail(memberOrError.error); + } + + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.UPDATE, + memberOrError.getValue() ); + if (authorizedFieldsOrError.isFailure) { + return Result.fail(authorizedFieldsOrError.error); + } - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.UPDATE, - member - ); + const authorizedFields = authorizedFieldsOrError.getValue(); - const authorizedPayload: UpdatePayload = { - ...(authorizedFields.has('firstName') && { - firstName: payload.firstName, - }), - ...(authorizedFields.has('lastName') && { lastName: payload.lastName }), - ...(authorizedFields.has('phoneNumber') && { - phoneNumber: payload.phoneNumber, - }), - ...(authorizedFields.has('email') && { email: payload.email }), - ...(authorizedFields.has('pr') && { pr: payload.pr }), - ...(authorizedFields.has('age') && { age: payload.age }), - ...(authorizedFields.has('salary') && { salary: payload.salary }), - }; + const authorizedPayload: UpdatePayload = { + ...(authorizedFields.has('firstName') && { + firstName: payload.firstName, + }), + ...(authorizedFields.has('lastName') && { lastName: payload.lastName }), + ...(authorizedFields.has('phoneNumber') && { + phoneNumber: payload.phoneNumber, + }), + ...(authorizedFields.has('email') && { email: payload.email }), + ...(authorizedFields.has('pr') && { pr: payload.pr }), + ...(authorizedFields.has('age') && { age: payload.age }), + ...(authorizedFields.has('salary') && { salary: payload.salary }), + }; - if (Object.keys(authorizedPayload).length === 0) { - throw new MemberNothingToUpdateError(memberId); - } + if (Object.keys(authorizedPayload).length === 0) { + throw new MemberNothingToUpdateError(memberId); + } - await this.repository.updateMember( - this.authorizer.currentUser, - memberId, - authorizedPayload - ); + await this.repository.updateMember( + this.authorizer.currentUser, + memberId, + authorizedPayload + ); - return { - result: true, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { result: false }; - } - throw error; - } + return Result.ok({ + result: true, + }); } } diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts index 31249e9..1c67e31 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts @@ -1,6 +1,7 @@ import { User } from '@/modules/users/domain/user'; +import { Result } from '@/shared/core/result'; import { Member } from '../../domain/member'; export interface ListAllMembersRepository { - queryMembers(user: User): Promise; + queryMembers(user: User): Promise>; } diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts index c880496..f257baf 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts @@ -1,6 +1,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../domain/member'; import { DisplayableMember } from '../../dtos/displayableMember'; import { ListAllMembersRepository } from './listAllMembersRepository'; @@ -10,53 +11,60 @@ export type ListAllMembersResponse = { members: Partial[]; }; -export class ListAllMembersService { +export class ListAllMembersService + implements UseCase> +{ constructor( private readonly authorizer: Authorizer, private readonly repository: ListAllMembersRepository ) {} - async execute(): Promise { - try { - const members = await this.repository.queryMembers( - this.authorizer.currentUser - ); - const authorizedMembers: DisplayableMember[] = []; - for (const member of members) { - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.READ, - member - ); - - const authorizedMember: DisplayableMember = { - ...member.createObjectWithAuthorizedFields(authorizedFields), - editable: false, - isLoggedInUser: false, - }; - - const allowedActions = await this.authorizer.authorizedActionsForUser( + async execute(): Promise> { + const membersOrError = await this.repository.queryMembers( + this.authorizer.currentUser + ); + if (membersOrError.isFailure) { + return Result.fail(membersOrError.error); + } + + const authorizedMembers: DisplayableMember[] = []; + for (const member of membersOrError.getValue()) { + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.READ, member ); - if (allowedActions.has(MEMBER_ACTIONS.UPDATE)) { - authorizedMember.editable = true; - } + if (authorizedFieldsOrError.isFailure) { + return Result.fail(authorizedFieldsOrError.error); + } - if (member.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMember.isLoggedInUser = true; - } + const authorizedMember: DisplayableMember = { + ...member.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: false, + isLoggedInUser: false, + }; - authorizedMembers.push(authorizedMember); + const allowedActionsOrError = + await this.authorizer.authorizedActionsForUser(member); + if (allowedActionsOrError.isFailure) { + return Result.fail(allowedActionsOrError.error); } - return { - members: authorizedMembers, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { members: [] }; + if (allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE)) { + authorizedMember.editable = true; } - throw error; + + if (member.id === this.authorizer.currentUser.memberInfo.id) { + authorizedMember.isLoggedInUser = true; + } + + authorizedMembers.push(authorizedMember); } + + return Result.ok({ + members: authorizedMembers, + }); } } diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts index 1eb32b9..af7010e 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts @@ -1,6 +1,7 @@ import { User } from '@/modules/users/domain/user'; +import { Result } from '@/shared/core/result'; import { Member } from '../../domain/member'; export interface ShowMemberDetailRepository { - queryMember(user: User, memberId: string): Promise; + queryMember(user: User, memberId: string): Promise>; } diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts index e13f330..b0874f7 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts @@ -1,5 +1,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../domain/member'; import { DisplayableMember } from '../../dtos/displayableMember'; import { ShowMemberDetailRepository } from './showMemberDetailRepository'; @@ -12,7 +14,9 @@ export type ShowMemberDetailResponse = { member: DisplayableMember; }; -export class ShowMemberDetailService { +export class ShowMemberDetailService + implements UseCase> +{ constructor( private readonly authorizer: Authorizer, private readonly repository: ShowMemberDetailRepository @@ -20,51 +24,62 @@ export class ShowMemberDetailService { async execute( req: ShowMemberDetailRequest - ): Promise { - const member = await this.repository.queryMember( + ): Promise> { + const memberOrError = await this.repository.queryMember( this.authorizer.currentUser, req.memberId ); + if (memberOrError.isFailure) { + return Result.fail(memberOrError.error); + } - try { - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.READ, - member - ); - const authorizedMember: DisplayableMember = { - ...member.createObjectWithAuthorizedFields(authorizedFields), - editable: false, - isLoggedInUser: false, - }; - - const allowedActions = await this.authorizer.authorizedActionsForUser( + const member = memberOrError.getValue(); + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.READ, member ); - let authorizedFieldsToUpdate: string[] = []; - if (allowedActions.has(MEMBER_ACTIONS.UPDATE)) { - authorizedMember.editable = true; + const authorizedMemberOrError: DisplayableMember = { + ...member.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: false, + isLoggedInUser: false, + }; + + const allowedActionsOrError = + await this.authorizer.authorizedActionsForUser(member); + if (allowedActionsOrError.isFailure) { + return Result.fail(allowedActionsOrError.error); + } - const fields = await this.authorizer.authorizedFieldsForUser( + let authorizedFieldsToUpdate: string[] = []; + if (allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE)) { + authorizedMemberOrError.editable = true; + + const fieldsOrError = + await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.UPDATE, member ); - // FIXME: exclude readonly fields such as id, joinedAt - authorizedFieldsToUpdate = Array.from(fields.values()).map((f) => f); + if (fieldsOrError.isFailure) { + return Result.fail(fieldsOrError.error); } - if (member.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMember.isLoggedInUser = true; - } + const fields = fieldsOrError.getValue(); - return { - editableFields: authorizedFieldsToUpdate, - member: authorizedMember, - }; - } catch (error) { - // TODO: return 404 NOT FOUND - throw error; + // FIXME: exclude readonly fields such as id, joinedAt + authorizedFieldsToUpdate = Array.from(fields.values()).map((f) => f); } + + if (member.id === this.authorizer.currentUser.memberInfo.id) { + authorizedMemberOrError.isLoggedInUser = true; + } + + return Result.ok({ + editableFields: authorizedFieldsToUpdate, + member: authorizedMemberOrError, + }); } } diff --git a/server/src/modules/users/infra/repos/prismaUserRepository.ts b/server/src/modules/users/infra/repos/prismaUserRepository.ts index dce6a0f..8ca4c9e 100644 --- a/server/src/modules/users/infra/repos/prismaUserRepository.ts +++ b/server/src/modules/users/infra/repos/prismaUserRepository.ts @@ -4,6 +4,7 @@ import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { UserNotFoundError } from '@/modules/auth/shared/errors/userNotFoundError'; import { Department } from '@/modules/members/domain/department'; import { Member } from '@/modules/members/domain/member'; +import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; import { User } from '../../domain/user'; import { @@ -17,24 +18,28 @@ export class PrismaUserRepository implements GetLoggedInUserInfoRepository { private readonly prisma: PrismaClient ) {} - async queryUserInfo(user: User): Promise { - const query = await this.dataFilter.authorizedQuery( - user, - USER_MENU_ITEM_ACTIONS.READ, - UserMenuItemOrm - ); + async queryUserInfo(user: User): Promise> { + try { + const query = await this.dataFilter.authorizedQuery( + user, + USER_MENU_ITEM_ACTIONS.READ, + UserMenuItemOrm + ); - const menuItems = await this.prisma.userMenuItem.findMany({ - select: { - name: true, - }, - where: query, - orderBy: { order: 'asc' }, - }); + const menuItems = await this.prisma.userMenuItem.findMany({ + select: { + name: true, + }, + where: query, + orderBy: { order: 'asc' }, + }); - return { - userMenu: menuItems, - }; + return Result.ok({ + userMenu: menuItems, + }); + } catch (err: any) { + return Result.fail(err); + } } async getUser(userId: string): Promise { diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts index 519cedc..138414f 100644 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts @@ -1,8 +1,9 @@ +import { Result } from '@/shared/core/result'; import { User } from '../../domain/user'; export type UserInfo = { userMenu: { name: string }[]; }; export interface GetLoggedInUserInfoRepository { - queryUserInfo(user: User): Promise; + queryUserInfo(user: User): Promise>; } diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts index ffc150a..2cad2cb 100644 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts @@ -1,5 +1,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; import { GetLoggedInUserInfoRepository } from './getLoggedInUserInfoRepository'; export type GetLoggedInUserInfoRequest = {}; @@ -11,30 +13,32 @@ export type UserMenuItem = { name: string; }; -export class GetLoggedInUserInfoService { +export class GetLoggedInUserInfoService + implements + UseCase> +{ constructor( private readonly authorizer: Authorizer, private readonly repository: GetLoggedInUserInfoRepository ) {} - async execute(): Promise { - try { - const userInfo = await this.repository.queryUserInfo( - this.authorizer.currentUser - ); - this.authorizer.currentUser.username; - return { - username: this.authorizer.currentUser.username, - userMenu: userInfo.userMenu, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { + async execute(): Promise> { + const userInfoOrError = await this.repository.queryUserInfo( + this.authorizer.currentUser + ); + if (userInfoOrError.isFailure) { + if (userInfoOrError.error instanceof NotAuthorizedError) { + return Result.ok({ username: this.authorizer.currentUser.username, userMenu: [], - }; + }); } - throw error; + return Result.fail(userInfoOrError.error); } + + return Result.ok({ + username: this.authorizer.currentUser.username, + userMenu: userInfoOrError.getValue().userMenu, + }); } } diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts deleted file mode 100644 index f0607b6..0000000 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; -import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; -import { User } from '@/modules/users/domain/user'; -import { PrismaClient } from '@prisma/client'; -import { - GetLoggedInUserInfoRepository, - UserInfo, -} from '../getLoggedInUserInfoRepository'; - -export class GetLoggedInUserInfoSqliteRepository - implements GetLoggedInUserInfoRepository -{ - constructor( - private readonly dataFilter: DataFilter, - private readonly prisma: PrismaClient - ) {} - - async queryUserInfo(user: User): Promise { - const query = await this.dataFilter.authorizedQuery( - user, - USER_MENU_ITEM_ACTIONS.READ, - UserMenuItemOrm - ); - - const menuItems = await this.prisma.userMenuItem.findMany({ - select: { - name: true, - }, - where: query, - orderBy: { order: 'asc' }, - }); - - return { - userMenu: menuItems, - }; - } -} diff --git a/server/src/shared/asyncHandler.ts b/server/src/shared/asyncHandler.ts deleted file mode 100644 index 439a571..0000000 --- a/server/src/shared/asyncHandler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextFunction, Request, RequestHandler, Response } from 'express'; - -export function asyncHandler(fn: RequestHandler) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - await fn(req, res, next); - } catch (err) { - next(err); - } - }; -} diff --git a/server/src/shared/core/useCaseError.ts b/server/src/shared/core/errors/useCaseError.ts similarity index 100% rename from server/src/shared/core/useCaseError.ts rename to server/src/shared/core/errors/useCaseError.ts diff --git a/server/src/shared/core/result.ts b/server/src/shared/core/result.ts index 6bc34b9..31a24df 100644 --- a/server/src/shared/core/result.ts +++ b/server/src/shared/core/result.ts @@ -1,7 +1,7 @@ export class Result { public isSuccess: boolean; public isFailure: boolean; - public error?: Error | null; + public error!: Error; private _value?: T; private constructor(isSuccess: boolean, error?: Error | null, value?: T) { @@ -16,7 +16,9 @@ export class Result { this.isSuccess = isSuccess; this.isFailure = !isSuccess; - this.error = error; + if (error) { + this.error = error; + } this._value = value; Object.freeze(this); diff --git a/server/src/shared/core/useCase.ts b/server/src/shared/core/useCase.ts index b7a7b1a..2c1ca12 100644 --- a/server/src/shared/core/useCase.ts +++ b/server/src/shared/core/useCase.ts @@ -1,3 +1,5 @@ -export interface UseCase { +import { Result } from './result'; + +export interface UseCase> { execute(request?: Req): Promise | Res; } diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index 58e96e3..4518781 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -10,7 +10,6 @@ import { createServer } from '@graphql-yoga/node'; import { createResolvers } from './resolvers'; import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { Authorizer } from '@/modules/auth/shared/authorizer'; -import { GetLoggedInUserInfoSqliteRepository } from '@/modules/users/useCases/getLoggedInUserInfo/repository/getLoggedInUserInfoSqliteRepository'; import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; import { ListAllMembersService } from '@/modules/members/useCases/listAllMembers/listAllMembersService'; @@ -30,20 +29,18 @@ type UseCaseDependencies = { dataFilter: DataFilter; authorizer: Authorizer; prisma: PrismaClient; + prismaUserRepository: PrismaUserRepository; }; const createUseCases = ({ dataFilter, authorizer, prisma, + prismaUserRepository, }: UseCaseDependencies) => { - const getLoggedInUserInfoRepository = new GetLoggedInUserInfoSqliteRepository( - dataFilter, - prisma - ); const getLoggedInUserInfoService = new GetLoggedInUserInfoService( authorizer, - getLoggedInUserInfoRepository + prismaUserRepository ); const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); @@ -87,7 +84,12 @@ export async function startServer() { loaders: [new GraphQLFileLoader()], }); - const useCases = createUseCases({ dataFilter, authorizer, prisma }); + const useCases = createUseCases({ + dataFilter, + authorizer, + prisma, + prismaUserRepository, + }); const resolvers = createResolvers(useCases); const graphQLServer = createServer({ schema: { diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts index 81005dc..4ba49e8 100644 --- a/server/src/shared/infra/http/resolvers.ts +++ b/server/src/shared/infra/http/resolvers.ts @@ -20,60 +20,61 @@ export const createResolvers = ({ return { Query: { userInfo: async () => { - try { - const result = await getLoggedInUserInfoService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; + const resultOrError = await getLoggedInUserInfoService.execute(); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return; } + return resultOrError.getValue(); }, listAllMembers: async () => { - try { - const result = await listAllMembersService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; + const resultOrError = await listAllMembersService.execute(); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return; } + return resultOrError.getValue(); }, showMemberDetail: async (_, { id }) => { - try { - const result = await showMemberDetailService.execute({ - memberId: id, - }); - return result; - } catch (err) { - console.error(err); - throw err; + const resultOrError = await showMemberDetailService.execute({ + memberId: id, + }); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: + return; } + return resultOrError.getValue(); }, }, Mutation: { editMemberDetail: async (_, { input }) => { - try { - const { id: memberId, ...payload } = input; + const { id: memberId, ...payload } = input; - const result = await editMemberDetailService.execute({ - memberId, - payload: { - ...(payload.age && { age: payload.age }), - ...(payload.departmentId && { - departmentId: payload.departmentId, - }), - ...(payload.email && { email: payload.email }), - ...(payload.firstName && { firstName: payload.firstName }), - ...(payload.lastName && { lastName: payload.lastName }), - ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), - ...(payload.pr && { pr: payload.pr }), - ...(payload.salary && { salary: payload.salary }), - }, - }); - return result; - } catch (err) { - console.error(err); - throw err; + const resultOrError = await editMemberDetailService.execute({ + memberId, + payload: { + ...(payload.age && { age: payload.age }), + ...(payload.departmentId && { + departmentId: payload.departmentId, + }), + ...(payload.email && { email: payload.email }), + ...(payload.firstName && { firstName: payload.firstName }), + ...(payload.lastName && { lastName: payload.lastName }), + ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), + ...(payload.pr && { pr: payload.pr }), + ...(payload.salary && { salary: payload.salary }), + }, + }); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return; } + + return resultOrError.getValue(); }, }, }; From f1eb03bc689ef449a7de667e205ddb1c2df7b387 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 16:19:15 +0900 Subject: [PATCH 05/12] move domain models to dtos --- server/src/modules/auth/shared/authorizer.ts | 2 +- server/src/modules/auth/shared/createOso.ts | 8 ++++---- .../{domain/department.ts => dtos/departmentDTO.ts} | 0 .../{displayableMember.ts => displayableMemberDTO.ts} | 2 +- .../members/{domain/member.ts => dtos/memberDTO.ts} | 2 +- .../modules/members/infra/repos/prismaMemberRepository.ts | 6 +++--- .../editMemberDetail/editMemberDetailRepository.ts | 4 ++-- .../useCases/editMemberDetail/editMemberDetailService.ts | 2 +- .../useCases/listAllMembers/listAllMembersRepository.ts | 4 ++-- .../useCases/listAllMembers/listAllMembersService.ts | 4 ++-- .../showMemberDetail/showMemberDetailRepository.ts | 4 ++-- .../useCases/showMemberDetail/showMemberDetailService.ts | 4 ++-- .../src/modules/users/{domain/user.ts => dtos/userDTO.ts} | 2 +- .../{domain/userMenuItem.ts => dtos/userMenuItemDTO.ts} | 0 .../src/modules/users/infra/repos/prismaUserRepository.ts | 6 +++--- .../getLoggedInUserInfo/getLoggedInUserInfoRepository.ts | 2 +- server/src/shared/core/errors/appError.ts | 1 + 17 files changed, 27 insertions(+), 26 deletions(-) rename server/src/modules/members/{domain/department.ts => dtos/departmentDTO.ts} (100%) rename server/src/modules/members/dtos/{displayableMember.ts => displayableMemberDTO.ts} (70%) rename server/src/modules/members/{domain/member.ts => dtos/memberDTO.ts} (95%) rename server/src/modules/users/{domain/user.ts => dtos/userDTO.ts} (72%) rename server/src/modules/users/{domain/userMenuItem.ts => dtos/userMenuItemDTO.ts} (100%) diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts index 47d5a21..bbb4a72 100644 --- a/server/src/modules/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -1,4 +1,4 @@ -import { User } from '@/modules/users/domain/user'; +import { User } from '@/modules/users/dtos/userDTO'; import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; import { Result } from '@/shared/core/result'; diff --git a/server/src/modules/auth/shared/createOso.ts b/server/src/modules/auth/shared/createOso.ts index be50f4c..f9e2e03 100644 --- a/server/src/modules/auth/shared/createOso.ts +++ b/server/src/modules/auth/shared/createOso.ts @@ -7,10 +7,10 @@ import { Oso } from 'oso'; import { Filter, Relation } from 'oso/dist/src/dataFiltering'; import { PrimitivePropertyNames } from '@/shared/sharedTypes'; import { prisma } from '@/shared/infra/http/app'; -import { Department } from '@/modules/members/domain/department'; -import { Member } from '@/modules/members/domain/member'; -import { UserMenuItem } from '@/modules/users/domain/userMenuItem'; -import { User } from '@/modules/users/domain/user'; +import { Department } from '@/modules/members/dtos/departmentDTO'; +import { Member } from '@/modules/members/dtos/memberDTO'; +import { UserMenuItem } from '@/modules/users/dtos/userMenuItemDTO'; +import { User } from '@/modules/users/dtos/userDTO'; // FIXME: Since prisma objects are POJOs, we need to create classes // to pass to Oso by ourselves. diff --git a/server/src/modules/members/domain/department.ts b/server/src/modules/members/dtos/departmentDTO.ts similarity index 100% rename from server/src/modules/members/domain/department.ts rename to server/src/modules/members/dtos/departmentDTO.ts diff --git a/server/src/modules/members/dtos/displayableMember.ts b/server/src/modules/members/dtos/displayableMemberDTO.ts similarity index 70% rename from server/src/modules/members/dtos/displayableMember.ts rename to server/src/modules/members/dtos/displayableMemberDTO.ts index b53e64b..5a00729 100644 --- a/server/src/modules/members/dtos/displayableMember.ts +++ b/server/src/modules/members/dtos/displayableMemberDTO.ts @@ -1,4 +1,4 @@ -import { Member } from '../domain/member'; +import { Member } from './memberDTO'; export type DisplayableMember = Partial & { editable: boolean; diff --git a/server/src/modules/members/domain/member.ts b/server/src/modules/members/dtos/memberDTO.ts similarity index 95% rename from server/src/modules/members/domain/member.ts rename to server/src/modules/members/dtos/memberDTO.ts index 219358b..45d240b 100644 --- a/server/src/modules/members/domain/member.ts +++ b/server/src/modules/members/dtos/memberDTO.ts @@ -1,5 +1,5 @@ import { NonFunctionPropertyNames } from '@/shared/sharedTypes'; -import { Department } from './department'; +import { Department } from './departmentDTO'; export class Member { static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index f42adf1..c37e26e 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -1,11 +1,11 @@ import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { DataFilter } from '@/modules/auth/shared/dataFilter'; -import { User } from '@/modules/users/domain/user'; +import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; -import { Department } from '../../domain/department'; -import { Member } from '../../domain/member'; +import { Department } from '../../dtos/departmentDTO'; +import { Member } from '../../dtos/memberDTO'; import { EditMemberDetailRepository, UpdatePayload, diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts index 498ec86..79d2723 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts @@ -1,6 +1,6 @@ -import { User } from '@/modules/users/domain/user'; +import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; -import { Member } from '../../domain/member'; +import { Member } from '../../dtos/memberDTO'; export type UpdatePayload = { firstName?: string; diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts index 142f26d..105fe3b 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts @@ -2,7 +2,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../domain/member'; +import { Member } from '../../dtos/memberDTO'; import { MemberNothingToUpdateError } from '../errors/memberNothingToUpdateError'; import { EditMemberDetailRepository, diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts index 1c67e31..5df8315 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts @@ -1,6 +1,6 @@ -import { User } from '@/modules/users/domain/user'; +import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; -import { Member } from '../../domain/member'; +import { Member } from '../../dtos/memberDTO'; export interface ListAllMembersRepository { queryMembers(user: User): Promise>; diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts index f257baf..bd11274 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts @@ -2,8 +2,8 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../domain/member'; -import { DisplayableMember } from '../../dtos/displayableMember'; +import { Member } from '../../dtos/memberDTO'; +import { DisplayableMember } from '../../dtos/displayableMemberDTO'; import { ListAllMembersRepository } from './listAllMembersRepository'; export type ListAllMembersRequest = {}; diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts index af7010e..d8ef415 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts @@ -1,6 +1,6 @@ -import { User } from '@/modules/users/domain/user'; +import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; -import { Member } from '../../domain/member'; +import { Member } from '../../dtos/memberDTO'; export interface ShowMemberDetailRepository { queryMember(user: User, memberId: string): Promise>; diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts index b0874f7..18e7b97 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts @@ -2,8 +2,8 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../domain/member'; -import { DisplayableMember } from '../../dtos/displayableMember'; +import { Member } from '../../dtos/memberDTO'; +import { DisplayableMember } from '../../dtos/displayableMemberDTO'; import { ShowMemberDetailRepository } from './showMemberDetailRepository'; export type ShowMemberDetailRequest = { diff --git a/server/src/modules/users/domain/user.ts b/server/src/modules/users/dtos/userDTO.ts similarity index 72% rename from server/src/modules/users/domain/user.ts rename to server/src/modules/users/dtos/userDTO.ts index 9d14d98..5185639 100644 --- a/server/src/modules/users/domain/user.ts +++ b/server/src/modules/users/dtos/userDTO.ts @@ -1,4 +1,4 @@ -import { Member } from '@/modules/members/domain/member'; +import { Member } from '@/modules/members/dtos/memberDTO'; export class User { constructor( diff --git a/server/src/modules/users/domain/userMenuItem.ts b/server/src/modules/users/dtos/userMenuItemDTO.ts similarity index 100% rename from server/src/modules/users/domain/userMenuItem.ts rename to server/src/modules/users/dtos/userMenuItemDTO.ts diff --git a/server/src/modules/users/infra/repos/prismaUserRepository.ts b/server/src/modules/users/infra/repos/prismaUserRepository.ts index 8ca4c9e..fe91234 100644 --- a/server/src/modules/users/infra/repos/prismaUserRepository.ts +++ b/server/src/modules/users/infra/repos/prismaUserRepository.ts @@ -2,11 +2,11 @@ import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions' import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { UserNotFoundError } from '@/modules/auth/shared/errors/userNotFoundError'; -import { Department } from '@/modules/members/domain/department'; -import { Member } from '@/modules/members/domain/member'; +import { Department } from '@/modules/members/dtos/departmentDTO'; +import { Member } from '@/modules/members/dtos/memberDTO'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; -import { User } from '../../domain/user'; +import { User } from '../../dtos/userDTO'; import { GetLoggedInUserInfoRepository, UserInfo, diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts index 138414f..4638091 100644 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts @@ -1,5 +1,5 @@ import { Result } from '@/shared/core/result'; -import { User } from '../../domain/user'; +import { User } from '../../dtos/userDTO'; export type UserInfo = { userMenu: { name: string }[]; diff --git a/server/src/shared/core/errors/appError.ts b/server/src/shared/core/errors/appError.ts index 92ef5cf..c97a627 100644 --- a/server/src/shared/core/errors/appError.ts +++ b/server/src/shared/core/errors/appError.ts @@ -11,6 +11,7 @@ export abstract class AppError extends Error { export const ErrorCodes = { BAD_REQUEST: 'bad_request', INVALID_OPERATION: 'invalid_operation', + INVALID_ARGUMENT: 'invalid_argument', USER_NOT_FOUND: 'user_not_found', MEMBER_NOT_FOUND: 'member_not_found', } as const; From b8597a6e5e4a59e81bce8df61cf7c29191ffcfd7 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Fri, 29 Apr 2022 17:16:41 +0900 Subject: [PATCH 06/12] changed query use cases to directly use prisma --- server/src/modules/auth/shared/authorizer.ts | 25 +++++- ...horized-error.ts => notAuthorizedError.ts} | 0 .../auth/shared/repository/osoDataFilter.ts | 2 +- server/src/modules/members/dtos/memberDTO.ts | 26 ++++++ .../infra/repos/prismaMemberRepository.ts | 46 +--------- .../listAllMembersRepository.ts | 7 -- .../listAllMembers/listAllMembersService.ts | 33 ++++--- .../showMemberDetailRepository.ts | 7 -- .../showMemberDetailService.ts | 25 ++++-- server/src/modules/users/dtos/userDTO.ts | 35 ++++++++ .../users/infra/repos/prismaUserRepository.ts | 89 ------------------- .../getLoggedInUserInfoRepository.ts | 9 -- .../getLoggedInUserInfoService.ts | 34 +++---- server/src/shared/infra/http/app.ts | 16 ++-- 14 files changed, 150 insertions(+), 204 deletions(-) rename server/src/modules/auth/shared/errors/{not-authorized-error.ts => notAuthorizedError.ts} (100%) delete mode 100644 server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts delete mode 100644 server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts delete mode 100644 server/src/modules/users/infra/repos/prismaUserRepository.ts delete mode 100644 server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts index bbb4a72..2f2b72a 100644 --- a/server/src/modules/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -1,14 +1,15 @@ import { User } from '@/modules/users/dtos/userDTO'; -import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; import { Result } from '@/shared/core/result'; +import { PrismaClient } from '@prisma/client'; import { Oso } from 'oso'; +import { UserNotFoundError } from './errors/userNotFoundError'; export class Authorizer { _currentUser?: User; constructor( - private readonly repository: PrismaUserRepository, + private readonly prisma: PrismaClient, private readonly oso: Oso ) {} @@ -20,7 +21,25 @@ export class Authorizer { } async setUser(userId: string): Promise { - const user = await this.repository.getUser(userId); + const userRecord = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + include: { + member: { + include: { + department: true, + }, + }, + }, + }); + + if (!userRecord) { + throw new UserNotFoundError(userId); + } + + const user = User.createFromOrmModel(userRecord); + this._currentUser = user; } diff --git a/server/src/modules/auth/shared/errors/not-authorized-error.ts b/server/src/modules/auth/shared/errors/notAuthorizedError.ts similarity index 100% rename from server/src/modules/auth/shared/errors/not-authorized-error.ts rename to server/src/modules/auth/shared/errors/notAuthorizedError.ts diff --git a/server/src/modules/auth/shared/repository/osoDataFilter.ts b/server/src/modules/auth/shared/repository/osoDataFilter.ts index d2c02e5..a36c7be 100644 --- a/server/src/modules/auth/shared/repository/osoDataFilter.ts +++ b/server/src/modules/auth/shared/repository/osoDataFilter.ts @@ -1,6 +1,6 @@ import { Oso } from 'oso'; import { DataFilter } from '../dataFilter'; -import { NotAuthorizedError } from '../errors/not-authorized-error'; +import { NotAuthorizedError } from '../errors/notAuthorizedError'; export class OsoDataFilter implements DataFilter { constructor(private readonly oso: Oso) {} diff --git a/server/src/modules/members/dtos/memberDTO.ts b/server/src/modules/members/dtos/memberDTO.ts index 45d240b..0e30d33 100644 --- a/server/src/modules/members/dtos/memberDTO.ts +++ b/server/src/modules/members/dtos/memberDTO.ts @@ -1,5 +1,9 @@ import { NonFunctionPropertyNames } from '@/shared/sharedTypes'; import { Department } from './departmentDTO'; +import { + Member as PrismaMember, + Department as PrismaDepartment, +} from '@prisma/client'; export class Member { static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ @@ -41,4 +45,26 @@ export class Member { return authorizedMember; } + + static createFromOrmModel( + member: PrismaMember & { department: PrismaDepartment } + ): Member { + return new Member( + member.id, + member.avatar, + member.firstName, + member.lastName, + member.age, + member.salary, + new Department( + member.department.id, + member.department.name, + member.department.managerMemberId + ), + member.joinedAt, + member.phoneNumber, + member.email, + member.pr + ); + } } diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index c37e26e..49b86ba 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -11,15 +11,8 @@ import { UpdatePayload, } from '../../useCases/editMemberDetail/editMemberDetailRepository'; import { MemberNotFoundError } from '../../useCases/errors/memberNotFoundError'; -import { ListAllMembersRepository } from '../../useCases/listAllMembers/listAllMembersRepository'; -import { ShowMemberDetailRepository } from '../../useCases/showMemberDetail/showMemberDetailRepository'; -export class PrismaMemberRepository - implements - ShowMemberDetailRepository, - EditMemberDetailRepository, - ListAllMembersRepository -{ +export class PrismaMemberRepository implements EditMemberDetailRepository { private readonly prisma: PrismaClient; constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { this.prisma = prisma; @@ -68,41 +61,4 @@ export class PrismaMemberRepository ); return Result.ok(member); } - - async queryMembers(user: User): Promise> { - const query = await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - const records = await this.prisma.member.findMany({ - where: query, - include: { - department: true, - }, - }); - - const members = records.map((member) => { - return new Member( - member.id, - member.avatar, - member.firstName, - member.lastName, - member.age, - member.salary, - new Department( - member.department.id, - member.department.name, - member.department.managerMemberId - ), - member.joinedAt, - member.phoneNumber, - member.email, - member.pr - ); - }); - - return Result.ok(members); - } } diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts deleted file mode 100644 index 5df8315..0000000 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersRepository.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { User } from '@/modules/users/dtos/userDTO'; -import { Result } from '@/shared/core/result'; -import { Member } from '../../dtos/memberDTO'; - -export interface ListAllMembersRepository { - queryMembers(user: User): Promise>; -} diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts index bd11274..a20cde6 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts @@ -4,7 +4,9 @@ import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../dtos/memberDTO'; import { DisplayableMember } from '../../dtos/displayableMemberDTO'; -import { ListAllMembersRepository } from './listAllMembersRepository'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { PrismaClient } from '@prisma/client'; export type ListAllMembersRequest = {}; export type ListAllMembersResponse = { @@ -16,30 +18,37 @@ export class ListAllMembersService { constructor( private readonly authorizer: Authorizer, - private readonly repository: ListAllMembersRepository + private readonly dataFilter: DataFilter, + private readonly prisma: PrismaClient ) {} async execute(): Promise> { - const membersOrError = await this.repository.queryMembers( - this.authorizer.currentUser + const query = await this.dataFilter.authorizedQuery( + this.authorizer.currentUser, + MEMBER_ACTIONS.READ, + MemberOrm ); - if (membersOrError.isFailure) { - return Result.fail(membersOrError.error); - } + const memberModels = await this.prisma.member.findMany({ + where: query, + include: { + department: true, + }, + }); const authorizedMembers: DisplayableMember[] = []; - for (const member of membersOrError.getValue()) { + for (const memberModel of memberModels) { + const memberDto = Member.createFromOrmModel(memberModel); const authorizedFieldsOrError = await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.READ, - member + memberDto ); if (authorizedFieldsOrError.isFailure) { return Result.fail(authorizedFieldsOrError.error); } const authorizedMember: DisplayableMember = { - ...member.createObjectWithAuthorizedFields( + ...memberDto.createObjectWithAuthorizedFields( authorizedFieldsOrError.getValue() ), editable: false, @@ -47,7 +56,7 @@ export class ListAllMembersService }; const allowedActionsOrError = - await this.authorizer.authorizedActionsForUser(member); + await this.authorizer.authorizedActionsForUser(memberModel); if (allowedActionsOrError.isFailure) { return Result.fail(allowedActionsOrError.error); } @@ -56,7 +65,7 @@ export class ListAllMembersService authorizedMember.editable = true; } - if (member.id === this.authorizer.currentUser.memberInfo.id) { + if (memberModel.id === this.authorizer.currentUser.memberInfo.id) { authorizedMember.isLoggedInUser = true; } diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts deleted file mode 100644 index d8ef415..0000000 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailRepository.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { User } from '@/modules/users/dtos/userDTO'; -import { Result } from '@/shared/core/result'; -import { Member } from '../../dtos/memberDTO'; - -export interface ShowMemberDetailRepository { - queryMember(user: User, memberId: string): Promise>; -} diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts index 18e7b97..a13869d 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts @@ -4,7 +4,10 @@ import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../dtos/memberDTO'; import { DisplayableMember } from '../../dtos/displayableMemberDTO'; -import { ShowMemberDetailRepository } from './showMemberDetailRepository'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; +import { PrismaClient } from '@prisma/client'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { MemberNotFoundError } from '../errors/memberNotFoundError'; export type ShowMemberDetailRequest = { memberId: string; @@ -19,21 +22,29 @@ export class ShowMemberDetailService { constructor( private readonly authorizer: Authorizer, - private readonly repository: ShowMemberDetailRepository + private readonly dataFilter: DataFilter, + private readonly prisma: PrismaClient ) {} async execute( req: ShowMemberDetailRequest ): Promise> { - const memberOrError = await this.repository.queryMember( + await this.dataFilter.authorizedQuery( this.authorizer.currentUser, - req.memberId + MEMBER_ACTIONS.READ, + MemberOrm ); - if (memberOrError.isFailure) { - return Result.fail(memberOrError.error); + + const record = await this.prisma.member.findUnique({ + where: { id: req.memberId }, + include: { department: true }, + }); + + if (!record) { + return Result.fail(new MemberNotFoundError(req.memberId)); } - const member = memberOrError.getValue(); + const member = Member.createFromOrmModel(record); const authorizedFieldsOrError = await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.READ, diff --git a/server/src/modules/users/dtos/userDTO.ts b/server/src/modules/users/dtos/userDTO.ts index 5185639..5418984 100644 --- a/server/src/modules/users/dtos/userDTO.ts +++ b/server/src/modules/users/dtos/userDTO.ts @@ -1,4 +1,10 @@ +import { Department } from '@/modules/members/dtos/departmentDTO'; import { Member } from '@/modules/members/dtos/memberDTO'; +import { + User as PrismaUser, + Member as PrismaMember, + Department as PrismaDepartment, +} from '@prisma/client'; export class User { constructor( @@ -7,4 +13,33 @@ export class User { public memberInfo: Member, public isAdmin: boolean ) {} + + static createFromOrmModel( + userRecord: PrismaUser & { + member: PrismaMember & { department: PrismaDepartment }; + } + ): User { + return new User( + userRecord.id, + userRecord.username, + new Member( + userRecord.member.id, + userRecord.member.avatar, + userRecord.member.firstName, + userRecord.member.lastName, + userRecord.member.age, + userRecord.member.salary, + new Department( + userRecord.member.department.id, + userRecord.member.department.name, + userRecord.member.department.managerMemberId + ), + userRecord.member.joinedAt, + userRecord.member.phoneNumber, + userRecord.member.email, + userRecord.member.pr + ), + userRecord.isAdmin + ); + } } diff --git a/server/src/modules/users/infra/repos/prismaUserRepository.ts b/server/src/modules/users/infra/repos/prismaUserRepository.ts deleted file mode 100644 index fe91234..0000000 --- a/server/src/modules/users/infra/repos/prismaUserRepository.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; -import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; -import { UserNotFoundError } from '@/modules/auth/shared/errors/userNotFoundError'; -import { Department } from '@/modules/members/dtos/departmentDTO'; -import { Member } from '@/modules/members/dtos/memberDTO'; -import { Result } from '@/shared/core/result'; -import { PrismaClient } from '@prisma/client'; -import { User } from '../../dtos/userDTO'; -import { - GetLoggedInUserInfoRepository, - UserInfo, -} from '../../useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository'; - -export class PrismaUserRepository implements GetLoggedInUserInfoRepository { - constructor( - private readonly dataFilter: DataFilter, - private readonly prisma: PrismaClient - ) {} - - async queryUserInfo(user: User): Promise> { - try { - const query = await this.dataFilter.authorizedQuery( - user, - USER_MENU_ITEM_ACTIONS.READ, - UserMenuItemOrm - ); - - const menuItems = await this.prisma.userMenuItem.findMany({ - select: { - name: true, - }, - where: query, - orderBy: { order: 'asc' }, - }); - - return Result.ok({ - userMenu: menuItems, - }); - } catch (err: any) { - return Result.fail(err); - } - } - - async getUser(userId: string): Promise { - const userRecord = await this.prisma.user.findUnique({ - where: { - id: userId, - }, - include: { - member: { - include: { - department: true, - }, - }, - }, - }); - - if (!userRecord) { - throw new UserNotFoundError(userId); - } - - // TODO: implement helper method - const user = new User( - userRecord.id, - userRecord.username, - new Member( - userRecord.member.id, - userRecord.member.avatar, - userRecord.member.firstName, - userRecord.member.lastName, - userRecord.member.age, - userRecord.member.salary, - new Department( - userRecord.member.department.id, - userRecord.member.department.name, - userRecord.member.department.managerMemberId - ), - userRecord.member.joinedAt, - userRecord.member.phoneNumber, - userRecord.member.email, - userRecord.member.pr - ), - userRecord.isAdmin - ); - - return user; - } -} diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts deleted file mode 100644 index 4638091..0000000 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Result } from '@/shared/core/result'; -import { User } from '../../dtos/userDTO'; - -export type UserInfo = { - userMenu: { name: string }[]; -}; -export interface GetLoggedInUserInfoRepository { - queryUserInfo(user: User): Promise>; -} diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts index 2cad2cb..ebf8d4f 100644 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts @@ -1,8 +1,10 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; -import { NotAuthorizedError } from '@/modules/auth/shared/errors/not-authorized-error'; +import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; +import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { GetLoggedInUserInfoRepository } from './getLoggedInUserInfoRepository'; +import { PrismaClient } from '@prisma/client'; export type GetLoggedInUserInfoRequest = {}; export type GetLoggedInUserInfoResponse = { @@ -19,26 +21,28 @@ export class GetLoggedInUserInfoService { constructor( private readonly authorizer: Authorizer, - private readonly repository: GetLoggedInUserInfoRepository + private readonly dataFilter: DataFilter, + private readonly prisma: PrismaClient ) {} async execute(): Promise> { - const userInfoOrError = await this.repository.queryUserInfo( - this.authorizer.currentUser + const query = await this.dataFilter.authorizedQuery( + this.authorizer.currentUser, + USER_MENU_ITEM_ACTIONS.READ, + UserMenuItemOrm ); - if (userInfoOrError.isFailure) { - if (userInfoOrError.error instanceof NotAuthorizedError) { - return Result.ok({ - username: this.authorizer.currentUser.username, - userMenu: [], - }); - } - return Result.fail(userInfoOrError.error); - } + + const menuItems = await this.prisma.userMenuItem.findMany({ + select: { + name: true, + }, + where: query, + orderBy: { order: 'asc' }, + }); return Result.ok({ username: this.authorizer.currentUser.username, - userMenu: userInfoOrError.getValue().userMenu, + userMenu: menuItems, }); } } diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index 4518781..19be84f 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -19,7 +19,6 @@ import { createCoreOso, createSqliteDataFilterOso, } from '@/modules/auth/shared/createOso'; -import { PrismaUserRepository } from '@/modules/users/infra/repos/prismaUserRepository'; import { OsoDataFilter } from '@/modules/auth/shared/repository/osoDataFilter'; import { createCheckLoggedInMiddleware } from '@/modules/auth/shared/checkLoggedInMiddleware'; @@ -29,29 +28,30 @@ type UseCaseDependencies = { dataFilter: DataFilter; authorizer: Authorizer; prisma: PrismaClient; - prismaUserRepository: PrismaUserRepository; }; const createUseCases = ({ dataFilter, authorizer, prisma, - prismaUserRepository, }: UseCaseDependencies) => { const getLoggedInUserInfoService = new GetLoggedInUserInfoService( authorizer, - prismaUserRepository + dataFilter, + prisma ); const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); const listAllMembersService = new ListAllMembersService( authorizer, - prismaMemberRepository + dataFilter, + prisma ); const showMemberDetailService = new ShowMemberDetailService( authorizer, - prismaMemberRepository + dataFilter, + prisma ); const editMemberDetailService = new EditMemberDetailService( authorizer, @@ -72,8 +72,7 @@ export async function startServer() { const dataFilterOso = await createSqliteDataFilterOso(); const dataFilter = new OsoDataFilter(dataFilterOso); - const prismaUserRepository = new PrismaUserRepository(dataFilter, prisma); - const authorizer = new Authorizer(prismaUserRepository, oso); + const authorizer = new Authorizer(prisma, oso); // express const app: Application = express(); @@ -88,7 +87,6 @@ export async function startServer() { dataFilter, authorizer, prisma, - prismaUserRepository, }); const resolvers = createResolvers(useCases); const graphQLServer = createServer({ From 32a049a61e71a7d0e40b0dac0ed3592193406637 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Mon, 2 May 2022 09:18:59 +0900 Subject: [PATCH 07/12] merge data filter oso to authorizer --- server/src/modules/auth/shared/authorizer.ts | 24 ++++++++++++--- server/src/modules/auth/shared/dataFilter.ts | 9 ------ .../auth/shared/repository/osoDataFilter.ts | 29 ------------------- .../infra/repos/prismaMemberRepository.ts | 17 +++++++---- .../editMemberDetailRepository.ts | 8 ++--- .../editMemberDetailService.ts | 11 ++----- .../listAllMembers/listAllMembersService.ts | 5 +--- .../showMemberDetailService.ts | 5 +--- .../getLoggedInUserInfoService.ts | 5 +--- server/src/shared/infra/http/app.ts | 26 ++++------------- 10 files changed, 43 insertions(+), 96 deletions(-) delete mode 100644 server/src/modules/auth/shared/dataFilter.ts delete mode 100644 server/src/modules/auth/shared/repository/osoDataFilter.ts diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts index 2f2b72a..5a292fb 100644 --- a/server/src/modules/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -3,6 +3,7 @@ import { InvalidOperationError } from '@/shared/core/errors/invalidOperationErro import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; import { Oso } from 'oso'; +import { NotAuthorizedError } from './errors/notAuthorizedError'; import { UserNotFoundError } from './errors/userNotFoundError'; export class Authorizer { @@ -10,7 +11,8 @@ export class Authorizer { constructor( private readonly prisma: PrismaClient, - private readonly oso: Oso + private readonly coreOso: Oso, + private readonly filterOso: Oso ) {} get currentUser() { @@ -44,7 +46,7 @@ export class Authorizer { } isAllowed(actor: any, action: any, resource: any): Promise { - return this.oso.isAllowed(actor, action, resource); + return this.coreOso.isAllowed(actor, action, resource); } async authorizedFieldsForUser( @@ -73,13 +75,27 @@ export class Authorizer { } } + async authorizedQueryForUser(action: any, resource: any) { + const query = (await this.filterOso.authorizedQuery( + this.currentUser, + action, + resource + )) as any; + if (query === null) { + throw new NotAuthorizedError( + `no rules found: actor:${this.currentUser}, action:${action}, resource:${resource}` + ); + } + return query; + } + private async authorizedFields(actor: any, action: any, resource: R) { - const set = await this.oso.authorizedFields(actor, action, resource); + const set = await this.coreOso.authorizedFields(actor, action, resource); return set as Set; } private async authorizedActions(actor: any, resource: R) { - const set = await this.oso.authorizedActions(actor, resource); + const set = await this.coreOso.authorizedActions(actor, resource); return set as Set; } } diff --git a/server/src/modules/auth/shared/dataFilter.ts b/server/src/modules/auth/shared/dataFilter.ts deleted file mode 100644 index 88feb36..0000000 --- a/server/src/modules/auth/shared/dataFilter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DataFilter { - authorizedResources( - actor: any, - action: any, - resource: any - ): Promise; - - authorizedQuery(actor: any, action: any, resource: any): Promise; -} diff --git a/server/src/modules/auth/shared/repository/osoDataFilter.ts b/server/src/modules/auth/shared/repository/osoDataFilter.ts deleted file mode 100644 index a36c7be..0000000 --- a/server/src/modules/auth/shared/repository/osoDataFilter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Oso } from 'oso'; -import { DataFilter } from '../dataFilter'; -import { NotAuthorizedError } from '../errors/notAuthorizedError'; - -export class OsoDataFilter implements DataFilter { - constructor(private readonly oso: Oso) {} - - authorizedResources( - actor: any, - action: any, - resource: any - ): Promise { - return this.oso.authorizedResources(actor, action, resource); - } - - async authorizedQuery(actor: any, action: any, resource: any) { - const query = (await this.oso.authorizedQuery( - actor, - action, - resource - )) as any; - if (query === null) { - throw new NotAuthorizedError( - `no rules found: actor:${actor}, action:${action}, resource:${resource}` - ); - } - return query; - } -} diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index 49b86ba..1b7933d 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -1,6 +1,6 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { MemberOrm } from '@/modules/auth/shared/createOso'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; @@ -14,24 +14,29 @@ import { MemberNotFoundError } from '../../useCases/errors/memberNotFoundError'; export class PrismaMemberRepository implements EditMemberDetailRepository { private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { + constructor(private readonly authorizer: Authorizer, prisma: PrismaClient) { this.prisma = prisma; } async updateMember( - user: User, memberId: string, payload: UpdatePayload ): Promise> { - await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); + await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); await this.prisma.member.update({ where: { id: memberId }, data: payload }); return Result.ok(); } - async queryMember(user: User, memberId: string): Promise> { - await this.dataFilter.authorizedQuery(user, MEMBER_ACTIONS.READ, MemberOrm); + async queryMember(memberId: string): Promise> { + await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); const record = await this.prisma.member.findUnique({ where: { id: memberId }, diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts index 79d2723..1c529f6 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts @@ -14,10 +14,6 @@ export type UpdatePayload = { }; export interface EditMemberDetailRepository { - queryMember(user: User, memberId: string): Promise>; - updateMember( - user: User, - memberId: string, - payload: UpdatePayload - ): Promise>; + queryMember(memberId: string): Promise>; + updateMember(memberId: string, payload: UpdatePayload): Promise>; } diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts index 105fe3b..acb0466 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts @@ -29,10 +29,7 @@ export class EditMemberDetailService memberId, payload, }: EditMemberDetailRequest): Promise> { - const memberOrError = await this.repository.queryMember( - this.authorizer.currentUser, - memberId - ); + const memberOrError = await this.repository.queryMember(memberId); if (memberOrError.isFailure) { return Result.fail(memberOrError.error); } @@ -66,11 +63,7 @@ export class EditMemberDetailService throw new MemberNothingToUpdateError(memberId); } - await this.repository.updateMember( - this.authorizer.currentUser, - memberId, - authorizedPayload - ); + await this.repository.updateMember(memberId, authorizedPayload); return Result.ok({ result: true, diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts index a20cde6..27620bb 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts @@ -4,7 +4,6 @@ import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../dtos/memberDTO'; import { DisplayableMember } from '../../dtos/displayableMemberDTO'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { PrismaClient } from '@prisma/client'; @@ -18,13 +17,11 @@ export class ListAllMembersService { constructor( private readonly authorizer: Authorizer, - private readonly dataFilter: DataFilter, private readonly prisma: PrismaClient ) {} async execute(): Promise> { - const query = await this.dataFilter.authorizedQuery( - this.authorizer.currentUser, + const query = await this.authorizer.authorizedQueryForUser( MEMBER_ACTIONS.READ, MemberOrm ); diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts index a13869d..eade78a 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts @@ -4,7 +4,6 @@ import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; import { Member } from '../../dtos/memberDTO'; import { DisplayableMember } from '../../dtos/displayableMemberDTO'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { PrismaClient } from '@prisma/client'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { MemberNotFoundError } from '../errors/memberNotFoundError'; @@ -22,15 +21,13 @@ export class ShowMemberDetailService { constructor( private readonly authorizer: Authorizer, - private readonly dataFilter: DataFilter, private readonly prisma: PrismaClient ) {} async execute( req: ShowMemberDetailRequest ): Promise> { - await this.dataFilter.authorizedQuery( - this.authorizer.currentUser, + await this.authorizer.authorizedQueryForUser( MEMBER_ACTIONS.READ, MemberOrm ); diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts index ebf8d4f..7427b1c 100644 --- a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts +++ b/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts @@ -1,7 +1,6 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; import { PrismaClient } from '@prisma/client'; @@ -21,13 +20,11 @@ export class GetLoggedInUserInfoService { constructor( private readonly authorizer: Authorizer, - private readonly dataFilter: DataFilter, private readonly prisma: PrismaClient ) {} async execute(): Promise> { - const query = await this.dataFilter.authorizedQuery( - this.authorizer.currentUser, + const query = await this.authorizer.authorizedQueryForUser( USER_MENU_ITEM_ACTIONS.READ, UserMenuItemOrm ); diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index 19be84f..a1549a5 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -8,7 +8,6 @@ import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { createContext } from './context'; import { createServer } from '@graphql-yoga/node'; import { createResolvers } from './resolvers'; -import { DataFilter } from '@/modules/auth/shared/dataFilter'; import { Authorizer } from '@/modules/auth/shared/authorizer'; import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; @@ -19,38 +18,26 @@ import { createCoreOso, createSqliteDataFilterOso, } from '@/modules/auth/shared/createOso'; -import { OsoDataFilter } from '@/modules/auth/shared/repository/osoDataFilter'; import { createCheckLoggedInMiddleware } from '@/modules/auth/shared/checkLoggedInMiddleware'; export const prisma = new PrismaClient(); type UseCaseDependencies = { - dataFilter: DataFilter; authorizer: Authorizer; prisma: PrismaClient; }; -const createUseCases = ({ - dataFilter, - authorizer, - prisma, -}: UseCaseDependencies) => { +const createUseCases = ({ authorizer, prisma }: UseCaseDependencies) => { const getLoggedInUserInfoService = new GetLoggedInUserInfoService( authorizer, - dataFilter, prisma ); - const prismaMemberRepository = new PrismaMemberRepository(dataFilter, prisma); + const prismaMemberRepository = new PrismaMemberRepository(authorizer, prisma); - const listAllMembersService = new ListAllMembersService( - authorizer, - dataFilter, - prisma - ); + const listAllMembersService = new ListAllMembersService(authorizer, prisma); const showMemberDetailService = new ShowMemberDetailService( authorizer, - dataFilter, prisma ); const editMemberDetailService = new EditMemberDetailService( @@ -67,12 +54,10 @@ const createUseCases = ({ }; export async function startServer() { - const oso = await createCoreOso(); - + const coreOso = await createCoreOso(); const dataFilterOso = await createSqliteDataFilterOso(); - const dataFilter = new OsoDataFilter(dataFilterOso); - const authorizer = new Authorizer(prisma, oso); + const authorizer = new Authorizer(prisma, coreOso, dataFilterOso); // express const app: Application = express(); @@ -84,7 +69,6 @@ export async function startServer() { }); const useCases = createUseCases({ - dataFilter, authorizer, prisma, }); From 07d431fe912095075f186e9763f81b1685717abe Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Mon, 2 May 2022 09:38:54 +0900 Subject: [PATCH 08/12] change use case structure to reflect command and query --- .../modules/members/infra/repos/prismaMemberRepository.ts | 3 +-- .../editMemberDetail/editMemberDetailRepository.ts | 3 +-- .../editMemberDetail/editMemberDetailService.ts | 4 ++-- .../{ => query}/listAllMembers/listAllMembersService.ts | 4 ++-- .../showMemberDetail/showMemberDetailService.ts | 6 +++--- .../getLoggedInUserInfo/getLoggedInUserInfoService.ts | 0 server/src/shared/infra/http/app.ts | 8 ++++---- server/src/shared/infra/http/resolvers.ts | 8 ++++---- 8 files changed, 17 insertions(+), 19 deletions(-) rename server/src/modules/members/useCases/{ => command}/editMemberDetail/editMemberDetailRepository.ts (81%) rename server/src/modules/members/useCases/{ => command}/editMemberDetail/editMemberDetailService.ts (94%) rename server/src/modules/members/useCases/{ => query}/listAllMembers/listAllMembersService.ts (94%) rename server/src/modules/members/useCases/{ => query}/showMemberDetail/showMemberDetailService.ts (93%) rename server/src/modules/users/useCases/{ => query}/getLoggedInUserInfo/getLoggedInUserInfoService.ts (100%) diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index 1b7933d..41a7713 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -1,7 +1,6 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { MemberOrm } from '@/modules/auth/shared/createOso'; -import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; import { Department } from '../../dtos/departmentDTO'; @@ -9,7 +8,7 @@ import { Member } from '../../dtos/memberDTO'; import { EditMemberDetailRepository, UpdatePayload, -} from '../../useCases/editMemberDetail/editMemberDetailRepository'; +} from '../../useCases/command/editMemberDetail/editMemberDetailRepository'; import { MemberNotFoundError } from '../../useCases/errors/memberNotFoundError'; export class PrismaMemberRepository implements EditMemberDetailRepository { diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts similarity index 81% rename from server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts rename to server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts index 1c529f6..1493a75 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts @@ -1,6 +1,5 @@ -import { User } from '@/modules/users/dtos/userDTO'; import { Result } from '@/shared/core/result'; -import { Member } from '../../dtos/memberDTO'; +import { Member } from '../../../dtos/memberDTO'; export type UpdatePayload = { firstName?: string; diff --git a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts similarity index 94% rename from server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts rename to server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts index acb0466..7615016 100644 --- a/server/src/modules/members/useCases/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts @@ -2,8 +2,8 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../dtos/memberDTO'; -import { MemberNothingToUpdateError } from '../errors/memberNothingToUpdateError'; +import { Member } from '../../../dtos/memberDTO'; +import { MemberNothingToUpdateError } from '../../errors/memberNothingToUpdateError'; import { EditMemberDetailRepository, UpdatePayload, diff --git a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts similarity index 94% rename from server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts rename to server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts index 27620bb..748cdd2 100644 --- a/server/src/modules/members/useCases/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts @@ -2,8 +2,8 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../dtos/memberDTO'; -import { DisplayableMember } from '../../dtos/displayableMemberDTO'; +import { Member } from '../../../dtos/memberDTO'; +import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { PrismaClient } from '@prisma/client'; diff --git a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts similarity index 93% rename from server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts rename to server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts index eade78a..ab6dcee 100644 --- a/server/src/modules/members/useCases/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts @@ -2,11 +2,11 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../dtos/memberDTO'; -import { DisplayableMember } from '../../dtos/displayableMemberDTO'; +import { Member } from '../../../dtos/memberDTO'; +import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; import { PrismaClient } from '@prisma/client'; import { MemberOrm } from '@/modules/auth/shared/createOso'; -import { MemberNotFoundError } from '../errors/memberNotFoundError'; +import { MemberNotFoundError } from '../../errors/memberNotFoundError'; export type ShowMemberDetailRequest = { memberId: string; diff --git a/server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService.ts similarity index 100% rename from server/src/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService.ts rename to server/src/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService.ts diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index a1549a5..a134d1d 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -9,11 +9,11 @@ import { createContext } from './context'; import { createServer } from '@graphql-yoga/node'; import { createResolvers } from './resolvers'; import { Authorizer } from '@/modules/auth/shared/authorizer'; -import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; -import { ListAllMembersService } from '@/modules/members/useCases/listAllMembers/listAllMembersService'; -import { ShowMemberDetailService } from '@/modules/members/useCases/showMemberDetail/showMemberDetailService'; -import { EditMemberDetailService } from '@/modules/members/useCases/editMemberDetail/editMemberDetailService'; +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; import { createCoreOso, createSqliteDataFilterOso, diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts index 4ba49e8..f794773 100644 --- a/server/src/shared/infra/http/resolvers.ts +++ b/server/src/shared/infra/http/resolvers.ts @@ -1,7 +1,7 @@ -import { EditMemberDetailService } from '@/modules/members/useCases/editMemberDetail/editMemberDetailService'; -import { ListAllMembersService } from '@/modules/members/useCases/listAllMembers/listAllMembersService'; -import { ShowMemberDetailService } from '@/modules/members/useCases/showMemberDetail/showMemberDetailService'; -import { GetLoggedInUserInfoService } from '@/modules/users/useCases/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; import { Resolvers } from './generated/resolver-types'; type Dependencies = { From 973ac179fa83116237cdead2a34b7e93902d41df Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Mon, 2 May 2022 09:48:46 +0900 Subject: [PATCH 09/12] add DTO suffix to dtos --- server/src/modules/auth/shared/authorizer.ts | 6 ++--- server/src/modules/auth/shared/createOso.ts | 24 ++++++++++++------- .../src/modules/members/dtos/departmentDTO.ts | 2 +- .../members/dtos/displayableMemberDTO.ts | 4 ++-- server/src/modules/members/dtos/memberDTO.ts | 24 +++++++++---------- .../infra/repos/prismaMemberRepository.ts | 10 ++++---- .../editMemberDetailRepository.ts | 4 ++-- .../editMemberDetailService.ts | 4 ++-- .../listAllMembers/listAllMembersService.ts | 8 +++---- .../showMemberDetailService.ts | 8 +++---- server/src/modules/users/dtos/userDTO.ts | 16 ++++++------- 11 files changed, 58 insertions(+), 52 deletions(-) diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts index 5a292fb..977200e 100644 --- a/server/src/modules/auth/shared/authorizer.ts +++ b/server/src/modules/auth/shared/authorizer.ts @@ -1,4 +1,4 @@ -import { User } from '@/modules/users/dtos/userDTO'; +import { UserDTO } from '@/modules/users/dtos/userDTO'; import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; @@ -7,7 +7,7 @@ import { NotAuthorizedError } from './errors/notAuthorizedError'; import { UserNotFoundError } from './errors/userNotFoundError'; export class Authorizer { - _currentUser?: User; + _currentUser?: UserDTO; constructor( private readonly prisma: PrismaClient, @@ -40,7 +40,7 @@ export class Authorizer { throw new UserNotFoundError(userId); } - const user = User.createFromOrmModel(userRecord); + const user = UserDTO.createFromOrmModel(userRecord); this._currentUser = user; } diff --git a/server/src/modules/auth/shared/createOso.ts b/server/src/modules/auth/shared/createOso.ts index f9e2e03..a74c7e4 100644 --- a/server/src/modules/auth/shared/createOso.ts +++ b/server/src/modules/auth/shared/createOso.ts @@ -7,17 +7,17 @@ import { Oso } from 'oso'; import { Filter, Relation } from 'oso/dist/src/dataFiltering'; import { PrimitivePropertyNames } from '@/shared/sharedTypes'; import { prisma } from '@/shared/infra/http/app'; -import { Department } from '@/modules/members/dtos/departmentDTO'; -import { Member } from '@/modules/members/dtos/memberDTO'; +import { DepartmentDTO } from '@/modules/members/dtos/departmentDTO'; +import { MemberDTO } from '@/modules/members/dtos/memberDTO'; import { UserMenuItem } from '@/modules/users/dtos/userMenuItemDTO'; -import { User } from '@/modules/users/dtos/userDTO'; +import { UserDTO } from '@/modules/users/dtos/userDTO'; // FIXME: Since prisma objects are POJOs, we need to create classes // to pass to Oso by ourselves. // https://github.com/prisma/prisma/issues/5315 export class DepartmentOrm { static entityFieldMap: Record< - PrimitivePropertyNames, + PrimitivePropertyNames, PrimitivePropertyNames > = { id: 'id', @@ -31,7 +31,7 @@ export class DepartmentOrm { } export class MemberOrm { static entityFieldMap: Record< - PrimitivePropertyNames, + PrimitivePropertyNames, PrimitivePropertyNames > = { id: 'id', @@ -122,7 +122,7 @@ export async function createSqliteDataFilterOso() { }); // Since User will always be the LoggedInUser, we use the core entity class - osoDataFilter.registerClass(User, { + osoDataFilter.registerClass(UserDTO, { name: 'User', }); osoDataFilter.registerClass(UserMenuItemOrm, { @@ -165,10 +165,16 @@ export async function createSqliteDataFilterOso() { export async function createCoreOso() { const oso = new Oso(); - oso.registerClass(User); + oso.registerClass(UserDTO, { + name: 'User', + }); oso.registerClass(UserMenuItem); - oso.registerClass(Department); - oso.registerClass(Member); + oso.registerClass(DepartmentDTO, { + name: 'Department', + }); + oso.registerClass(MemberDTO, { + name: 'Member', + }); await oso.loadFiles(policyFiles); return oso; diff --git a/server/src/modules/members/dtos/departmentDTO.ts b/server/src/modules/members/dtos/departmentDTO.ts index be5a462..c868d81 100644 --- a/server/src/modules/members/dtos/departmentDTO.ts +++ b/server/src/modules/members/dtos/departmentDTO.ts @@ -1,4 +1,4 @@ -export class Department { +export class DepartmentDTO { constructor( public id: string, public name: string, diff --git a/server/src/modules/members/dtos/displayableMemberDTO.ts b/server/src/modules/members/dtos/displayableMemberDTO.ts index 5a00729..5a0298f 100644 --- a/server/src/modules/members/dtos/displayableMemberDTO.ts +++ b/server/src/modules/members/dtos/displayableMemberDTO.ts @@ -1,6 +1,6 @@ -import { Member } from './memberDTO'; +import { MemberDTO } from './memberDTO'; -export type DisplayableMember = Partial & { +export type DisplayableMember = Partial & { editable: boolean; isLoggedInUser: boolean; }; diff --git a/server/src/modules/members/dtos/memberDTO.ts b/server/src/modules/members/dtos/memberDTO.ts index 0e30d33..9311b55 100644 --- a/server/src/modules/members/dtos/memberDTO.ts +++ b/server/src/modules/members/dtos/memberDTO.ts @@ -1,12 +1,12 @@ import { NonFunctionPropertyNames } from '@/shared/sharedTypes'; -import { Department } from './departmentDTO'; +import { DepartmentDTO } from './departmentDTO'; import { Member as PrismaMember, Department as PrismaDepartment, } from '@prisma/client'; -export class Member { - static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ +export class MemberDTO { + static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ 'id', 'avatar', 'firstName', @@ -17,7 +17,7 @@ export class Member { 'email', 'pr', ]; - static PRIVATE_FIELDS: NonFunctionPropertyNames[] = ['age', 'salary']; + static PRIVATE_FIELDS: NonFunctionPropertyNames[] = ['age', 'salary']; constructor( public id: string, @@ -26,7 +26,7 @@ export class Member { public lastName: string, public age: number, public salary: number, - public department: Department, + public department: DepartmentDTO, public joinedAt: Date, public phoneNumber: string, public email: string, @@ -35,12 +35,12 @@ export class Member { createObjectWithAuthorizedFields( this: any, - fields: Set - ): Partial { - const authorizedMember: Partial = {}; + fields: Set + ): Partial { + const authorizedMember: Partial = {}; for (const field of Array.from(fields.values())) { - authorizedMember[field as keyof Partial] = this[field]; + authorizedMember[field as keyof Partial] = this[field]; } return authorizedMember; @@ -48,15 +48,15 @@ export class Member { static createFromOrmModel( member: PrismaMember & { department: PrismaDepartment } - ): Member { - return new Member( + ): MemberDTO { + return new MemberDTO( member.id, member.avatar, member.firstName, member.lastName, member.age, member.salary, - new Department( + new DepartmentDTO( member.department.id, member.department.name, member.department.managerMemberId diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts index 41a7713..2d52c06 100644 --- a/server/src/modules/members/infra/repos/prismaMemberRepository.ts +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -3,8 +3,8 @@ import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { Result } from '@/shared/core/result'; import { PrismaClient } from '@prisma/client'; -import { Department } from '../../dtos/departmentDTO'; -import { Member } from '../../dtos/memberDTO'; +import { DepartmentDTO } from '../../dtos/departmentDTO'; +import { MemberDTO } from '../../dtos/memberDTO'; import { EditMemberDetailRepository, UpdatePayload, @@ -31,7 +31,7 @@ export class PrismaMemberRepository implements EditMemberDetailRepository { return Result.ok(); } - async queryMember(memberId: string): Promise> { + async queryMember(memberId: string): Promise> { await this.authorizer.authorizedQueryForUser( MEMBER_ACTIONS.READ, MemberOrm @@ -46,14 +46,14 @@ export class PrismaMemberRepository implements EditMemberDetailRepository { throw new MemberNotFoundError(memberId); } - const member = new Member( + const member = new MemberDTO( record.id, record.avatar, record.firstName, record.lastName, record.age, record.salary, - new Department( + new DepartmentDTO( record.department.id, record.department.name, record.department.managerMemberId diff --git a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts index 1493a75..c5d1a14 100644 --- a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts @@ -1,5 +1,5 @@ import { Result } from '@/shared/core/result'; -import { Member } from '../../../dtos/memberDTO'; +import { MemberDTO } from '../../../dtos/memberDTO'; export type UpdatePayload = { firstName?: string; @@ -13,6 +13,6 @@ export type UpdatePayload = { }; export interface EditMemberDetailRepository { - queryMember(memberId: string): Promise>; + queryMember(memberId: string): Promise>; updateMember(memberId: string, payload: UpdatePayload): Promise>; } diff --git a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts index 7615016..caf6271 100644 --- a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts @@ -2,7 +2,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../../dtos/memberDTO'; +import { MemberDTO } from '../../../dtos/memberDTO'; import { MemberNothingToUpdateError } from '../../errors/memberNothingToUpdateError'; import { EditMemberDetailRepository, @@ -35,7 +35,7 @@ export class EditMemberDetailService } const authorizedFieldsOrError = - await this.authorizer.authorizedFieldsForUser( + await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.UPDATE, memberOrError.getValue() ); diff --git a/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts index 748cdd2..1242db2 100644 --- a/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts @@ -2,14 +2,14 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../../dtos/memberDTO'; +import { MemberDTO } from '../../../dtos/memberDTO'; import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; import { MemberOrm } from '@/modules/auth/shared/createOso'; import { PrismaClient } from '@prisma/client'; export type ListAllMembersRequest = {}; export type ListAllMembersResponse = { - members: Partial[]; + members: Partial[]; }; export class ListAllMembersService @@ -34,9 +34,9 @@ export class ListAllMembersService const authorizedMembers: DisplayableMember[] = []; for (const memberModel of memberModels) { - const memberDto = Member.createFromOrmModel(memberModel); + const memberDto = MemberDTO.createFromOrmModel(memberModel); const authorizedFieldsOrError = - await this.authorizer.authorizedFieldsForUser( + await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.READ, memberDto ); diff --git a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts index ab6dcee..85801c2 100644 --- a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts @@ -2,7 +2,7 @@ import { Authorizer } from '@/modules/auth/shared/authorizer'; import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; import { Result } from '@/shared/core/result'; import { UseCase } from '@/shared/core/useCase'; -import { Member } from '../../../dtos/memberDTO'; +import { MemberDTO } from '../../../dtos/memberDTO'; import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; import { PrismaClient } from '@prisma/client'; import { MemberOrm } from '@/modules/auth/shared/createOso'; @@ -41,9 +41,9 @@ export class ShowMemberDetailService return Result.fail(new MemberNotFoundError(req.memberId)); } - const member = Member.createFromOrmModel(record); + const member = MemberDTO.createFromOrmModel(record); const authorizedFieldsOrError = - await this.authorizer.authorizedFieldsForUser( + await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.READ, member ); @@ -67,7 +67,7 @@ export class ShowMemberDetailService authorizedMemberOrError.editable = true; const fieldsOrError = - await this.authorizer.authorizedFieldsForUser( + await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.UPDATE, member ); diff --git a/server/src/modules/users/dtos/userDTO.ts b/server/src/modules/users/dtos/userDTO.ts index 5418984..60da892 100644 --- a/server/src/modules/users/dtos/userDTO.ts +++ b/server/src/modules/users/dtos/userDTO.ts @@ -1,16 +1,16 @@ -import { Department } from '@/modules/members/dtos/departmentDTO'; -import { Member } from '@/modules/members/dtos/memberDTO'; +import { DepartmentDTO } from '@/modules/members/dtos/departmentDTO'; +import { MemberDTO } from '@/modules/members/dtos/memberDTO'; import { User as PrismaUser, Member as PrismaMember, Department as PrismaDepartment, } from '@prisma/client'; -export class User { +export class UserDTO { constructor( public id: string, public username: string, - public memberInfo: Member, + public memberInfo: MemberDTO, public isAdmin: boolean ) {} @@ -18,18 +18,18 @@ export class User { userRecord: PrismaUser & { member: PrismaMember & { department: PrismaDepartment }; } - ): User { - return new User( + ): UserDTO { + return new UserDTO( userRecord.id, userRecord.username, - new Member( + new MemberDTO( userRecord.member.id, userRecord.member.avatar, userRecord.member.firstName, userRecord.member.lastName, userRecord.member.age, userRecord.member.salary, - new Department( + new DepartmentDTO( userRecord.member.department.id, userRecord.member.department.name, userRecord.member.department.managerMemberId From c3bf299f22fa4ff830510043ae7f8752f1e5002b Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Mon, 2 May 2022 10:28:29 +0900 Subject: [PATCH 10/12] refactor list and show authorize logic --- .../listAllMembers/listAllMembersService.ts | 33 +++++------ .../showMemberDetailService.ts | 56 ++++++++----------- 2 files changed, 37 insertions(+), 52 deletions(-) diff --git a/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts index 1242db2..1fc0a76 100644 --- a/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts +++ b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts @@ -9,7 +9,7 @@ import { PrismaClient } from '@prisma/client'; export type ListAllMembersRequest = {}; export type ListAllMembersResponse = { - members: Partial[]; + members: DisplayableMember[]; }; export class ListAllMembersService @@ -32,7 +32,7 @@ export class ListAllMembersService }, }); - const authorizedMembers: DisplayableMember[] = []; + const displayableMembers: DisplayableMember[] = []; for (const memberModel of memberModels) { const memberDto = MemberDTO.createFromOrmModel(memberModel); const authorizedFieldsOrError = @@ -44,33 +44,26 @@ export class ListAllMembersService return Result.fail(authorizedFieldsOrError.error); } - const authorizedMember: DisplayableMember = { - ...memberDto.createObjectWithAuthorizedFields( - authorizedFieldsOrError.getValue() - ), - editable: false, - isLoggedInUser: false, - }; - const allowedActionsOrError = - await this.authorizer.authorizedActionsForUser(memberModel); + await this.authorizer.authorizedActionsForUser(memberDto); if (allowedActionsOrError.isFailure) { return Result.fail(allowedActionsOrError.error); } - if (allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE)) { - authorizedMember.editable = true; - } - - if (memberModel.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMember.isLoggedInUser = true; - } + const displayableMember: DisplayableMember = { + ...memberDto.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE), + isLoggedInUser: + memberDto.id === this.authorizer.currentUser.memberInfo.id, + }; - authorizedMembers.push(authorizedMember); + displayableMembers.push(displayableMember); } return Result.ok({ - members: authorizedMembers, + members: displayableMembers, }); } } diff --git a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts index 85801c2..e2c31d2 100644 --- a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts @@ -41,53 +41,45 @@ export class ShowMemberDetailService return Result.fail(new MemberNotFoundError(req.memberId)); } - const member = MemberDTO.createFromOrmModel(record); + const memberDto = MemberDTO.createFromOrmModel(record); const authorizedFieldsOrError = await this.authorizer.authorizedFieldsForUser( MEMBER_ACTIONS.READ, - member + memberDto ); - const authorizedMemberOrError: DisplayableMember = { - ...member.createObjectWithAuthorizedFields( - authorizedFieldsOrError.getValue() - ), - editable: false, - isLoggedInUser: false, - }; - const allowedActionsOrError = - await this.authorizer.authorizedActionsForUser(member); + await this.authorizer.authorizedActionsForUser(memberDto); if (allowedActionsOrError.isFailure) { return Result.fail(allowedActionsOrError.error); } - let authorizedFieldsToUpdate: string[] = []; - if (allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE)) { - authorizedMemberOrError.editable = true; - - const fieldsOrError = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.UPDATE, - member - ); - if (fieldsOrError.isFailure) { - return Result.fail(fieldsOrError.error); - } - - const fields = fieldsOrError.getValue(); + const displayableMemberOrError: DisplayableMember = { + ...memberDto.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE), + isLoggedInUser: + memberDto.id === this.authorizer.currentUser.memberInfo.id, + }; - // FIXME: exclude readonly fields such as id, joinedAt - authorizedFieldsToUpdate = Array.from(fields.values()).map((f) => f); + const editableFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.UPDATE, + memberDto + ); + if (editableFieldsOrError.isFailure) { + return Result.fail(editableFieldsOrError.error); } - if (member.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMemberOrError.isLoggedInUser = true; - } + // FIXME: exclude readonly fields such as id, joinedAt + const editableFields: string[] = Array.from( + editableFieldsOrError.getValue().values() + ); return Result.ok({ - editableFields: authorizedFieldsToUpdate, - member: authorizedMemberOrError, + editableFields: editableFields, + member: displayableMemberOrError, }); } } From dd2e9a0278d7962d1e98cc99645836f5b1053164 Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Sat, 28 Jan 2023 08:55:26 +0900 Subject: [PATCH 11/12] update --- server/codegen.yml | 18 +- server/package-lock.json | 718 +++++++++++++++++- server/package.json | 2 +- server/schema.graphql | 8 +- .../infra/http/__tests__/resolvers.test.ts | 86 +++ server/src/shared/infra/http/app.ts | 37 +- .../infra/http/generated/resolver-types.ts | 34 +- server/src/shared/infra/http/resolvers.ts | 10 +- 8 files changed, 823 insertions(+), 90 deletions(-) create mode 100644 server/src/shared/infra/http/__tests__/resolvers.test.ts diff --git a/server/codegen.yml b/server/codegen.yml index f5d5b38..bed6fd7 100644 --- a/server/codegen.yml +++ b/server/codegen.yml @@ -9,12 +9,12 @@ generates: config: mapperTypeSuffix: Model mappers: - ListAllMembersResponse: '@/members/list-all-members/listAllMembersService#ListAllMembersResponse' - ShowMemberDetailResponse: '@/members/show-member-detail/showMemberDetailService#ShowMemberDetailResponse' - UserInfo: '@/users/get-logged-in-user-info/getLoggedInUserInfoService#GetLoggedInUserInfoResponse' - UserMenuItem: '@/users/get-logged-in-user-info/getLoggedInUserInfoService#UserMenuItem' - Member: '@/members/shared/member#DisplayableMember' - Department: '@/members/shared/department#Department' - EditMemberDetailInput: '@/members/edit-member-detail/editMemberDetailService#EditMemberDetailRequest' - EditMemberDetailResponse: '@/members/edit-member-detail/editMemberDetailService#EditMemberDetailResponse' - contextType: '@/context#Context' + ListAllMembersResponse: '@/modules/members/useCases/query/listAllMembers/listAllMembersService#ListAllMembersResponse' + ShowMemberDetailResponse: '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService#ShowMemberDetailResponse' + UserInfo: '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService#GetLoggedInUserInfoResponse' + UserMenuItem: '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService#UserMenuItem' + Member: '@/modules/members/dtos/displayableMemberDTO#DisplayableMember' + Department: '@/modules/members/dtos/departmentDTO#DepartmentDTO' + EditMemberDetailInput: '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService#EditMemberDetailRequest' + EditMemberDetailResponse: '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService#EditMemberDetailResponse' + contextType: '@/shared/infra/http/context#Context' diff --git a/server/package-lock.json b/server/package-lock.json index 2322333..1e54971 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,10 +12,10 @@ "dependencies": { "@graphql-tools/graphql-file-loader": "^7.3.10", "@graphql-tools/load": "^7.5.9", - "@graphql-yoga/node": "^2.3.0", "@prisma/client": "^3.12.0", "express": "^4.17.1", "graphql": "^16.3.0", + "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", "sqlite3": "^5.0.2" @@ -2772,6 +2772,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.3.1.tgz", "integrity": "sha512-AnYUci7EGyA8flml881lDvVDl6n/u6GQpVIOTsZVO29d4/rPqSJ2KFguDD3mjDL406doTTLNuDI4ndxfJl6fmQ==", + "dev": true, "dependencies": { "@envelop/types": "2.2.0" }, @@ -2783,6 +2784,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@envelop/disable-introspection/-/disable-introspection-3.3.1.tgz", "integrity": "sha512-THR8UnRQQB5nCLmITXvebwXwSHvFjS+ThA3RRVXpFX9EupMbTFN5a4NHty7+BYD798c3VrBZ/InbMlEWqw1c9g==", + "dev": true, "peerDependencies": { "@envelop/core": "^2.3.1", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" @@ -2792,6 +2794,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-4.3.1.tgz", "integrity": "sha512-IqerCVjvVTiGvSZ8qSpdEc55hhiuekufJd0+ldWtyMPznhMaYOzpLifFUhjhhD7004eJM17n9vjJQFa7fIGz8Q==", + "dev": true, "dependencies": { "tiny-lru": "7.0.6" }, @@ -2804,6 +2807,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@envelop/types/-/types-2.2.0.tgz", "integrity": "sha512-Lghvfs0kh53G5mUKpCMlB/FhHh3O8SSR4hewB7JyE9hOEu/9h/6u+GHH/OEgdaRHky1Sae5Jf4grO+h21ka4ig==", + "dev": true, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } @@ -2812,6 +2816,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-4.3.1.tgz", "integrity": "sha512-lmtu9idhdWqbYkcFoFsL1ED4y38DLvj6EDEwE9tULXWuZm4WWmlNQAmKHAwB1d3kGVQAMtxM59crkOOJGRBgHQ==", + "dev": true, "dependencies": { "tiny-lru": "7.0.6" }, @@ -3078,6 +3083,45 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-tools/executor": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.12.tgz", + "integrity": "sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==", + "dependencies": { + "@graphql-tools/utils": "9.1.4", + "@graphql-typed-document-node/core": "3.1.1", + "@repeaterjs/repeater": "3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/@graphql-tools/executor/node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/@graphql-tools/git-loader": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.1.12.tgz", @@ -3642,6 +3686,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/common/-/common-2.3.0.tgz", "integrity": "sha512-EPKK97953c8E1FiaLHMMGqLKtoAN5H9qHr0AiAzMlruJHn0JcbMf2qFTUXklCsuk6UEwNtxeHX42zim11O/E1g==", + "dev": true, "dependencies": { "@envelop/core": "^2.0.0", "@envelop/disable-introspection": "^3.0.0", @@ -3663,6 +3708,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/node/-/node-2.3.0.tgz", "integrity": "sha512-uofEmKIDYthJuCcdhbgU0VW5i2cCqZVKIiEW/xbwvCOBJt439k46D+M6youiQYJ1miaA/m0btbuZ1sAo/TLjdQ==", + "dev": true, "dependencies": { "@envelop/core": "^2.0.0", "@graphql-tools/utils": "^8.6.0", @@ -3679,6 +3725,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-2.0.0.tgz", "integrity": "sha512-HlG+gIddjIP3/BDrMZymdzmzDjNdYuSGMxx6+1JA83gAEVRDR4yOoT4QeNKYqRhLK9xca/Hxp1PfBpquSa244Q==", + "dev": true, + "dependencies": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.3.1" + } + }, + "node_modules/@graphql-yoga/typed-event-target": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-1.0.0.tgz", + "integrity": "sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==", "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.3.1" @@ -4051,6 +4107,52 @@ "node": ">= 8" } }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", + "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-schema/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", + "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.0", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.1", + "webcrypto-core": "^1.7.4" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@peculiar/webcrypto/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/@prisma/client": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.12.0.tgz", @@ -4075,7 +4177,7 @@ "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz", "integrity": "sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==", - "devOptional": true, + "dev": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { @@ -4440,6 +4542,39 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "node_modules/@whatwg-node/events": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", + "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==" + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.6.2.tgz", + "integrity": "sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==", + "dependencies": { + "@peculiar/webcrypto": "^1.4.0", + "abort-controller": "^3.0.0", + "busboy": "^1.6.0", + "form-data-encoder": "^1.7.1", + "formdata-node": "^4.3.1", + "node-fetch": "^2.6.7", + "undici": "^5.12.0", + "urlpattern-polyfill": "^6.0.2", + "web-streams-polyfill": "^3.2.0" + } + }, + "node_modules/@whatwg-node/server": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.5.8.tgz", + "integrity": "sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==", + "dependencies": { + "@whatwg-node/fetch": "0.6.2", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@types/node": "^18.0.6" + } + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -4712,6 +4847,24 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asn1js/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -5158,6 +5311,17 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -5773,6 +5937,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.2.5.tgz", "integrity": "sha512-6IR+JN6o2UMNj2f3fu0ZVkZeP0h22DRKzq78SiMenkqyBYyGIT1AkZjHkItvh0A80LdsAlWENHUpvapapePucw==", + "dev": true, "dependencies": { "abort-controller": "^3.0.0", "form-data-encoder": "^1.7.1", @@ -7054,6 +7219,131 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/graphql-yoga": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-3.4.0.tgz", + "integrity": "sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==", + "dependencies": { + "@envelop/core": "3.0.4", + "@envelop/parser-cache": "^5.0.4", + "@envelop/validation-cache": "^5.0.5", + "@graphql-tools/executor": "0.0.12", + "@graphql-tools/schema": "^9.0.0", + "@graphql-tools/utils": "^9.0.1", + "@graphql-yoga/subscription": "^3.1.0", + "@whatwg-node/fetch": "0.6.2", + "@whatwg-node/server": "0.5.8", + "dset": "^3.1.1", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "graphql": "^15.2.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/core": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.4.tgz", + "integrity": "sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==", + "dependencies": { + "@envelop/types": "3.0.1", + "tslib": "2.4.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/parser-cache": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-5.0.4.tgz", + "integrity": "sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==", + "dependencies": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@envelop/core": "^3.0.4", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-3.0.1.tgz", + "integrity": "sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/validation-cache": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-5.0.5.tgz", + "integrity": "sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==", + "dependencies": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@envelop/core": "^3.0.4", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/merge": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.16.tgz", + "integrity": "sha512-In0kcOZcPIpYOKaqdrJ3thdLPE7TutFnL9tbrHUy2zCinR2O/blpRC48jPckcs0HHrUQ0pGT4HqvzMkZUeEBAw==", + "dependencies": { + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/schema": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.14.tgz", + "integrity": "sha512-U6k+HY3Git+dsOEhq+dtWQwYg2CAgue8qBvnBXoKu5eEeH284wymMUoNm0e4IycOgMCJANVhClGEBIkLRu3FQQ==", + "dependencies": { + "@graphql-tools/merge": "8.3.16", + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-yoga/subscription": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-3.1.0.tgz", + "integrity": "sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==", + "dependencies": { + "@graphql-yoga/typed-event-target": "^1.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "0.0.2", + "tslib": "^2.3.1" + } + }, + "node_modules/graphql-yoga/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/graphql-yoga/node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -9492,7 +9782,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10609,7 +10898,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.12.0.tgz", "integrity": "sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" @@ -10704,6 +10993,27 @@ "node": ">=8" } }, + "node_modules/pvtsutils": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", + "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/pvtsutils/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -11484,6 +11794,14 @@ "node": ">= 0.6" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -11855,6 +12173,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", + "dev": true, "engines": { "node": ">=6" } @@ -12216,9 +12535,12 @@ "dev": true }, "node_modules/undici": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", - "integrity": "sha512-VhUpiZ3No1DOPPQVQnsDZyfcbTTcHdcgWej1PdFnSvOeJmOVDgiOHkunJmBLfmjt4CqgPQddPVjSWW0dsTs5Yg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.16.0.tgz", + "integrity": "sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==", + "dependencies": { + "busboy": "^1.6.0" + }, "engines": { "node": ">=12.18" } @@ -12356,6 +12678,14 @@ "node": ">=4" } }, + "node_modules/urlpattern-polyfill": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", + "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", + "dependencies": { + "braces": "^3.0.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12497,6 +12827,23 @@ "node": ">= 8" } }, + "node_modules/webcrypto-core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz", + "integrity": "sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==", + "dependencies": { + "@peculiar/asn1-schema": "^2.1.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/webcrypto-core/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -12676,8 +13023,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -14781,6 +15127,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.3.1.tgz", "integrity": "sha512-AnYUci7EGyA8flml881lDvVDl6n/u6GQpVIOTsZVO29d4/rPqSJ2KFguDD3mjDL406doTTLNuDI4ndxfJl6fmQ==", + "dev": true, "requires": { "@envelop/types": "2.2.0" } @@ -14789,12 +15136,13 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@envelop/disable-introspection/-/disable-introspection-3.3.1.tgz", "integrity": "sha512-THR8UnRQQB5nCLmITXvebwXwSHvFjS+ThA3RRVXpFX9EupMbTFN5a4NHty7+BYD798c3VrBZ/InbMlEWqw1c9g==", - "requires": {} + "dev": true }, "@envelop/parser-cache": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-4.3.1.tgz", "integrity": "sha512-IqerCVjvVTiGvSZ8qSpdEc55hhiuekufJd0+ldWtyMPznhMaYOzpLifFUhjhhD7004eJM17n9vjJQFa7fIGz8Q==", + "dev": true, "requires": { "tiny-lru": "7.0.6" } @@ -14803,12 +15151,13 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@envelop/types/-/types-2.2.0.tgz", "integrity": "sha512-Lghvfs0kh53G5mUKpCMlB/FhHh3O8SSR4hewB7JyE9hOEu/9h/6u+GHH/OEgdaRHky1Sae5Jf4grO+h21ka4ig==", - "requires": {} + "dev": true }, "@envelop/validation-cache": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-4.3.1.tgz", "integrity": "sha512-lmtu9idhdWqbYkcFoFsL1ED4y38DLvj6EDEwE9tULXWuZm4WWmlNQAmKHAwB1d3kGVQAMtxM59crkOOJGRBgHQ==", + "dev": true, "requires": { "tiny-lru": "7.0.6" } @@ -15022,6 +15371,38 @@ "value-or-promise": "1.0.11" } }, + "@graphql-tools/executor": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.12.tgz", + "integrity": "sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==", + "requires": { + "@graphql-tools/utils": "9.1.4", + "@graphql-typed-document-node/core": "3.1.1", + "@repeaterjs/repeater": "3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "dependencies": { + "@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "requires": { + "tslib": "^2.4.0" + } + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==" + } + } + }, "@graphql-tools/git-loader": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.1.12.tgz", @@ -15421,8 +15802,7 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -15450,13 +15830,13 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@graphql-yoga/common": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/common/-/common-2.3.0.tgz", "integrity": "sha512-EPKK97953c8E1FiaLHMMGqLKtoAN5H9qHr0AiAzMlruJHn0JcbMf2qFTUXklCsuk6UEwNtxeHX42zim11O/E1g==", + "dev": true, "requires": { "@envelop/core": "^2.0.0", "@envelop/disable-introspection": "^3.0.0", @@ -15475,6 +15855,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/node/-/node-2.3.0.tgz", "integrity": "sha512-uofEmKIDYthJuCcdhbgU0VW5i2cCqZVKIiEW/xbwvCOBJt439k46D+M6youiQYJ1miaA/m0btbuZ1sAo/TLjdQ==", + "dev": true, "requires": { "@envelop/core": "^2.0.0", "@graphql-tools/utils": "^8.6.0", @@ -15488,6 +15869,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-2.0.0.tgz", "integrity": "sha512-HlG+gIddjIP3/BDrMZymdzmzDjNdYuSGMxx6+1JA83gAEVRDR4yOoT4QeNKYqRhLK9xca/Hxp1PfBpquSa244Q==", + "dev": true, + "requires": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.3.1" + } + }, + "@graphql-yoga/typed-event-target": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-1.0.0.tgz", + "integrity": "sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==", "requires": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.3.1" @@ -15762,8 +16153,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz", "integrity": "sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg==", - "dev": true, - "requires": {} + "dev": true }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -15788,6 +16178,50 @@ "fastq": "^1.6.0" } }, + "@peculiar/asn1-schema": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", + "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", + "requires": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@peculiar/webcrypto": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", + "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", + "requires": { + "@peculiar/asn1-schema": "^2.3.0", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.1", + "webcrypto-core": "^1.7.4" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "@prisma/client": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.12.0.tgz", @@ -15800,7 +16234,7 @@ "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz", "integrity": "sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==", - "devOptional": true + "dev": true }, "@prisma/engines-version": { "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", @@ -16144,6 +16578,36 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@whatwg-node/events": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", + "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==" + }, + "@whatwg-node/fetch": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.6.2.tgz", + "integrity": "sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==", + "requires": { + "@peculiar/webcrypto": "^1.4.0", + "abort-controller": "^3.0.0", + "busboy": "^1.6.0", + "form-data-encoder": "^1.7.1", + "formdata-node": "^4.3.1", + "node-fetch": "^2.6.7", + "undici": "^5.12.0", + "urlpattern-polyfill": "^6.0.2", + "web-streams-polyfill": "^3.2.0" + } + }, + "@whatwg-node/server": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.5.8.tgz", + "integrity": "sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==", + "requires": { + "@whatwg-node/fetch": "0.6.2", + "tslib": "^2.3.1" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -16353,6 +16817,23 @@ "safer-buffer": "~2.1.0" } }, + "asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "requires": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -16699,6 +17180,14 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -17195,6 +17684,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.2.5.tgz", "integrity": "sha512-6IR+JN6o2UMNj2f3fu0ZVkZeP0h22DRKzq78SiMenkqyBYyGIT1AkZjHkItvh0A80LdsAlWENHUpvapapePucw==", + "dev": true, "requires": { "abort-controller": "^3.0.0", "form-data-encoder": "^1.7.1", @@ -18138,8 +18628,7 @@ "version": "0.0.23", "resolved": "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.23.tgz", "integrity": "sha512-3Ivlyfjaw3BWmGtUSnMpP/a4dcXCp0mJtj0PiPG14OKUizaMKlSEX+LX2Qed0LrxwniIwvU6B4w/koVjEPyWJg==", - "dev": true, - "requires": {} + "dev": true }, "graphql-request": { "version": "4.2.0", @@ -18184,8 +18673,111 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.7.0.tgz", "integrity": "sha512-8yYuvnyqIjlJ/WfebOyu2GSOQeFauRxnfuTveY9yvrDGs2g3kR9Nv4gu40AKvRHbXlSJwTbMJ6dVxAtEyKwVRA==", - "dev": true, - "requires": {} + "dev": true + }, + "graphql-yoga": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-3.4.0.tgz", + "integrity": "sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==", + "requires": { + "@envelop/core": "3.0.4", + "@envelop/parser-cache": "^5.0.4", + "@envelop/validation-cache": "^5.0.5", + "@graphql-tools/executor": "0.0.12", + "@graphql-tools/schema": "^9.0.0", + "@graphql-tools/utils": "^9.0.1", + "@graphql-yoga/subscription": "^3.1.0", + "@whatwg-node/fetch": "0.6.2", + "@whatwg-node/server": "0.5.8", + "dset": "^3.1.1", + "tslib": "^2.3.1" + }, + "dependencies": { + "@envelop/core": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.4.tgz", + "integrity": "sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==", + "requires": { + "@envelop/types": "3.0.1", + "tslib": "2.4.0" + } + }, + "@envelop/parser-cache": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-5.0.4.tgz", + "integrity": "sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==", + "requires": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + } + }, + "@envelop/types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-3.0.1.tgz", + "integrity": "sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@envelop/validation-cache": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-5.0.5.tgz", + "integrity": "sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==", + "requires": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/merge": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.16.tgz", + "integrity": "sha512-In0kcOZcPIpYOKaqdrJ3thdLPE7TutFnL9tbrHUy2zCinR2O/blpRC48jPckcs0HHrUQ0pGT4HqvzMkZUeEBAw==", + "requires": { + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.14.tgz", + "integrity": "sha512-U6k+HY3Git+dsOEhq+dtWQwYg2CAgue8qBvnBXoKu5eEeH284wymMUoNm0e4IycOgMCJANVhClGEBIkLRu3FQQ==", + "requires": { + "@graphql-tools/merge": "8.3.16", + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + } + }, + "@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@graphql-yoga/subscription": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-3.1.0.tgz", + "integrity": "sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==", + "requires": { + "@graphql-yoga/typed-event-target": "^1.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "0.0.2", + "tslib": "^2.3.1" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==" + } + } }, "har-schema": { "version": "2.0.0", @@ -18770,8 +19362,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "dev": true, - "requires": {} + "dev": true }, "isstream": { "version": "0.1.2", @@ -19163,8 +19754,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.0.6", @@ -20110,7 +20700,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -20178,8 +20767,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.0.tgz", "integrity": "sha512-3QRZIS707pZQnijHdhbttXRWwrHhZJ/gzolneoxKVz9N/xmsvY/7Ls8lpnI9gxbgxjcHsAVEW3mgwiZCo6kkJQ==", - "dev": true, - "requires": {} + "dev": true }, "methods": { "version": "1.1.2", @@ -20947,7 +21535,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.12.0.tgz", "integrity": "sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==", - "devOptional": true, + "dev": true, "requires": { "@prisma/engines": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" } @@ -21022,6 +21610,26 @@ "escape-goat": "^2.0.0" } }, + "pvtsutils": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", + "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", + "requires": { + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -21642,6 +22250,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -21919,7 +22532,8 @@ "tiny-lru": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==" + "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", + "dev": true }, "title-case": { "version": "3.0.3", @@ -22157,9 +22771,12 @@ "dev": true }, "undici": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", - "integrity": "sha512-VhUpiZ3No1DOPPQVQnsDZyfcbTTcHdcgWej1PdFnSvOeJmOVDgiOHkunJmBLfmjt4CqgPQddPVjSWW0dsTs5Yg==" + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.16.0.tgz", + "integrity": "sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==", + "requires": { + "busboy": "^1.6.0" + } }, "unique-string": { "version": "2.0.0", @@ -22268,6 +22885,14 @@ "prepend-http": "^2.0.0" } }, + "urlpattern-polyfill": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", + "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", + "requires": { + "braces": "^3.0.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -22385,6 +23010,25 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, + "webcrypto-core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz", + "integrity": "sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==", + "requires": { + "@peculiar/asn1-schema": "^2.1.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -22493,8 +23137,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "dev": true, - "requires": {} + "dev": true }, "xdg-basedir": { "version": "4.0.0", @@ -22523,8 +23166,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/server/package.json b/server/package.json index ae3faad..6e669d0 100644 --- a/server/package.json +++ b/server/package.json @@ -22,10 +22,10 @@ "dependencies": { "@graphql-tools/graphql-file-loader": "^7.3.10", "@graphql-tools/load": "^7.5.9", - "@graphql-yoga/node": "^2.3.0", "@prisma/client": "^3.12.0", "express": "^4.17.1", "graphql": "^16.3.0", + "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", "sqlite3": "^5.0.2" diff --git a/server/schema.graphql b/server/schema.graphql index bd6753b..d7fe0e6 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -1,11 +1,11 @@ type Query { - userInfo: UserInfo! - listAllMembers: ListAllMembersResponse! - showMemberDetail(id: ID!): ShowMemberDetailResponse! + userInfo: UserInfo + listAllMembers: ListAllMembersResponse + showMemberDetail(id: ID!): ShowMemberDetailResponse } type Mutation { - editMemberDetail(input: EditMemberDetailInput!): EditMemberDetailResponse! + editMemberDetail(input: EditMemberDetailInput!): EditMemberDetailResponse } input EditMemberDetailInput { diff --git a/server/src/shared/infra/http/__tests__/resolvers.test.ts b/server/src/shared/infra/http/__tests__/resolvers.test.ts new file mode 100644 index 0000000..cbbd577 --- /dev/null +++ b/server/src/shared/infra/http/__tests__/resolvers.test.ts @@ -0,0 +1,86 @@ +import { createGraphQLServer } from '@/shared/infra/http/app'; +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { Result } from '@/shared/core/result'; + +jest.mock( + '@/modules/members/useCases/query/listAllMembers/listAllMembersService' +); +jest.mock( + '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService' +); +jest.mock( + '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService' +); +jest.mock( + '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService' +); + +// const MockedListAllMembersService = ListAllMembersService as jest.MockedClass< +// typeof ListAllMembersService +// >; +const MockedListAllMembersService = ListAllMembersService as jest.Mock; +const MockedShowMemberDetailService = ShowMemberDetailService as jest.Mock; +const MockedEditMemberDetailService = EditMemberDetailService as jest.Mock; +const MockedGetLoggedInUserInfoService = + GetLoggedInUserInfoService as jest.Mock; + +describe('resolvers', () => { + beforeEach(() => { + MockedListAllMembersService.mockClear(); + MockedShowMemberDetailService.mockClear(); + MockedEditMemberDetailService.mockClear(); + MockedGetLoggedInUserInfoService.mockClear(); + }); + + test('test', async () => { + // MockedGetLoggedInUserInfoService.mockImplementation(() => { + // return { + // prisma: null, + // authorizer: null, + // execute: async () => Result.ok({ username: 'test', userMenu: [] }), + // }; + // }); + + const mock = + new MockedGetLoggedInUserInfoService() as jest.Mocked; + mock.execute.mockResolvedValue( + Result.ok({ username: 'test', userMenu: [] }) + ); + + const server = await createGraphQLServer({ + getLoggedInUserInfoService: mock, + listAllMembersService: new MockedListAllMembersService(), + showMemberDetailService: new MockedShowMemberDetailService(), + editMemberDetailService: new MockedEditMemberDetailService(), + }); + + const response = await server.fetch('/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: ` + query { + userInfo { + username + userMenu { + name + } + } + }`, + }), + }); + + console.log('response', response); + expect(response.status).toBe(200); + const executionResult = await response.json(); + console.log('executionResult', executionResult); + // expect(executionResult).toEqual({ + // data: { + // getLoggedInUserInfo: { + }); +}); diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts index a134d1d..60c56fa 100644 --- a/server/src/shared/infra/http/app.ts +++ b/server/src/shared/infra/http/app.ts @@ -6,8 +6,8 @@ import { PrismaClient } from '@prisma/client'; import { loadSchema } from '@graphql-tools/load'; import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { createContext } from './context'; -import { createServer } from '@graphql-yoga/node'; -import { createResolvers } from './resolvers'; +import { createYoga, createSchema } from 'graphql-yoga'; +import { createResolvers, Dependencies } from './resolvers'; import { Authorizer } from '@/modules/auth/shared/authorizer'; import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; @@ -63,24 +63,11 @@ export async function startServer() { const app: Application = express(); app.use(express.json()); - // // Build apollo-server-based graphql endpoint (trial) - const schema = await loadSchema('schema.graphql', { - loaders: [new GraphQLFileLoader()], - }); - const useCases = createUseCases({ authorizer, prisma, }); - const resolvers = createResolvers(useCases); - const graphQLServer = createServer({ - schema: { - typeDefs: schema, - resolvers: resolvers, - }, - context: createContext(), - plugins: [], - }); + const graphQLServer = await createGraphQLServer(useCases); app.use('/graphql', createCheckLoggedInMiddleware(authorizer)); @@ -91,3 +78,21 @@ export async function startServer() { console.log(`App is listening on port ${port} !`); }); } + +export async function createGraphQLServer(deps: Dependencies) { + const schema = await loadSchema('schema.graphql', { + loaders: [new GraphQLFileLoader()], + }); + + const resolvers = createResolvers(deps); + const graphQLServer = createYoga({ + schema: createSchema({ + typeDefs: schema, + resolvers: resolvers, + }), + context: createContext(), + plugins: [], + }); + + return graphQLServer; +} diff --git a/server/src/shared/infra/http/generated/resolver-types.ts b/server/src/shared/infra/http/generated/resolver-types.ts index 66de5b2..bdc9917 100644 --- a/server/src/shared/infra/http/generated/resolver-types.ts +++ b/server/src/shared/infra/http/generated/resolver-types.ts @@ -1,11 +1,11 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; -import { ListAllMembersResponse as ListAllMembersResponseModel } from '@/members/list-all-members/listAllMembersService'; -import { ShowMemberDetailResponse as ShowMemberDetailResponseModel } from '@/members/show-member-detail/showMemberDetailService'; -import { GetLoggedInUserInfoResponse as GetLoggedInUserInfoResponseModel, UserMenuItem as UserMenuItemModel } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; -import { DisplayableMember as DisplayableMemberModel } from '@/members/shared/member'; -import { Department as DepartmentModel } from '@/members/shared/department'; -import { EditMemberDetailRequest as EditMemberDetailRequestModel, EditMemberDetailResponse as EditMemberDetailResponseModel } from '@/members/edit-member-detail/editMemberDetailService'; -import { Context } from '@/context'; +import { ListAllMembersResponse as ListAllMembersResponseModel } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailResponse as ShowMemberDetailResponseModel } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { GetLoggedInUserInfoResponse as GetLoggedInUserInfoResponseModel, UserMenuItem as UserMenuItemModel } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { DisplayableMember as DisplayableMemberModel } from '@/modules/members/dtos/displayableMemberDTO'; +import { DepartmentDTO as DepartmentDTOModel } from '@/modules/members/dtos/departmentDTO'; +import { EditMemberDetailRequest as EditMemberDetailRequestModel, EditMemberDetailResponse as EditMemberDetailResponseModel } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { Context } from '@/shared/infra/http/context'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -70,7 +70,7 @@ export type Member = { export type Mutation = { __typename?: 'Mutation'; - editMemberDetail: EditMemberDetailResponse; + editMemberDetail?: Maybe; }; @@ -80,9 +80,9 @@ export type MutationEditMemberDetailArgs = { export type Query = { __typename?: 'Query'; - listAllMembers: ListAllMembersResponse; - showMemberDetail: ShowMemberDetailResponse; - userInfo: UserInfo; + listAllMembers?: Maybe; + showMemberDetail?: Maybe; + userInfo?: Maybe; }; @@ -178,7 +178,7 @@ export type DirectiveResolverFn; DateTime: ResolverTypeWrapper; - Department: ResolverTypeWrapper; + Department: ResolverTypeWrapper; EditMemberDetailInput: ResolverTypeWrapper; EditMemberDetailResponse: ResolverTypeWrapper; ID: ResolverTypeWrapper; @@ -197,7 +197,7 @@ export type ResolversTypes = { export type ResolversParentTypes = { Boolean: Scalars['Boolean']; DateTime: Scalars['DateTime']; - Department: DepartmentModel; + Department: DepartmentDTOModel; EditMemberDetailInput: EditMemberDetailRequestModel; EditMemberDetailResponse: EditMemberDetailResponseModel; ID: Scalars['ID']; @@ -251,13 +251,13 @@ export type MemberResolvers = { - editMemberDetail?: Resolver>; + editMemberDetail?: Resolver, ParentType, ContextType, RequireFields>; }; export type QueryResolvers = { - listAllMembers?: Resolver; - showMemberDetail?: Resolver>; - userInfo?: Resolver; + listAllMembers?: Resolver, ParentType, ContextType>; + showMemberDetail?: Resolver, ParentType, ContextType, RequireFields>; + userInfo?: Resolver, ParentType, ContextType>; }; export type ShowMemberDetailResponseResolvers = { diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts index f794773..4fe586f 100644 --- a/server/src/shared/infra/http/resolvers.ts +++ b/server/src/shared/infra/http/resolvers.ts @@ -4,7 +4,7 @@ import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMe import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; import { Resolvers } from './generated/resolver-types'; -type Dependencies = { +export type Dependencies = { getLoggedInUserInfoService: GetLoggedInUserInfoService; listAllMembersService: ListAllMembersService; showMemberDetailService: ShowMemberDetailService; @@ -24,7 +24,7 @@ export const createResolvers = ({ if (resultOrError.isFailure) { console.error(resultOrError.error); // TODO: error handling - return; + return null; } return resultOrError.getValue(); }, @@ -33,7 +33,7 @@ export const createResolvers = ({ if (resultOrError.isFailure) { console.error(resultOrError.error); // TODO: error handling - return; + return null; } return resultOrError.getValue(); }, @@ -44,7 +44,7 @@ export const createResolvers = ({ if (resultOrError.isFailure) { console.error(resultOrError.error); // TODO: - return; + return null; } return resultOrError.getValue(); }, @@ -71,7 +71,7 @@ export const createResolvers = ({ if (resultOrError.isFailure) { console.error(resultOrError.error); // TODO: error handling - return; + return null; } return resultOrError.getValue(); From 4a4c6deb1847ddc6ce495ab4bc7e561d750bfd0a Mon Sep 17 00:00:00 2001 From: Ken Fukuyama Date: Wed, 1 Feb 2023 17:28:44 +0900 Subject: [PATCH 12/12] test: add graphql and usecase tests --- server/package-lock.json | 47 ++- server/package.json | 4 +- .../editMemberDetailService.ts | 14 +- .../showMemberDetailService.ts | 4 +- .../infra/http/__tests__/resolvers.test.ts | 315 ++++++++++++++---- server/src/shared/infra/http/resolvers.ts | 25 +- server/tests/factories/member.ts | 25 ++ server/tests/helpers/dependencies.ts | 57 ++++ server/tests/helpers/graphql.ts | 36 ++ 9 files changed, 441 insertions(+), 86 deletions(-) create mode 100644 server/tests/factories/member.ts create mode 100644 server/tests/helpers/dependencies.ts create mode 100644 server/tests/helpers/graphql.ts diff --git a/server/package-lock.json b/server/package-lock.json index 1e54971..39855a6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -18,7 +18,8 @@ "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^5.0.2" + "sqlite3": "^5.0.2", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^6.2.0", @@ -32,6 +33,7 @@ "@types/node": "^16.11.27", "@types/supertest": "^2.0.11", "@types/yargs": "^17.0.4", + "fishery": "^2.2.2", "jest": "^27.3.1", "nodemon": "^2.0.14", "prisma": "^3.12.0", @@ -6740,6 +6742,15 @@ "node": ">=8" } }, + "node_modules/fishery": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", + "integrity": "sha512-jeU0nDhPHJkupmjX+r9niKgVMTBDB8X+U/pktoGHAiWOSyNlMd0HhmqnjrpjUOCDPJYaSSu4Ze16h6dZOKSp2w==", + "dev": true, + "dependencies": { + "lodash.mergewith": "^4.6.2" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -9608,6 +9619,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -13086,6 +13103,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -18317,6 +18342,15 @@ "path-exists": "^4.0.0" } }, + "fishery": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", + "integrity": "sha512-jeU0nDhPHJkupmjX+r9niKgVMTBDB8X+U/pktoGHAiWOSyNlMd0HhmqnjrpjUOCDPJYaSSu4Ze16h6dZOKSp2w==", + "dev": true, + "requires": { + "lodash.mergewith": "^4.6.2" + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -20563,6 +20597,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -23211,6 +23251,11 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==" } } } diff --git a/server/package.json b/server/package.json index 6e669d0..508ee35 100644 --- a/server/package.json +++ b/server/package.json @@ -28,7 +28,8 @@ "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^5.0.2" + "sqlite3": "^5.0.2", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^6.2.0", @@ -42,6 +43,7 @@ "@types/node": "^16.11.27", "@types/supertest": "^2.0.11", "@types/yargs": "^17.0.4", + "fishery": "^2.2.2", "jest": "^27.3.1", "nodemon": "^2.0.14", "prisma": "^3.12.0", diff --git a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts index caf6271..471524a 100644 --- a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts @@ -8,10 +8,22 @@ import { EditMemberDetailRepository, UpdatePayload, } from './editMemberDetailRepository'; +import z from 'zod'; + +export const editMemberDetailPayloadSchema = z.object({ + firstName: z.string().optional(), + lastName: z.string().optional(), + age: z.number().optional(), + salary: z.number().optional(), + departmentId: z.string().uuid().optional(), + phoneNumber: z.string().optional(), + email: z.string().email().optional(), + pr: z.string().optional(), +}); export type EditMemberDetailRequest = { memberId: string; - payload: UpdatePayload; + payload: z.infer; }; export type EditMemberDetailResponse = { result: boolean; diff --git a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts index e2c31d2..a70b06d 100644 --- a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts +++ b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts @@ -54,7 +54,7 @@ export class ShowMemberDetailService return Result.fail(allowedActionsOrError.error); } - const displayableMemberOrError: DisplayableMember = { + const displayableMember: DisplayableMember = { ...memberDto.createObjectWithAuthorizedFields( authorizedFieldsOrError.getValue() ), @@ -79,7 +79,7 @@ export class ShowMemberDetailService return Result.ok({ editableFields: editableFields, - member: displayableMemberOrError, + member: displayableMember, }); } } diff --git a/server/src/shared/infra/http/__tests__/resolvers.test.ts b/server/src/shared/infra/http/__tests__/resolvers.test.ts index cbbd577..9a154de 100644 --- a/server/src/shared/infra/http/__tests__/resolvers.test.ts +++ b/server/src/shared/infra/http/__tests__/resolvers.test.ts @@ -1,69 +1,45 @@ -import { createGraphQLServer } from '@/shared/infra/http/app'; -import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; -import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; -import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; -import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { + GetLoggedInUserInfoResponse, + GetLoggedInUserInfoService, +} from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; import { Result } from '@/shared/core/result'; - -jest.mock( - '@/modules/members/useCases/query/listAllMembers/listAllMembersService' -); -jest.mock( - '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService' -); -jest.mock( - '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService' -); -jest.mock( - '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService' -); - -// const MockedListAllMembersService = ListAllMembersService as jest.MockedClass< -// typeof ListAllMembersService -// >; -const MockedListAllMembersService = ListAllMembersService as jest.Mock; -const MockedShowMemberDetailService = ShowMemberDetailService as jest.Mock; -const MockedEditMemberDetailService = EditMemberDetailService as jest.Mock; -const MockedGetLoggedInUserInfoService = - GetLoggedInUserInfoService as jest.Mock; +import { + EditMemberDetailInput, + Mutation, + Query, +} from '@/shared/infra/http/generated/resolver-types'; +import { runQuery } from '@/../tests/helpers/graphql'; +import { MockedDependencies } from '@/../tests/helpers/dependencies'; +import { ListAllMembersResponse } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { displayableMemberFactory } from '@/../tests/factories/member'; +import { ShowMemberDetailResponse } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import faker from '@faker-js/faker'; describe('resolvers', () => { + let mockContainer: MockedDependencies; + beforeEach(() => { - MockedListAllMembersService.mockClear(); - MockedShowMemberDetailService.mockClear(); - MockedEditMemberDetailService.mockClear(); - MockedGetLoggedInUserInfoService.mockClear(); + mockContainer = new MockedDependencies(); + mockContainer.clear(); }); - test('test', async () => { - // MockedGetLoggedInUserInfoService.mockImplementation(() => { - // return { - // prisma: null, - // authorizer: null, - // execute: async () => Result.ok({ username: 'test', userMenu: [] }), - // }; - // }); - - const mock = - new MockedGetLoggedInUserInfoService() as jest.Mocked; - mock.execute.mockResolvedValue( - Result.ok({ username: 'test', userMenu: [] }) - ); - - const server = await createGraphQLServer({ - getLoggedInUserInfoService: mock, - listAllMembersService: new MockedListAllMembersService(), - showMemberDetailService: new MockedShowMemberDetailService(), - editMemberDetailService: new MockedEditMemberDetailService(), - }); + describe('Query', () => { + describe('userInfo', () => { + it('should call GetLoggedInUserInfoService', async () => { + const { dependencies } = mockContainer; - const response = await server.fetch('/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: ` + const expectedResponse: GetLoggedInUserInfoResponse = { + username: 'test', + userMenu: [], + }; + + dependencies.getLoggedInUserInfoService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` query { userInfo { username @@ -71,16 +47,219 @@ describe('resolvers', () => { name } } - }`, - }), + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + userInfo: expectedResponse, + }); + }); + }); + + describe('listAllMembers', () => { + it('should call ListAllMembersService', async () => { + const { dependencies } = mockContainer; + + const expectedResponse: ListAllMembersResponse = { + members: displayableMemberFactory.buildList(2), + }; + + dependencies.listAllMembersService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` + query { + listAllMembers { + members { + id + avatar + firstName + lastName + age + salary + department { + id + name + managerMemberId + } + joinedAt + phoneNumber + email + pr + editable + isLoggedInUser + } + } + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + listAllMembers: { + members: expectedResponse.members.map((member) => ({ + ...member, + joinedAt: member.joinedAt?.toISOString(), + })), + }, + }); + }); + }); + + describe('showMemberDetail', () => { + it('should call ShowMemberDetailService', async () => { + const { dependencies } = mockContainer; + + const expectedResponse: ShowMemberDetailResponse = { + member: displayableMemberFactory.build(), + editableFields: [], + }; + + dependencies.showMemberDetailService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` + query { + showMemberDetail(id: "1") { + member { + id + avatar + firstName + lastName + age + salary + department { + id + name + managerMemberId + } + joinedAt + phoneNumber + email + pr + editable + isLoggedInUser + } + } + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + showMemberDetail: { + member: { + ...expectedResponse.member, + joinedAt: expectedResponse.member.joinedAt?.toISOString(), + }, + }, + }); + }); }); + }); + + describe('Mutation', () => { + describe('editMemberDetail', () => { + it('should call editMemberDetailService with valid payload', async () => { + // Arrange + const { dependencies } = mockContainer; + dependencies.editMemberDetailService.execute.mockResolvedValue( + Result.ok({ result: true }) + ); - console.log('response', response); - expect(response.status).toBe(200); - const executionResult = await response.json(); - console.log('executionResult', executionResult); - // expect(executionResult).toEqual({ - // data: { - // getLoggedInUserInfo: { + // Act + const response = await runQuery( + { + query: ` + mutation ($input: EditMemberDetailInput!) { + editMemberDetail(input: $input) { + result + } + }`, + variables: { input: createEditMemberDetailInput() }, + }, + { + dependencies: dependencies, + } + ); + + // Assert + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + editMemberDetail: { + result: true, + }, + }); + }); + + it('should call editMemberDetailService with invalid payload', async () => { + // Arrange + const { dependencies } = mockContainer; + dependencies.editMemberDetailService.execute.mockResolvedValue( + Result.fail(new Error('invalid payload')) + ); + + // Act + const response = await runQuery( + { + query: ` + mutation ($input: EditMemberDetailInput!) { + editMemberDetail(input: $input) { + result + } + }`, + variables: { input: createEditMemberDetailInput() }, + }, + { + dependencies: dependencies, + } + ); + + // Assert + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + editMemberDetail: null, + }); + }); + }); }); }); + +function createEditMemberDetailInput(): EditMemberDetailInput { + return { + id: faker.datatype.uuid(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + age: faker.datatype.number(), + salary: faker.datatype.number(), + departmentId: faker.datatype.uuid(), + phoneNumber: faker.phone.phoneNumber(), + email: faker.internet.email(), + pr: faker.lorem.paragraph(), + }; +} diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts index 4fe586f..797a8ef 100644 --- a/server/src/shared/infra/http/resolvers.ts +++ b/server/src/shared/infra/http/resolvers.ts @@ -1,4 +1,7 @@ -import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { + editMemberDetailPayloadSchema, + EditMemberDetailService, +} from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; @@ -53,20 +56,16 @@ export const createResolvers = ({ editMemberDetail: async (_, { input }) => { const { id: memberId, ...payload } = input; + const parsedPayload = editMemberDetailPayloadSchema.safeParse(payload); + if (!parsedPayload.success) { + // TODO: error handling + console.error(parsedPayload.error); + return null; + } + const resultOrError = await editMemberDetailService.execute({ memberId, - payload: { - ...(payload.age && { age: payload.age }), - ...(payload.departmentId && { - departmentId: payload.departmentId, - }), - ...(payload.email && { email: payload.email }), - ...(payload.firstName && { firstName: payload.firstName }), - ...(payload.lastName && { lastName: payload.lastName }), - ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), - ...(payload.pr && { pr: payload.pr }), - ...(payload.salary && { salary: payload.salary }), - }, + payload: parsedPayload.data, }); if (resultOrError.isFailure) { console.error(resultOrError.error); diff --git a/server/tests/factories/member.ts b/server/tests/factories/member.ts new file mode 100644 index 0000000..503ff1b --- /dev/null +++ b/server/tests/factories/member.ts @@ -0,0 +1,25 @@ +import { DisplayableMember } from '@/modules/members/dtos/displayableMemberDTO'; +import { Factory } from 'fishery'; +import faker from '@faker-js/faker'; + +export const displayableMemberFactory = Factory.define( + () => ({ + id: faker.datatype.uuid(), + avatar: faker.image.avatar(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + age: faker.datatype.number({ min: 25, max: 60 }), + salary: faker.datatype.number({ min: 40000, max: 90000 }), + joinedAt: faker.date.past(), + phoneNumber: faker.phone.phoneNumber('###-####-####'), + email: faker.internet.exampleEmail(), + pr: faker.lorem.sentence(), + department: { + id: faker.datatype.uuid(), + name: faker.name.jobArea(), + managerMemberId: faker.datatype.uuid(), + }, + editable: faker.datatype.boolean(), + isLoggedInUser: faker.datatype.boolean(), + }) +); diff --git a/server/tests/helpers/dependencies.ts b/server/tests/helpers/dependencies.ts new file mode 100644 index 0000000..324313d --- /dev/null +++ b/server/tests/helpers/dependencies.ts @@ -0,0 +1,57 @@ +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; + +jest.mock( + '@/modules/members/useCases/query/listAllMembers/listAllMembersService' +); +jest.mock( + '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService' +); +jest.mock( + '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService' +); +jest.mock( + '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService' +); + +const MockedListAllMembersService = ListAllMembersService as jest.Mock; +const MockedShowMemberDetailService = ShowMemberDetailService as jest.Mock; +const MockedEditMemberDetailService = EditMemberDetailService as jest.Mock; +const MockedGetLoggedInUserInfoService = + GetLoggedInUserInfoService as jest.Mock; + +export class MockedDependencies { + private readonly getLoggedInUserInfoService: jest.Mocked; + private readonly listAllMembersService: jest.Mocked; + private readonly showMemberDetailService: jest.Mocked; + private readonly editMemberDetailService: jest.Mocked; + + constructor() { + this.getLoggedInUserInfoService = + new MockedGetLoggedInUserInfoService() as jest.Mocked; + this.listAllMembersService = + new MockedListAllMembersService() as jest.Mocked; + this.showMemberDetailService = + new MockedShowMemberDetailService() as jest.Mocked; + this.editMemberDetailService = + new MockedEditMemberDetailService() as jest.Mocked; + } + + get dependencies() { + return { + getLoggedInUserInfoService: this.getLoggedInUserInfoService, + listAllMembersService: this.listAllMembersService, + showMemberDetailService: this.showMemberDetailService, + editMemberDetailService: this.editMemberDetailService, + }; + } + + clear() { + MockedGetLoggedInUserInfoService.mockClear(); + MockedListAllMembersService.mockClear(); + MockedShowMemberDetailService.mockClear(); + MockedEditMemberDetailService.mockClear(); + } +} diff --git a/server/tests/helpers/graphql.ts b/server/tests/helpers/graphql.ts new file mode 100644 index 0000000..367cbd2 --- /dev/null +++ b/server/tests/helpers/graphql.ts @@ -0,0 +1,36 @@ +import { createGraphQLServer } from '@/shared/infra/http/app'; +import { Dependencies } from '@/shared/infra/http/resolvers'; +import { ExecutionResult } from 'graphql'; + +interface RunQueryBody { + query: string; + variables?: Record; +} + +interface RunQueryOptions { + dependencies: Dependencies; +} + +interface RunQueryResponse extends Response { + json(): Promise; +} + +export async function runQuery( + body: RunQueryBody, + opts: RunQueryOptions +): Promise>> { + const server = await createGraphQLServer(opts.dependencies); + + const response = await server.fetch('/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: body.query, + variables: body.variables, + }), + }); + + return response; +}