From 1d40d7e15f87e67d5b827edfe7b4403ea06dd7de Mon Sep 17 00:00:00 2001 From: lnn0q Date: Sat, 17 May 2025 21:41:07 +0300 Subject: [PATCH] added resolvers, updated model, updated navbar --- app/client/components/BookSearchList.tsx | 84 +++++++++++++++++ app/client/components/IconBadge.tsx | 27 ++++++ app/client/components/WorkingList.tsx | 24 +++-- app/client/components/WorkingListCard.tsx | 29 +----- app/client/components/WorkingListSearch.tsx | 18 ++-- .../components/WorkingProgressStatusBadge.tsx | 2 +- app/client/components/WorkingRatingBadge.tsx | 2 +- app/client/helpers.tsx | 6 +- app/client/pages/__root.tsx | 30 ++++++- app/client/pages/index.tsx | 90 +------------------ app/client/pages/new/book.tsx | 66 ++++++++++++++ app/client/routeTree.gen.ts | 52 +++++------ app/server/graphql/graphql.ts | 11 ++- .../graphql/resolvers/bookChapterMany.ts | 25 ++++++ .../graphql/resolvers/bookChapterOne.ts | 17 ++++ .../graphql/resolvers/bookManyResolver.ts | 29 ++++++ .../graphql/resolvers/bookOneResolver.ts | 17 ++++ .../resolvers/mappers/bookChapterMapper.ts | 2 +- .../graphql/resolvers/mappers/bookMapper.ts | 46 +++++++++- .../graphql/resolvers/mappers/reviewMapper.ts | 2 +- .../resolvers/mappers/workingMapper.ts | 65 -------------- app/server/graphql/schema.graphql | 41 +++------ app/server/prisma/schema.prisma | 55 ++++-------- 23 files changed, 439 insertions(+), 301 deletions(-) create mode 100644 app/client/components/BookSearchList.tsx create mode 100644 app/client/components/IconBadge.tsx create mode 100644 app/client/pages/new/book.tsx create mode 100644 app/server/graphql/resolvers/bookChapterMany.ts create mode 100644 app/server/graphql/resolvers/bookChapterOne.ts create mode 100644 app/server/graphql/resolvers/bookManyResolver.ts create mode 100644 app/server/graphql/resolvers/bookOneResolver.ts delete mode 100644 app/server/graphql/resolvers/mappers/workingMapper.ts diff --git a/app/client/components/BookSearchList.tsx b/app/client/components/BookSearchList.tsx new file mode 100644 index 0000000..d219200 --- /dev/null +++ b/app/client/components/BookSearchList.tsx @@ -0,0 +1,84 @@ +import { gql } from '@apollo/client'; +import { Skeleton, Stack } from '@chakra-ui/react'; +import { BooksListQuery } from '@generated/types'; +import { useState } from 'react'; +import { Pagination } from '../components/Pagination'; +import { WorkingList } from '../components/WorkingList'; +import { WorkingListSearch } from '../components/WorkingListSearch'; +import { usePaginatedQuery } from '../hooks/usePaginatedQuery'; + +export function BookSearchList() { + const [workingSearchValue, setWorkingSearchValue] = useState(''); + const [workingsListPage, setWorkingsListPage] = useState(1); + const [workingsListPageSize, setWorkingsListPageSize] = useState(2); + const startIndex = (workingsListPage - 1) * workingsListPageSize; + + const { data } = usePaginatedQuery({ + query, + input: { + filter: { + terms: workingSearchValue, + offset: startIndex, + size: workingsListPageSize + } + } + }); + + return ( + <> + {data != null ? ( + <> + { + setWorkingSearchValue(value); + }} + /> + + + + {data.bookMany.items.length != 0 && ( + + )} + + ) : ( + <> + + + + + + + + + )} + + ); +} + +const query = gql` + query BooksList($input: BookManyInput!) { + bookMany(input: $input) { + items { + id + title + description + author { + id + username + } + updatedAt + progressStatus + ratingTotal + reviewsTotal + wordsTotal + chaptersTotal + } + total + } + } +`; diff --git a/app/client/components/IconBadge.tsx b/app/client/components/IconBadge.tsx new file mode 100644 index 0000000..a85caac --- /dev/null +++ b/app/client/components/IconBadge.tsx @@ -0,0 +1,27 @@ +import { Badge, BadgeProps, Box, Icon, Stack } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +export function IconBadge({ + children, + iconClass, + backgroundColor, + size = 'lg' +}: { + children?: ReactNode; + iconClass: string; + backgroundColor?: BadgeProps['backgroundColor']; + size?: 'sm' | 'md' | 'lg' | 'xs'; +}) { + return ( + + + + + + + {children} + + + + ); +} diff --git a/app/client/components/WorkingList.tsx b/app/client/components/WorkingList.tsx index 8b0bfaa..7a181dc 100644 --- a/app/client/components/WorkingList.tsx +++ b/app/client/components/WorkingList.tsx @@ -1,14 +1,28 @@ -import { List } from '@chakra-ui/react'; +import { Card, List, Text } from '@chakra-ui/react'; import { WorkingCardProps, WorkingListCard } from './WorkingListCard'; export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) { return ( - {workingsList.map(workingData => ( - - + {workingsList.length != 0 ? ( + workingsList.map(workingData => ( + + + + )) + ) : ( + + + Nothing is here yet! + - ))} + )} ); } diff --git a/app/client/components/WorkingListCard.tsx b/app/client/components/WorkingListCard.tsx index 7f934d1..b32f95e 100644 --- a/app/client/components/WorkingListCard.tsx +++ b/app/client/components/WorkingListCard.tsx @@ -1,6 +1,6 @@ -import { Badge, BadgeProps, Box, Card, Heading, Icon, Link, Stack, Text } from '@chakra-ui/react'; +import { Badge, Box, Card, Heading, Link, Stack, Text } from '@chakra-ui/react'; import { WorkingProgressStatus } from '@generated/types'; -import { ReactNode } from 'react'; +import { IconBadge } from './IconBadge'; import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge'; import { WorkingRatingBadge } from './WorkingRatingBadge'; @@ -88,28 +88,3 @@ export function WorkingListCard({ ); } - -export function IconBadge({ - children, - iconClass, - backgroundColor, - size = 'lg' -}: { - children?: ReactNode; - iconClass: string; - backgroundColor?: BadgeProps['backgroundColor']; - size?: 'sm' | 'md' | 'lg' | 'xs'; -}) { - return ( - - - - - - - {children} - - - - ); -} diff --git a/app/client/components/WorkingListSearch.tsx b/app/client/components/WorkingListSearch.tsx index c30dfdc..4a85652 100644 --- a/app/client/components/WorkingListSearch.tsx +++ b/app/client/components/WorkingListSearch.tsx @@ -1,19 +1,13 @@ import { Button, Icon, Input, InputGroup, Stack } from '@chakra-ui/react'; +import { useState } from 'react'; -export function WorkingListSearch({ - value, - onChange, - onSubmit -}: { - value: string; - onChange: (value: string) => void; - onSubmit: (value: string) => void; -}) { +export function WorkingListSearch({ onSubmit }: { onSubmit: (value: string) => void }) { + const [searchValue, setSearchValue] = useState(''); return (
{ event.preventDefault(); - onSubmit(value); + onSubmit(searchValue); }} > @@ -26,8 +20,8 @@ export function WorkingListSearch({ } > onChange(event.currentTarget.value)} + value={searchValue} + onChange={event => setSearchValue(event.currentTarget.value)} placeholder="Search works..." /> diff --git a/app/client/components/WorkingProgressStatusBadge.tsx b/app/client/components/WorkingProgressStatusBadge.tsx index 016077e..1666221 100644 --- a/app/client/components/WorkingProgressStatusBadge.tsx +++ b/app/client/components/WorkingProgressStatusBadge.tsx @@ -32,6 +32,6 @@ export function WorkingProgressStatusBadge({ function toWorkingProgressStatusIcon(value: WorkingProgressStatus): string { if (value === WorkingProgressStatus.InProgress) return 'nf nf-fa-clock'; if (value === WorkingProgressStatus.Frozen) return 'nf nf-fa-snowflake'; - if (value === WorkingProgressStatus.Finished) return 'nf nf-fa-circle_check'; + if (value === WorkingProgressStatus.Complete) return 'nf nf-fa-circle_check'; assertUnreachable(value); } diff --git a/app/client/components/WorkingRatingBadge.tsx b/app/client/components/WorkingRatingBadge.tsx index b32c635..42fe43c 100644 --- a/app/client/components/WorkingRatingBadge.tsx +++ b/app/client/components/WorkingRatingBadge.tsx @@ -1,5 +1,5 @@ import { BadgeProps, Text } from '@chakra-ui/react'; -import { IconBadge } from './WorkingListCard'; +import { IconBadge } from './IconBadge'; interface WorkingRatingParams { rating: string; diff --git a/app/client/helpers.tsx b/app/client/helpers.tsx index d9e8ff2..250cd6c 100644 --- a/app/client/helpers.tsx +++ b/app/client/helpers.tsx @@ -7,20 +7,20 @@ export function assertUnreachable(value: never): never { export function toBgColorByProgressStatus(value: WorkingProgressStatus): string { if (value === WorkingProgressStatus.InProgress) return 'yellow.500'; if (value === WorkingProgressStatus.Frozen) return 'blue.500'; - if (value === WorkingProgressStatus.Finished) return 'green.500'; + if (value === WorkingProgressStatus.Complete) return 'green.500'; assertUnreachable(value); } export function toFgColorByProgressStatus(value: WorkingProgressStatus): string { if (value === WorkingProgressStatus.InProgress) return 'fg.warning'; if (value === WorkingProgressStatus.Frozen) return 'fg.info'; - if (value === WorkingProgressStatus.Finished) return 'fg.success'; + if (value === WorkingProgressStatus.Complete) return 'fg.success'; assertUnreachable(value); } export function toWorkingProgressStatusLabel(value: WorkingProgressStatus): string { if (value === WorkingProgressStatus.InProgress) return 'In progress'; if (value === WorkingProgressStatus.Frozen) return 'Frozen'; - if (value === WorkingProgressStatus.Finished) return 'Finished'; + if (value === WorkingProgressStatus.Complete) return 'Complete'; assertUnreachable(value); } diff --git a/app/client/pages/__root.tsx b/app/client/pages/__root.tsx index 74c93cd..375c0d8 100644 --- a/app/client/pages/__root.tsx +++ b/app/client/pages/__root.tsx @@ -1,4 +1,14 @@ -import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react'; +import { + Link as ChakraLink, + Icon, + Input, + InputGroup, + Kbd, + Menu, + Separator, + Stack, + Text +} from '@chakra-ui/react'; import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router'; import React from 'react'; import { Avatar } from '../components/ui/avatar'; @@ -8,6 +18,7 @@ import { Tooltip } from '../components/ui/tooltip'; import { useAuth } from '../hooks/auth'; import { useIdentity } from '../hooks/identity'; import { appName } from '../main'; +// import { Link as ChakraLink } from '../components/ui/link'; const TanStackRouterDevtools = process.env.NODE_ENV === 'production' @@ -33,7 +44,7 @@ export const Route = createRootRouteWithContext()({ return ( - + @@ -51,6 +62,21 @@ export const Route = createRootRouteWithContext()({ {/* */} {/* */} {/* */} + + {identity != null && ( + <> + + + editor + + + + + create + + + + )} diff --git a/app/client/pages/index.tsx b/app/client/pages/index.tsx index 3ac6e6a..d9bed64 100644 --- a/app/client/pages/index.tsx +++ b/app/client/pages/index.tsx @@ -1,105 +1,21 @@ -import { gql } from '@apollo/client'; -import { Skeleton, Stack } from '@chakra-ui/react'; -import { WorkingListQuery } from '@generated/types'; +import { Stack } from '@chakra-ui/react'; import { createFileRoute } from '@tanstack/react-router'; -import { useState } from 'react'; -import { Pagination } from '../components/Pagination'; -import { WorkingList } from '../components/WorkingList'; -import { WorkingListSearch } from '../components/WorkingListSearch'; -import { usePaginatedQuery } from '../hooks/usePaginatedQuery'; +import { BookSearchList } from '../components/BookSearchList'; export const Route = createFileRoute('/')({ component: RouteComponent }); function RouteComponent() { - const [workingSearchValue, setWorkingSearchValue] = useState(''); - const [workingsListPage, setWorkingsListPage] = useState(1); - const [workingsListPageSize, setWorkingsListPageSize] = useState(2); - const startIndex = (workingsListPage - 1) * workingsListPageSize; - - const { data } = usePaginatedQuery({ - query, - input: { - filter: { - terms: workingSearchValue, - offset: startIndex, - size: workingsListPageSize - } - } - }); - return ( - {data != null ? ( - <> - { - alert(value); - }} - onChange={value => setWorkingSearchValue(value)} - /> - - ({ - ...item, - chaptersTotal: item.chapters.total, - reviewsTotal: item.reviews.total - }))} - /> - - - - ) : ( - <> - - - - - - - - - )} + ); } -const query = gql` - query WorkingList($input: WorkingManyInput!) { - workingMany(input: $input) { - items { - id - type - title - description - author { - id - username - } - updatedAt - progressStatus - ratingTotal - reviewsTotal - book { - id - chaptersTotal - wordsTotal - } - } - total - } - } -`; - // const workingData1: WorkingCardProps = { // id: '1', // title: 'Worth the Candle', diff --git a/app/client/pages/new/book.tsx b/app/client/pages/new/book.tsx new file mode 100644 index 0000000..2321b92 --- /dev/null +++ b/app/client/pages/new/book.tsx @@ -0,0 +1,66 @@ +import { Heading, Input, Stack } from '@chakra-ui/react'; +import { Button } from '@client/components/ui/button'; +import { Field } from '@client/components/ui/field'; +import { valibotResolver } from '@hookform/resolvers/valibot'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { useForm } from 'react-hook-form'; +import * as v from 'valibot'; + +export const Route = createFileRoute('/new/book')({ + component: RouteComponent, + beforeLoad: ({ context }) => { + if (!context.isAuthenticated) throw redirect({ to: '/' }); + } +}); + +function RouteComponent() { + return ( + + New book + + + ); +} + +// function LoginForm() { +// const schema = v.pipe( +// v.object({ +// email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')), +// password: v.pipe(v.string(), v.nonEmpty('Password is required')) +// }) +// ); +// +// const { +// register, +// handleSubmit, +// formState: { errors } +// } = useForm({ resolver: valibotResolver(schema) }); +// +// const { login } = useAuth(); +// +// return ( +//
{ +// const session = await login({ +// email: values.email, +// password: values.password +// }); +// if (session != null) window.history.back(); +// })} +// > +// +// +// +// +// +// +// +// +// +//
+// ); +// } diff --git a/app/client/routeTree.gen.ts b/app/client/routeTree.gen.ts index 5c6aa85..d94cd30 100644 --- a/app/client/routeTree.gen.ts +++ b/app/client/routeTree.gen.ts @@ -15,11 +15,11 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './pages/__root' import { Route as RegisterImport } from './pages/register' import { Route as LoginImport } from './pages/login' -import { Route as ExampleImport } from './pages/example' import { Route as EditorImport } from './pages/editor' import { Route as AboutImport } from './pages/about' import { Route as IndexImport } from './pages/index' import { Route as ProfileUsernameImport } from './pages/profile/$username' +import { Route as NewBookImport } from './pages/new/book' // Create Virtual Routes @@ -45,12 +45,6 @@ const LoginRoute = LoginImport.update({ getParentRoute: () => rootRoute, } as any) -const ExampleRoute = ExampleImport.update({ - id: '/example', - path: '/example', - getParentRoute: () => rootRoute, -} as any) - const EditorRoute = EditorImport.update({ id: '/editor', path: '/editor', @@ -75,6 +69,12 @@ const ProfileUsernameRoute = ProfileUsernameImport.update({ getParentRoute: () => rootRoute, } as any) +const NewBookRoute = NewBookImport.update({ + id: '/new/book', + path: '/new/book', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -100,13 +100,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof EditorImport parentRoute: typeof rootRoute } - '/example': { - id: '/example' - path: '/example' - fullPath: '/example' - preLoaderRoute: typeof ExampleImport - parentRoute: typeof rootRoute - } '/login': { id: '/login' path: '/login' @@ -128,6 +121,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BookLazyImport parentRoute: typeof rootRoute } + '/new/book': { + id: '/new/book' + path: '/new/book' + fullPath: '/new/book' + preLoaderRoute: typeof NewBookImport + parentRoute: typeof rootRoute + } '/profile/$username': { id: '/profile/$username' path: '/profile/$username' @@ -144,10 +144,10 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute '/editor': typeof EditorRoute - '/example': typeof ExampleRoute '/login': typeof LoginRoute '/register': typeof RegisterRoute '/book': typeof BookLazyRoute + '/new/book': typeof NewBookRoute '/profile/$username': typeof ProfileUsernameRoute } @@ -155,10 +155,10 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute '/editor': typeof EditorRoute - '/example': typeof ExampleRoute '/login': typeof LoginRoute '/register': typeof RegisterRoute '/book': typeof BookLazyRoute + '/new/book': typeof NewBookRoute '/profile/$username': typeof ProfileUsernameRoute } @@ -167,10 +167,10 @@ export interface FileRoutesById { '/': typeof IndexRoute '/about': typeof AboutRoute '/editor': typeof EditorRoute - '/example': typeof ExampleRoute '/login': typeof LoginRoute '/register': typeof RegisterRoute '/book': typeof BookLazyRoute + '/new/book': typeof NewBookRoute '/profile/$username': typeof ProfileUsernameRoute } @@ -180,30 +180,30 @@ export interface FileRouteTypes { | '/' | '/about' | '/editor' - | '/example' | '/login' | '/register' | '/book' + | '/new/book' | '/profile/$username' fileRoutesByTo: FileRoutesByTo to: | '/' | '/about' | '/editor' - | '/example' | '/login' | '/register' | '/book' + | '/new/book' | '/profile/$username' id: | '__root__' | '/' | '/about' | '/editor' - | '/example' | '/login' | '/register' | '/book' + | '/new/book' | '/profile/$username' fileRoutesById: FileRoutesById } @@ -212,10 +212,10 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AboutRoute: typeof AboutRoute EditorRoute: typeof EditorRoute - ExampleRoute: typeof ExampleRoute LoginRoute: typeof LoginRoute RegisterRoute: typeof RegisterRoute BookLazyRoute: typeof BookLazyRoute + NewBookRoute: typeof NewBookRoute ProfileUsernameRoute: typeof ProfileUsernameRoute } @@ -223,10 +223,10 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AboutRoute: AboutRoute, EditorRoute: EditorRoute, - ExampleRoute: ExampleRoute, LoginRoute: LoginRoute, RegisterRoute: RegisterRoute, BookLazyRoute: BookLazyRoute, + NewBookRoute: NewBookRoute, ProfileUsernameRoute: ProfileUsernameRoute, } @@ -243,10 +243,10 @@ export const routeTree = rootRoute "/", "/about", "/editor", - "/example", "/login", "/register", "/book", + "/new/book", "/profile/$username" ] }, @@ -259,9 +259,6 @@ export const routeTree = rootRoute "/editor": { "filePath": "editor.tsx" }, - "/example": { - "filePath": "example.tsx" - }, "/login": { "filePath": "login.tsx" }, @@ -271,6 +268,9 @@ export const routeTree = rootRoute "/book": { "filePath": "book.lazy.tsx" }, + "/new/book": { + "filePath": "new/book.tsx" + }, "/profile/$username": { "filePath": "profile/$username.tsx" } diff --git a/app/server/graphql/graphql.ts b/app/server/graphql/graphql.ts index 9acb166..3fed2f2 100644 --- a/app/server/graphql/graphql.ts +++ b/app/server/graphql/graphql.ts @@ -2,10 +2,12 @@ import { MutationResolvers, QueryResolvers } from '@generated/types'; /* Query */ -import { BookOneResolver } from './resolvers/bookOneResolver'; -import { BooksManyResolver } from './resolvers/booksManyResolver'; import { IdentityResolver } from './resolvers/identityResolver'; import { UserOneResolver } from './resolvers/userOneResolver'; +import { BookChapterManyResolver } from './resolvers/bookChapterMany'; +import { BookChapterOneResolver } from './resolvers/bookChapterOne'; +import { BookManyResolver } from './resolvers/bookManyResolver'; +import { BookOneResolver } from './resolvers/bookOneResolver'; /* Mutation */ import { LoginResolver } from './resolvers/auth/login'; @@ -13,11 +15,12 @@ import { LogoutResolver } from './resolvers/auth/logout'; import { SignUpResolver } from './resolvers/auth/signUp'; export const Query: QueryResolvers = { - booksMany: BooksManyResolver, bookOne: BookOneResolver, + bookMany: BookManyResolver, + bookChapterOne: BookChapterOneResolver, + bookChapterMany: BookChapterManyResolver, identity: IdentityResolver, userOne: UserOneResolver - // workingMany: }; export const Mutation: MutationResolvers = { diff --git a/app/server/graphql/resolvers/bookChapterMany.ts b/app/server/graphql/resolvers/bookChapterMany.ts new file mode 100644 index 0000000..ca0aba4 --- /dev/null +++ b/app/server/graphql/resolvers/bookChapterMany.ts @@ -0,0 +1,25 @@ +import { IdentityContext } from '@app/server/identity'; +import { prisma } from '@app/server/prisma/client'; +import { BookChapterManyOutput, QueryBookChapterManyArgs } from '@generated/types'; +import { toBookChapterSchema } from './mappers/bookChapterMapper'; + +export async function BookChapterManyResolver( + _: any, + { input }: QueryBookChapterManyArgs, + { identity }: IdentityContext +): Promise { + const { bookId, filter } = input; + + const items = await prisma.bookChapter.findMany({ + where: { + bookId + }, + skip: filter.offset ?? 0, + take: filter.size ?? 10 + }); + + return { + items: await Promise.all(items.map(value => toBookChapterSchema(value))), + total: items.length + }; +} diff --git a/app/server/graphql/resolvers/bookChapterOne.ts b/app/server/graphql/resolvers/bookChapterOne.ts new file mode 100644 index 0000000..b76e8d4 --- /dev/null +++ b/app/server/graphql/resolvers/bookChapterOne.ts @@ -0,0 +1,17 @@ +import { IdentityContext } from '@app/server/identity'; +import { prisma } from '@app/server/prisma/client'; +import { BookChapter, QueryBookChapterOneArgs } from '@generated/types'; +import { toBookChapterSchema } from './mappers/bookChapterMapper'; + +export async function BookChapterOneResolver( + _: any, + { id }: QueryBookChapterOneArgs, + { identity }: IdentityContext +): Promise { + const model = await prisma.bookChapter.findUniqueOrThrow({ + where: { + id + } + }); + return toBookChapterSchema(model); +} diff --git a/app/server/graphql/resolvers/bookManyResolver.ts b/app/server/graphql/resolvers/bookManyResolver.ts new file mode 100644 index 0000000..00630d8 --- /dev/null +++ b/app/server/graphql/resolvers/bookManyResolver.ts @@ -0,0 +1,29 @@ +import { IdentityContext } from '@app/server/identity'; +import { prisma } from '@app/server/prisma/client'; +import { BookManyOutput, QueryBookManyArgs } from '@generated/types'; +import { toBookSchema } from './mappers/bookMapper'; + +export async function BookManyResolver( + _: any, + { input }: QueryBookManyArgs, + { identity }: IdentityContext +): Promise { + const { filter } = input; + const terms = filter.terms?.toLowerCase(); + + const items = await prisma.book.findMany({ + where: { + OR: + terms != null + ? [{ title: { contains: terms } }, { author: { username: terms } }] + : undefined + }, + skip: filter.offset ?? 0, + take: filter.size ?? 10 + }); + + return { + items: await Promise.all(items.map(value => toBookSchema(value))), + total: items.length + }; +} diff --git a/app/server/graphql/resolvers/bookOneResolver.ts b/app/server/graphql/resolvers/bookOneResolver.ts new file mode 100644 index 0000000..daf2bbe --- /dev/null +++ b/app/server/graphql/resolvers/bookOneResolver.ts @@ -0,0 +1,17 @@ +import { IdentityContext } from '@app/server/identity'; +import { prisma } from '@app/server/prisma/client'; +import { Book, QueryBookOneArgs } from '@generated/types'; +import { toBookSchema } from './mappers/bookMapper'; + +export async function BookOneResolver( + _: any, + { id }: QueryBookOneArgs, + { identity }: IdentityContext +): Promise { + const model = await prisma.book.findUniqueOrThrow({ + where: { + id + } + }); + return toBookSchema(model); +} diff --git a/app/server/graphql/resolvers/mappers/bookChapterMapper.ts b/app/server/graphql/resolvers/mappers/bookChapterMapper.ts index b4713f2..b8d5f9e 100644 --- a/app/server/graphql/resolvers/mappers/bookChapterMapper.ts +++ b/app/server/graphql/resolvers/mappers/bookChapterMapper.ts @@ -3,7 +3,7 @@ import { prisma } from '@app/server/prisma/client'; import { BookChapter as BookChapterSchema } from '@generated/types'; import { BookChapter } from '@prisma/client'; -export async function toBookChapterMapping(model: BookChapter): Promise { +export async function toBookChapterSchema(model: BookChapter): Promise { return { id: model.id, title: model.title, diff --git a/app/server/graphql/resolvers/mappers/bookMapper.ts b/app/server/graphql/resolvers/mappers/bookMapper.ts index eb2904a..27ce10d 100644 --- a/app/server/graphql/resolvers/mappers/bookMapper.ts +++ b/app/server/graphql/resolvers/mappers/bookMapper.ts @@ -1,14 +1,28 @@ +import { assertUnreachable } from '@app/client/helpers'; import { call } from '@app/common'; import { prisma } from '@app/server/prisma/client'; -import { Book as BookSchema } from '@generated/types'; -import { Book } from '@prisma/client'; +import { + Book as BookSchema, + WorkingProgressStatus as WorkingProgressStatusSchema +} from '@generated/types'; +import { Book, WorkingProgressStatus } from '@prisma/client'; +import { toUserSchema } from './userMapper'; export async function toBookSchema(model: Book): Promise { return { id: model.id, - workingId: model.workingId, + title: model.title, + description: model.description, + author: await call(async () => { + const userModel = await prisma.user.findUniqueOrThrow({ where: { id: model.authorId } }); + return toUserSchema(userModel); + }), createdAt: model.createdAt.toISOString(), updatedAt: model.updatedAt.toISOString(), + progressStatus: call(() => { + ensureWorkingProgressStatus(model.progressStatus); + return model.progressStatus; + }), wordsTotal: model.wordsTotal, chaptersTotal: await call(async () => { const chaptersCount = await prisma.bookChapter.count({ @@ -17,6 +31,32 @@ export async function toBookSchema(model: Book): Promise { } }); return chaptersCount; + }), + // todo: ratings implementation + ratingTotal: await call(async () => { + const ratingsCount = await prisma.rating.count({ + where: { + bookId: model.id + } + }); + return ratingsCount; + }), + reviewsTotal: await call(async () => { + const reviewsCount = await prisma.review.count({ + where: { + bookId: model.id + } + }); + return reviewsCount; }) }; } + +function ensureWorkingProgressStatus( + value: WorkingProgressStatus +): asserts value is WorkingProgressStatusSchema { + if (value === WorkingProgressStatus.inProgress) return; + if (value === WorkingProgressStatus.frozen) return; + if (value === WorkingProgressStatus.complete) return; + assertUnreachable(value); +} diff --git a/app/server/graphql/resolvers/mappers/reviewMapper.ts b/app/server/graphql/resolvers/mappers/reviewMapper.ts index b9beb55..76e3236 100644 --- a/app/server/graphql/resolvers/mappers/reviewMapper.ts +++ b/app/server/graphql/resolvers/mappers/reviewMapper.ts @@ -16,7 +16,7 @@ export async function toReviewSchema(model: Review): Promise { return toUserSchema(userModel); }), text: model.text, - //todo: ratings implementation + // todo: ratings implementation ratingTotal: await call(async () => { return 0; }) diff --git a/app/server/graphql/resolvers/mappers/workingMapper.ts b/app/server/graphql/resolvers/mappers/workingMapper.ts deleted file mode 100644 index ddc46bd..0000000 --- a/app/server/graphql/resolvers/mappers/workingMapper.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { assertUnreachable } from '@app/client/helpers'; -import { call } from '@app/common'; -import { prisma } from '@app/server/prisma/client'; -import { - WorkingProgressStatus as WorkingProgressStatusSchema, - Working as WorkingSchema, - WorkingType as WorkingTypeSchema -} from '@generated/types'; -import { Working, WorkingProgressStatus, WorkingType } from '@prisma/client'; -import { toUserSchema } from './userMapper'; - -export async function toWorkingSchema(model: Working): Promise { - return { - id: model.id, - title: model.title, - description: model.description, - author: await call(async () => { - const userModel = await prisma.user.findUniqueOrThrow({ where: { id: model.authorId } }); - return toUserSchema(userModel); - }), - createdAt: model.createdAt.toISOString(), - updatedAt: model.updatedAt.toISOString(), - type: call(() => { - ensureWorkingType(model.type); - return model.type; - }), - progressStatus: call(() => { - ensureWorkingProgressStatus(model.progressStatus); - return model.progressStatus; - }), - //todo: ratings implementation - //todo: book/notes implementation - ratingTotal: await call(async () => { - const ratingsCount = await prisma.rating.count({ - where: { - workingId: model.id - } - }); - return ratingsCount; - }), - reviewsTotal: await call(async () => { - const reviewsCount = await prisma.review.count({ - where: { - workingId: model.id - } - }); - return reviewsCount; - }) - }; -} - -function ensureWorkingType(value: WorkingType): asserts value is WorkingTypeSchema { - if (value == WorkingType.book) return; - if (value == WorkingType.notepad) return; - assertUnreachable(value); -} - -function ensureWorkingProgressStatus( - value: WorkingProgressStatus -): asserts value is WorkingProgressStatusSchema { - if (value == WorkingProgressStatus.inProgress) return; - if (value == WorkingProgressStatus.frozen) return; - if (value == WorkingProgressStatus.complete) return; - assertUnreachable(value); -} diff --git a/app/server/graphql/schema.graphql b/app/server/graphql/schema.graphql index d307e16..95d2457 100644 --- a/app/server/graphql/schema.graphql +++ b/app/server/graphql/schema.graphql @@ -3,7 +3,11 @@ scalar Void type Query { identity: Identity! userOne(id: ID!): User! - workingMany(input: WorkingManyInput!): WorkingManyOutput! + bookOne(id: ID!): Book! + bookMany(input: BookManyInput!): BookManyOutput! + bookChapterOne(id: ID!): BookChapter! + bookChapterMany(input: BookChapterManyInput!): BookChapterManyOutput! + reviewMany(input: ReviewManyInput!): ReviewManyOutput! } type Mutation { @@ -48,14 +52,8 @@ input PaginationFilterInput { size: Int } -input WorkingManyInput { +input BookManyInput { filter: PaginationFilterInput! - type: WorkingType! -} - -enum WorkingType { - book - notepad } enum WorkingProgressStatus { @@ -64,33 +62,23 @@ enum WorkingProgressStatus { complete } -type WorkingManyOutput { - items: [Working!]! +type BookManyOutput { + items: [Book!]! total: Int! } -type Working { +type Book { id: ID! title: String! description: String! author: User! createdAt: String! updatedAt: String! - type: WorkingType! - book: Book - notepad: Notepad progressStatus: WorkingProgressStatus! - ratingTotal: Int! - reviewsTotal: Int! -} - -type Book { - id: ID! - workingId: ID! - createdAt: String! - updatedAt: String! wordsTotal: Int! chaptersTotal: Int! + ratingTotal: Int! + reviewsTotal: Int! } type BookChapter { @@ -104,6 +92,7 @@ type BookChapter { } input BookChapterManyInput { + bookId: String! filter: PaginationFilterInput! } @@ -112,10 +101,6 @@ type BookChapterManyOutput { total: Int! } -type Notepad { - id: ID! -} - type Review { id: ID! author: User! @@ -124,6 +109,8 @@ type Review { } input ReviewManyInput { + bookId: String + bookChapterId: String filter: PaginationFilterInput! } diff --git a/app/server/prisma/schema.prisma b/app/server/prisma/schema.prisma index 5f235c4..2994037 100644 --- a/app/server/prisma/schema.prisma +++ b/app/server/prisma/schema.prisma @@ -21,37 +21,12 @@ model User { isDarkMode Boolean @default(true) - workings Working[] - reviews Review[] - ratings Rating[] -} - -model Working { - id String @id - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - title String @db.VarChar(255) - description String @db.MediumText - progressStatus WorkingProgressStatus @default(inProgress) - - type WorkingType - - author User @relation(fields: [authorId], references: [id]) - authorId String - - book Book? + books Book[] reviews Review[] ratings Rating[] } -enum WorkingType { - book - notepad -} - enum WorkingProgressStatus { inProgress frozen @@ -64,12 +39,18 @@ model Book { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - wordsTotal Int @default(0) + title String @db.VarChar(255) + description String @db.MediumText + progressStatus WorkingProgressStatus @default(inProgress) - working Working @relation(fields: [workingId], references: [id]) - workingId String @unique @db.VarChar(255) + author User @relation(fields: [authorId], references: [id]) + authorId String - chapters BookChapter[] + wordsTotal Int @default(0) + chapters BookChapter[] + + reviews Review[] + ratings Rating[] } model BookChapter { @@ -82,6 +63,8 @@ model BookChapter { foreword String @db.MediumText footnote String @db.MediumText + wordsTotal Int @default(0) + book Book @relation(fields: [bookId], references: [id]) bookId String @db.VarChar(255) @@ -100,11 +83,11 @@ model Review { user User @relation(fields: [userId], references: [id]) userId String @db.VarChar(255) - working Working? @relation(fields: [workingId], references: [id]) - workingId String? @db.VarChar(255) - bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id]) bookChapterId String? @db.VarChar(255) + + book Book? @relation(fields: [bookId], references: [id]) + bookId String? } enum RatingType { @@ -123,9 +106,9 @@ model Rating { user User @relation(fields: [userId], references: [id]) userId String @db.VarChar(255) - working Working? @relation(fields: [workingId], references: [id]) - workingId String? @db.VarChar(255) - bookChapter BookChapter? @relation(fields: [bookChapterId], references: [id]) bookChapterId String? @db.VarChar(255) + + book Book? @relation(fields: [bookId], references: [id]) + bookId String? }