From d58ab4ca9ab2819ec3238780c9b68acce86f6676 Mon Sep 17 00:00:00 2001 From: Johannes Horn Date: Thu, 8 Oct 2026 15:25:45 +0200 Subject: [PATCH 1/4] feat: add BookSearchResult type and book(id) query to schema --- .../books/resolvers/BookSearchResult.ts | 4 +++ .../src/schema/books/resolvers/Query/book.ts | 8 ++++++ backend/src/schema/books/schema.graphql | 8 +++++- backend/src/schema/resolvers.generated.ts | 9 ++++--- backend/src/schema/schema.generated.graphqls | 8 +++++- backend/src/schema/typeDefs.generated.ts | 2 +- backend/src/schema/types.generated.ts | 25 +++++++++++++++++-- 7 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 backend/src/schema/books/resolvers/BookSearchResult.ts create mode 100644 backend/src/schema/books/resolvers/Query/book.ts diff --git a/backend/src/schema/books/resolvers/BookSearchResult.ts b/backend/src/schema/books/resolvers/BookSearchResult.ts new file mode 100644 index 0000000..5ea3903 --- /dev/null +++ b/backend/src/schema/books/resolvers/BookSearchResult.ts @@ -0,0 +1,4 @@ +import type { BookSearchResultResolvers } from "./../../types.generated"; +export const BookSearchResult: BookSearchResultResolvers = { + /* Implement BookSearchResult resolver logic here */ +}; diff --git a/backend/src/schema/books/resolvers/Query/book.ts b/backend/src/schema/books/resolvers/Query/book.ts new file mode 100644 index 0000000..e966f8c --- /dev/null +++ b/backend/src/schema/books/resolvers/Query/book.ts @@ -0,0 +1,8 @@ +import type { QueryResolvers } from "./../../../types.generated"; +export const book: NonNullable = async ( + _parent, + _arg, + _ctx, +) => { + /* Implement Query.book resolver logic here */ +}; diff --git a/backend/src/schema/books/schema.graphql b/backend/src/schema/books/schema.graphql index 50cdc2f..cd16580 100644 --- a/backend/src/schema/books/schema.graphql +++ b/backend/src/schema/books/schema.graphql @@ -24,5 +24,11 @@ extend type Query { publisher: String limit: Int = 12 offset: Int = 0 - ): [Book] + ): BookSearchResult! + book(id: Int!): Book +} + +type BookSearchResult { + books: [Book!]! + totalCount: Int! } diff --git a/backend/src/schema/resolvers.generated.ts b/backend/src/schema/resolvers.generated.ts index 5f9d8f6..022f932 100644 --- a/backend/src/schema/resolvers.generated.ts +++ b/backend/src/schema/resolvers.generated.ts @@ -1,10 +1,13 @@ /* This file was automatically generated. DO NOT UPDATE MANUALLY. */ import type { Resolvers } from './types.generated'; - import { searchBooks as Query_searchBooks } from './books/resolvers/Query/searchBooks'; + import { book as Query_book } from './books/resolvers/Query/book'; +import { searchBooks as Query_searchBooks } from './books/resolvers/Query/searchBooks'; import { Book } from './books/resolvers/Book'; +import { BookSearchResult } from './books/resolvers/BookSearchResult'; export const resolvers: Resolvers = { - Query: { searchBooks: Query_searchBooks }, + Query: { book: Query_book,searchBooks: Query_searchBooks }, - Book: Book + Book: Book, +BookSearchResult: BookSearchResult } \ No newline at end of file diff --git a/backend/src/schema/schema.generated.graphqls b/backend/src/schema/schema.generated.graphqls index 3eb7209..5ac6a75 100644 --- a/backend/src/schema/schema.generated.graphqls +++ b/backend/src/schema/schema.generated.graphqls @@ -14,6 +14,12 @@ type Book { title: String! } +type BookSearchResult { + books: [Book!]! + totalCount: Int! +} + type Query { - searchBooks(author: String, bookFormat: String, language: String, limit: Int = 12, offset: Int = 0, publisher: String, series: String, title: String): [Book] + book(id: Int!): Book + searchBooks(author: String, bookFormat: String, language: String, limit: Int = 12, offset: Int = 0, publisher: String, series: String, title: String): BookSearchResult! } \ No newline at end of file diff --git a/backend/src/schema/typeDefs.generated.ts b/backend/src/schema/typeDefs.generated.ts index 33982ad..7c33094 100644 --- a/backend/src/schema/typeDefs.generated.ts +++ b/backend/src/schema/typeDefs.generated.ts @@ -1,2 +1,2 @@ import type { DocumentNode } from 'graphql'; - export const typeDefs = {"kind":"Document","definitions":[{"name":{"kind":"Name","value":"Query"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"searchBooks"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}],"directives":[],"interfaces":[]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Book"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"isbn"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"description"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"edition"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"pages"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publishDate"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"coverImgUrl"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}]},{"kind":"SchemaDefinition","operationTypes":[{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"operation":"query"}]}]} as unknown as DocumentNode \ No newline at end of file + export const typeDefs = {"kind":"Document","definitions":[{"name":{"kind":"Name","value":"Query"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"searchBooks"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSearchResult"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"book"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}],"directives":[],"interfaces":[]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Book"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"isbn"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"description"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"edition"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"pages"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publishDate"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"coverImgUrl"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"BookSearchResult"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"books"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"totalCount"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}]},{"kind":"SchemaDefinition","operationTypes":[{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"operation":"query"}]}]} as unknown as DocumentNode \ No newline at end of file diff --git a/backend/src/schema/types.generated.ts b/backend/src/schema/types.generated.ts index e7dc620..a4712cf 100644 --- a/backend/src/schema/types.generated.ts +++ b/backend/src/schema/types.generated.ts @@ -28,9 +28,21 @@ export type Book = { title: Scalars['String']['output']; }; +export type BookSearchResult = { + __typename?: 'BookSearchResult'; + books: Array; + totalCount: Scalars['Int']['output']; +}; + export type Query = { __typename?: 'Query'; - searchBooks?: Maybe>>; + book?: Maybe; + searchBooks: BookSearchResult; +}; + + +export type QuerybookArgs = { + id: Scalars['Int']['input']; }; @@ -121,6 +133,7 @@ export type ResolversTypes = { Book: ResolverTypeWrapper; String: ResolverTypeWrapper; Int: ResolverTypeWrapper; + BookSearchResult: ResolverTypeWrapper; Query: ResolverTypeWrapper>; Boolean: ResolverTypeWrapper; }; @@ -130,6 +143,7 @@ export type ResolversParentTypes = { Book: Book; String: Scalars['String']['output']; Int: Scalars['Int']['output']; + BookSearchResult: BookSearchResult; Query: Record; Boolean: Scalars['Boolean']['output']; }; @@ -150,12 +164,19 @@ export type BookResolvers; }; +export type BookSearchResultResolvers = { + books?: Resolver, ParentType, ContextType>; + totalCount?: Resolver; +}; + export type QueryResolvers = { - searchBooks?: Resolver>>, ParentType, ContextType, RequireFields>; + book?: Resolver, ParentType, ContextType, RequireFields>; + searchBooks?: Resolver>; }; export type Resolvers = { Book?: BookResolvers; + BookSearchResult?: BookSearchResultResolvers; Query?: QueryResolvers; }; From a4dc54d4b443300b2764df07e0c467950bf2188c Mon Sep 17 00:00:00 2001 From: Johannes Horn Date: Thu, 8 Oct 2026 15:32:45 +0200 Subject: [PATCH 2/4] feat: add resolver for book(id) --- backend/src/schema/books/resolvers/Query/book.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/books/resolvers/Query/book.ts b/backend/src/schema/books/resolvers/Query/book.ts index e966f8c..efd2be1 100644 --- a/backend/src/schema/books/resolvers/Query/book.ts +++ b/backend/src/schema/books/resolvers/Query/book.ts @@ -1,8 +1,11 @@ import type { QueryResolvers } from "./../../../types.generated"; +import { db } from "~/db"; export const book: NonNullable = async ( _parent, - _arg, + args, _ctx, ) => { - /* Implement Query.book resolver logic here */ + const foundBook = await db.query.books.findFirst({ where: { id: args.id } }); + + return foundBook ?? null; }; From f04c124706695f3fa4e2aa85a3fd9f8f8ec92222 Mon Sep 17 00:00:00 2001 From: Johannes Horn Date: Thu, 8 Oct 2026 15:55:02 +0200 Subject: [PATCH 3/4] feat: add totalCount and stable ordering to searchBooks --- .../books/resolvers/Query/searchBooks.ts | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/backend/src/schema/books/resolvers/Query/searchBooks.ts b/backend/src/schema/books/resolvers/Query/searchBooks.ts index 80fe730..03ed08c 100644 --- a/backend/src/schema/books/resolvers/Query/searchBooks.ts +++ b/backend/src/schema/books/resolvers/Query/searchBooks.ts @@ -1,4 +1,6 @@ +import { and, asc, ilike, type SQL } from "drizzle-orm"; import { db } from "~/db"; +import { books } from "~/db/schema"; import type { QueryResolvers } from "~/schema/types.generated"; export const searchBooks: NonNullable = async ( @@ -6,28 +8,35 @@ export const searchBooks: NonNullable = async ( args, _ctx, ) => { - return db.query.books.findMany({ - where: { - title: { - ilike: args.title ? `%${args.title ?? ""}%` : undefined, - }, - series: { - ilike: args.series ? `%${args.series ?? ""}%` : undefined, - }, - author: { - ilike: args.author ? `%${args.author ?? ""}%` : undefined, - }, - language: { - ilike: args.language ? `%${args.language ?? ""}%` : undefined, - }, - bookFormat: { - ilike: args.bookFormat ? `%${args.bookFormat ?? ""}%` : undefined, - }, - publisher: { - ilike: args.publisher ? `%${args.publisher ?? ""}%` : undefined, - }, - }, - limit: args.limit, - offset: args.offset, - }); + const conditions: SQL[] = []; + + if (args.title) conditions.push(ilike(books.title, `%${args.title}%`)); + if (args.series) conditions.push(ilike(books.series, `%${args.series}%`)); + if (args.author) conditions.push(ilike(books.author, `%${args.author}%`)); + if (args.language) + conditions.push(ilike(books.language, `%${args.language}%`)); + if (args.bookFormat) + conditions.push(ilike(books.bookFormat, `%${args.bookFormat}%`)); + if (args.publisher) + conditions.push(ilike(books.publisher, `%${args.publisher}%`)); + + // Here we build the filter once with Drizzle's SQL builder, so the same filter + // can be used both for fetching one page and for counting all matches. + const filter = and(...conditions); + + const [foundBooks, totalCount] = await Promise.all([ + db + .select() + .from(books) + .where(filter) + .orderBy(asc(books.id)) + .limit(args.limit) + .offset(args.offset), + db.$count(books, filter), + ]); + + return { + books: foundBooks, + totalCount, + }; }; From 027777fb78d449f6a9a4859d451a192da8efe5ea Mon Sep 17 00:00:00 2001 From: Johannes Horn Date: Fri, 9 Oct 2026 11:39:39 +0200 Subject: [PATCH 4/4] Update backend/src/schema/books/resolvers/BookSearchResult.ts Co-authored-by: Martin Alexander Kaminski --- backend/src/schema/books/resolvers/BookSearchResult.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/schema/books/resolvers/BookSearchResult.ts b/backend/src/schema/books/resolvers/BookSearchResult.ts index 5ea3903..1c388d6 100644 --- a/backend/src/schema/books/resolvers/BookSearchResult.ts +++ b/backend/src/schema/books/resolvers/BookSearchResult.ts @@ -1,4 +1,2 @@ import type { BookSearchResultResolvers } from "./../../types.generated"; -export const BookSearchResult: BookSearchResultResolvers = { - /* Implement BookSearchResult resolver logic here */ -}; +export const BookSearchResult: BookSearchResultResolvers = {};