From 2040ecc7af8d5f08660b6d00f66eabbd84a47e78 Mon Sep 17 00:00:00 2001 From: Vincent <407859+vincentchalamon@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:04:12 +0200 Subject: [PATCH] feat: introduce BookRepository as abstraction of OpenLibrary (#471) --- .github/workflows/ci.yml | 6 -- api/config/packages/framework.yaml | 4 + .../BookRepositoryInterface.php | 12 +++ .../BookRepository/ChainBookRepository.php | 31 +++++++ .../BookRepository/GutendexBookRepository.php | 40 +++++++++ .../OpenLibraryBookRepository.php | 47 ++++++++++ .../RestrictedBookRepositoryInterface.php | 15 ++++ api/src/Entity/Book.php | 3 +- .../State/Processor/BookPersistProcessor.php | 34 +++----- api/src/Validator/BookUrl.php | 25 ++++++ api/src/Validator/BookUrlValidator.php | 36 ++++++++ api/tests/Api/Admin/BookTest.php | 24 +++--- .../Processor/BookPersistProcessorTest.php | 86 ++----------------- pwa/components/admin/book/BookInput.tsx | 58 +++++++++++-- pwa/next-env.d.ts | 2 +- pwa/tests/admin/BookCreate.spec.ts | 18 ++-- pwa/tests/admin/BookEdit.spec.ts | 14 +-- pwa/types/Gutendex/Search.ts | 8 ++ pwa/types/Gutendex/SearchDoc.ts | 8 ++ pwa/types/OpenLibrary/Book.ts | 4 +- pwa/types/OpenLibrary/Search.ts | 2 +- pwa/types/OpenLibrary/Work.ts | 2 +- 22 files changed, 332 insertions(+), 147 deletions(-) create mode 100644 api/src/BookRepository/BookRepositoryInterface.php create mode 100644 api/src/BookRepository/ChainBookRepository.php create mode 100644 api/src/BookRepository/GutendexBookRepository.php create mode 100644 api/src/BookRepository/OpenLibraryBookRepository.php create mode 100644 api/src/BookRepository/RestrictedBookRepositoryInterface.php create mode 100644 api/src/Validator/BookUrl.php create mode 100644 api/src/Validator/BookUrlValidator.php create mode 100644 pwa/types/Gutendex/Search.ts create mode 100644 pwa/types/Gutendex/SearchDoc.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c12f4184..c8e48c00a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,12 +209,6 @@ jobs: run: | docker compose ps docker compose logs - - - name: Debug Services - if: failure() - run: | - docker compose ps - docker compose logs - uses: actions/upload-artifact@v4 if: failure() diff --git a/api/config/packages/framework.yaml b/api/config/packages/framework.yaml index ec3dcaa22..8b53393bb 100644 --- a/api/config/packages/framework.yaml +++ b/api/config/packages/framework.yaml @@ -29,6 +29,10 @@ framework: # use scoped client to ease mock on functional tests security.authorization.client: base_uri: '%env(OIDC_SERVER_URL_INTERNAL)%/' + open_library.client: + base_uri: 'https://openlibrary.org/' + gutendex.client: + base_uri: 'https://gutendex.com/' when@test: framework: diff --git a/api/src/BookRepository/BookRepositoryInterface.php b/api/src/BookRepository/BookRepositoryInterface.php new file mode 100644 index 000000000..9920948bb --- /dev/null +++ b/api/src/BookRepository/BookRepositoryInterface.php @@ -0,0 +1,12 @@ + $repositories */ + public function __construct( + #[AutowireIterator(tag: RestrictedBookRepositoryInterface::TAG)] + private iterable $repositories, + ) { + } + + public function find(string $url): ?Book + { + foreach ($this->repositories as $repository) { + if ($repository->supports($url)) { + return $repository->find($url); + } + } + + return null; + } +} diff --git a/api/src/BookRepository/GutendexBookRepository.php b/api/src/BookRepository/GutendexBookRepository.php new file mode 100644 index 000000000..6d1d25445 --- /dev/null +++ b/api/src/BookRepository/GutendexBookRepository.php @@ -0,0 +1,40 @@ + ['Accept' => 'application/json']]; + $response = $this->gutendexClient->request('GET', $url, $options); + if (200 !== $response->getStatusCode()) { + return null; + } + + $book = new Book(); + + $data = $this->decoder->decode($response->getContent(), 'json'); + $book->title = $data['title']; + $book->author = $data['authors'][0]['name'] ?? null; + + return $book; + } +} diff --git a/api/src/BookRepository/OpenLibraryBookRepository.php b/api/src/BookRepository/OpenLibraryBookRepository.php new file mode 100644 index 000000000..a2ced9c30 --- /dev/null +++ b/api/src/BookRepository/OpenLibraryBookRepository.php @@ -0,0 +1,47 @@ + ['Accept' => 'application/json']]; + $response = $this->openLibraryClient->request('GET', $url, $options); + if (200 !== $response->getStatusCode()) { + return null; + } + + $book = new Book(); + + $data = $this->decoder->decode($response->getContent(), 'json'); + $book->title = $data['title']; + + $book->author = null; + if (isset($data['authors'][0]['key'])) { + $author = $this->openLibraryClient->request('GET', $data['authors'][0]['key'] . '.json', $options); + if (isset($author['name'])) { + $book->author = $author['name']; + } + } + + return $book; + } +} diff --git a/api/src/BookRepository/RestrictedBookRepositoryInterface.php b/api/src/BookRepository/RestrictedBookRepositoryInterface.php new file mode 100644 index 000000000..d0cd7ac94 --- /dev/null +++ b/api/src/BookRepository/RestrictedBookRepositoryInterface.php @@ -0,0 +1,15 @@ + @@ -24,8 +23,7 @@ public function __construct( #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, - private HttpClientInterface $client, - private DecoderInterface $decoder, + private BookRepositoryInterface $bookRepository, ) { } @@ -34,27 +32,17 @@ public function __construct( */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Book { - $book = $this->getData($data->book); - $data->title = $book['title']; - - $data->author = null; - if (isset($book['authors'][0]['key'])) { - $author = $this->getData('https://openlibrary.org' . $book['authors'][0]['key'] . '.json'); - if (isset($author['name'])) { - $data->author = $author['name']; - } + $book = $this->bookRepository->find($data->book); + + // this should never happen + if (!$book instanceof Book) { + throw new NotFoundHttpException(); } + $data->title = $book->title; + $data->author = $book->author; + // save entity return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } - - private function getData(string $uri): array - { - return $this->decoder->decode($this->client->request(Request::METHOD_GET, $uri, [ - 'headers' => [ - 'Accept' => 'application/json', - ], - ])->getContent(), 'json'); - } } diff --git a/api/src/Validator/BookUrl.php b/api/src/Validator/BookUrl.php new file mode 100644 index 000000000..a6b3f20eb --- /dev/null +++ b/api/src/Validator/BookUrl.php @@ -0,0 +1,25 @@ +message = $message ?? $this->message; + } + + public function getTargets(): string + { + return self::PROPERTY_CONSTRAINT; + } +} diff --git a/api/src/Validator/BookUrlValidator.php b/api/src/Validator/BookUrlValidator.php new file mode 100644 index 000000000..6a8a7bccb --- /dev/null +++ b/api/src/Validator/BookUrlValidator.php @@ -0,0 +1,36 @@ +bookRepository->find($value)) { + $this->context->buildViolation($constraint->message)->addViolation(); + } + } +} diff --git a/api/tests/Api/Admin/BookTest.php b/api/tests/Api/Admin/BookTest.php index bbfa1cdf1..6a1de90bf 100644 --- a/api/tests/Api/Admin/BookTest.php +++ b/api/tests/Api/Admin/BookTest.php @@ -241,7 +241,7 @@ public function asNonAdminUserICannotCreateABook(int $expectedCode, string $hydr $this->client->request('POST', '/admin/books', $options + [ 'json' => [ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::NewCondition->value, ], 'headers' => [ @@ -328,7 +328,7 @@ public static function getInvalidData(): iterable ]; yield 'invalid condition' => [ [ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => 'invalid condition', ], Response::HTTP_UNPROCESSABLE_ENTITY, @@ -377,7 +377,7 @@ public function asAdminUserICanCreateABook(): void $response = $this->client->request('POST', '/admin/books', [ 'auth_bearer' => $token, 'json' => [ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::NewCondition->value, ], 'headers' => [ @@ -390,10 +390,10 @@ public function asAdminUserICanCreateABook(): void self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); self::assertEquals('; rel="mercure"', $response->getHeaders(false)['link'][1]); self::assertJsonContains([ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::NewCondition->value, - 'title' => 'Foundation', - 'author' => 'Isaac Asimov', + 'title' => 'Youth', + 'author' => 'Asimov, Isaac', ]); self::assertMatchesJsonSchema(file_get_contents(__DIR__ . '/schemas/Book/item.json')); $id = preg_replace('/^.*\/(.+)$/', '$1', $response->toArray()['@id']); @@ -429,7 +429,7 @@ public function asNonAdminUserICannotUpdateBook(int $expectedCode, string $hydra $this->client->request('PUT', '/admin/books/' . $book->getId(), $options + [ 'json' => [ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::NewCondition->value, ], 'headers' => [ @@ -504,7 +504,7 @@ public function asAdminUserICannotUpdateABookWithInvalidData(array $data, int $s public function asAdminUserICanUpdateABook(): void { $book = BookFactory::createOne([ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', ]); self::getMercureHub()->reset(); @@ -517,7 +517,7 @@ public function asAdminUserICanUpdateABook(): void 'json' => [ '@id' => '/books/' . $book->getId(), // Must set all data because of standard PUT - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::DamagedCondition->value, ], 'headers' => [ @@ -530,10 +530,10 @@ public function asAdminUserICanUpdateABook(): void self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); self::assertEquals('; rel="mercure"', $response->getHeaders(false)['link'][1]); self::assertJsonContains([ - 'book' => 'https://openlibrary.org/books/OL28346544M.json', + 'book' => 'https://gutendex.com/books/31547.json', 'condition' => BookCondition::DamagedCondition->value, - 'title' => 'Foundation', - 'author' => 'Isaac Asimov', + 'title' => 'Youth', + 'author' => 'Asimov, Isaac', ]); self::assertMatchesJsonSchema(file_get_contents(__DIR__ . '/schemas/Book/item.json')); self::assertCount(1, self::getMercureMessages()); diff --git a/api/tests/State/Processor/BookPersistProcessorTest.php b/api/tests/State/Processor/BookPersistProcessorTest.php index 91beb3fd3..1c1d57f82 100644 --- a/api/tests/State/Processor/BookPersistProcessorTest.php +++ b/api/tests/State/Processor/BookPersistProcessorTest.php @@ -6,21 +6,17 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use App\BookRepository\BookRepositoryInterface; use App\Entity\Book; use App\State\Processor\BookPersistProcessor; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\Serializer\Encoder\DecoderInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; final class BookPersistProcessorTest extends TestCase { private MockObject|ProcessorInterface $persistProcessorMock; - private HttpClientInterface|MockObject $clientMock; - private MockObject|ResponseInterface $responseMock; - private DecoderInterface|MockObject $decoderMock; + private MockObject|BookRepositoryInterface $bookRepositoryMock; private Book|MockObject $objectMock; private MockObject|Operation $operationMock; private BookPersistProcessor $processor; @@ -28,17 +24,14 @@ final class BookPersistProcessorTest extends TestCase protected function setUp(): void { $this->persistProcessorMock = $this->createMock(ProcessorInterface::class); - $this->clientMock = $this->createMock(HttpClientInterface::class); - $this->responseMock = $this->createMock(ResponseInterface::class); - $this->decoderMock = $this->createMock(DecoderInterface::class); + $this->bookRepositoryMock = $this->createMock(BookRepositoryInterface::class); $this->objectMock = $this->createMock(Book::class); $this->objectMock->book = 'https://openlibrary.org/books/OL2055137M.json'; $this->operationMock = $this->createMock(Operation::class); $this->processor = new BookPersistProcessor( $this->persistProcessorMock, - $this->clientMock, - $this->decoderMock + $this->bookRepositoryMock ); } @@ -49,73 +42,10 @@ public function itUpdatesBookDataBeforeSaveAndSendMercureUpdates(): void $expectedData->title = 'Foundation'; $expectedData->author = 'Dan Simmons'; - $this->clientMock - ->expects($this->exactly(2)) - ->method('request') -// ->withConsecutive( -// [ -// Request::METHOD_GET, 'https://openlibrary.org/books/OL2055137M.json', [ -// 'headers' => [ -// 'Accept' => 'application/json', -// ], -// ], -// ], -// [ -// Request::METHOD_GET, 'https://openlibrary.org/authors/OL34221A.json', [ -// 'headers' => [ -// 'Accept' => 'application/json', -// ], -// ], -// ], -// ) - ->willReturnOnConsecutiveCalls($this->responseMock, $this->responseMock) - ; - $this->responseMock - ->expects($this->exactly(2)) - ->method('getContent') - ->willReturnOnConsecutiveCalls( - json_encode([ - 'title' => 'Foundation', - 'authors' => [ - ['key' => '/authors/OL34221A'], - ], - ]), - json_encode([ - 'name' => 'Dan Simmons', - ]), - ) - ; - $this->decoderMock - ->expects($this->exactly(2)) - ->method('decode') -// ->withConsecutive( -// [ -// json_encode([ -// 'title' => 'Foundation', -// 'authors' => [ -// ['key' => '/authors/OL34221A'], -// ], -// ]), -// 'json', -// ], -// [ -// json_encode([ -// 'name' => 'Dan Simmons', -// ]), -// 'json', -// ], -// ) - ->willReturnOnConsecutiveCalls( - [ - 'title' => 'Foundation', - 'authors' => [ - ['key' => '/authors/OL34221A'], - ], - ], - [ - 'name' => 'Dan Simmons', - ], - ) + $this->bookRepositoryMock + ->expects($this->once()) + ->method('find') + ->willReturn($expectedData) ; $this->persistProcessorMock ->expects($this->once()) diff --git a/pwa/components/admin/book/BookInput.tsx b/pwa/components/admin/book/BookInput.tsx index 1340bd848..57589f9ee 100644 --- a/pwa/components/admin/book/BookInput.tsx +++ b/pwa/components/admin/book/BookInput.tsx @@ -5,8 +5,10 @@ import { TextInput, type TextInputProps, useInput } from "react-admin"; import { useQuery } from "@tanstack/react-query"; import { useWatch } from "react-hook-form"; -import { Search } from "../../../types/OpenLibrary/Search"; -import { SearchDoc } from "../../../types/OpenLibrary/SearchDoc"; +import { Search as OpenLibrarySearch } from "../../../types/OpenLibrary/Search"; +import { SearchDoc as OpenLibrarySearchDoc } from "../../../types/OpenLibrary/SearchDoc"; +import { Search as GutendexSearch } from "../../../types/Gutendex/Search"; +import { SearchDoc as GutendexSearchDoc } from "../../../types/Gutendex/SearchDoc"; interface Result { title: string; @@ -35,10 +37,10 @@ const fetchOpenLibrarySearch = async ( next: { revalidate: 3600 }, } ); - const results: Search = await response.json(); + const results: OpenLibrarySearch = await response.json(); return results.docs - .filter((result: SearchDoc) => { + .filter((result: OpenLibrarySearchDoc) => { return ( typeof result.title !== "undefined" && typeof result.author_name !== "undefined" && @@ -68,6 +70,50 @@ const fetchOpenLibrarySearch = async ( } }; +const fetchGutendexSearch = async ( + query: string, + signal?: AbortSignal | undefined +): Promise> => { + try { + const response = await fetch( + `https://gutendex.com/books?search=${query.replace( + / - /, + " " + )}`, + { + signal, + method: "GET", + next: { revalidate: 3600 }, + } + ); + const results: GutendexSearch = await response.json(); + + return results.results + .filter((result: GutendexSearchDoc) => { + return ( + typeof result.id !== "undefined" && + typeof result.title !== "undefined" && + typeof result.authors !== "undefined" && + result.authors.length > 0 + ); + }) + .map(({ id, title, authors }): Result => { + return { + // @ts-ignore + title, + // @ts-ignore + author: authors[0].name, + // @ts-ignore + value: `https://gutendex.com/books/${id}.json`, + }; + }); + } catch (error) { + console.error(error); + + return Promise.resolve([]); + } +}; + export const BookInput = (props: BookInputProps) => { const { field: { ref, ...field }, @@ -89,7 +135,7 @@ export const BookInput = (props: BookInputProps) => { } controller.current = new AbortController(); - return await fetchOpenLibrarySearch( + return await fetchGutendexSearch( searchQuery, controller.current.signal ); @@ -132,7 +178,7 @@ export const BookInput = (props: BookInputProps) => { {...field} {...props} source="book" - label="Open Library Book" + label="Book Reference" /> )} /> diff --git a/pwa/next-env.d.ts b/pwa/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/pwa/next-env.d.ts +++ b/pwa/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/pwa/tests/admin/BookCreate.spec.ts b/pwa/tests/admin/BookCreate.spec.ts index e98960a1a..44465a802 100644 --- a/pwa/tests/admin/BookCreate.spec.ts +++ b/pwa/tests/admin/BookCreate.spec.ts @@ -7,12 +7,12 @@ test.describe("Create a book @admin", () => { }); test("I can create a book @write", async ({ bookPage, page }) => { - // fill in Open Library Book - await page.getByLabel("Open Library Book").fill("Foundation - Isaac Asimov"); - await page.getByRole("listbox").getByText("Foundation - Isaac Asimov", { exact: true }).waitFor({ state: "visible" }); - await page.getByRole("listbox").getByText("Foundation - Isaac Asimov", { exact: true }).click(); + // fill in Book Reference + await page.getByLabel("Book Reference").fill("Asimov"); + await page.getByRole("listbox").getByText("Let's Get Together - Asimov, Isaac", { exact: true }).waitFor({ state: "visible" }); + await page.getByRole("listbox").getByText("Let's Get Together - Asimov, Isaac", { exact: true }).click(); await expect(page.getByRole("listbox")).not.toBeAttached(); - await expect(page.getByLabel("Open Library Book")).toHaveValue("Foundation - Isaac Asimov"); + await expect(page.getByLabel("Book Reference")).toHaveValue("Let's Get Together - Asimov, Isaac"); // fill in condition await page.getByLabel("Condition").click(); @@ -23,18 +23,18 @@ test.describe("Create a book @admin", () => { // submit form await page.getByRole("button", { name: "Save", exact: true }).click(); - await expect(page.getByLabel("Open Library Book")).not.toBeAttached(); + await expect(page.getByLabel("Book Reference")).not.toBeAttached(); await expect(page.getByText("Element created")).toBeVisible(); }); // todo need work in api-platform/core about error handling // test("I cannot create a book with an already used Open Library value @read", async ({ bookPage, page }) => { - // // fill in Open Library Book - // await page.getByLabel("Open Library Book").fill("Hyperion - Dan Simmons"); + // // fill in Book Reference + // await page.getByLabel("Book Reference").fill("Hyperion - Dan Simmons"); // await page.getByRole("listbox").getByText("Hyperion - Dan Simmons", { exact: true }).waitFor({ state: "visible" }); // await page.getByRole("listbox").getByText("Hyperion - Dan Simmons", { exact: true }).click(); // await expect(page.getByRole("listbox")).not.toBeAttached(); - // await expect(page.getByLabel("Open Library Book")).toHaveValue("Hyperion - Dan Simmons"); + // await expect(page.getByLabel("Book Reference")).toHaveValue("Hyperion - Dan Simmons"); // // // fill in condition // await page.getByLabel("Condition").click(); diff --git a/pwa/tests/admin/BookEdit.spec.ts b/pwa/tests/admin/BookEdit.spec.ts index 519ced0e6..5ead784bb 100644 --- a/pwa/tests/admin/BookEdit.spec.ts +++ b/pwa/tests/admin/BookEdit.spec.ts @@ -7,12 +7,12 @@ test.describe("Edit a book @admin", () => { }); test("I can edit a book @write", async ({ page }) => { - // fill in Open Library Book - await page.getByLabel("Open Library Book").fill("Eon - Greg Bear"); - await page.getByRole("listbox").getByText("Eon - Greg Bear", { exact: true }).waitFor({ state: "visible" }); - await page.getByRole("listbox").getByText("Eon - Greg Bear", { exact: true }).click(); + // fill in Book Reference + await page.getByLabel("Book Reference").fill("Asimov"); + await page.getByRole("listbox").getByText("The Genetic Effects of Radiation - Asimov, Isaac", { exact: true }).waitFor({ state: "visible" }); + await page.getByRole("listbox").getByText("The Genetic Effects of Radiation - Asimov, Isaac", { exact: true }).click(); await expect(page.getByRole("listbox")).not.toBeAttached(); - await expect(page.getByLabel("Open Library Book")).toHaveValue("Eon - Greg Bear"); + await expect(page.getByLabel("Book Reference")).toHaveValue("The Genetic Effects of Radiation - Asimov, Isaac"); // fill in condition await page.getByLabel("Condition").click(); @@ -23,7 +23,7 @@ test.describe("Edit a book @admin", () => { // submit form await page.getByRole("button", { name: "Save", exact: true }).click(); - await expect(page.getByLabel("Open Library Book")).not.toBeAttached(); + await expect(page.getByLabel("Book Reference")).not.toBeAttached(); await expect(page.getByText("Element updated")).toBeVisible(); }); @@ -33,7 +33,7 @@ test.describe("Edit a book @admin", () => { await expect(page.getByRole("button", { name: "Confirm" })).toBeVisible(); await page.getByRole("button", { name: "Confirm" }).click(); await page.getByRole("button", { name: "Confirm" }).waitFor({ state: "detached" }); - await expect(page.getByLabel("Open Library Book")).not.toBeAttached(); + await expect(page.getByLabel("Book Reference")).not.toBeAttached(); await expect(page.getByText("Element deleted")).toBeVisible(); }); }); diff --git a/pwa/types/Gutendex/Search.ts b/pwa/types/Gutendex/Search.ts new file mode 100644 index 000000000..b77ef5f69 --- /dev/null +++ b/pwa/types/Gutendex/Search.ts @@ -0,0 +1,8 @@ +import { SearchDoc } from "./SearchDoc"; + +export class Search { + constructor( + public results: Array, + ) { + } +} diff --git a/pwa/types/Gutendex/SearchDoc.ts b/pwa/types/Gutendex/SearchDoc.ts new file mode 100644 index 000000000..8e85da4f8 --- /dev/null +++ b/pwa/types/Gutendex/SearchDoc.ts @@ -0,0 +1,8 @@ +export class SearchDoc { + constructor( + public id?: number, + public title?: string, + public authors?: Array, + ) { + } +} diff --git a/pwa/types/OpenLibrary/Book.ts b/pwa/types/OpenLibrary/Book.ts index 9cb6c481e..d9fe5ef82 100644 --- a/pwa/types/OpenLibrary/Book.ts +++ b/pwa/types/OpenLibrary/Book.ts @@ -1,5 +1,5 @@ -import { type Description } from "../../types/OpenLibrary/Description"; -import { type Item } from "../../types/OpenLibrary/Item"; +import { type Description } from "./Description"; +import { type Item } from "./Item"; export class Book { constructor( diff --git a/pwa/types/OpenLibrary/Search.ts b/pwa/types/OpenLibrary/Search.ts index a5b0ffe92..5ff5bfd16 100644 --- a/pwa/types/OpenLibrary/Search.ts +++ b/pwa/types/OpenLibrary/Search.ts @@ -1,4 +1,4 @@ -import { SearchDoc } from "../../types/OpenLibrary/SearchDoc"; +import { SearchDoc } from "./SearchDoc"; export class Search { constructor( diff --git a/pwa/types/OpenLibrary/Work.ts b/pwa/types/OpenLibrary/Work.ts index 42fcffeb7..eb8e5632d 100644 --- a/pwa/types/OpenLibrary/Work.ts +++ b/pwa/types/OpenLibrary/Work.ts @@ -1,4 +1,4 @@ -import { type Description } from "../../types/OpenLibrary/Description"; +import { type Description } from "./Description"; export class Work { constructor(