From 5679d0b803a8e1a7cf39cc1b956f37d1b949de83 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 08:33:33 +0900 Subject: [PATCH 01/19] Change API specification (#168) --- api/bundle.yml | 117 ++++++++++++++--------- api/components/examples/book.yml | 18 ++-- api/components/examples/google-book.yml | 12 +++ api/components/schemas/GoogleBook.yml | 24 +++++ api/openapi.yml | 29 +++--- api/paths/book.yml | 118 ------------------------ api/paths/google-books.yml | 111 ++++++++++++++++++++++ 7 files changed, 248 insertions(+), 181 deletions(-) create mode 100644 api/components/examples/google-book.yml create mode 100644 api/components/schemas/GoogleBook.yml create mode 100644 api/paths/google-books.yml diff --git a/api/bundle.yml b/api/bundle.yml index 0c738f11..7603e16e 100644 --- a/api/bundle.yml +++ b/api/bundle.yml @@ -20,6 +20,8 @@ tags: description: 貸出履歴に関するAPI - name: auth description: 認証に関するAPI + - name: google-books + description: Google Books から書籍の情報を取得するAPI paths: /books: get: @@ -337,12 +339,12 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' - /books/search: + /googlebooks: get: tags: - - book - operationId: searchBooks - summary: 書籍を検索する + - google-books + operationId: searchGoogleBooks + summary: Google Books から書籍を検索する security: [] parameters: - name: page @@ -409,51 +411,44 @@ paths: books: type: array items: - type: object - properties: - id: - type: string - title: - type: string - authors: - type: array - items: - type: string - publisher: - type: string - publishedDate: - type: string - description: - type: string - thumbnail: - type: string - isbn: - type: string - required: - - id - - title - - authors + $ref: '#/components/schemas/GoogleBook' required: - totalBook - books - example: - totalBook: 30 - books: - - id: 5-OgzgEACAAJ - title: 計算機プログラムの構造と解釈 - authors: - - Harold Abelson - - Gerald Jay Sussman - - Julie Sussman - publisher: 翔泳社 - publishedDate: '2018-07-01' - description: 言わずと知れた計算機科学の古典的名著 - thumbnail: http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api - isbn: '9784798135984' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' + /googlebooks/{volumeId}: + get: + tags: + - google-books + operationId: getGoogleBook + summary: Google Books から特定の書籍の情報を取得する + security: [] + parameters: + - name: volumeId + in: path + description: Google BooksのID + required: true + schema: + type: string + responses: + '200': + description: 情報の取得に成功した + content: + application/json: + schema: + $ref: '#/components/schemas/GoogleBook' + examples: + book: + $ref: '#/components/examples/google-book' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /users: get: tags: @@ -1025,6 +1020,31 @@ components: type: string required: - message + GoogleBook: + type: object + properties: + id: + type: string + title: + type: string + authors: + type: array + items: + type: string + publisher: + type: string + publishedDate: + type: string + description: + type: string + thumbnail: + type: string + isbn: + type: string + required: + - id + - title + - authors User: type: object properties: @@ -1109,6 +1129,19 @@ components: thumbnail: http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api isbn: '9784798135984' stock: 1 + google-book: + value: + id: 5-OgzgEACAAJ + title: 計算機プログラムの構造と解釈 + authors: + - Harold Abelson + - Gerald Jay Sussman + - Julie Sussman + publisher: 翔泳社 + publishedDate: '2018-07-01' + description: 言わずと知れた計算機科学の古典的名著 + thumbnail: http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api + isbn: '9784798135984' user: value: id: 1 diff --git a/api/components/examples/book.yml b/api/components/examples/book.yml index a4633596..6f1bc205 100644 --- a/api/components/examples/book.yml +++ b/api/components/examples/book.yml @@ -1,13 +1,13 @@ value: id: 1 - title: "計算機プログラムの構造と解釈" + title: '計算機プログラムの構造と解釈' authors: - - "Harold Abelson" - - "Gerald Jay Sussman" - - "Julie Sussman" - publisher: "翔泳社" - publishedDate: "2012-07-06" - description: "言わずと知れた計算機科学の古典的名著" - thumbnail: "http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api" - isbn: "9784798135984" + - 'Harold Abelson' + - 'Gerald Jay Sussman' + - 'Julie Sussman' + publisher: '翔泳社' + publishedDate: '2012-07-06' + description: '言わずと知れた計算機科学の古典的名著' + thumbnail: 'http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api' + isbn: '9784798135984' stock: 1 diff --git a/api/components/examples/google-book.yml b/api/components/examples/google-book.yml new file mode 100644 index 00000000..7e03b011 --- /dev/null +++ b/api/components/examples/google-book.yml @@ -0,0 +1,12 @@ +value: + id: 5-OgzgEACAAJ + title: '計算機プログラムの構造と解釈' + authors: + - 'Harold Abelson' + - 'Gerald Jay Sussman' + - 'Julie Sussman' + publisher: '翔泳社' + publishedDate: '2018-07-01' + description: '言わずと知れた計算機科学の古典的名著' + thumbnail: 'http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api' + isbn: '9784798135984' diff --git a/api/components/schemas/GoogleBook.yml b/api/components/schemas/GoogleBook.yml new file mode 100644 index 00000000..2eb779f0 --- /dev/null +++ b/api/components/schemas/GoogleBook.yml @@ -0,0 +1,24 @@ +type: object +properties: + id: + type: string + title: + type: string + authors: + type: array + items: + type: string + publisher: + type: string + publishedDate: + type: string + description: + type: string + thumbnail: + type: string + isbn: + type: string +required: + - id + - title + - authors diff --git a/api/openapi.yml b/api/openapi.yml index 7e572417..d4439e00 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -3,15 +3,15 @@ info: title: KITCC Library API contact: name: KITCC - url: "https://www.kitcc.org/" - email: "question@kitcc.org" + url: 'https://www.kitcc.org/' + email: 'question@kitcc.org' license: name: MIT url: https://en.wikipedia.org/wiki/MIT_License - version: "1.0.0" + version: '1.0.0' servers: - - url: "https://kitcc-library-api.kitcc.workers.dev" + - url: 'https://kitcc-library-api.kitcc.workers.dev' tags: - name: book @@ -22,25 +22,30 @@ tags: description: 貸出履歴に関するAPI - name: auth description: 認証に関するAPI + - name: google-books + description: Google Books から書籍の情報を取得するAPI paths: /books: - $ref: "./paths/book.yml#/books" + $ref: './paths/book.yml#/books' /books/{bookId}: - $ref: "./paths/book.yml#/book" - /books/search: - $ref: "./paths/book.yml#/search" + $ref: './paths/book.yml#/book' + + /googlebooks: + $ref: paths/google-books.yml#/books + /googlebooks/{volumeId}: + $ref: paths/google-books.yml#/book /users: - $ref: "./paths/user.yml#/users" + $ref: './paths/user.yml#/users' /users/{userId}: - $ref: "paths/user.yml#/user" + $ref: 'paths/user.yml#/user' /loans: - $ref: "./paths/loan.yml#/loans" + $ref: './paths/loan.yml#/loans' /auth: - $ref: "./paths/auth.yml#/auth" + $ref: './paths/auth.yml#/auth' components: securitySchemes: diff --git a/api/paths/book.yml b/api/paths/book.yml index 20a7df59..8ae22fea 100644 --- a/api/paths/book.yml +++ b/api/paths/book.yml @@ -323,121 +323,3 @@ book: $ref: '../components/responses/4xx.yml#/NotFound' '500': $ref: '../components/responses/5xx.yml#/InternalServerError' - -search: - get: - tags: - - book - operationId: searchBooks - summary: 書籍を検索する - security: [] - parameters: - - name: page - in: query - description: ページ番号 - required: false - schema: - type: string - pattern: ^[1-9]\d*$ - minimum: 1 - default: 1 - - name: limit - in: query - description: 1ページあたりの表示数 - required: false - schema: - type: string - pattern: ^[1-9]\d*$ - minimum: 1 - maximum: 40 - default: 10 - - name: keyword - in: query - description: 検索キーワード - required: false - schema: - type: string - - name: intitle - in: query - description: タイトル - required: false - schema: - type: string - - name: inauthor - in: query - description: 著者 - required: false - schema: - type: string - - name: inpublisher - in: query - description: 出版社 - required: false - schema: - type: string - - name: isbn - in: query - description: ISBN - required: false - schema: - type: string - pattern: '^\d{10}(\d{3})?$' - responses: - '200': - description: 検索に成功した - content: - application/json: - schema: - type: object - properties: - totalBook: - description: 総書籍数 - type: integer - books: - type: array - items: - type: object - properties: - id: - type: string - title: - type: string - authors: - type: array - items: - type: string - publisher: - type: string - publishedDate: - type: string - description: - type: string - thumbnail: - type: string - isbn: - type: string - required: - - id - - title - - authors - required: - - totalBook - - books - example: - totalBook: 30 - books: - - id: 5-OgzgEACAAJ - title: '計算機プログラムの構造と解釈' - authors: - - 'Harold Abelson' - - 'Gerald Jay Sussman' - - 'Julie Sussman' - publisher: '翔泳社' - publishedDate: '2018-07-01' - description: '言わずと知れた計算機科学の古典的名著' - thumbnail: 'http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api' - isbn: '9784798135984' - '400': - $ref: '../components/responses/4xx.yml#/BadRequest' - '500': - $ref: '../components/responses/5xx.yml#/InternalServerError' diff --git a/api/paths/google-books.yml b/api/paths/google-books.yml new file mode 100644 index 00000000..67df3421 --- /dev/null +++ b/api/paths/google-books.yml @@ -0,0 +1,111 @@ +books: + get: + tags: + - google-books + operationId: searchGoogleBooks + summary: Google Books から書籍を検索する + security: [] + parameters: + - name: page + in: query + description: ページ番号 + required: false + schema: + type: string + pattern: ^[1-9]\d*$ + minimum: 1 + default: 1 + - name: limit + in: query + description: 1ページあたりの表示数 + required: false + schema: + type: string + pattern: ^[1-9]\d*$ + minimum: 1 + maximum: 40 + default: 10 + - name: keyword + in: query + description: 検索キーワード + required: false + schema: + type: string + - name: intitle + in: query + description: タイトル + required: false + schema: + type: string + - name: inauthor + in: query + description: 著者 + required: false + schema: + type: string + - name: inpublisher + in: query + description: 出版社 + required: false + schema: + type: string + - name: isbn + in: query + description: ISBN + required: false + schema: + type: string + pattern: '^\d{10}(\d{3})?$' + responses: + '200': + description: 検索に成功した + content: + application/json: + schema: + type: object + properties: + totalBook: + description: 総書籍数 + type: integer + books: + type: array + items: + $ref: '../components/schemas/GoogleBook.yml' + required: + - totalBook + - books + '400': + $ref: '../components/responses/4xx.yml#/BadRequest' + '500': + $ref: '../components/responses/5xx.yml#/InternalServerError' + +book: + get: + tags: + - google-books + operationId: getGoogleBook + summary: Google Books から特定の書籍の情報を取得する + security: [] + parameters: + - name: volumeId + in: path + description: Google BooksのID + required: true + schema: + type: string + responses: + '200': + description: 情報の取得に成功した + content: + application/json: + schema: + $ref: '../components/schemas/GoogleBook.yml' + examples: + book: + $ref: '../components/examples/google-book.yml' + '400': + $ref: '../components/responses/4xx.yml#/BadRequest' + '404': + $ref: '../components/responses/4xx.yml#/NotFound' + '500': + $ref: '../components/responses/5xx.yml#/InternalServerError' From 2848742ca044a1c9ca10fccb66d6bc8b117273ca Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 08:34:36 +0900 Subject: [PATCH 02/19] Regenerate orval (#168) --- backend/src/schema.ts | 39 +++-- frontend/client/client.schemas.ts | 28 ++-- frontend/client/client.ts | 119 +++++++++++--- frontend/test/mocks/mock.ts | 149 +++++++++++++++--- frontend/test/mocks/model/getGoogleBook200.ts | 17 ++ frontend/test/mocks/model/googleBook.ts | 17 ++ frontend/test/mocks/model/index.ts | 5 + .../test/mocks/model/searchGoogleBooks200.ts | 13 ++ .../model/searchGoogleBooks200BooksItem.ts | 17 ++ .../mocks/model/searchGoogleBooksParams.ts | 37 +++++ 10 files changed, 370 insertions(+), 71 deletions(-) create mode 100644 frontend/test/mocks/model/getGoogleBook200.ts create mode 100644 frontend/test/mocks/model/googleBook.ts create mode 100644 frontend/test/mocks/model/searchGoogleBooks200.ts create mode 100644 frontend/test/mocks/model/searchGoogleBooks200BooksItem.ts create mode 100644 frontend/test/mocks/model/searchGoogleBooksParams.ts diff --git a/backend/src/schema.ts b/backend/src/schema.ts index 32eceeee..b6dc45e2 100644 --- a/backend/src/schema.ts +++ b/backend/src/schema.ts @@ -151,26 +151,26 @@ export const deleteBookParams = zod.object({ /** - * @summary 書籍を検索する + * @summary Google Books から書籍を検索する */ -export const searchBooksQueryPageRegExp = new RegExp('^[1-9]\\d*$'); -export const searchBooksQueryLimitMax = 40; +export const searchGoogleBooksQueryPageRegExp = new RegExp('^[1-9]\\d*$'); +export const searchGoogleBooksQueryLimitMax = 40; -export const searchBooksQueryLimitRegExp = new RegExp('^[1-9]\\d*$'); -export const searchBooksQueryIsbnRegExp = new RegExp('^\\d{10}(\\d{3})?$'); +export const searchGoogleBooksQueryLimitRegExp = new RegExp('^[1-9]\\d*$'); +export const searchGoogleBooksQueryIsbnRegExp = new RegExp('^\\d{10}(\\d{3})?$'); -export const searchBooksQueryParams = zod.object({ - "page": zod.string().min(1).regex(searchBooksQueryPageRegExp).optional(), - "limit": zod.string().min(1).max(searchBooksQueryLimitMax).regex(searchBooksQueryLimitRegExp).optional(), +export const searchGoogleBooksQueryParams = zod.object({ + "page": zod.string().min(1).regex(searchGoogleBooksQueryPageRegExp).optional(), + "limit": zod.string().min(1).max(searchGoogleBooksQueryLimitMax).regex(searchGoogleBooksQueryLimitRegExp).optional(), "keyword": zod.string().optional(), "intitle": zod.string().optional(), "inauthor": zod.string().optional(), "inpublisher": zod.string().optional(), - "isbn": zod.string().regex(searchBooksQueryIsbnRegExp).optional() + "isbn": zod.string().regex(searchGoogleBooksQueryIsbnRegExp).optional() }) -export const searchBooksResponse = zod.object({ +export const searchGoogleBooksResponse = zod.object({ "totalBook": zod.number(), "books": zod.array(zod.object({ "id": zod.string(), @@ -185,6 +185,25 @@ export const searchBooksResponse = zod.object({ }) +/** + * @summary Google Books から特定の書籍の情報を取得する + */ +export const getGoogleBookParams = zod.object({ + "volumeId": zod.string() +}) + +export const getGoogleBookResponse = zod.object({ + "id": zod.string(), + "title": zod.string(), + "authors": zod.array(zod.string()), + "publisher": zod.string().optional(), + "publishedDate": zod.string().optional(), + "description": zod.string().optional(), + "thumbnail": zod.string().optional(), + "isbn": zod.string().optional() +}) + + /** * ページ番号が指定されなかった場合は1ページ目を返す * @summary ユーザーの情報を取得する diff --git a/frontend/client/client.schemas.ts b/frontend/client/client.schemas.ts index 36aa982f..410c5bbc 100644 --- a/frontend/client/client.schemas.ts +++ b/frontend/client/client.schemas.ts @@ -144,24 +144,13 @@ name?: string; email?: string; }; -export type SearchBooks200BooksItem = { - authors: string[]; - description?: string; - id: string; - isbn?: string; - publishedDate?: string; - publisher?: string; - thumbnail?: string; - title: string; -}; - -export type SearchBooks200 = { - books: SearchBooks200BooksItem[]; +export type SearchGoogleBooks200 = { + books: GoogleBook[]; /** 総書籍数 */ totalBook: number; }; -export type SearchBooksParams = { +export type SearchGoogleBooksParams = { /** * ページ番号 */ @@ -305,6 +294,17 @@ export interface User { sessionToken?: string | null; } +export interface GoogleBook { + authors: string[]; + description?: string; + id: string; + isbn?: string; + publishedDate?: string; + publisher?: string; + thumbnail?: string; + title: string; +} + export interface Error { message: string; } diff --git a/frontend/client/client.ts b/frontend/client/client.ts index d65cf596..31561b5d 100644 --- a/frontend/client/client.ts +++ b/frontend/client/client.ts @@ -32,12 +32,13 @@ import type { GetLoansParams, GetUsers200, GetUsersParams, + GoogleBook, InternalServerErrorResponse, Loan, LoginBody, NotFoundResponse, - SearchBooks200, - SearchBooksParams, + SearchGoogleBooks200, + SearchGoogleBooksParams, UnauthorizedResponse, UpdateBookBody, UpdateUserBody, @@ -499,15 +500,15 @@ export const useDeleteBook = { +export const getSearchGoogleBooksUrl = (params?: SearchGoogleBooksParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { @@ -517,12 +518,12 @@ export const getSearchBooksUrl = (params?: SearchBooksParams,) => { } }); - return normalizedParams.size ? `https://localhost:8787/books/search?${normalizedParams.toString()}` : `https://localhost:8787/books/search` + return normalizedParams.size ? `https://localhost:8787/googlebooks?${normalizedParams.toString()}` : `https://localhost:8787/googlebooks` } -export const searchBooks = async (params?: SearchBooksParams, options?: RequestInit): Promise => { +export const searchGoogleBooks = async (params?: SearchGoogleBooksParams, options?: RequestInit): Promise => { - return customFetch>(getSearchBooksUrl(params), + return customFetch>(getSearchGoogleBooksUrl(params), { ...options, method: 'GET' @@ -533,43 +534,119 @@ export const searchBooks = async (params?: SearchBooksParams, options?: RequestI -export const getSearchBooksQueryKey = (params?: SearchBooksParams,) => { - return [`https://localhost:8787/books/search`, ...(params ? [params]: [])] as const; +export const getSearchGoogleBooksQueryKey = (params?: SearchGoogleBooksParams,) => { + return [`https://localhost:8787/googlebooks`, ...(params ? [params]: [])] as const; } -export const getSearchBooksQueryOptions = >, TError = BadRequestResponse | InternalServerErrorResponse>(params?: SearchBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} +export const getSearchGoogleBooksQueryOptions = >, TError = BadRequestResponse | InternalServerErrorResponse>(params?: SearchGoogleBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} ) => { const {query: queryOptions, request: requestOptions} = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getSearchBooksQueryKey(params); + const queryKey = queryOptions?.queryKey ?? getSearchGoogleBooksQueryKey(params); - const queryFn: QueryFunction>> = ({ signal }) => searchBooks(params, { signal, ...requestOptions }); + const queryFn: QueryFunction>> = ({ signal }) => searchGoogleBooks(params, { signal, ...requestOptions }); - return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } } -export type SearchBooksQueryResult = NonNullable>> -export type SearchBooksQueryError = BadRequestResponse | InternalServerErrorResponse +export type SearchGoogleBooksQueryResult = NonNullable>> +export type SearchGoogleBooksQueryError = BadRequestResponse | InternalServerErrorResponse /** - * @summary 書籍を検索する + * @summary Google Books から書籍を検索する */ -export function useSearchBooks>, TError = BadRequestResponse | InternalServerErrorResponse>( - params?: SearchBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} +export function useSearchGoogleBooks>, TError = BadRequestResponse | InternalServerErrorResponse>( + params?: SearchGoogleBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} ): UseQueryResult & { queryKey: QueryKey } { - const queryOptions = getSearchBooksQueryOptions(params,options) + const queryOptions = getSearchGoogleBooksQueryOptions(params,options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + +/** + * @summary Google Books から特定の書籍の情報を取得する + */ +export type getGoogleBookResponse = { + data: GoogleBook; + status: number; + headers: Headers; +} + +export const getGetGoogleBookUrl = (volumeId: string,) => { + + + return `https://localhost:8787/googlebooks/${volumeId}` +} + +export const getGoogleBook = async (volumeId: string, options?: RequestInit): Promise => { + + return customFetch>(getGetGoogleBookUrl(volumeId), + { + ...options, + method: 'GET' + + + } +);} + + + +export const getGetGoogleBookQueryKey = (volumeId: string,) => { + return [`https://localhost:8787/googlebooks/${volumeId}`] as const; + } + + +export const getGetGoogleBookQueryOptions = >, TError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse>(volumeId: string, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetGoogleBookQueryKey(volumeId); + + + + const queryFn: QueryFunction>> = ({ signal }) => getGoogleBook(volumeId, { signal, ...requestOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(volumeId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } +} + +export type GetGoogleBookQueryResult = NonNullable>> +export type GetGoogleBookQueryError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse + + +/** + * @summary Google Books から特定の書籍の情報を取得する + */ + +export function useGetGoogleBook>, TError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse>( + volumeId: string, options?: { query?:UseQueryOptions>, TError, TData>, request?: SecondParameter} + + ): UseQueryResult & { queryKey: QueryKey } { + + const queryOptions = getGetGoogleBookQueryOptions(volumeId,options) const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; diff --git a/frontend/test/mocks/mock.ts b/frontend/test/mocks/mock.ts index 4d10244d..02b4dd1a 100644 --- a/frontend/test/mocks/mock.ts +++ b/frontend/test/mocks/mock.ts @@ -30,7 +30,7 @@ import type { InternalServerErrorResponse, LoginBody, NotFoundResponse, - SearchBooksParams, + SearchGoogleBooksParams, UnauthorizedResponse, UpdateBookBody, UpdateUserBody, @@ -51,8 +51,9 @@ import type { GetBooks200, GetLoans200, GetUsers200, + GoogleBook, Loan, - SearchBooks200, + SearchGoogleBooks200, User } from './model' @@ -536,15 +537,15 @@ export const useDeleteBook = { +export const getSearchGoogleBooksUrl = (params?: SearchGoogleBooksParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { @@ -554,12 +555,12 @@ export const getSearchBooksUrl = (params?: SearchBooksParams,) => { } }); - return normalizedParams.size ? `https://localhost:8787/books/search?${normalizedParams.toString()}` : `https://localhost:8787/books/search` + return normalizedParams.size ? `https://localhost:8787/googlebooks?${normalizedParams.toString()}` : `https://localhost:8787/googlebooks` } -export const searchBooks = async (params?: SearchBooksParams, options?: RequestInit): Promise => { +export const searchGoogleBooks = async (params?: SearchGoogleBooksParams, options?: RequestInit): Promise => { - const res = await fetch(getSearchBooksUrl(params), + const res = await fetch(getSearchGoogleBooksUrl(params), { ...options, method: 'GET' @@ -575,43 +576,124 @@ export const searchBooks = async (params?: SearchBooksParams, options?: RequestI -export const getSearchBooksQueryKey = (params?: SearchBooksParams,) => { - return [`https://localhost:8787/books/search`, ...(params ? [params]: [])] as const; +export const getSearchGoogleBooksQueryKey = (params?: SearchGoogleBooksParams,) => { + return [`https://localhost:8787/googlebooks`, ...(params ? [params]: [])] as const; } -export const getSearchBooksQueryOptions = >, TError = BadRequestResponse | InternalServerErrorResponse>(params?: SearchBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} +export const getSearchGoogleBooksQueryOptions = >, TError = BadRequestResponse | InternalServerErrorResponse>(params?: SearchGoogleBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} ) => { const {query: queryOptions, fetch: fetchOptions} = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getSearchBooksQueryKey(params); + const queryKey = queryOptions?.queryKey ?? getSearchGoogleBooksQueryKey(params); - const queryFn: QueryFunction>> = ({ signal }) => searchBooks(params, { signal, ...fetchOptions }); + const queryFn: QueryFunction>> = ({ signal }) => searchGoogleBooks(params, { signal, ...fetchOptions }); - return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } } -export type SearchBooksQueryResult = NonNullable>> -export type SearchBooksQueryError = BadRequestResponse | InternalServerErrorResponse +export type SearchGoogleBooksQueryResult = NonNullable>> +export type SearchGoogleBooksQueryError = BadRequestResponse | InternalServerErrorResponse /** - * @summary 書籍を検索する + * @summary Google Books から書籍を検索する */ -export function useSearchBooks>, TError = BadRequestResponse | InternalServerErrorResponse>( - params?: SearchBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} +export function useSearchGoogleBooks>, TError = BadRequestResponse | InternalServerErrorResponse>( + params?: SearchGoogleBooksParams, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} ): UseQueryResult & { queryKey: QueryKey } { - const queryOptions = getSearchBooksQueryOptions(params,options) + const queryOptions = getSearchGoogleBooksQueryOptions(params,options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + +/** + * @summary Google Books から特定の書籍の情報を取得する + */ +export type getGoogleBookResponse = { + data: GoogleBook; + status: number; + headers: Headers; +} + +export const getGetGoogleBookUrl = (volumeId: string,) => { + + + return `https://localhost:8787/googlebooks/${volumeId}` +} + +export const getGoogleBook = async (volumeId: string, options?: RequestInit): Promise => { + + const res = await fetch(getGetGoogleBookUrl(volumeId), + { + ...options, + method: 'GET' + + + } + + ) + const data = await res.json() + + return { status: res.status, data, headers: res.headers } +} + + + +export const getGetGoogleBookQueryKey = (volumeId: string,) => { + return [`https://localhost:8787/googlebooks/${volumeId}`] as const; + } + + +export const getGetGoogleBookQueryOptions = >, TError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse>(volumeId: string, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetGoogleBookQueryKey(volumeId); + + + + const queryFn: QueryFunction>> = ({ signal }) => getGoogleBook(volumeId, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(volumeId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } +} + +export type GetGoogleBookQueryResult = NonNullable>> +export type GetGoogleBookQueryError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse + + +/** + * @summary Google Books から特定の書籍の情報を取得する + */ + +export function useGetGoogleBook>, TError = BadRequestResponse | NotFoundResponse | InternalServerErrorResponse>( + volumeId: string, options?: { query?:UseQueryOptions>, TError, TData>, fetch?: RequestInit} + + ): UseQueryResult & { queryKey: QueryKey } { + + const queryOptions = getGetGoogleBookQueryOptions(volumeId,options) const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; @@ -1419,7 +1501,9 @@ export const getGetBookResponseMock = (): Book => ({"id":1,"title":"計算機プ export const getUpdateBookResponseMock = (): Book => ({"id":1,"title":"計算機プログラムの構造と解釈","authors":["Harold Abelson","Gerald Jay Sussman","Julie Sussman"],"publisher":"翔泳社","publishedDate":"2012-07-06","description":"言わずと知れた計算機科学の古典的名著","thumbnail":"http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api","isbn":"9784798135984","stock":1}) -export const getSearchBooksResponseMock = (): SearchBooks200 => ({"totalBook":30,"books":[{"id":"5-OgzgEACAAJ","title":"計算機プログラムの構造と解釈","authors":["Harold Abelson","Gerald Jay Sussman","Julie Sussman"],"publisher":"翔泳社","publishedDate":"2018-07-01","description":"言わずと知れた計算機科学の古典的名著","thumbnail":"http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api","isbn":"9784798135984"}]}) +export const getSearchGoogleBooksResponseMock = (overrideResponse: Partial< SearchGoogleBooks200 > = {}): SearchGoogleBooks200 => ({books: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({authors: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => (faker.string.alpha(20))), description: faker.helpers.arrayElement([faker.string.alpha(20), undefined]), id: faker.string.alpha(20), isbn: faker.helpers.arrayElement([faker.string.alpha(20), undefined]), publishedDate: faker.helpers.arrayElement([faker.string.alpha(20), undefined]), publisher: faker.helpers.arrayElement([faker.string.alpha(20), undefined]), thumbnail: faker.helpers.arrayElement([faker.string.alpha(20), undefined]), title: faker.string.alpha(20)})), totalBook: faker.number.int({min: undefined, max: undefined}), ...overrideResponse}) + +export const getGetGoogleBookResponseMock = (): GoogleBook => ({"id":"5-OgzgEACAAJ","title":"計算機プログラムの構造と解釈","authors":["Harold Abelson","Gerald Jay Sussman","Julie Sussman"],"publisher":"翔泳社","publishedDate":"2018-07-01","description":"言わずと知れた計算機科学の古典的名著","thumbnail":"http://books.google.com/books/content?id=LlH-oAEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api","isbn":"9784798135984"}) export const getGetUsersResponseMock = (overrideResponse: Partial< GetUsers200 > = {}): GetUsers200 => ({totalUser: faker.number.int({min: undefined, max: undefined}), users: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({id: faker.helpers.arrayElement([faker.number.int({min: undefined, max: undefined}), undefined]), name: faker.helpers.arrayElement([faker.string.alpha(20), undefined])})), ...overrideResponse}) @@ -1506,12 +1590,24 @@ export const getDeleteBookMockHandler = (overrideResponse?: void | ((info: Param }) } -export const getSearchBooksMockHandler = (overrideResponse?: SearchBooks200 | ((info: Parameters[1]>[0]) => Promise | SearchBooks200)) => { - return http.get('*/books/search', async (info) => { +export const getSearchGoogleBooksMockHandler = (overrideResponse?: SearchGoogleBooks200 | ((info: Parameters[1]>[0]) => Promise | SearchGoogleBooks200)) => { + return http.get('*/googlebooks', async (info) => { + + return new HttpResponse(JSON.stringify(overrideResponse !== undefined + ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) + : getSearchGoogleBooksResponseMock()), + { status: 200, + headers: { 'Content-Type': 'application/json' } + }) + }) +} + +export const getGetGoogleBookMockHandler = (overrideResponse?: GoogleBook | ((info: Parameters[1]>[0]) => Promise | GoogleBook)) => { + return http.get('*/googlebooks/:volumeId', async (info) => { return new HttpResponse(JSON.stringify(overrideResponse !== undefined ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) - : getSearchBooksResponseMock()), + : getGetGoogleBookResponseMock()), { status: 200, headers: { 'Content-Type': 'application/json' } }) @@ -1640,7 +1736,8 @@ export const getKITCCLibraryAPIMock = () => [ getGetBookMockHandler(), getUpdateBookMockHandler(), getDeleteBookMockHandler(), - getSearchBooksMockHandler(), + getSearchGoogleBooksMockHandler(), + getGetGoogleBookMockHandler(), getGetUsersMockHandler(), getCreateUserMockHandler(), getDeleteUsersMockHandler(), diff --git a/frontend/test/mocks/model/getGoogleBook200.ts b/frontend/test/mocks/model/getGoogleBook200.ts new file mode 100644 index 00000000..897406a1 --- /dev/null +++ b/frontend/test/mocks/model/getGoogleBook200.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.3.0 🍺 + * Do not edit manually. + * KITCC Library API + * OpenAPI spec version: 1.0.0 + */ + +export type GetGoogleBook200 = { + authors: string[]; + description?: string; + id: string; + isbn?: string; + publishedDate?: string; + publisher?: string; + thumbnail?: string; + title: string; +}; diff --git a/frontend/test/mocks/model/googleBook.ts b/frontend/test/mocks/model/googleBook.ts new file mode 100644 index 00000000..78d046a4 --- /dev/null +++ b/frontend/test/mocks/model/googleBook.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.3.0 🍺 + * Do not edit manually. + * KITCC Library API + * OpenAPI spec version: 1.0.0 + */ + +export interface GoogleBook { + authors: string[]; + description?: string; + id: string; + isbn?: string; + publishedDate?: string; + publisher?: string; + thumbnail?: string; + title: string; +} diff --git a/frontend/test/mocks/model/index.ts b/frontend/test/mocks/model/index.ts index 418e9ebf..26b9a1d2 100644 --- a/frontend/test/mocks/model/index.ts +++ b/frontend/test/mocks/model/index.ts @@ -16,6 +16,7 @@ export * from './error'; export * from './getBooks200'; export * from './getBooksParams'; export * from './getBooksSort'; +export * from './getGoogleBook200'; export * from './getLoans200'; export * from './getLoans200LoansItem'; export * from './getLoansParams'; @@ -23,6 +24,7 @@ export * from './getLoansSort'; export * from './getUsers200'; export * from './getUsers200UsersItem'; export * from './getUsersParams'; +export * from './googleBook'; export * from './internalServerErrorResponse'; export * from './loan'; export * from './loginBody'; @@ -30,6 +32,9 @@ export * from './notFoundResponse'; export * from './searchBooks200'; export * from './searchBooks200BooksItem'; export * from './searchBooksParams'; +export * from './searchGoogleBooks200'; +export * from './searchGoogleBooks200BooksItem'; +export * from './searchGoogleBooksParams'; export * from './unauthorizedResponse'; export * from './updateBookBody'; export * from './updateUserBody'; diff --git a/frontend/test/mocks/model/searchGoogleBooks200.ts b/frontend/test/mocks/model/searchGoogleBooks200.ts new file mode 100644 index 00000000..dd689b23 --- /dev/null +++ b/frontend/test/mocks/model/searchGoogleBooks200.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.3.0 🍺 + * Do not edit manually. + * KITCC Library API + * OpenAPI spec version: 1.0.0 + */ +import type { GoogleBook } from './googleBook'; + +export type SearchGoogleBooks200 = { + books: GoogleBook[]; + /** 総書籍数 */ + totalBook: number; +}; diff --git a/frontend/test/mocks/model/searchGoogleBooks200BooksItem.ts b/frontend/test/mocks/model/searchGoogleBooks200BooksItem.ts new file mode 100644 index 00000000..c41dda7c --- /dev/null +++ b/frontend/test/mocks/model/searchGoogleBooks200BooksItem.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.3.0 🍺 + * Do not edit manually. + * KITCC Library API + * OpenAPI spec version: 1.0.0 + */ + +export type SearchGoogleBooks200BooksItem = { + authors: string[]; + description?: string; + id: string; + isbn?: string; + publishedDate?: string; + publisher?: string; + thumbnail?: string; + title: string; +}; diff --git a/frontend/test/mocks/model/searchGoogleBooksParams.ts b/frontend/test/mocks/model/searchGoogleBooksParams.ts new file mode 100644 index 00000000..2f6f2f49 --- /dev/null +++ b/frontend/test/mocks/model/searchGoogleBooksParams.ts @@ -0,0 +1,37 @@ +/** + * Generated by orval v7.3.0 🍺 + * Do not edit manually. + * KITCC Library API + * OpenAPI spec version: 1.0.0 + */ + +export type SearchGoogleBooksParams = { +/** + * ページ番号 + */ +page?: string; +/** + * 1ページあたりの表示数 + */ +limit?: string; +/** + * 検索キーワード + */ +keyword?: string; +/** + * タイトル + */ +intitle?: string; +/** + * 著者 + */ +inauthor?: string; +/** + * 出版社 + */ +inpublisher?: string; +/** + * ISBN + */ +isbn?: string; +}; From 25c17981e0f64bdcc7909ceaf31c8b5743f05738 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 08:35:07 +0900 Subject: [PATCH 03/19] Change API implementation (#168) --- backend/src/api/book.ts | 166 +-------------------- backend/src/api/google-books.ts | 206 ++++++++++++++++++++++++++ backend/src/api/user.ts | 18 +-- backend/src/index.ts | 2 + backend/test/api/books.test.ts | 24 --- backend/test/api/google-books.test.ts | 26 ++++ 6 files changed, 249 insertions(+), 193 deletions(-) create mode 100644 backend/src/api/google-books.ts create mode 100644 backend/test/api/google-books.test.ts diff --git a/backend/src/api/book.ts b/backend/src/api/book.ts index 07675912..b43c1ba9 100644 --- a/backend/src/api/book.ts +++ b/backend/src/api/book.ts @@ -11,8 +11,6 @@ import { getBookResponse, getBooksQueryParams, getBooksResponse, - searchBooksQueryParams, - searchBooksResponse, updateBookBody, updateBookParams, updateBookResponse, @@ -21,158 +19,6 @@ import { isLoggedIn } from '../utils/auth'; const app = new Hono<{ Bindings: Env }>(); -// Google Books APIsのレスポンスボディ -// 200(OK)の場合 -interface GoogleBookVolume { - totalItems: number; - items?: [ - { - id: string; - volumeInfo: { - title: string; - authors?: string[]; - publisher?: string; - publishedDate?: string; - description?: string; - imageLinks?: { - thumbnail: string; - }; - industryIdentifiers?: { - type: string; - identifier: string; - }[]; - }; - }, - ]; -} -// 400(Bad Request)の場合 -interface GoogleApiError { - error: { - code: number; - message: string; - errors: { - message: string; - domain: string; - reason: string; - }[]; - }; -} - -// GET /search のレスポンス -interface GoogleBook { - id: string; - title: string; - authors: string[]; - publisher: string; - publishedDate: string; - description: string; - thumbnail: string; - isbn: string; -} - -app.get( - '/search', - zValidator('query', searchBooksQueryParams, (result, ctx) => { - if (!result.success) { - return ctx.json( - { - message: 'Query Parameter Validation Error', - error: result.error, - }, - 400, - ); - } - }), - async (ctx) => { - const query = ctx.req.valid('query'); - - const page = parseInt(query['page'] ?? '1'); - const limit = parseInt(query['limit'] ?? '10'); - - delete query['page']; - delete query['limit']; - - // 絞り込み条件を作成する - let terms = query['keyword'] ?? ''; - for (const [key, value] of Object.entries(query)) { - if (value) { - terms += `+${key}:${value}`; - } - } - - // クエリパラメータを作成する - const params = new URLSearchParams({ - q: terms, - startIndex: String((page - 1) * limit), - maxResults: String(limit), - key: ctx.env.GOOGLE_BOOKS_API_KEY, - }); - - // Google Books APIsにリクエストを送信する - // prettier-ignore - const response = await fetch(`https://www.googleapis.com/books/v1/volumes?${params}`); - if (response.status !== 200) { - const error: GoogleApiError = await response.json(); - return ctx.json(error, 400); - } - - const volumeResult: GoogleBookVolume = await response.json(); - // ヒットした書籍を格納する配列 - const hitBooks: GoogleBook[] = []; - - // 書籍がヒットしたか確認する - if (volumeResult.hasOwnProperty('items')) { - // ヒットした書籍を配列に格納する - for (const item of volumeResult.items ?? []) { - const book = item.volumeInfo; - - // ISBNを取得する - let isbn = undefined; - if (book.hasOwnProperty('industryIdentifiers')) { - // prettier-ignore - for(const identifier of book.industryIdentifiers!) { - if (identifier.type === 'ISBN_13') { - isbn = identifier.identifier; - break; - } else if (identifier.type === 'ISBN_10') { - isbn = identifier.identifier; - } - } - } - - // 書籍を配列に追加する - hitBooks.push({ - id: item.id, - title: book.title, - authors: book.authors ?? [], - publisher: book.publisher ?? '', - publishedDate: book.publishedDate ?? '', - description: book.description ?? '', - thumbnail: book.imageLinks?.thumbnail ?? '', - isbn: isbn ?? '', - }); - } - } - - const responseBody = { - totalBook: volumeResult.totalItems, - books: hitBooks, - }; - const result = searchBooksResponse.safeParse(responseBody); - if (!result.success) { - console.error(result.error); - return ctx.json( - { - message: 'Response Validation Error', - }, - 500, - ); - } else { - return ctx.json(result.data); - } - }, -); - app.get( '/', zValidator('query', getBooksQueryParams, (result, ctx) => { @@ -370,13 +216,13 @@ app.get( }), async (ctx) => { const param = ctx.req.valid('param'); - const id = parseInt(param['bookId']); + const bookId = parseInt(param['bookId']); const db = drizzle(ctx.env.DB); const books: SelectBook[] = await db .select() .from(bookTable) - .where(eq(bookTable.id, id)); + .where(eq(bookTable.id, bookId)); if (books.length === 0) { return ctx.notFound(); @@ -433,7 +279,7 @@ app.patch( } const param = ctx.req.valid('param'); - const id = parseInt(param['bookId']); + const bookId = parseInt(param['bookId']); const book = ctx.req.valid('json'); @@ -443,7 +289,7 @@ app.patch( updatedBook = await db .update(bookTable) .set(book) - .where(eq(bookTable.id, id)) + .where(eq(bookTable.id, bookId)) .returning(); } catch (err) { if (err instanceof Error) { @@ -500,12 +346,12 @@ app.delete( } const param = ctx.req.valid('param'); - const id = parseInt(param['bookId']); + const bookId = parseInt(param['bookId']); const db = drizzle(ctx.env.DB); const deletedBook = await db .delete(bookTable) - .where(eq(bookTable.id, id)) + .where(eq(bookTable.id, bookId)) .returning(); if (deletedBook.length === 0) { diff --git a/backend/src/api/google-books.ts b/backend/src/api/google-books.ts new file mode 100644 index 00000000..f8ace86f --- /dev/null +++ b/backend/src/api/google-books.ts @@ -0,0 +1,206 @@ +import { zValidator } from '@hono/zod-validator'; +import { Hono } from 'hono'; +import { + getGoogleBookParams, + getGoogleBookResponse, + searchGoogleBooksQueryParams, + searchGoogleBooksResponse, +} from '../schema'; + +const app = new Hono<{ Bindings: Env }>(); + +/* Google Books APIのレスポンスボディ */ +// Google Booksの書籍情報 +interface GoogleBooksVolumeInfo { + title: string; + authors?: string[]; + publisher?: string; + publishedDate?: string; + description?: string; + imageLinks?: { + thumbnail: string; + }; + industryIdentifiers?: { + type: string; + identifier: string; + }[]; +} +// 400(Bad Request)の場合 +interface GoogleAPIError { + error: { + code: number; + message: string; + errors: { + message: string; + domain: string; + reason: string; + }[]; + }; +} + +const GOOGLE_BOOOKS_BASE_URL = 'https://www.googleapis.com/books/v1/volumes'; + +const getGoogleBookInfo = (volumeInfo: GoogleBooksVolumeInfo) => { + // ISBNを取得する + let isbn = undefined; + if (volumeInfo.hasOwnProperty('industryIdentifiers')) { + for (const identifier of volumeInfo.industryIdentifiers!) { + if (identifier.type === 'ISBN_13') { + isbn = identifier.identifier; + break; + } else if (identifier.type === 'ISBN_10') { + isbn = identifier.identifier; + } + } + } + + // 書籍情報を返す + return { + title: volumeInfo.title, + authors: volumeInfo.authors ?? [], + publisher: volumeInfo.publisher ?? '', + publishedDate: volumeInfo.publishedDate ?? '', + description: volumeInfo.description ?? '', + thumbnail: volumeInfo.imageLinks?.thumbnail ?? '', + isbn: isbn ?? '', + }; +}; + +app.get( + '/', + zValidator('query', searchGoogleBooksQueryParams, (result, ctx) => { + if (!result.success) { + return ctx.json( + { + message: 'Query Parameter Validation Error', + error: result.error, + }, + 400, + ); + } + }), + async (ctx) => { + const query = ctx.req.valid('query'); + + const page = parseInt(query['page'] ?? '1'); + const limit = parseInt(query['limit'] ?? '10'); + + delete query['page']; + delete query['limit']; + + // 絞り込み条件を作成する + let terms = query['keyword'] ?? ''; + for (const [key, value] of Object.entries(query)) { + if (value) { + terms += `+${key}:${value}`; + } + } + + // クエリパラメータを作成する + const params = new URLSearchParams({ + q: terms, + startIndex: String((page - 1) * limit), + maxResults: String(limit), + key: ctx.env.GOOGLE_BOOKS_API_KEY, + }); + + // Google Books APIにリクエストを送信する + const response = await fetch(`${GOOGLE_BOOOKS_BASE_URL}?${params}`); + if (response.status !== 200) { + const error: GoogleAPIError = await response.json(); + return ctx.json(error, { status: response.status }); + } + + const volumeResult: { + totalItems: number; + items?: [ + { + id: string; + volumeInfo: GoogleBooksVolumeInfo; + }, + ]; + } = await response.json(); + // ヒットした書籍を格納する配列 + const hitBooks = []; + + // 書籍がヒットしたか確認する + if (volumeResult.hasOwnProperty('items')) { + // ヒットした書籍を配列に格納する + for (const item of volumeResult.items ?? []) { + // 書籍情報を取得する + const bookInfo = getGoogleBookInfo(item.volumeInfo); + + // 書籍を配列に追加する + hitBooks.push({ ...bookInfo, id: item.id }); + } + } + + const responseBody = { + totalBook: volumeResult.totalItems, + books: hitBooks, + }; + const result = searchGoogleBooksResponse.safeParse(responseBody); + if (!result.success) { + console.error(result.error); + return ctx.json( + { + message: 'Response Validation Error', + }, + 500, + ); + } else { + return ctx.json(result.data); + } + }, +); + +app.get( + '/:volumeId', + zValidator('param', getGoogleBookParams, (result, ctx) => { + if (!result.success) { + return ctx.json( + { + message: 'Path Paramter Validation Error', + error: result.error, + }, + 400, + ); + } + }), + async (ctx) => { + const param = ctx.req.valid('param'); + const volumeId = param['volumeId']; + + const response = await fetch(`${GOOGLE_BOOOKS_BASE_URL}/${volumeId}`); + if (response.status !== 200) { + console.log(response); + const error: GoogleAPIError = await response.json(); + return ctx.json(error, { status: response.status }); + } + + const { + id, + volumeInfo, + }: { + id: string; + volumeInfo: GoogleBooksVolumeInfo; + } = await response.json(); + + const bookInfo = getGoogleBookInfo(volumeInfo); + + const result = getGoogleBookResponse.safeParse({ ...bookInfo, id: id }); + if (!result.success) { + console.error(result.error); + return ctx.json( + { + message: 'Response Validation Error', + }, + 500, + ); + } else { + return ctx.json(result.data); + } + }, +); + +export default app; diff --git a/backend/src/api/user.ts b/backend/src/api/user.ts index 5900d4d0..0d871dbb 100644 --- a/backend/src/api/user.ts +++ b/backend/src/api/user.ts @@ -211,13 +211,13 @@ app.get( } const param = ctx.req.valid('param'); - const id = parseInt(param['userId']); + const userId = parseInt(param['userId']); const db = drizzle(ctx.env.DB); const users: SelectUser[] = await db .select() .from(userTable) - .where(eq(userTable.id, id)); + .where(eq(userTable.id, userId)); if (users.length === 0) { return ctx.notFound(); @@ -273,12 +273,12 @@ app.patch( } const param = ctx.req.valid('param'); - const id = parseInt(param['userId']); + const userId = parseInt(param['userId']); const userIdCookie = getCookie(ctx, 'user_id', 'secure'); - const userId = Number(userIdCookie); + const currentUserId = Number(userIdCookie); - if (id !== userId) { + if (currentUserId !== userId) { // ログインユーザと更新対象のユーザが異なる return ctx.json( { @@ -307,7 +307,7 @@ app.patch( const user = await db .select() .from(userTable) - .where(eq(userTable.id, id)); + .where(eq(userTable.id, userId)); if (user.length === 0) { return ctx.notFound(); @@ -335,7 +335,7 @@ app.patch( ? await generateHash(newUser.newPassword) : undefined, }) - .where(eq(userTable.id, id)) + .where(eq(userTable.id, userId)) .returning(); } catch (err) { if (err instanceof Error) { @@ -392,12 +392,12 @@ app.delete( } const param = ctx.req.valid('param'); - const id = parseInt(param['userId']); + const userId = parseInt(param['userId']); const db = drizzle(ctx.env.DB); const deletedUser = await db .delete(userTable) - .where(eq(userTable.id, id)) + .where(eq(userTable.id, userId)) .returning(); if (deletedUser.length === 0) { diff --git a/backend/src/index.ts b/backend/src/index.ts index 339fed07..d1dcc331 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,6 +4,7 @@ import { logger } from 'hono/logger'; import { prettyJSON } from 'hono/pretty-json'; import auth from './api/auth'; import book from './api/book'; +import googleBooks from './api/google-books'; import loan from './api/loan'; import user from './api/user'; @@ -40,5 +41,6 @@ app.route('/books', book); app.route('/users', user); app.route('/auth', auth); app.route('/loans', loan); +app.route('/googlebooks', googleBooks); export default app; diff --git a/backend/test/api/books.test.ts b/backend/test/api/books.test.ts index f65fae42..d2cc90aa 100644 --- a/backend/test/api/books.test.ts +++ b/backend/test/api/books.test.ts @@ -384,27 +384,3 @@ describe('DELETE /books', () => { expect(response.status).toBe(401); }); }); - -describe('GET /books/search', () => { - it('should return 400 when page is not a number', async () => { - // pageに数字以外を指定する - const response = await app.request('/books/search?page=a', {}, env); - - expect(response.status).toBe(400); - }); - - it('should return 400 when limit is not a number', async () => { - // limitに数字以外を指定する - const response = await app.request('/books/search?limit=a', {}, env); - - expect(response.status).toBe(400); - }); - - it('should return 400 when isbn is not 10|13 digits number', async () => { - // ISBNに10 or 13桁以外の数字を指定する - // prettier-ignore - const response = await app.request('/books/search?isbn=123456789', {}, env); - - expect(response.status).toBe(400); - }); -}); diff --git a/backend/test/api/google-books.test.ts b/backend/test/api/google-books.test.ts new file mode 100644 index 00000000..b06b12a1 --- /dev/null +++ b/backend/test/api/google-books.test.ts @@ -0,0 +1,26 @@ +import app from '@/src/index'; +import { env } from 'cloudflare:test'; + +describe('GET /googlebooks', () => { + it('should return 400 when page is not a number', async () => { + // pageに数字以外を指定する + const response = await app.request('/googlebooks?page=a', {}, env); + + expect(response.status).toBe(400); + }); + + it('should return 400 when limit is not a number', async () => { + // limitに数字以外を指定する + const response = await app.request('/googlebooks?limit=a', {}, env); + + expect(response.status).toBe(400); + }); + + it('should return 400 when isbn is not 10|13 digits number', async () => { + // ISBNに10 or 13桁以外の数字を指定する + // prettier-ignore + const response = await app.request('/googlebooks?isbn=123456789', {}, env); + + expect(response.status).toBe(400); + }); +}); From 41de022fcf62d5f97bf1a84580acb313d6ccdef3 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:09:05 +0900 Subject: [PATCH 04/19] Add API key to Google Books API request (#168) --- backend/src/api/google-books.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/api/google-books.ts b/backend/src/api/google-books.ts index f8ace86f..059444b6 100644 --- a/backend/src/api/google-books.ts +++ b/backend/src/api/google-books.ts @@ -171,9 +171,10 @@ app.get( const param = ctx.req.valid('param'); const volumeId = param['volumeId']; - const response = await fetch(`${GOOGLE_BOOOKS_BASE_URL}/${volumeId}`); + const response = await fetch( + `${GOOGLE_BOOOKS_BASE_URL}/${volumeId}?key=${ctx.env.GOOGLE_BOOKS_API_KEY}`, + ); if (response.status !== 200) { - console.log(response); const error: GoogleAPIError = await response.json(); return ctx.json(error, { status: response.status }); } From bfe23d21e17ba5c8a7dd2726232922ef33edd332 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:09:34 +0900 Subject: [PATCH 05/19] Install html-react-parser (#168) --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 101 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 3126e328..bd18596c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@tanstack/react-query": "^5.59.15", "add": "^2.0.6", "dayjs": "^1.11.13", + "html-react-parser": "^5.1.19", "isbot": "^4.1.0", "jotai": "^2.10.1", "jsonpath-plus": "^10.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 95ee4bfa..0b024ceb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + html-react-parser: + specifier: ^5.1.19 + version: 5.1.19(react@19.0.0-rc-09111202-20241011)(types-react@19.0.0-rc.1) isbot: specifier: ^4.1.0 version: 4.4.0 @@ -2243,6 +2246,19 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -2794,10 +2810,25 @@ packages: resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + html-dom-parser@5.0.11: + resolution: {integrity: sha512-iORudm2K0c0DYeEj4AbrG9PFzgp1dpFGkJUAiBlVTkeyaNf2YYIs1b0dF7rQUPnDZimkLx+Jls+CvRIKO/++Tg==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-react-parser@5.1.19: + resolution: {integrity: sha512-ecjQg5KDhM+Yv3tRRfdp0fYSdSYHI1FQEDqou0g8NO7mXuoK8ksbYGRjeslqWO6QWX3PKREVWnC8VS1FSZaFHA==} + peerDependencies: + '@types/react': 0.14 || 15 || 16 || 17 || 18 + react: 0.14 || 15 || 16 || 17 || 18 + peerDependenciesMeta: + '@types/react': + optional: true + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2863,6 +2894,9 @@ packages: inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -4015,6 +4049,9 @@ packages: react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-property@2.0.2: + resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -4448,9 +4485,15 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.16: + resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + sugarss@4.0.1: resolution: {integrity: sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==} engines: {node: '>=12.0'} @@ -7305,6 +7348,24 @@ snapshots: '@babel/runtime': 7.25.7 csstype: 3.1.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@16.4.5: {} duplexify@3.7.1: @@ -7600,7 +7661,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -7613,7 +7674,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -7635,7 +7696,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8150,11 +8211,33 @@ snapshots: dependencies: lru-cache: 7.18.3 + html-dom-parser@5.0.11: + dependencies: + domhandler: 5.0.3 + htmlparser2: 9.1.0 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 optional: true + html-react-parser@5.1.19(react@19.0.0-rc-09111202-20241011)(types-react@19.0.0-rc.1): + dependencies: + domhandler: 5.0.3 + html-dom-parser: 5.0.11 + react: 19.0.0-rc-09111202-20241011 + react-property: 2.0.2 + style-to-js: 1.1.16 + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -8220,6 +8303,8 @@ snapshots: inline-style-parser@0.1.1: {} + inline-style-parser@0.2.4: {} + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -9545,6 +9630,8 @@ snapshots: react: 19.0.0-rc-09111202-20241011 react-dom: 19.0.0-rc-09111202-20241011(react@19.0.0-rc-09111202-20241011) + react-property@2.0.2: {} + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.6(react@19.0.0-rc-09111202-20241011)(types-react@19.0.0-rc.1): @@ -10059,10 +10146,18 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.16: + dependencies: + style-to-object: 1.0.8 + style-to-object@0.4.4: dependencies: inline-style-parser: 0.1.1 + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + sugarss@4.0.1(postcss@8.4.47): dependencies: postcss: 8.4.47 From 0bd24ec352711fa0bbb85627ea83bd11c073543d Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:12:06 +0900 Subject: [PATCH 06/19] Delete BookDetailComponent (#168) --- .../book-detail/BookDetailComponent.tsx | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 frontend/app/components/book-detail/BookDetailComponent.tsx diff --git a/frontend/app/components/book-detail/BookDetailComponent.tsx b/frontend/app/components/book-detail/BookDetailComponent.tsx deleted file mode 100644 index cf8ad1d9..00000000 --- a/frontend/app/components/book-detail/BookDetailComponent.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Grid, rem, Stack } from '@mantine/core'; -import ErrorComponent from '~/components/common/error/ErrorComponent'; - -import { getBookResponse, getLoansResponse } from 'client/client'; -import BookDetailActionPanel from './BookDetailActionPanel'; -import BookDetailContent from './BookDetailContent'; - -interface BookDetailComponentProps { - bookResponse: getBookResponse; - loansResponse?: getLoansResponse; -} - -const BookDetailComponent = ({ - bookResponse, - loansResponse, -}: BookDetailComponentProps) => { - switch (bookResponse.status) { - case 400: - return ; - case 404: - return ; - case 500: - return ; - } - - return ( - - - - - - - - - - - ); -}; - -export default BookDetailComponent; From d0ac11c20db52eac1e066e3a7b5b3bb480dcd2d6 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:13:17 +0900 Subject: [PATCH 07/19] Parse book description (#168) --- frontend/app/components/book-detail/BookDetailDescription.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/book-detail/BookDetailDescription.tsx b/frontend/app/components/book-detail/BookDetailDescription.tsx index 0d549258..fecd4f33 100644 --- a/frontend/app/components/book-detail/BookDetailDescription.tsx +++ b/frontend/app/components/book-detail/BookDetailDescription.tsx @@ -1,11 +1,12 @@ import { Text } from '@mantine/core'; +import parse from 'html-react-parser'; interface BookDetailDescriptionProps { description: string; } const BookDetailDescription = ({ description }: BookDetailDescriptionProps) => { - return {description}; + return {parse(description)}; }; export default BookDetailDescription; From 584a0517735c2ca8448405f7edd55e4c99aec470 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:16:11 +0900 Subject: [PATCH 08/19] Define google book schema (#168) --- frontend/test/mocks/model/searchBooks200.ts | 8 +-- .../mocks/model/searchBooks200BooksItem.ts | 18 +++--- .../test/mocks/model/searchBooksParams.ts | 58 +++++++++---------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/frontend/test/mocks/model/searchBooks200.ts b/frontend/test/mocks/model/searchBooks200.ts index 494d8776..8dcb6cb3 100644 --- a/frontend/test/mocks/model/searchBooks200.ts +++ b/frontend/test/mocks/model/searchBooks200.ts @@ -4,10 +4,10 @@ * KITCC Library API * OpenAPI spec version: 1.0.0 */ -import type { SearchBooks200BooksItem } from './searchBooks200BooksItem'; +import type { GoogleBook } from './searchBooks200BooksItem'; export type SearchBooks200 = { - books: SearchBooks200BooksItem[]; - /** 総書籍数 */ - totalBook: number; + books: GoogleBook[]; + /** 総書籍数 */ + totalBook: number; }; diff --git a/frontend/test/mocks/model/searchBooks200BooksItem.ts b/frontend/test/mocks/model/searchBooks200BooksItem.ts index 992d48f5..bd31c698 100644 --- a/frontend/test/mocks/model/searchBooks200BooksItem.ts +++ b/frontend/test/mocks/model/searchBooks200BooksItem.ts @@ -5,13 +5,13 @@ * OpenAPI spec version: 1.0.0 */ -export type SearchBooks200BooksItem = { - authors: string[]; - description?: string; - id: string; - isbn?: string; - publishedDate?: string; - publisher?: string; - thumbnail?: string; - title: string; +export type GoogleBook = { + authors: string[]; + description?: string; + id: string; + isbn?: string; + publishedDate?: string; + publisher?: string; + thumbnail?: string; + title: string; }; diff --git a/frontend/test/mocks/model/searchBooksParams.ts b/frontend/test/mocks/model/searchBooksParams.ts index 769c60bd..2dc1800a 100644 --- a/frontend/test/mocks/model/searchBooksParams.ts +++ b/frontend/test/mocks/model/searchBooksParams.ts @@ -5,33 +5,33 @@ * OpenAPI spec version: 1.0.0 */ -export type SearchBooksParams = { -/** - * ページ番号 - */ -page?: string; -/** - * 1ページあたりの表示数 - */ -limit?: string; -/** - * 検索キーワード - */ -keyword?: string; -/** - * タイトル - */ -intitle?: string; -/** - * 著者 - */ -inauthor?: string; -/** - * 出版社 - */ -inpublisher?: string; -/** - * ISBN - */ -isbn?: string; +export type SearchGoogleBooksParams = { + /** + * ページ番号 + */ + page?: string; + /** + * 1ページあたりの表示数 + */ + limit?: string; + /** + * 検索キーワード + */ + keyword?: string; + /** + * タイトル + */ + intitle?: string; + /** + * 著者 + */ + inauthor?: string; + /** + * 出版社 + */ + inpublisher?: string; + /** + * ISBN + */ + isbn?: string; }; From f83fbe3df24477426d55fa94ab11fb82acded0b2 Mon Sep 17 00:00:00 2001 From: shunsei Date: Thu, 5 Dec 2024 16:22:10 +0900 Subject: [PATCH 09/19] Change search books type name (#168) --- .../GlobalBookDetailContent.tsx | 8 +++--- .../GlobalBookDetailContentTable.tsx | 4 +-- .../GlobalBookDetailControlButtons.tsx | 26 ++++++++++--------- .../GLobalBookSearchAuthorForm.tsx | 6 ++--- .../global-books/GlobalBookCards.tsx | 6 ++--- .../GlobalBookDetailSearchForm.tsx | 17 +++++++----- .../GlobalBookKeywordSearchForm.tsx | 15 ++++++----- .../global-books/GlobalBookListComponent.tsx | 18 ++++++------- .../GlobalBookSearchComponent.tsx | 20 +++++++------- .../global-books/GlobalBookSearchIsbnForm.tsx | 6 ++--- .../GlobalBookSearchKeywordForm.tsx | 6 ++--- .../GlobalBookSearchPublisherForm.tsx | 6 ++--- .../GlobalBookSearchTitleForm.tsx | 6 ++--- 13 files changed, 75 insertions(+), 69 deletions(-) diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx index f321d3ae..67cf95ac 100644 --- a/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx +++ b/frontend/app/components/global-book-detail/GlobalBookDetailContent.tsx @@ -1,12 +1,12 @@ import { Stack } from '@mantine/core'; -import { SearchBooks200BooksItem } from 'client/client.schemas'; -import GlobalBookDetailContentTable from './GlobalBookDetailContentTable'; -import BookDetailTitle from '../book-detail/BookDetailTitle'; +import { GoogleBook } from 'client/client.schemas'; import BookDetailDescription from '../book-detail/BookDetailDescription'; +import BookDetailTitle from '../book-detail/BookDetailTitle'; +import GlobalBookDetailContentTable from './GlobalBookDetailContentTable'; import GlobalBookDetailLink from './GlobalBookDetailLink'; interface GlobalBookDetailContentProps { - book: SearchBooks200BooksItem; + book: GoogleBook; bookId?: number; } diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx index 28c4564b..82c24a91 100644 --- a/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx +++ b/frontend/app/components/global-book-detail/GlobalBookDetailContentTable.tsx @@ -1,9 +1,9 @@ import { Group, rem, Stack, Table, Text } from '@mantine/core'; -import { SearchBooks200BooksItem } from 'client/client.schemas'; +import { GoogleBook } from 'client/client.schemas'; import GlobalBookDetailAuthorBadge from './GlobalBookDetailAuthorBadge'; interface GlobalBookDetailContentTableProps { - book: SearchBooks200BooksItem; + book: GoogleBook; } const GlobalBookDetailContentTable = ({ diff --git a/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx b/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx index dd0d25b4..1f17d3ab 100644 --- a/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx +++ b/frontend/app/components/global-book-detail/GlobalBookDetailControlButtons.tsx @@ -1,42 +1,44 @@ import { Button } from '@mantine/core'; import { useSubmit } from '@remix-run/react'; -import { CreateBookBody, SearchBooks200BooksItem } from 'client/client.schemas'; +import { CreateBookBody, GoogleBook } from 'client/client.schemas'; import { BiSolidBookAdd } from 'react-icons/bi'; interface GlobalBookDetailControlButtonsProps { - searchBook: SearchBooks200BooksItem; + book: GoogleBook; totalBook: number; } const GlobalBookDetailControlButtons = ({ - searchBook, + book, totalBook, }: GlobalBookDetailControlButtonsProps) => { const submit = useSubmit(); - const addBookData: CreateBookBody = { - authors: searchBook.authors, - description: searchBook.description ?? '', - isbn: searchBook.isbn ?? '', - publishedDate: searchBook.publishedDate ?? '', - publisher: searchBook.publisher ?? '', + const bookData: CreateBookBody = { + authors: book.authors, + description: book.description ?? '', + isbn: book.isbn ?? '', + publishedDate: book.publishedDate ?? '', + publisher: book.publisher ?? '', stock: 1, - thumbnail: searchBook.thumbnail, - title: searchBook.title, + thumbnail: book.thumbnail, + title: book.title, }; return (