Skip to content

Conversation

@niklhut
Copy link
Owner

@niklhut niklhut commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Email/password auth (signup/signin/signout), protected routes, ISBN lookup, two‑step add flow, book details, paginated library with multi‑select & bulk delete, OpenLibrary lookup and cover handling.
  • Improvements

    • New responsive header and book card, streamlined layout, updated logo, refreshed royal‑blue/amber theme, revised homepage copy and CTAs.
  • Infrastructure

    • Migration system switched to SQLite, blob storage support added, effect‑driven server orchestration and layered services.
  • Documentation

    • Added agent instructions and product pitch; README updated.
  • Chores

    • Removed example env and legacy DB snapshots; simplified CI and package scripts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

Rebrands project to Libroo, migrates persisted storage from PostgreSQL to SQLite with new migrations, introduces Effect-TS layered services (Db, Storage, Auth, Book, OpenLibrary), adds many server API routes, pages, components, auth composables/middleware, and updates build/CI/package configuration.

Changes

Cohort / File(s) Summary
Project identity & config
\.env.example, README.md, package.json, nuxt.config.ts, drizzle.config.ts, pnpm-workspace.yaml, renovate.json, .github/workflows/ci.yml, tsconfig.json
Rebrand to "librooapp"; update package metadata, packageManager and scripts; replace @nuxt/ui-pro with new modules; switch Drizzle/Nitro config toward SQLite and hub settings; add pnpm workspace ignore list; simplify CI; remove renovate baseBranches; adjust TypeScript project references.
UI theme, layout & assets
app/app.config.ts, app/assets/css/main.css, app/app.vue, app/layouts/default.vue, app/components/AppLogo.vue
Replace green palette with royal-blue/vivid-amber, change UI import to @nuxt/ui, remove static theme flag, add :root and dark-mode variables, simplify root App to use NuxtLayout/NuxtPage and add default layout, modify logo SVG sizing and color variables.
Components & UI
app/components/*
app/components/AppHeader.vue, app/components/BookCard.vue, app/components/TemplateMenu.vue, app/components/AppLogo.vue
Add AppHeader (auth-aware links, sign-out flow), add BookCard (typed props, selectable mode, emits), update TemplateMenu items/alignment, adjust logo asset.
Pages (public & auth)
app/pages/*
app/pages/index.vue, app/pages/auth/*.vue, app/pages/library/*
Replace home content with Libroo copy; add auth pages (login/register with Zod validation); add library pages: list (paginated, multi-select, bulk delete), add (ISBN lookup + add flow), detail (view/remove).
Auth composables & middleware
app/composables/auth.ts, app/composables/use-auth-client.ts (removed), app/middleware/auth.global.ts
Add useAuth composable and exported authClient using better-auth; remove legacy useAuthClient; add global SSR-safe auth route middleware performing redirect to /auth/login when required.
Database schemas & migrations (SQLite)
server/db/schema/*.ts, server/db/migrations/sqlite/*, server/db/migrations/sqlite/meta/*
Remove Postgres schema/migrations and add SQLite-specific schema split (auth.ts, domain.ts), add new SQLite migrations and meta snapshots for books, user_books, loans, account, session, user, verification and schema evolution.
Removed Postgres artifacts
server/db/schema.ts, server/db/index.ts, server/db/relations.ts, server/db/auth-schema.ts, server/db/migrations/*, server/db/migrations/meta/*
Delete previous PostgreSQL table definitions, relations, migration SQL and migration meta/journal files.
Server API routes
server/api/*
server/api/books/*, server/api/blob/[...pathname].get.ts
Add effect-based endpoints: paginated list, lookup, add, detail, delete, batch-delete for books; blob GET route; handlers use effectHandler and require auth; request validation via shared Zod schemas.
Services & repositories (Effect layers)
server/services/*, server/repositories/*
Add layered services and repositories: DbService, StorageService, AuthService, BookService; BookRepository and OpenLibraryRepository with OpenLibrary integration, cover download/storage and rich error types; provide Live Layers and Context tags.
Effect utilities & handler
server/utils/effect.ts, server/utils/effectHandler.ts, server/utils/auth.ts
Add MainLive composition and service wiring, runEffect helper, centralized error-to-H3 mapping, and effectHandler wrapper that enforces requireAuth; rework auth helpers to use hub:db schema and runtime config helpers.
Storage / Blob support
server/services/storage.service.ts, server/api/blob/*
Implement StorageService (put/get/delete/list) via dynamic hub:blob import with typed errors and helpers; implement blob GET API setting content-type and Cache-Control.
Business/domain logic
server/repositories/book.repository.ts, server/services/book.service.ts
New book repository and service exposing add/get/remove/batch flows, local-first lookup with OpenLibrary fallback, cover download & WebP conversion, blob storage, pagination mapping and error variants.
Validation & shared types
shared/utils/schemas.ts, shared/types/*
Add Zod validation schemas (ISBN, batch delete) and shared TypeScript types: LibraryBook, BookDetails, BookLookupResult, BatchDeleteResult, pagination types.
Build/dev tasks & removed hooks
server/tasks/db/migrate.ts, server/utils/nitro-hooks.ts, server/tsconfig.json
Remove programmatic migration task and nitro hook that copied Drizzle migrations; delete server-specific tsconfig; root tsconfig changed to explicit project references.
Docs & misc
Agents.md, Pitch.md, pnpm-workspace.yaml
Add architecture/agent docs and product pitch; add pnpm workspace ignore config and other miscellaneous docs.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant Middleware as Auth Middleware
    participant AuthClient as better-auth
    participant Server as Nitro/API
    participant DB as SQLite

    User->>Browser: Navigate to protected page (/library)
    Browser->>Middleware: route guard (meta.auth)
    Middleware->>AuthClient: authClient.useSession(useFetch)
    AuthClient->>Server: server-side session check
    Server->>DB: query session data
    alt session exists
        DB-->>Server: session record
        Server-->>AuthClient: session data
        Middleware-->>Browser: allow navigation
    else no session
        Middleware-->>Browser: redirect to /auth/login?redirect=...
    end
Loading
sequenceDiagram
    actor User
    participant Browser
    participant API as /api/books/lookup
    participant EffectHandler
    participant BookService
    participant Repo as BookRepository
    participant OpenLibrary
    participant Storage

    User->>Browser: Submit ISBN
    Browser->>API: POST /api/books/lookup { isbn }
    API->>EffectHandler: validate & auth
    EffectHandler->>BookService: lookupBook(isbn)
    BookService->>Repo: findByIsbn(isbn)
    alt found locally
        Repo-->>BookService: local data
    else not found
        BookService->>OpenLibrary: lookupByISBN
        OpenLibrary-->>BookService: metadata + coverUrl
        BookService->>Storage: download & putBlob
        Storage-->>BookService: blob pathname
        BookService->>Repo: insert book + user_books
    end
    BookService-->>API: return lookup result
    API-->>Browser: 200 + payload
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hop where royal-blue spines softly glow,

Amber tabs flicker where the lamplights go.
Sqlite seeds tucked snug in layered ground,
Effects hum steady, services all sound.
Libroo hops — catalog, lend, and grow.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Setup and very basic version' is vague and generic, using non-descriptive terms that fail to convey meaningful information about the extensive changeset, which includes database migrations, authentication, UI redesign, book management features, and fundamental project restructuring. Consider a more specific title that captures the primary objective, such as 'Initial project setup with auth, database, and book library features' or 'Establish Libroo foundation with SQLite, authentication, and book management'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ccd26a and de8de97.

📒 Files selected for processing (1)
  • server/utils/auth.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T21:08:13.933Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/utils/effect.ts:30-38
Timestamp: 2026-01-13T21:08:13.933Z
Learning: In Effect-TS code, when combining service tags (Context.Tag) for multiple services, use a union type (Context1.Tag | Context2.Tag) rather than an intersection (&). Service tags are nominal types and cannot be meaningfully intersected; a union correctly represents combined service requirements in the Effect<R> parameter, while runtime Layer composition handles provisioning. Apply this guidance to TypeScript files that define or compose Effect-TS service contexts (e.g., server/utils/effect.ts and similar files).

Applied to files:

  • server/utils/auth.ts
📚 Learning: 2026-01-13T21:24:30.028Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: shared/utils/schemas.ts:11-13
Timestamp: 2026-01-13T21:24:30.028Z
Learning: In TypeScript files using Zod v4.3.5 or newer, use the error option for both schema-level configuration and validators (min, max, etc.). The message parameter is deprecated. Example: z.string({ error: 'Required' }).min(10, { error: 'Too short' }). Apply this pattern to all Zod schema definitions in TS files to avoid deprecated API usage.

Applied to files:

  • server/utils/auth.ts
📚 Learning: 2026-01-15T15:10:03.761Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/services/storage.service.ts:55-92
Timestamp: 2026-01-15T15:10:03.761Z
Learning: In NuxtHub server-side code, the blob API from Cloudflare R2 is provided via the virtual module hub:blob and is auto-imported. In files under server/ (e.g., server/services/storage.service.ts), you can call blob.put(), blob.get(), blob.delete(), blob.list(), etc., without an explicit import. Do not add import statements for blob in server code. Ensure the files are under the server directory and that TypeScript paths resolve to the virtual module in NuxtHub.

Applied to files:

  • server/utils/auth.ts
🔇 Additional comments (7)
server/utils/auth.ts (7)

1-4: LGTM!

Imports are correctly structured. The use of NuxtHub virtual modules (hub:db and hub:db:schema) is appropriate for server-side code. Based on learnings, these are auto-imported in the NuxtHub server environment.


6-12: LGTM!

The interface is well-structured with clear separation of concerns. Using literal types for runtimeConfigKey provides compile-time safety for the known configuration keys.


14-52: Well-implemented unified secret resolution.

The function correctly:

  • Prioritizes environment variables, then falls back to Nuxt runtime config
  • Handles CLI contexts where useRuntimeConfig may not be available
  • Validates values with proper trim() checks for consistency
  • Enforces production safeguards appropriately

The type assertion on line 51 is safe since the isEmpty check guarantees value is a non-empty string at that point.


54-62: LGTM!

The function correctly enforces a production-only error when the auth secret is missing, addressing the previously identified security concern. The error message is clear and actionable.


64-71: LGTM!

Appropriately uses a warning instead of an error for the URL, as some production deployments might work with the default. The warning message clearly indicates potential authentication issues.


96-96: LGTM!

Exporting the Auth type alias using typeof auth is a good practice for ensuring type consistency across the codebase.


73-94: Configuration is correct and production-ready.

The better-auth instance is properly configured with:

  • Dynamic URL and secret resolution via helper functions with environment variable and Nuxt runtime config fallback
  • Drizzle adapter correctly wired to SQLite with schema
  • Email/password authentication enabled
  • Social providers placeholder for future expansion

Helper functions include proper validation and production safety checks, with a fallback for the auth secret and a warning when the auth URL is not configured in production.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/TemplateMenu.vue (1)

1-48: Remove the unused TemplateMenu component.

This component is not imported or used anywhere in the codebase and contains only external Nuxt UI Pro template links. The AppHeader component already provides all necessary Libroo-branded navigation.

🤖 Fix all issues with AI agents
In @app/middleware/auth.ts:
- Around line 1-8: The auth middleware's logic is inverted: when no session
exists (from authClient.useSession in defineNuxtRouteMiddleware) it currently
redirects only if to.meta.auth === false; change this to redirect when the route
requires auth (i.e., to.meta.auth is not strictly false). Update the conditional
around navigateTo('/auth/login') so it triggers when !session.value &&
to.meta.auth !== false (or equivalently to.meta.auth !== false/Boolean check for
truthy requirement), ensuring protected routes (to.meta.auth !== false) send
unauthenticated users to the login page.

In @app/pages/auth/login.vue:
- Around line 52-84: The catch block in onSubmit uses the banned catch (e: any);
change it to catch (e: unknown) and safely extract a message: check if e is an
instance of Error and use e.message, otherwise coerce to string (e.g., String(e)
or JSON.stringify fallback) before assigning to error.value and passing to
toast; update the catch handling in onSubmit to perform this type-safe
extraction and keep the rest of the error toast logic unchanged.
- Around line 16-25: The redirect query is not sanitized, enabling open-redirect
risks; update the computed redirectPath (where you read route.query.redirect) to
validate the value before use: only accept strings that start with a single '/'
and not with '//' (e.g., check redirectStr.startsWith('/') &&
!redirectStr.startsWith('//') or use a regex like /^\/(?!\/)/), otherwise fall
back to '/library'; keep the existing watch(session...) and
navigateTo(redirectPath.value) but ensure redirectPath.value always returns a
safe in-app path.
- Around line 44-48: The Zod schema uses legacy positional message args
(z.email('...'), z.string('...')) which is invalid in Zod v4; update the schema
definition used in schema to pass error messages via the unified error option
object: call z.email({ error: 'Please enter a valid email address' }) and
z.string({ error: 'Password is required' }).min(1, { error: 'Password is
required' }) so the email and password validators use the { error: ... } form
instead of positional string arguments.

In @app/pages/auth/register.vue:
- Around line 68-104: The catch block in onSubmit uses catch (e: any) which
fails CI; change it to catch (e: unknown) and safely narrow the error before
using .message (e.g., check if e is an instance of Error and extract message,
otherwise stringify or fallback to 'An unexpected error occurred'), then assign
error.value and use that safe message in the toast; update only the catch
section inside onSubmit to perform this type-safe error handling.
- Around line 55-64: The Zod schema uses outdated positional message args;
update the validation to use the v4 object-form error option by replacing
occurrences like z.string('...') and z.email('...') with z.string({ error: '...'
}) and z.email({ error: '...' }) respectively inside the const schema =
z.object({...}) definition (leave .min(...) and the .refine(...) intact),
ensuring each field (name, email, password, confirmPassword) uses the { error:
'...' } form so Zod v4.3.5 validates and reports messages correctly.

In @app/pages/library/[id].vue:
- Around line 37-67: The formatDate function incorrectly parses year-only
strings like "2015" as full Dates so the year-only branch never runs; update
formatDate to test the /^\d{4}$/ year-only pattern before attempting new
Date(...) and return the raw year when matched, then proceed with parsing other
formats; also remove any trailing whitespace on the affected lines in formatDate
to satisfy CI.
- Around line 74-94: The removeBook function currently uses an unsafe catch
signature (catch (error: any)) and fragile message extraction; change to catch
(error: unknown) and narrow the type before reading properties: check if it's a
string, an Error instance (use (error instanceof Error).message), or an object
with a data?.message or message field, and fall back to a generic message; then
pass that computed message into toast.add. Update references in removeBook
(userBookId, $fetch, toast) accordingly so CI no longer complains about any
typing and error messages are robust.

In @app/pages/library/index.vue:
- Around line 33-83: The problem is that books is a computed view over
data.value.items so when loadMore increments page and useFetch replaces data,
the UI shows only the new page; fix by maintaining an accumulated reactive list
and appending new page items instead of replacing them: introduce a ref like
accumulatedBooks and update books computed to return accumulatedBooks.value (and
update allSelected/hasBooks/selectedCount to use it), then change loadMore (or
the fetch response handler) to fetch the next page and push/concat new items
into accumulatedBooks.value while updating pagination.value from
data.value.pagination; ensure selectedBooks logic still references the book ids
from accumulatedBooks.

In @server/api/books/lookup.post.ts:
- Around line 11-36: localBookEffect is swallowing DB errors (returning null),
subjects JSON is parsed without try/catch, and OpenLibrary lookup uses raw
body.isbn instead of the normalized value; fix by letting database errors
propagate (or map them to a clear Effect failure) instead of catching to null in
localBookEffect so real DB failures surface, wrap JSON.parse of
localBook.subjects in a safe try/catch (or JSON.parse wrapper) and fall back to
null on parse errors, and ensure the OpenLibrary request uses normalizedISBN
(not body.isbn) so both local and remote lookups use the same normalized key.

In @server/db/schema/auth.ts:
- Around line 27-41: The account table schema defined by sqliteTable(account) is
missing a uniqueness constraint on the (providerId, accountId) pair; update the
sqliteTable call for account to include a constraints callback (e.g., add the
third argument or the (t) => [...] callback) and return
unique().on(t.providerId, t.accountId) so Drizzle enforces uniqueness for
providerId + accountId, keeping all existing columns (id, accountId, providerId,
userId, etc.) unchanged.

In @server/repositories/book.repository.ts:
- Around line 16-28: The BookNotFoundError defined in this file conflicts with
OpenLibrary's error tag; rename the repository-specific error tag (change class
BookNotFoundError to use a distinct tag like 'BookRepositoryNotFoundError' or
similarly unique identifier) and update all places that reference that class
(throws and imports) to the new name, then update effectHandler.ts to map/handle
the new tag and branch property access so it reads bookId for the repository
error and isbn for the OpenLibrary error.

In @server/utils/auth.ts:
- Around line 7-8: The current config uses a hardcoded fallback for the auth
secret (secret: process.env.BETTER_AUTH_SECRET ||
'libroo-dev-secret-change-in-production'), which is a security risk; change the
logic so that when NODE_ENV === 'production' (or isProduction flag) and
process.env.BETTER_AUTH_SECRET is missing/empty you throw an error during
startup, otherwise allow a dev-only fallback; update the code paths referencing
the secret key (the secret config property and any initialization that reads
BETTER_AUTH_SECRET) to validate presence and fail fast in production with a
clear error message.

In @server/utils/drizzle.ts:
- Around line 1-14: This file is dead/commented-out code; either delete it or
replace the commented blocks with a clear placeholder explaining intent. Remove
the file entirely if the Drizzle integration was intentionally removed (git
history preserves it); otherwise, if you want to keep a placeholder for future
Drizzle work, remove all commented code and add a single TODO comment at the top
referencing an issue/track ID and explaining that useDrizzle, DrizzleDB, and
schema were intentionally removed and will be reintroduced when the migration is
complete. Ensure no commented implementations remain and update any docs or
issue links referenced in the TODO.

In @server/utils/effect.ts:
- Around line 14-33: MainLive is redundantly re-providing BaseServicesLive
because RepositoriesLive already includes BookRepositoryLive wired with
BaseServicesLive; fix by making MainLive just the already-composed
RepositoriesLive (i.e., export MainLive as RepositoriesLive / directly re-export
the composed layer) instead of calling Layer.provideMerge(RepositoriesLive,
BaseServicesLive). Alternatively, for clearer separation move
OpenLibraryRepositoryLive into the RepositoriesLive composition alongside
BookRepositoryLive and then export MainLive as the single composed
RepositoriesLive so base services aren’t provided twice.
🟡 Minor comments (6)
README.md-17-17 (1)

17-17: Documentation inconsistency: Stack mentions PostgreSQL but implementation uses SQLite.

The README states "Drizzle ORM + PostgreSQL", but drizzle.config.ts shows the project now uses SQLite with a local file at .data/db/sqlite.db. Update the documentation to reflect the actual stack.

📝 Suggested fix
-- [Drizzle ORM](https://orm.drizzle.team/) + PostgreSQL
+- [Drizzle ORM](https://orm.drizzle.team/) + SQLite
app/pages/library/add.vue-33-35 (1)

33-35: Incorrect Zod 4 API usage for z.string().

In Zod 4, z.string() no longer accepts an error message as the first argument. The 'ISBN is required' message will be ignored. Use the unified error parameter or rely on .min() for the validation message.

Suggested fix
 const isbnSchema = z.object({
-  isbn: z.string('ISBN is required').min(10, 'ISBN must be at least 10 characters').max(17, 'ISBN is too long')
+  isbn: z.string().min(10, 'ISBN must be at least 10 characters').max(17, 'ISBN is too long')
 })

Based on library documentation for Zod 4.

app/composables/auth.ts-3-4 (1)

3-4: Add explicit baseURL to match server configuration.

The client-side createAuthClient should be configured with the same baseURL pattern as the server-side auth setup. Currently, the server explicitly sets baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000', but the client has no baseURL configuration and will attempt to infer it from requests, which the better-auth library explicitly marks as not recommended. Update the client configuration to:

export const authClient = createAuthClient({
  baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000'
})

This ensures consistent behavior across development and production environments.

app/components/AppHeader.vue-42-58 (1)

42-58: Fix linting errors flagged by CI.

The pipeline is failing due to formatting issues:

  • Line 51: More than 1 blank line not allowed
  • Lines 45-46, 53: Attributes should be on separate lines per vue/max-attributes-per-line
  • Line 58: Missing newline at end of file
🔧 Proposed fix
 <template>
   <UHeader :links="links">
     <template #left>
-      <NuxtLink to="/" class="flex items-center gap-2">
-        <UIcon name="i-lucide-book-open-check" class="text-2xl text-primary" />
+      <NuxtLink
+        to="/"
+        class="flex items-center gap-2"
+      >
+        <UIcon
+          name="i-lucide-book-open-check"
+          class="text-2xl text-primary"
+        />
         <span class="font-bold text-xl">Libroo</span>
       </NuxtLink>
     </template>
 
-
     <template #right>
-      <UNavigationMenu :items="links" class="hidden lg:flex" variant="link" />
+      <UNavigationMenu
+        :items="links"
+        class="hidden lg:flex"
+        variant="link"
+      />
 
       <UColorModeButton />
     </template>
   </UHeader>
 </template>
+
server/api/books/lookup.post.ts-17-28 (1)

17-28: Inconsistent ISBN format in responses.

The endpoint returns different ISBN formats depending on the outcome: normalized from OpenLibrary (line 61) or local DB (line 44), but raw body.isbn in the not-found error (line 75). Although lookupByISBN() normalizes internally (openLibrary.repository.ts line 119), the inconsistency creates confusing API responses.

Additionally, the only validation is a basic string check (line 18)—a clearly-invalid value like "abc" will still hit the database and API.

Use normalizedISBN consistently across all response paths, and add basic format validation:

Suggested fix
@@
+    const normalizedISBN = body.isbn.replace(/[-\s]/g, '')
+    if (!/^\d{10}(\d{3})?$/.test(normalizedISBN)) {
+      return yield* Effect.fail(
+        createError({ statusCode: 400, message: 'ISBN must be 10 or 13 digits' })
+      )
+    }
@@
     const lookupEffect = lookupByISBN(body.isbn).pipe(
       Effect.catchTag('BookNotFoundError', () =>
         Effect.succeed({
           found: false,
-          isbn: body.isbn,
+          isbn: normalizedISBN,
           message: 'Book not found on OpenLibrary'
         })
       )
     )
server/repositories/book.repository.ts-288-302 (1)

288-302: Misleading error property in removeFromLibrary.

When the deletion fails to find a record, BookNotFoundError is created with bookId: userBookId. This is semantically incorrect—it's a userBookId, not a bookId.

🐛 Suggested fix
           if (result.length === 0) {
-            return yield* Effect.fail(new BookNotFoundError({ bookId: userBookId }))
+            return yield* Effect.fail(new BookNotFoundError({ bookId: userBookId })) // Note: This is actually userBookId
           }

Or better, add a userBookId field to the error type if you rename it as suggested earlier.

🧹 Nitpick comments (31)
Pitch.md (1)

1-52: Excellent pitch document with clear vision and structure!

The document effectively articulates Libroo's purpose, target audience, and differentiators. The problem-solution structure is compelling, and the core pillars are well-defined.

Minor: Consider adjusting markdown heading hierarchy

The document starts with h3 (###) headings. Standard markdown practice is to start with h2 (##) after the main title (h1 #), incrementing by one level at a time. This is flagged by markdownlint but is purely a formatting convention.

If you'd like to follow standard markdown conventions:

-### **Vision**
+## **Vision**

Apply the same change to other top-level sections (The Problem, The Solution, Target Audience, Philosophy & Values).

drizzle.config.ts (1)

7-9: Consider making the database path configurable.

The hardcoded SQLite path works for local development, but consider using an environment variable for flexibility across different environments (development, testing, production).

💡 Optional improvement
+import process from 'node:process'
+
 export default defineConfig({
   dialect: 'sqlite',
   schema: './server/db/schema/index.ts',
   out: './server/db/migrations',
   dbCredentials: {
-    url: '.data/db/sqlite.db'
+    url: process.env.DATABASE_URL || '.data/db/sqlite.db'
   }
 })
app/components/AppLogo.vue (1)

9-37: Inconsistent fill color usage across paths.

The first two paths (lines 10-11, 14-15) use currentColor while the remaining paths use var(--ui-primary). If the parent element's color property differs from --ui-primary, parts of the logo will render in different colors.

Consider using var(--ui-primary) consistently across all paths, or currentColor throughout if the logo should inherit color from its context.

♻️ Suggested fix for consistency
     <path
       d="M377 200C379.16 200 381 198.209 381 196V103C381 103 386 112 395 127L434 194C435.785 197.74 439.744 200 443 200H470V50H443C441.202 50 439 51.4941 439 54V148L421 116L385 55C383.248 51.8912 379.479 50 376 50H350V200H377Z"
-      fill="currentColor"
+      fill="var(--ui-primary)"
     />
     <path
       d="M726 92H739C742.314 92 745 89.3137 745 86V60H773V92H800V116H773V159C773 169.5 778.057 174 787 174H800V200H783C759.948 200 745 185.071 745 160V116H726V92Z"
-      fill="currentColor"
+      fill="var(--ui-primary)"
     />
server/db/migrations/sqlite/meta/0002_snapshot.json (1)

277-295: Consider adding indexes on foreign key columns for query performance.

The loans table has a foreign key on user_book_id but no index. Similarly, user_books lacks indexes on user_id and book_id. As the dataset grows, queries joining these tables or filtering by these columns will benefit from indexes.

This is a generated snapshot file, so the change should be made in the schema definition (server/db/schema/domain.ts) and regenerated.

server/db/schema/domain.ts (1)

27-34: Consider adding a composite unique constraint on (userId, bookId).

The userBooks junction table allows inserting the same user-book pair multiple times. If a user should only own each book once, add a unique constraint to prevent duplicate entries.

♻️ Suggested fix using Drizzle's unique constraint
+import { sqliteTable, text, integer, unique } from 'drizzle-orm/sqlite-core'
-import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
 import { user } from './auth'

 // ... books table ...

 // User's book ownership (junction table)
 // Links users to books they own
-export const userBooks = sqliteTable('user_books', {
+export const userBooks = sqliteTable('user_books', {
   id: text('id').primaryKey(),
   userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
   bookId: text('book_id').notNull().references(() => books.id, { onDelete: 'cascade' }),
   addedAt: integer('added_at', { mode: 'timestamp' }).notNull()
-})
+}, (table) => [
+  unique().on(table.userId, table.bookId)
+])
app/components/BookCard.vue (3)

2-12: Unused bookId prop.

The bookId prop is defined but never used in the component. Either remove it or clarify its intended purpose.

Suggested fix
 interface Props {
   id: string
-  bookId: string
   title: string
   author: string
   isbn?: string | null
   coverPath?: string | null
   addedAt?: string | Date
   selected?: boolean
   selectable?: boolean
 }

84-88: Add keyboard accessibility to selectable mode.

The selectable <div> is only click-accessible. For keyboard users, add role="button", tabindex="0", and handle @keydown.enter / @keydown.space.

Suggested fix
   <div
     v-else
     class="block cursor-pointer"
+    role="button"
+    tabindex="0"
     @click="handleClick"
+    @keydown.enter="handleClick"
+    @keydown.space.prevent="handleClick"
   >

63-68: Formatting: class attribute should be on a new line.

Static analysis flagged this line. Consider reformatting for consistency.

Suggested fix
-          <div v-else class="w-full h-full flex items-center justify-center bg-muted aspect-[1/1.5]">
+          <div
+            v-else
+            class="w-full h-full flex items-center justify-center bg-muted aspect-[1/1.5]"
+          >
package.json (2)

21-21: Consider moving @nuxt/test-utils to devDependencies.

Test utilities are typically development-only and shouldn't be bundled with production code.

Suggested fix
   "dependencies": {
     ...
-    "@nuxt/test-utils": "3.22.0",
     ...
   },
   "devDependencies": {
     "@nuxt/eslint": "^1.12.1",
+    "@nuxt/test-utils": "3.22.0",
     "drizzle-kit": "^0.31.8",
     ...
   },

14-30: Inconsistent version pinning strategy.

Some packages use exact versions (e.g., "@nuxt/image": "2.0.0") while others use caret ranges (e.g., "nuxt": "^4.2.2"). Consider standardizing for predictable builds—either pin all versions or use ranges consistently.

server/db/schema/auth.ts (1)

43-50: Inconsistent nullable timestamps in verification table.

createdAt and updatedAt are nullable here but required (.notNull()) in other tables. Verify this is intentional per Better Auth requirements.

app/components/AppHeader.vue (1)

7-10: Consider adding error handling to sign-out flow.

If signOut() fails, the user is still redirected to /, potentially leaving them in an inconsistent auth state.

🔧 Optional improvement
 async function handleSignOut() {
-  await signOut()
-  router.push('/')
+  try {
+    await signOut()
+  } finally {
+    router.push('/')
+  }
 }
app/pages/library/[id].vue (1)

237-269: Add rel="noopener noreferrer" to external links opened with target="_blank".
Prevents tabnabbing for the Open Library links.
-->

app/pages/auth/login.vue (1)

90-92: Fix template formatting to satisfy CI (indentation/linebreak rules).
This is currently failing lint on Line 91.

-->

server/api/books/[id]/index.get.ts (1)

22-31: Preserve original DB error details safely.
new Error(\Database error: ${error}`)often becomes[object Object]; prefer String(error)(or includecause`) so logs are actionable.
-->

server/api/books/lookup.post.ts (1)

8-10: Public endpoint: consider rate-limit + caching to protect OpenLibrary and your server.
Since this is unauthenticated, it’s an easy target for request bursts.

Also applies to: 57-82

server/services/db.service.ts (1)

1-18: LGTM: clean Effect Tag/Layer facade for db access.
Optional: endpoints like server/api/books/lookup.post.ts could use getDb/DbService instead of importing db directly for consistency with the DI setup.

server/utils/effect.ts (1)

42-49: runEffect unused _event param: either use it or drop it for now.
If the goal is request-scoped dependencies, consider a follow-up pattern to inject per-request context; otherwise remove _event to keep the API tight.

app/pages/library/index.vue (1)

84-117: Bulk delete: prefer allSettled so one failure doesn’t cancel the batch.
With Promise.all (Line 97), a single failing delete prevents reporting partial success and skips cleanup logic you might want.

Proposed fix (partial success reporting)
@@
-    await Promise.all(deletePromises)
+    const results = await Promise.allSettled(deletePromises)
+    const successCount = results.filter(r => r.status === 'fulfilled').length
+    const failureCount = results.length - successCount
@@
     toast.add({
       title: 'Books removed',
-      description: `${selectedBooks.value.size} book(s) removed from your library`,
+      description: failureCount === 0
+        ? `${successCount} book(s) removed from your library`
+        : `${successCount} removed, ${failureCount} failed`,
       color: 'success'
     })
server/utils/effectHandler.ts (3)

38-58: Consider logging unexpected errors before returning a generic 500.

The errorToH3Error function silently swallows unrecognized errors by returning a generic message. In production, logging the original error (without exposing it to the client) would help with debugging.

♻️ Suggested improvement
   // Default to internal server error
-  const message = error instanceof Error ? error.message : 'Internal server error'
+  // Log unexpected errors for debugging (consider using a proper logger)
+  console.error('Unexpected error in effect handler:', error)
+  const message = 'Internal server error'
   return createError({ statusCode: 500, message })

91-98: Passing a dummy user when requireAuth: false is error-prone.

When needsAuth is false, a dummy user object { id: '', name: '', email: '' } is passed to the handler. This is misleading—handlers might inadvertently use this empty user assuming it's valid. Consider using effectHandlerPublic for non-auth endpoints instead, or make the user parameter optional/nullable.

♻️ Suggested alternative: Remove the `requireAuth` option

Since effectHandlerPublic already exists for public endpoints, consider removing the requireAuth option from effectHandler to avoid confusion:

-export function effectHandler<A>(
-  handler: (
-    event: H3Event,
-    user: { id: string, name: string, email: string }
-  ) => Effect.Effect<A, any, any>,
-  options: EffectHandlerOptions = {}
-) {
-  const { requireAuth: needsAuth = true } = options
+export function effectHandler<A>(
+  handler: (
+    event: H3Event,
+    user: { id: string, name: string, email: string }
+  ) => Effect.Effect<A, any, any>
+) {
   return defineEventHandler(async (event) => {
     const effect = Effect.gen(function* () {
-      let user: { id: string, name: string, email: string }
-
-      if (needsAuth) {
-        user = yield* requireAuth(event)
-      } else {
-        user = { id: '', name: '', email: '' }
-      }
-
+      const user = yield* requireAuth(event)
       return yield* handler(event, user)
     })

104-105: Consider refining the handler type signature to reduce reliance on type assertion.

The assertion as Effect.Effect<A, any, never> works correctly because MainLive does provide all services that handlers require: DbService, StorageService, AuthService, and BookRepository. However, the handler accepts Effect.Effect<A, any, any>, allowing theoretically unbounded service requirements. A more type-safe approach would be to constrain the handler signature to Effect.Effect<A, any, MainServices> or similar, eliminating the need for the assertion entirely.

server/services/auth.service.ts (2)

63-85: Simplify requireAuth by removing redundant Effect.flatMap.

The Effect.flatMap(..., user => Effect.succeed(user)) is a no-op that adds unnecessary complexity.

♻️ Simplified implementation
   requireAuth: event =>
-    Effect.flatMap(
-      Effect.tryPromise({
-        try: async () => {
-          const session = await auth.api.getSession({
-            headers: event.headers
-          })
-
-          if (!session) {
-            throw new UnauthorizedError({ message: 'Authentication required' })
-          }
-
-          return session.user as AuthUser
-        },
-        catch: (error) => {
-          if (error instanceof UnauthorizedError) {
-            return error
-          }
-          return new UnauthorizedError({ message: 'Authentication failed' })
-        }
-      }),
-      user => Effect.succeed(user)
-    )
+    Effect.tryPromise({
+      try: async () => {
+        const session = await auth.api.getSession({
+          headers: event.headers
+        })
+
+        if (!session) {
+          throw new UnauthorizedError({ message: 'Authentication required' })
+        }
+
+        return session.user as AuthUser
+      },
+      catch: (error) => {
+        if (error instanceof UnauthorizedError) {
+          return error
+        }
+        return new UnauthorizedError({ message: 'Authentication failed' })
+      }
+    })

41-61: Consider reusing requireAuth in getCurrentUser to reduce duplication.

Both methods have nearly identical session-fetching logic. getCurrentUser could delegate to requireAuth and wrap the result.

server/services/storage.service.ts (2)

27-79: Dynamic import is repeated in every method.

Consider caching the imported blob module or importing it once at the Layer level to avoid repeated dynamic imports on each operation.

♻️ Alternative: Import once at Layer creation
-export const StorageServiceLive = Layer.succeed(StorageService, {
-  put: (pathname, data, options) =>
-    Effect.tryPromise({
-      try: async () => {
-        const { blob } = await import('hub:blob')
+export const StorageServiceLive = Layer.effect(StorageService,
+  Effect.gen(function* () {
+    // Import blob module once
+    const { blob } = yield* Effect.promise(() => import('hub:blob'))
+    
+    return {
+      put: (pathname, data, options) =>
+        Effect.tryPromise({
+          try: async () => {
+            const result = await blob.put(pathname, data, {

44-44: Error message may produce [object Object] for non-Error exceptions.

When error is not an Error instance, string interpolation will produce unhelpful messages.

♻️ Suggested fix
-      catch: error => new Error(`Failed to put blob: ${error}`)
+      catch: error => new Error(`Failed to put blob: ${error instanceof Error ? error.message : String(error)}`)

Apply similarly to lines 53, 62, and 77.

server/repositories/openLibrary.repository.ts (4)

80-86: Remove dead code: extractWorkKeyFromUrl always returns null.

This function is declared but its implementation just returns null. It's never used in the codebase.

♻️ Remove unused function
-// Extract work key from edition key
-function extractWorkKeyFromUrl(url?: string): string | null {
-  if (!url) return null
-  // URL like: https://openlibrary.org/books/OL24303521M/...
-  // We need to find the works link from the book data
-  return null
-}

88-112: checkCoverExists lacks timeout protection.

HEAD requests to external services can hang indefinitely. Consider adding an AbortController with a timeout.

♻️ Add timeout to HEAD request
 async function checkCoverExists(coverUrl: string): Promise<boolean> {
   try {
     const urlWithParam = coverUrl.includes('?')
       ? `${coverUrl}&default=false`
       : `${coverUrl}?default=false`

-    const response = await fetch(urlWithParam, { method: 'HEAD' })
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), 5000)
+    
+    const response = await fetch(urlWithParam, { 
+      method: 'HEAD',
+      signal: controller.signal
+    })
+    
+    clearTimeout(timeoutId)

114-240: lookupByISBN makes up to 3 sequential API calls without timeout protection.

The function fetches from /api/books, then optionally /editions/{key}.json, then /works/{key}.json. Each call can hang, and failures in the optional calls are silently ignored. Consider:

  1. Adding timeouts to fetch calls
  2. Logging when optional enrichment fails (even if not propagating the error)
♻️ Add fetch helper with timeout
async function fetchWithTimeout(url: string, timeoutMs = 10000): Promise<Response> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
  try {
    const response = await fetch(url, { signal: controller.signal })
    return response
  } finally {
    clearTimeout(timeoutId)
  }
}

242-287: Silently returning null on storage failure may hide operational issues.

Line 284 catches all blob storage errors and returns null. While this prevents a missing cover from blocking book creation, it could mask persistent storage problems.

Consider logging the error before returning null:

-          Effect.catchAll(() => Effect.succeed(null))
+          Effect.catchAll((error) => {
+            console.error('Failed to store cover in blob storage:', error)
+            return Effect.succeed(null)
+          })
server/repositories/book.repository.ts (1)

104-200: Non-null assertions on book! after conditional reassignment.

After line 168 (book = newBook), the code uses book! multiple times. TypeScript can't track that book is definitely assigned after the if (!book) block. Consider refactoring to avoid the assertions.

♻️ Refactor to avoid non-null assertions
-          // If book doesn't exist, fetch from OpenLibrary and create it
-          if (!book) {
+          // If book doesn't exist, fetch from OpenLibrary and create it
+          const finalBook = book ?? (yield* Effect.gen(function* () {
             const openLibraryData = yield* lookupByISBN(isbn)
-
-            // Download cover to local storage
             const coverPath = yield* downloadCover(isbn, 'L')
-
             const newBookId = generateId()
             const now = new Date()
-
             const newBook = {
               id: newBookId,
               // ... rest of newBook
             }
-
             yield* Effect.tryPromise({
               try: () => dbService.db.insert(books).values(newBook),
               catch: error => new BookCreateError({ message: `Failed to insert book: ${error}` })
             })
-
-            book = newBook
-          }
+            return newBook
+          }))

           // Create userBooks entry
           // ... use finalBook instead of book!
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4a103 and ee7af94.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (64)
  • .env.example
  • .github/workflows/ci.yml
  • Agents.md
  • Pitch.md
  • README.md
  • app/app.config.ts
  • app/app.vue
  • app/assets/css/main.css
  • app/components/AppHeader.vue
  • app/components/AppLogo.vue
  • app/components/BookCard.vue
  • app/components/TemplateMenu.vue
  • app/composables/auth.ts
  • app/composables/use-auth-client.ts
  • app/layouts/default.vue
  • app/middleware/auth.ts
  • app/pages/auth/login.vue
  • app/pages/auth/register.vue
  • app/pages/index.vue
  • app/pages/library/[id].vue
  • app/pages/library/add.vue
  • app/pages/library/index.vue
  • drizzle.config.ts
  • nuxt.config.ts
  • package.json
  • pnpm-workspace.yaml
  • renovate.json
  • server/api/blob/[...pathname].get.ts
  • server/api/books/[id].delete.ts
  • server/api/books/[id]/index.get.ts
  • server/api/books/index.get.ts
  • server/api/books/index.post.ts
  • server/api/books/lookup.post.ts
  • server/db/auth-schema.ts
  • server/db/index.ts
  • server/db/migrations/0000_regular_malice.sql
  • server/db/migrations/0001_jazzy_living_lightning.sql
  • server/db/migrations/meta/0000_snapshot.json
  • server/db/migrations/meta/0001_snapshot.json
  • server/db/migrations/meta/_journal.json
  • server/db/migrations/sqlite/0000_equal_blade.sql
  • server/db/migrations/sqlite/0001_glossy_shooting_star.sql
  • server/db/migrations/sqlite/0002_jazzy_gauntlet.sql
  • server/db/migrations/sqlite/meta/0000_snapshot.json
  • server/db/migrations/sqlite/meta/0001_snapshot.json
  • server/db/migrations/sqlite/meta/0002_snapshot.json
  • server/db/migrations/sqlite/meta/_journal.json
  • server/db/relations.ts
  • server/db/schema.ts
  • server/db/schema/auth.ts
  • server/db/schema/domain.ts
  • server/repositories/book.repository.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/auth.service.ts
  • server/services/db.service.ts
  • server/services/storage.service.ts
  • server/tasks/db/migrate.ts
  • server/tsconfig.json
  • server/utils/auth.ts
  • server/utils/drizzle.ts
  • server/utils/effect.ts
  • server/utils/effectHandler.ts
  • server/utils/nitro-hooks.ts
  • tsconfig.json
💤 Files with no reviewable changes (15)
  • server/db/migrations/meta/_journal.json
  • server/db/index.ts
  • server/utils/nitro-hooks.ts
  • server/db/migrations/0001_jazzy_living_lightning.sql
  • server/db/migrations/meta/0000_snapshot.json
  • renovate.json
  • server/tasks/db/migrate.ts
  • app/composables/use-auth-client.ts
  • server/db/migrations/0000_regular_malice.sql
  • .env.example
  • server/db/migrations/meta/0001_snapshot.json
  • server/db/relations.ts
  • server/tsconfig.json
  • server/db/auth-schema.ts
  • server/db/schema.ts
🧰 Additional context used
🧬 Code graph analysis (12)
server/api/books/index.get.ts (3)
server/utils/effectHandler.ts (1)
  • effectHandler (79-113)
server/db/schema/auth.ts (1)
  • user (6-14)
server/repositories/book.repository.ts (2)
  • getLibrary (311-312)
  • UserBook (41-46)
server/db/schema/domain.ts (1)
server/db/schema/auth.ts (1)
  • user (6-14)
app/middleware/auth.ts (2)
server/db/schema/auth.ts (1)
  • session (16-25)
app/composables/auth.ts (1)
  • authClient (3-4)
server/services/db.service.ts (1)
server/utils/effect.ts (3)
  • Context (52-52)
  • Layer (52-52)
  • Effect (52-52)
server/api/blob/[...pathname].get.ts (2)
server/utils/effectHandler.ts (1)
  • effectHandlerPublic (118-133)
server/services/storage.service.ts (1)
  • getBlob (85-86)
server/api/books/[id].delete.ts (2)
server/utils/effectHandler.ts (1)
  • effectHandler (79-113)
server/repositories/book.repository.ts (1)
  • removeFromLibrary (317-318)
server/api/books/lookup.post.ts (3)
server/utils/effectHandler.ts (1)
  • effectHandlerPublic (118-133)
server/db/schema/domain.ts (1)
  • books (8-25)
server/repositories/openLibrary.repository.ts (1)
  • lookupByISBN (291-292)
server/api/books/[id]/index.get.ts (3)
server/utils/effectHandler.ts (1)
  • effectHandler (79-113)
server/services/db.service.ts (1)
  • DbService (10-10)
server/db/schema/domain.ts (2)
  • userBooks (29-34)
  • books (8-25)
server/services/auth.service.ts (3)
server/utils/effect.ts (4)
  • Data (52-52)
  • Effect (52-52)
  • Context (52-52)
  • Layer (52-52)
server/db/schema/auth.ts (2)
  • session (16-25)
  • user (6-14)
server/utils/auth.ts (1)
  • auth (6-27)
server/utils/effect.ts (5)
server/services/db.service.ts (2)
  • DbServiceLive (13-15)
  • DbService (10-10)
server/services/storage.service.ts (2)
  • StorageServiceLive (28-79)
  • StorageService (25-25)
server/services/auth.service.ts (2)
  • AuthServiceLive (41-86)
  • AuthService (38-38)
server/repositories/openLibrary.repository.ts (2)
  • OpenLibraryRepositoryLive (115-288)
  • OpenLibraryRepository (70-73)
server/repositories/book.repository.ts (2)
  • BookRepositoryLive (98-305)
  • BookRepository (90-90)
server/repositories/book.repository.ts (4)
server/repositories/openLibrary.repository.ts (5)
  • BookNotFoundError (7-10)
  • OpenLibraryApiError (12-14)
  • OpenLibraryRepository (70-73)
  • lookupByISBN (291-292)
  • downloadCover (294-295)
server/services/db.service.ts (1)
  • DbService (10-10)
server/services/storage.service.ts (1)
  • StorageService (25-25)
server/db/schema/domain.ts (2)
  • userBooks (29-34)
  • books (8-25)
server/utils/effectHandler.ts (2)
server/utils/effect.ts (2)
  • Effect (52-52)
  • MainLive (29-32)
server/services/auth.service.ts (1)
  • requireAuth (92-93)
🪛 GitHub Actions: ci
app/components/AppHeader.vue

[warning] 45-45: ESLint: vue/max-attributes-per-line violation. 'class' should be on a new line. Command: pnpm run lint.


[warning] 46-46: ESLint: vue/max-attributes-per-line violation. 'class' should be on a new line. Command: pnpm run lint.


[error] 51-51: ESLint: @stylistic/no-multiple-empty-lines violation. More than 1 blank line not allowed. Command: pnpm run lint.

🪛 GitHub Check: ci (ubuntu-latest, 22)
app/pages/auth/login.vue

[failure] 91-91:
Expected indentation of 17 spaces but found 8 spaces


[warning] 90-90:
'icon' should be on a new line


[warning] 90-90:
'title' should be on a new line


[warning] 90-90:
':loading' should be on a new line


[warning] 90-90:
':fields' should be on a new line


[warning] 90-90:
Expected a linebreak before this attribute


[failure] 74-74:
Unexpected any. Specify a different type

app/components/BookCard.vue

[warning] 63-63:
'class' should be on a new line

app/pages/library/[id].vue

[failure] 87-87:
Unexpected any. Specify a different type


[failure] 64-64:
Trailing spaces not allowed


[failure] 55-55:
Trailing spaces not allowed


[failure] 50-50:
Trailing spaces not allowed


[failure] 40-40:
Trailing spaces not allowed

app/components/AppHeader.vue

[failure] 58-58:
Newline required at end of file but not found


[warning] 53-53:
'variant' should be on a new line


[warning] 53-53:
'class' should be on a new line


[failure] 51-51:
More than 1 blank line not allowed


[warning] 46-46:
'class' should be on a new line


[warning] 45-45:
'class' should be on a new line

app/pages/auth/register.vue

[failure] 94-94:
Unexpected any. Specify a different type

🪛 LanguageTool
Pitch.md

[style] ~51-~51: Replacing this phrase with a shorter alternative might make your text sound more refined.
Context: ...ion is your business. Your library data belongs to you. * Aesthetic Utility: A clean inte...

(BELONG_TO_PRP)

🪛 markdownlint-cli2 (0.18.1)
Pitch.md

3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🔇 Additional comments (43)
.github/workflows/ci.yml (1)

14-25: LGTM! Action version upgrades are valid.

The actions/checkout@v6 is the latest version (v6.0.1, last published December 2, 2025), and actions/setup-node@v6 is also current as shown in the official documentation.

The workflow setup is well-structured with appropriate caching via pnpm.

pnpm-workspace.yaml (1)

1-6: LGTM!

The ignoredBuiltDependencies configuration is appropriate for avoiding unnecessary rebuilds of native/compiled dependencies during installation.

tsconfig.json (1)

1-10: LGTM!

This follows Nuxt 4's recommended TypeScript configuration using project references to the auto-generated tsconfig files.

server/db/migrations/sqlite/meta/_journal.json (1)

1-27: LGTM!

Auto-generated migration journal from drizzle-kit. The structure correctly tracks the three SQLite migrations.

server/db/migrations/sqlite/meta/0002_snapshot.json (1)

453-516: Missing composite unique constraint on user_books table.

The user_books junction table allows a user to add the same book multiple times since there's no unique constraint on (user_id, book_id). If a user should only have one entry per book, consider adding a composite unique constraint.

Agents.md (1)

1-112: Good project documentation for onboarding.

The documentation provides clear guidance on project architecture, conventions, and development workflow. This will help maintain consistency as the project grows.

nuxt.config.ts (2)

3-11: LGTM on module configuration.

The module additions align well with the project requirements: NuxtHub for database/blob storage, UI components, image handling, and test utilities.


25-32: Verify image provider configuration.

Setting provider: 'none' disables Nuxt Image's optimization features. Ensure this is intentional—if cover images are served from blob storage, you may still benefit from image optimization. Alternatively, consider using the ipx provider for local optimization.

server/db/schema/domain.ts (1)

8-25: Schema looks well-structured for the domain.

The books table design with unique ISBN for deduplication and optional metadata fields is appropriate. The timestamp handling with mode: 'timestamp' enables proper Date object mapping.

One minor consideration: storing subjects as a JSON text string works for SQLite, but you'll need to handle serialization/deserialization in application code.

server/db/migrations/sqlite/0000_equal_blade.sql (2)

18-28: Initial migration establishes base schema correctly.

The books table in this initial migration contains the core fields. The enhanced metadata columns (work_key, description, subjects, etc.) visible in domain.ts are presumably added in subsequent migrations (0001, 0002), which aligns with incremental schema evolution.


63-70: Verify foreign key execution order.

The user_books table references both user and books. Since SQLite's foreign key enforcement requires referenced tables to exist, ensure this migration runs after user (line 52) and books (line 18) table creation. The current ordering in this file handles this correctly.

app/pages/library/add.vue (2)

40-67: LGTM!

The lookup function properly handles loading state, clears previous results, and provides appropriate user feedback via toast notifications for both success and error scenarios.


69-99: LGTM!

The add-to-library flow properly guards against invalid state, manages loading indicators, and provides clear user feedback with navigation on success.

app/composables/auth.ts (1)

6-34: LGTM!

The useAuth composable provides a clean abstraction over the auth client with properly typed methods for session management and authentication flows.

server/db/schema/auth.ts (1)

6-25: LGTM!

The user and session tables are well-structured with appropriate constraints, unique indexes, and cascade delete behavior for the foreign key relationship.

app/app.config.ts (1)

1-9: LGTM!

The color configuration aligns with the new Libroo branding. Ensure the royal-blue and vivid-amber color scales are properly defined in the CSS.

app/layouts/default.vue (1)

1-13: LGTM!

Clean and simple default layout structure. The flex column with min-h-screen ensures proper full-height rendering, and the slot pattern correctly allows page content to be injected.

server/db/migrations/sqlite/0001_glossy_shooting_star.sql (1)

1-5: LGTM!

Migration correctly adds nullable columns for book metadata. The use of text type for subjects and publishers suggests JSON-serialized storage, which is a reasonable approach for SQLite.

server/db/migrations/sqlite/0002_jazzy_gauntlet.sql (1)

1-1: LGTM!

Simple migration adding the work_key column for OpenLibrary work identification.

server/api/books/index.get.ts (1)

9-34: LGTM!

Clean implementation with proper pagination bounds. The response DTO appropriately includes only essential fields for list views, keeping the payload lightweight.

server/api/books/[id].delete.ts (1)

5-23: LGTM!

Correct implementation with proper parameter validation and Effect-based error handling. The repository function signature matches the call order (userBookId, userId).

server/api/books/index.post.ts (1)

5-35: LGTM! Clean effect-based POST handler implementation.

The handler correctly:

  • Validates the request body with proper error handling
  • Checks ISBN presence and type
  • Delegates to the repository layer
  • Returns a well-structured response
app/app.vue (1)

1-11: LGTM! Clean app root using Nuxt layout system.

The simplified structure properly delegates UI to the layout system while keeping the UApp wrapper for @nuxt/ui styling.

server/api/blob/[...pathname].get.ts (2)

31-33: Good caching strategy for immutable blobs.

The 1-year max-age with immutable is appropriate for content-addressed storage. Make sure the blob pathnames include content hashes or are otherwise guaranteed to be immutable.


19-36: No changes needed - the return value is correct.

The getBlob service returns Effect.Effect<Blob | null, Error>, so blobData is a standard Web Blob object. Returning it directly is correct; the framework will handle the Blob response body properly. Accessing blobData.type to set the Content-Type header is also correct, as it's a standard property on Blob objects. The code is working as intended.

server/db/migrations/sqlite/meta/0000_snapshot.json (1)

367-374: SQLite boolean default representation.
email_verified is integer but default is false; confirm migration tooling maps this to 0 correctly in the emitted SQL.
-->

app/assets/css/main.css (1)

1-61: Theme variable setup looks coherent for Nuxt UI.
Only thing to double-check: setting --ui-radius: 0rem globally is a strong design choice—confirm it’s intentional for all components.
-->

server/api/books/[id]/index.get.ts (1)

46-61: Frontend type mismatch risk for addedAt (string vs Date/number).
The page types addedAt: string, but this handler returns row.user_books.addedAt (likely Date depending on drizzle column mode). Consider normalizing to ISO string explicitly to lock the API contract.

-->

app/pages/index.vue (1)

1-71: LGTM (copy + public homepage meta + auth links).
Just ensure your auth middleware/layout respects definePageMeta({ auth: false }) on this route.

server/utils/effectHandler.ts (3)

1-10: LGTM!

Clean imports and well-defined HttpErrorMapping interface for error translation.


12-36: LGTM!

Error mappings are well-structured with appropriate HTTP status codes for each domain error type.


118-133: LGTM!

effectHandlerPublic provides a clean way to handle public endpoints without authentication overhead.

server/services/auth.service.ts (2)

1-8: LGTM!

Clean imports and well-defined tagged error type for authentication failures.


10-29: LGTM!

The AuthUser and Session interfaces align with the database schema (server/db/schema/auth.ts) and Better Auth's session structure.

server/services/storage.service.ts (2)

1-25: LGTM!

Well-defined types and interface for blob storage operations.


81-92: LGTM!

Helper effects follow the established pattern for service access.

server/repositories/openLibrary.repository.ts (3)

1-14: LGTM!

Clean error types using Effect's Data.TaggedError pattern.


16-62: LGTM!

Comprehensive type definitions for OpenLibrary API responses.


290-295: LGTM!

Helper effects follow the established pattern.

server/repositories/book.repository.ts (4)

1-14: LGTM!

Clean imports with proper type-only imports for service dependencies.


30-63: LGTM!

Well-structured output types with proper pagination support.


65-95: LGTM!

Repository interface clearly defines the contract with proper Effect error types.


307-318: LGTM!

Helper effects follow the established pattern for service access.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In @app/middleware/auth.global.ts:
- Around line 1-18: The middleware references authClient but never imports it,
causing a runtime error; add the correct import for authClient at the top of the
file (matching how it’s exported from its module) and ensure you import any
required helpers used with it (e.g., useSession/useFetch usage stays unchanged);
verify whether authClient is a default or named export and update the import
statement accordingly so defineNuxtRouteMiddleware can call
authClient.useSession successfully.

In @app/pages/library/add.vue:
- Around line 33-35: The zod schema uses z.string('ISBN is required') which
won’t flag empty/missing fields in Zod 4; update the isbnSchema (and account for
formState.isbn initializing as an empty string) to assert presence explicitly by
replacing the error-parameter usage with a presence check such as
z.string().min(1, 'ISBN is required') and then keep the length constraint (e.g.,
.min(10, 'ISBN must be at least 10 characters').max(17, 'ISBN is too long')) so
the schema enforces both required and length validation for isbnSchema.

In @app/pages/library/index.vue:
- Around line 77-82: The loadMore flow currently just increments page (in
loadMore) which causes data to be refetched and replaces data.value/items;
change to maintain an accumulated list (e.g., allBooks ref) and update the UI to
use allBooks instead of data.items/books: create allBooks (ref<LibraryBook[]>),
watch data (or the fetch result) and if page.value === 1 replace allBooks.value,
otherwise append new items to allBooks.value; update loadMore to only increment
page (same) but ensure new fetch appends via the watcher; also ensure any
search/filter reset logic resets page.value to 1 and clears allBooks.value so
new queries start fresh; use identifiers loadMore, page, data, allBooks, books,
pagination in your changes.
- Around line 86-120: The deleteSelected function currently uses Promise.all
which aborts on the first failure and reports a generic error; change it to use
Promise.allSettled on the array built from
Array.from(selectedBooks.value).map(id => $fetch(`/api/books/${id}`, { method:
'DELETE' })), then compute how many settled as "fulfilled" vs "rejected",
collect failed ids (from the original selectedBooks list and the corresponding
settled results), and update the toast message to report successes and failures
(e.g., "X removed, Y failed" and include failed ids or error messages). Ensure
you still clear selectedBooks.value and toggle isSelectMode.value only for
successfully removed items (or leave selection for failed ids), keep
isDeleting.value toggled in finally, and call refresh() after handling partial
results.

In @server/api/blob/[...pathname].get.ts:
- Around line 5-37: The handler currently enforces authentication via
effectHandler/requireAuth but leaves the user param unused and sets a public
Cache-Control header; decide intended access model and adjust accordingly: if
blobs are public, switch to a non-authenticated handler (remove requireAuth
usage in effectHandler call or use a public variant) and keep the public
Cache-Control header; if blobs are private, remove the unused _user parameter
from the effectHandler callback, ensure requireAuth remains in effectHandler,
and change the setHeader call that sets "Cache-Control" to a private/no-cache
value appropriate for authenticated content. Reference
effectHandler/requireAuth, the unused _user parameter in the default export,
getBlob, and the setHeader lines when making the change.

In @server/api/books/[id]/index.get.ts:
- Line 5: The import in index.get.ts references symbols books and userBooks from
'hub:db:schema' which do not exist; open the schema module to discover the
actual exported table names (the ripgrep command suggested in the comment can
help), then either update this file's import to use the real exported
identifiers (e.g., whatever the schema actually exports) or add matching
re-exports in 'hub:db:schema' so that books and userBooks are exported; ensure
all subsequent code in index.get.ts uses the corrected identifiers consistently.
- Line 56: The code does an unsafe JSON.parse on bookData.subjects which can
throw; add a safe parser (e.g., safeJsonParse<T>(json: string | null): T | null)
or inline try-catch around JSON.parse and return null on failure, then replace
the current assignment subjects: bookData.subjects ?
JSON.parse(bookData.subjects) : null with subjects:
safeJsonParse(bookData.subjects) (or the inline try-catch result); ensure the
helper is exported/available in the same module or imported and that the
subjects property type allows null on parse failure.

In @server/api/books/lookup.post.ts:
- Line 58: The code uses a normalizedISBN for the local DB lookup but still
passes the raw body.isbn into lookupByISBN when building lookupEffect; update
the call so lookupByISBN receives normalizedISBN (not body.isbn) to ensure
consistent, normalized ISBN usage throughout (e.g., change the argument in
lookupEffect where lookupByISBN is invoked to normalizedISBN).
- Line 49: The inline JSON.parse of localBook.subjects in the response mapping
can throw on malformed JSON; update the mapping logic (where subjects:
localBook.subjects ? JSON.parse(localBook.subjects) : null) to safely parse:
wrap JSON.parse in a try/catch (or call a safeParse helper) and return null (or
an empty array) on parse errors, and optionally log the error via the existing
logger; make sure this change is applied where the response object for localBook
is constructed so the request cannot crash due to bad subjects JSON.
- Around line 30-36: The Effect.tryPromise wrapper around the DB lookup
(localBookEffect) swallows all errors by using catch: () => null, making it
impossible to tell "not found" from DB failures; change the catch to surface the
error (e.g., catch: (err) => { processLogger.error('DB lookup failed for ISBN',
err); throw err }) or simply rethrow the error so callers can distinguish
failures from a missing book, leaving only a null/undefined return for the
actual not-found case returned by db.select().from(books).where(eq(books.isbn,
normalizedISBN)).limit(1).

In @server/repositories/openLibrary.repository.ts:
- Line 167: bookData.notes can be an object, so change the description
assignment in openLibrary.repository.ts to normalize notes like description: if
bookData.notes is a string use it, otherwise use bookData.notes.value (or
notes?.value) and fall back to bookData.excerpts?.[0]?.text; also similarly
guard bookData.description if used elsewhere. Update the BookData interface (the
one that defines bookData / notes types) to allow notes?: string | { type?:
string; value?: string } so the compiler knows notes can be a string or an
object.
🧹 Nitpick comments (9)
app/pages/auth/login.vue (2)

26-30: Consider immediate check vs. watch for redirect.

The watch with { immediate: true } will trigger on initial load. However, if session is initially undefined or loading, this may not redirect immediately. Ensure the session is properly initialized before this check runs to avoid a flash of the login page for authenticated users.


114-122: Placeholder link for "Forgot password" functionality.

The "Forgot password" link points to #, indicating incomplete functionality. Consider either implementing this feature or removing the link to avoid user confusion.

Would you like me to open an issue to track implementing the password reset functionality?

server/utils/effectHandler.ts (2)

17-24: Consider adding error tags for completeness.

The error mappings handle several domain errors. As the application grows, consider extracting these mappings to a separate configuration file for easier maintenance, or using a discriminated union type to ensure all error types are handled.


108-112: Use H3's isError() utility for robust error detection instead of duck-typing.

The property check 'statusCode' in result && 'message' in result is fragile and may incorrectly identify unrelated objects as H3 errors. H3 provides a dedicated utility for this purpose.

♻️ Proposed fix
+import { isError } from 'h3'
+
 // ...
 
-    if (result && typeof result === 'object' && 'statusCode' in result && 'message' in result) {
+    if (isError(result)) {
       throw result
     }
app/pages/library/add.vue (2)

58-66: Consider extracting the duplicate error message extraction.

The same error message extraction pattern appears in both lookupISBN and addBookToLibrary. A shared helper would reduce duplication.

Example helper
function extractErrorMessage(err: unknown, fallback: string): string {
  if (err instanceof Error) return err.message
  return (err as { data?: { message?: string } })?.data?.message || fallback
}

Also applies to: 92-100


15-28: existsLocally flag is defined but not utilized in the UI.

The LookupResult interface includes existsLocally (line 27), which could be used to warn users that a book already exists in their library before they attempt to add it again. Consider displaying a notification or disabling the "Add to Library" button when existsLocally is true.

server/repositories/book.repository.ts (3)

205-215: Consider validating pagination parameters.

If page is 0 or negative, offset becomes negative. If pageSize is 0, Math.ceil(totalItems / pageSize) causes division by zero returning Infinity.

Consider adding validation or defensive defaults:

🛠️ Suggested validation
 try: async () => {
-  const { page, pageSize } = pagination
+  const page = Math.max(1, pagination.page)
+  const pageSize = Math.max(1, pagination.pageSize)
   const offset = (page - 1) * pageSize

Alternatively, validate at the API layer before calling this method.


280-280: Type assertion suggests a type mismatch with the schema.

The book.createdAt as unknown as string cast indicates the database may return a string rather than a Date. This is common with SQLite which stores dates as text.

Consider handling this consistently by either:

  1. Configuring Drizzle to handle date serialization
  2. Creating a helper function for date normalization across all repositories
🛠️ Helper function approach
function toDate(value: Date | string | number): Date {
  return value instanceof Date ? value : new Date(value)
}

Then use: createdAt: toDate(book.createdAt)


294-297: Misleading error property: bookId contains a userBookId.

In removeFromLibrary, the BookNotFoundError is constructed with bookId: userBookId, which is semantically incorrect and could confuse debugging.

🛠️ Suggested fix

Consider creating a dedicated UserBookNotFoundError type, or extend BookNotFoundError to accept userBookId:

 export class BookNotFoundError extends Data.TaggedError('BookNotFoundError')<{
   bookId?: string
   isbn?: string
+  userBookId?: string
 }> { }

Then:

-return yield* Effect.fail(new BookNotFoundError({ bookId: userBookId }))
+return yield* Effect.fail(new BookNotFoundError({ userBookId }))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee7af94 and 2b8261f.

📒 Files selected for processing (16)
  • Agents.md
  • app/components/AppHeader.vue
  • app/components/BookCard.vue
  • app/middleware/auth.global.ts
  • app/pages/auth/login.vue
  • app/pages/auth/register.vue
  • app/pages/library/[id].vue
  • app/pages/library/add.vue
  • app/pages/library/index.vue
  • nuxt.config.ts
  • server/api/blob/[...pathname].get.ts
  • server/api/books/[id]/index.get.ts
  • server/api/books/lookup.post.ts
  • server/repositories/book.repository.ts
  • server/repositories/openLibrary.repository.ts
  • server/utils/effectHandler.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/pages/library/[id].vue
  • app/components/AppHeader.vue
  • nuxt.config.ts
  • app/pages/auth/register.vue
  • Agents.md
🧰 Additional context used
🧬 Code graph analysis (3)
server/repositories/openLibrary.repository.ts (3)
server/repositories/book.repository.ts (1)
  • BookNotFoundError (17-20)
server/utils/effect.ts (4)
  • Data (52-52)
  • Effect (52-52)
  • Context (52-52)
  • Layer (52-52)
server/services/storage.service.ts (2)
  • StorageService (25-25)
  • putBlob (82-83)
app/middleware/auth.global.ts (2)
server/db/schema/auth.ts (1)
  • session (16-25)
app/composables/auth.ts (1)
  • authClient (3-4)
server/api/blob/[...pathname].get.ts (2)
server/utils/effectHandler.ts (1)
  • effectHandler (86-116)
server/services/storage.service.ts (1)
  • getBlob (85-86)
🪛 GitHub Actions: ci
server/api/books/[id]/index.get.ts

[error] 5-5: TypeScript error TS2305: Module '"hub:db:schema"' has no exported member 'books'.

🪛 GitHub Check: ci (ubuntu-latest, 22)
server/api/books/lookup.post.ts

[failure] 4-4:
Module '"hub:db:schema"' has no exported member 'books'.


[failure] 4-4:
Module '"hub:db:schema"' has no exported member 'books'.

server/api/books/[id]/index.get.ts

[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'books'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'books'.

server/repositories/book.repository.ts

[failure] 14-14:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 14-14:
Module '"hub:db:schema"' has no exported member 'books'.


[failure] 14-14:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 14-14:
Module '"hub:db:schema"' has no exported member 'books'.

🔇 Additional comments (21)
app/pages/auth/login.vue (3)

17-23: Good security practice with redirect path validation.

The regex /^\/(?!\/)/.test(redirect) correctly prevents open redirects by ensuring the path starts with a single / and not // (which could be interpreted as a protocol-relative URL).


58-90: LGTM!

The submit handler properly handles both API errors (via result.error) and exceptions, shows appropriate toast notifications, and manages loading state correctly with the finally block.


51-54: Verify the project's actual Zod version compatibility.

The code uses z.email() syntax, which is a Zod 4 feature (top-level format). In Zod 3, this would be z.string().email() instead. Confirm the project's package.json specifies Zod 4 as a dependency; if using Zod 3, the code will fail at runtime.

server/utils/effectHandler.ts (1)

42-67: LGTM on centralized error handling.

The handleError function provides a clean pattern for converting domain errors to HTTP errors. The use of Effect.logError for structured logging is appropriate. The tag-based error mapping is extensible.

server/api/blob/[...pathname].get.ts (1)

31-33: Long cache duration is appropriate for immutable content.

The 1-year cache with immutable directive is a good practice for content-addressable storage where the pathname includes a hash. Ensure pathnames are indeed content-addressed; otherwise, stale content could be served.

server/api/books/[id]/index.get.ts (1)

23-34: Good ownership verification pattern.

The query correctly enforces ownership by joining on userId and checking against the authenticated user. This prevents unauthorized access to other users' books.

app/components/BookCard.vue (2)

1-37: LGTM - Clean component setup with proper TypeScript typing.

The props interface, defaults, and event emission are well-structured. The coverUrl computed property correctly handles the blob path prefix.

Minor observation: the bookId prop (line 4) appears unused within this component - verify if it's needed for parent component logic or if it can be removed.


40-144: Dual rendering modes are well-implemented.

The template correctly separates navigable and selectable modes with appropriate event handling. The selection indicator with visual feedback (ring/shadow) provides good UX.

app/pages/library/add.vue (1)

113-346: Well-structured two-step UI flow.

The template cleanly separates the lookup and confirmation steps with appropriate loading states, error feedback via toasts, and clear visual hierarchy. Good use of Nuxt UI components.

app/pages/library/index.vue (1)

1-75: Good implementation of selection mode and UI states.

The selection logic with Set-based tracking, toggle functions, and visual feedback is well-implemented. The loading/empty states and responsive grid provide good UX.

Also applies to: 121-262

server/repositories/openLibrary.repository.ts (4)

7-14: Error types follow the codebase pattern correctly.

The BookNotFoundError and OpenLibraryApiError extend Data.TaggedError consistently with the existing pattern in book.repository.ts.


80-104: Cover existence check is thorough.

The checkCoverExists function properly handles the default=false parameter and content-length check to detect placeholder images. Good defensive implementation.


234-279: Cover download with WebP conversion is well-implemented.

The graceful degradation (returning null on failure rather than propagating errors) is appropriate for non-critical cover images. The sharp conversion to WebP with quality setting is a good optimization.


106-232: Complex but well-structured lookup implementation.

The multi-stage fetch (books API → edition → works) with proper error handling and data enrichment is comprehensive. The Effect pattern with tryPromise and typed error propagation is correctly implemented.

One consideration: the sequential cover URL validation (lines 146-151) adds latency. For a future optimization, these could be parallelized, but the current approach is acceptable for correctness.

server/api/books/lookup.post.ts (1)

4-4: The import import { books } from 'hub:db:schema' is valid. The hub: import alias is automatically configured by the @nuxthub/core module (loaded in nuxt.config.ts) based on the hub configuration. The books export exists in server/db/schema/domain.ts and is successfully used across multiple files without build or runtime failures.

Likely an incorrect or invalid review comment.

server/repositories/book.repository.ts (6)

16-28: LGTM!

Error types are well-structured using Effect's Data.TaggedError pattern, enabling type-safe error handling and pattern matching.


30-63: LGTM!

Well-defined interfaces with a reusable generic PaginatedResult<T> type for pagination.


65-87: LGTM!

The interface properly declares Effect signatures with comprehensive error types and service dependencies in the requirements channel.


89-95: LGTM!

Standard Effect context tag pattern, and crypto.randomUUID() is appropriate for generating unique IDs.


303-314: LGTM!

Idiomatic Effect-TS helper functions that provide convenient access to repository methods.


14-14: No issue found.

The books and userBooks tables are properly exported from server/db/schema/domain.ts. The hub:db:schema path alias is provided by NuxtHub and is correctly resolving throughout the codebase. The imports are valid and will not cause a CI failure.

Likely an incorrect or invalid review comment.

Comment on lines 33 to 35
const isbnSchema = z.object({
isbn: z.string('ISBN is required').min(10, 'ISBN must be at least 10 characters').max(17, 'ISBN is too long')
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and content
fd -e vue app/pages/library/add.vue

Repository: niklhut/libroo

Length of output: 40


🌐 Web query:

Zod 4 z.string() error message parameter required field validation

💡 Result:

Use the new unified error param and check whether the input is undefined. Example:

const schema = z.string({
error: (issue) => issue.input === undefined ? 'This field is required' : 'Must be a string'
});

  • The Zod v4 API unifies previous required_error/invalid_type_error into the single error option (the error function receives the issue and can return a string). [1][2][3]

Sources:
[1] Zod API docs (Defining schemas).
[2] Zod v4 migration / changelog (shows required_error removed and example using error).
[3] Zod custom-error docs (example using issue.input === undefined).


🏁 Script executed:

fd add.vue

Repository: niklhut/libroo

Length of output: 83


🏁 Script executed:

# Also check the repository structure
git ls-files | grep -E "add\.(vue|ts|js)$"

Repository: niklhut/libroo

Length of output: 83


🏁 Script executed:

cat -n app/pages/library/add.vue | head -50

Repository: niklhut/libroo

Length of output: 1534


Zod 4 requires unified error parameter for required-field validation.

In Zod 4, z.string('ISBN is required') passes the string to the error parameter for type coercion errors only—when non-strings are coerced to strings. This message won't appear for missing/undefined fields. Since formState.isbn initializes as an empty string, use .min(1, 'ISBN is required') to catch empty input, or use the unified error parameter with a function checking issue.input === undefined for complete control over both required and type-error messages.

🤖 Prompt for AI Agents
In @app/pages/library/add.vue around lines 33 - 35, The zod schema uses
z.string('ISBN is required') which won’t flag empty/missing fields in Zod 4;
update the isbnSchema (and account for formState.isbn initializing as an empty
string) to assert presence explicitly by replacing the error-parameter usage
with a presence check such as z.string().min(1, 'ISBN is required') and then
keep the length constraint (e.g., .min(10, 'ISBN must be at least 10
characters').max(17, 'ISBN is too long')) so the schema enforces both required
and length validation for isbnSchema.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 25-32: The CI workflow runs the "Lint" step before "Prepare",
which can fail if postinstall generates artifacts the linter needs; reorder the
steps so the "Prepare" step (run: pnpm run postinstall) executes before "Lint"
(run: pnpm run lint), keeping "Typecheck" after both, to ensure generated type
definitions/stubs exist before linting.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 516cec8 and 61c0cec.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
🔇 Additional comments (1)
.github/workflows/ci.yml (1)

11-19: No action required — all specified versions exist and are current.

actions/checkout@v6 (latest: v6.0.2), actions/setup-node@v6 (latest: v6.1.0), and Node.js 24 (LTS "Krypton") are all released and available as of January 2026.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 14-32: The dependency versions in package.json are inconsistently
pinned (some use exact versions like "@nuxt/image": "2.0.0" and "@nuxt/scripts":
"0.13.2", while others use caret ranges like "@nuxt/ui": "^4.3.0"), causing
unpredictable installs; pick a consistent strategy (either switch all entries to
exact versions for reproducible installs or use caret ranges for flexibility),
update the dependency entries such as "@nuxt/image", "@nuxt/scripts",
"@nuxt/ui", "@nuxt/hints", "@nuxt/test-utils", etc. to follow that chosen
strategy, and ensure you commit the lockfile (package-lock.json or
pnpm-lock.yaml/yarn.lock) to guarantee deterministic installs.
- Line 23: Move the "@nuxt/test-utils" entry out of dependencies into
devDependencies in package.json: remove the "@nuxt/test-utils": "3.22.0" line
from the top-level "dependencies" object and add the same entry under
"devDependencies" so test utilities are not bundled in production; after
editing, run your package manager (npm/yarn/pnpm) to update lockfile and
node_modules.
- Around line 41-42: Update the packageManager field value from "pnpm@10.26.1"
to "pnpm@10.27.0" (or a later 10.27+ version) in package.json to address
CVE-2025-69262; after changing the packageManager entry, run pnpm install to
regenerate the lockfile (pnpm-lock.yaml) so the lockfile reflects the upgraded
pnpm version.

In `@server/repositories/book.repository.ts`:
- Line 2: The import of HttpClient in server/repositories/book.repository.ts is
type-only and should use a type-only import to satisfy the compiler/CI; change
the statement importing HttpClient to a type import (e.g., use "import type {
HttpClient } from '...'" for the symbol HttpClient) and ensure any usages remain
only in type positions (function signatures or type annotations) so no runtime
import is emitted.

In `@server/services/auth.service.ts`:
- Around line 37-69: Extract the duplicate session-fetching logic inside
AuthServiceLive into a shared helper (e.g., fetchSession or getSessionFromEvent)
that calls auth.api.getSession({ headers: event.headers }) via Effect.tryPromise
and throws the same UnauthorizedError on failure or missing session; then update
AuthServiceLive.getCurrentUser to call that helper and return the Session, and
update AuthServiceLive.requireAuth to call the same helper and return
session.user as AuthUser. Ensure the helper reuses the same error types/messages
used currently (UnauthorizedError) and reference auth.api.getSession,
getCurrentUser, and requireAuth when making the replacements.

In `@server/services/book.service.ts`:
- Line 2: The import of HttpClient in book.service.ts is only used for type
annotations; replace the value import with a type-only import by changing the
import statement to use "import type" for HttpClient (e.g., update the import
that currently references HttpClient from '@effect/platform' to an import type
for HttpClient) so the compiler/tree-shaker treats it as a type-only import.

In `@server/utils/effect.ts`:
- Around line 88-93: The handleError function uses duck-typing to detect H3
errors; replace that check with H3's isError utility for consistency with
runEffect: import and call isError(error) in handleError and, when true, return
error as H3Error; keep the rest of the Effect.gen flow unchanged and ensure the
same H3Error typecast is used.
♻️ Duplicate comments (8)
app/pages/library/[id].vue (1)

49-73: Consider adding confirmation before removing a book.

The removeBook function performs an irreversible destructive action without user confirmation. This was flagged in a previous review as a nitpick but remains unaddressed.

♻️ Example using a confirmation pattern
 async function removeBook() {
+  if (!confirm('Are you sure you want to remove this book from your library?')) {
+    return
+  }
+
   try {
     await $fetch(`/api/books/${userBookId}`, {
       method: 'DELETE'
     })
server/repositories/openLibrary.repository.ts (2)

45-46: The notes field may be an object, not just a string.

OpenLibrary API can return notes as either a string or { type, value } object (similar to description on lines 62-63). Line 160 only handles the string case, which could result in [object Object] being displayed.

🐛 Proposed fix

Update the interface:

-    notes?: string
+    notes?: string | { type: string; value: string }

And update line 160:

-      let description = bookData.notes || bookData.excerpts?.[0]?.text
+      let description = (
+        typeof bookData.notes === 'string'
+          ? bookData.notes
+          : bookData.notes?.value
+      ) || bookData.excerpts?.[0]?.text

1-5: Missing import: putBlob is not imported but used on line 274.

The putBlob helper from storage.service.ts is called in downloadCover but not imported. Additionally, StorageService is referenced in the interface type on line 72 but not imported.

🐛 Proposed fix
 import { Context, Effect, Layer, Data, Duration } from 'effect'
 import { HttpClient } from '@effect/platform'
 import * as HCError from '@effect/platform/HttpClientError'
 import type { HttpClient as HttpClientType } from '@effect/platform'
 import sharp from 'sharp'
+import { putBlob, StorageService } from '../services/storage.service'
Agents.md (1)

1-72: LGTM! Comprehensive onboarding documentation.

The agent instructions document is well-structured, covering the 3-tier architecture, Effect-TS patterns, and development workflow. The guardrails (no fat handlers, local-first lookups, etc.) align with the PR's implementation.

Note: The missing trailing newline at line 72 was already flagged in a previous review.

server/repositories/book.repository.ts (2)

363-363: Wrap JSON.parse to handle malformed data.

If bookData.subjects contains invalid JSON, this will throw an unhandled exception. This was flagged in a previous review.

Proposed safe parsing helper
function safeJsonParse<T>(json: string | null): T | null {
  if (!json) return null
  try {
    return JSON.parse(json)
  } catch {
    return null
  }
}

// Usage:
subjects: safeJsonParse<string[]>(bookData.subjects),

184-197: Returned Book object is missing optional fields.

The Book interface (lines 27-41) includes optional fields (description, subjects, publishDate, publishers, numberOfPages, workKey), but the returned object only includes base fields. This inconsistency could cause issues for consumers expecting complete data.

Include all Book fields
           return {
             id: userBookId,
             bookId: book.id,
             book: {
               id: book.id,
               isbn: book.isbn,
               title: book.title,
               author: book.author,
               coverPath: book.coverPath,
               openLibraryKey: book.openLibraryKey,
-              createdAt: book.createdAt
+              createdAt: book.createdAt,
+              description: book.description ?? null,
+              subjects: book.subjects ?? null,
+              publishDate: book.publishDate ?? null,
+              publishers: book.publishers ?? null,
+              numberOfPages: book.numberOfPages ?? null,
+              workKey: book.workKey ?? null
             },
             addedAt
           }

The same issue applies to getLibrary (lines 241-250) and getBookById (lines 285-293).

server/services/book.service.ts (1)

140-140: Wrap JSON.parse to handle malformed data.

Same issue as in the repository - if localBook.subjects contains invalid JSON, this will throw an unhandled exception.

Use a shared safe parsing utility:

subjects: safeJsonParse<string[]>(localBook.subjects),
server/utils/effect.ts (1)

36-40: Redundant layer provisioning in MainLive.

ServicesLive already contains RepositoriesLive which contains BaseServicesLive. Providing BaseServicesLive again in MainLive creates unnecessary redundancy.

Simplify MainLive
 // Combined live layer for all services
-export const MainLive = Layer.provideMerge(
-  ServicesLive,
-  BaseServicesLive
-)
+export const MainLive = ServicesLive

Alternatively, if BaseServicesLive needs to be explicitly available at the top level for some reason, the current approach works but is redundant.

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61c0cec and dbafecd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • Agents.md
  • app/pages/library/[id].vue
  • app/pages/library/index.vue
  • package.json
  • server/repositories/book.repository.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/auth.service.ts
  • server/services/book.service.ts
  • server/services/storage.service.ts
  • server/utils/effect.ts
  • server/utils/effectHandler.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-13T21:08:13.933Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/utils/effect.ts:30-38
Timestamp: 2026-01-13T21:08:13.933Z
Learning: In Effect-TS code, when combining service tags (Context.Tag) for multiple services, use a union type (Context1.Tag | Context2.Tag) rather than an intersection (&). Service tags are nominal types and cannot be meaningfully intersected; a union correctly represents combined service requirements in the Effect<R> parameter, while runtime Layer composition handles provisioning. Apply this guidance to TypeScript files that define or compose Effect-TS service contexts (e.g., server/utils/effect.ts and similar files).

Applied to files:

  • server/services/auth.service.ts
  • server/utils/effectHandler.ts
  • server/repositories/openLibrary.repository.ts
  • server/utils/effect.ts
  • server/services/book.service.ts
  • server/services/storage.service.ts
  • server/repositories/book.repository.ts
📚 Learning: 2026-01-13T21:24:30.028Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: shared/utils/schemas.ts:11-13
Timestamp: 2026-01-13T21:24:30.028Z
Learning: In TypeScript files using Zod v4.3.5 or newer, use the error option for both schema-level configuration and validators (min, max, etc.). The message parameter is deprecated. Example: z.string({ error: 'Required' }).min(10, { error: 'Too short' }). Apply this pattern to all Zod schema definitions in TS files to avoid deprecated API usage.

Applied to files:

  • server/services/auth.service.ts
  • server/utils/effectHandler.ts
  • server/repositories/openLibrary.repository.ts
  • server/utils/effect.ts
  • server/services/book.service.ts
  • server/services/storage.service.ts
  • server/repositories/book.repository.ts
🧬 Code graph analysis (6)
server/services/auth.service.ts (3)
server/utils/effect.ts (4)
  • Data (134-134)
  • Effect (134-134)
  • Context (134-134)
  • Layer (134-134)
server/db/schema/auth.ts (1)
  • session (16-25)
server/utils/auth.ts (1)
  • auth (6-27)
server/utils/effectHandler.ts (3)
server/db/schema/auth.ts (1)
  • user (6-14)
server/utils/effect.ts (3)
  • Effect (134-134)
  • MainServices (43-50)
  • runEffect (116-131)
server/services/auth.service.ts (1)
  • requireAuth (75-76)
server/repositories/openLibrary.repository.ts (2)
server/utils/effect.ts (4)
  • Data (134-134)
  • Effect (134-134)
  • Context (134-134)
  • Layer (134-134)
server/services/storage.service.ts (2)
  • StorageService (31-31)
  • putBlob (100-101)
server/utils/effect.ts (6)
server/services/db.service.ts (2)
  • DbServiceLive (13-15)
  • DbService (10-10)
server/services/storage.service.ts (2)
  • StorageServiceLive (34-97)
  • StorageService (31-31)
server/services/auth.service.ts (2)
  • AuthServiceLive (41-69)
  • AuthService (38-38)
server/repositories/openLibrary.repository.ts (2)
  • OpenLibraryRepositoryLive (118-290)
  • OpenLibraryRepository (76-79)
server/repositories/book.repository.ts (2)
  • BookRepositoryLive (92-374)
  • BookRepository (84-84)
server/services/book.service.ts (2)
  • BookServiceLive (51-176)
  • BookService (47-47)
server/services/storage.service.ts (1)
server/utils/effect.ts (4)
  • Data (134-134)
  • Effect (134-134)
  • Context (134-134)
  • Layer (134-134)
server/repositories/book.repository.ts (5)
server/repositories/openLibrary.repository.ts (3)
  • OpenLibraryBookNotFoundError (8-11)
  • OpenLibraryApiError (13-15)
  • OpenLibraryRepository (76-79)
server/services/db.service.ts (1)
  • DbService (10-10)
shared/types/pagination.ts (2)
  • PaginationParams (1-4)
  • PaginatedResult (6-15)
server/db/schema/domain.ts (2)
  • books (8-25)
  • userBooks (29-34)
shared/types/book.ts (1)
  • BookDetails (31-46)
🪛 GitHub Actions: Typecheck and Lint
server/repositories/book.repository.ts

[error] 2-2: ESLint: All imports in the declaration are only used as types. Use 'import type'.

🪛 GitHub Check: typecheck
server/repositories/book.repository.ts

[failure] 324-324:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 304-304:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 274-274:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 232-232:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 212-212:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 181-181:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 163-163:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 127-127:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 109-109:
Unexpected parentheses around single function argument having a body with no curly braces


[failure] 2-2:
All imports in the declaration are only used as types. Use import type

🔇 Additional comments (24)
app/pages/library/[id].vue (3)

1-19: LGTM! Well-structured data fetching and cover URL computation.

The useFetch with cookie forwarding for authenticated requests and the coverUrl computed property are implemented correctly.


20-43: LGTM! The formatDate logic is now correct.

The function properly handles year-only strings (e.g., "2015") by checking with the regex first, before attempting Date parsing. This addresses the earlier concern where year-only strings would be parsed as January 1st of that year.


76-252: LGTM! Well-structured template with proper loading, error, and content states.

The template correctly handles all UI states (loading, not-found, book details) and renders the book information with proper responsiveness. The external links to OpenLibrary are implemented correctly with target="_blank".

app/pages/library/index.vue (6)

1-33: LGTM! Well-structured state management and data fetching.

The type imports, pagination state, and useFetch with computed query params are properly set up. Using shallowRef for the Set is appropriate.


35-53: LGTM! Accumulated books pattern is correctly implemented.

The watcher properly handles pagination by replacing items on page 1 and appending on subsequent pages, which addresses the previous "Load More replaces items" issue.


55-81: LGTM! Selection functions now use proper reactivity patterns.

All selection functions (toggleSelectMode, toggleBookSelection, toggleSelectAll) correctly reassign the Set to trigger reactivity, addressing the previous shallowRef.clear() issues.


83-94: LGTM! Load more with proper loading state.

The loadMore function correctly manages the isLoadingMore state and guards against concurrent requests.


96-161: LGTM! Batch delete with proper partial failure handling.

The implementation correctly uses a batch delete endpoint and handles partial success/failure scenarios with appropriate user feedback. The previous concerns about Promise.all partial failure are addressed by using a dedicated batch endpoint that returns removedIds and failedIds.


164-305: LGTM! Well-structured template with comprehensive UI states.

The template properly handles selection mode, loading states, empty states, book grid with selectable cards, and pagination controls. The "Load More" button correctly shows loading state via :loading="isLoadingMore".

server/services/auth.service.ts (2)

1-35: LGTM! Well-designed error types and interfaces.

The UnauthorizedError using Data.TaggedError follows Effect-TS patterns correctly. The AuthUser and Session interfaces are clearly defined and align with Better Auth's session structure.


71-76: LGTM! Helper effects follow Effect-TS idioms correctly.

The flatMap pattern for accessing the service and delegating to its methods is the idiomatic way to create helper effects in Effect-TS.

server/repositories/openLibrary.repository.ts (2)

86-115: LGTM! Helper functions with proper timeouts and error handling.

The fetchJson and checkCoverExists helpers are well-implemented with 5-second timeouts using Duration.seconds(5), which addresses the previous timeout concerns.


117-217: LGTM! lookupByISBN is well-structured with proper enrichment fallbacks.

The implementation correctly:

  • Normalizes ISBN and queries OpenLibrary
  • Handles description as both string and object types (lines 180-185)
  • Merges subjects from works data when needed
  • Uses Effect.catchAll with Effect.logDebug for optional enrichment failures
server/services/storage.service.ts (3)

1-31: LGTM! Well-designed storage service interface and error types.

The StorageError with operation context, BlobMetadata interface, and StorageServiceInterface are cleanly defined following Effect-TS patterns.


33-97: LGTM! Robust live implementation with lazy imports.

The StorageServiceLive implementation correctly:

  • Uses dynamic import for hub:blob to avoid build-time issues
  • Maps errors to StorageError with operation context
  • Normalizes timestamps in list results (line 94)
  • Uses Effect.tryPromise for proper error handling

99-110: LGTM! Helper effects follow Effect-TS conventions.

The putBlob, getBlob, deleteBlob, and listBlobs helpers use the idiomatic Effect.flatMap pattern for service access.

server/utils/effectHandler.ts (1)

23-41: LGTM! Clean Effect-based handler wrapper.

The implementation correctly:

  • Enforces authentication via requireAuth before invoking the handler
  • Composes the Effect pipeline with proper dependency injection
  • Delegates error handling and execution to runEffect

The design aligns well with the 3-tier architecture documented in Agents.md.

server/utils/effect.ts (1)

115-131: LGTM! Well-structured effect runner with proper error handling.

The runEffect function correctly:

  • Provides the MainLive layer
  • Catches all errors and converts them to H3 errors
  • Throws H3 errors for proper HTTP response handling

This addresses concerns from previous reviews about unhandled errors.

server/repositories/book.repository.ts (1)

91-198: LGTM! Well-structured addBookByISBN implementation.

The method correctly:

  • Checks for existing ownership before adding
  • Reuses shared book records when available
  • Fetches from OpenLibrary and downloads covers for new books
  • Uses proper error types for different failure scenarios

The Effect-based error handling with DatabaseError, BookAlreadyOwnedError, and BookCreateError provides good granularity.

server/services/book.service.ts (2)

93-120: LGTM! Well-implemented batch deletion with bounded concurrency.

The implementation correctly:

  • Uses bounded concurrency (concurrency: 10) to avoid overwhelming the database
  • Wraps each operation in Either to capture individual success/failure
  • Properly yields Effect.logError to execute the logging effect
  • Returns a structured result with removedIds and failedIds

This addresses concerns from previous reviews.


125-173: LGTM! Clean local-first lookup implementation.

The lookupBook method follows the documented "local-first" pattern from Agents.md:

  1. First checks BookRepository for existing local data
  2. Falls back to OpenLibraryRepository if not found locally
  3. Gracefully handles OpenLibraryBookNotFoundError with a proper fallback response

The ISBN normalization at line 127 is correctly applied before both the local lookup and OpenLibrary call.

package.json (3)

2-2: LGTM on project rename and lint:fix script.

The project rename to "librooapp" aligns with the Libroo rebranding, and the lint:fix script is a useful addition for developer workflow.

Also applies to: 11-11


20-20: Alpha dependency in production.

@nuxt/hints at version 1.0.0-alpha.5 is an alpha release. Alpha versions may have breaking changes or instability. Ensure this is intentional and consider pinning to a stable release when available.


32-32: No action required — codebase is already compatible with Zod v4.

The Zod schemas in shared/utils/schemas.ts correctly use the v4 unified error API with the error parameter (e.g., z.string({ error: '...' }), .min(10, { error: '...' })). No deprecated v3 patterns detected. The version specified in package.json ("zod": "^4.3.5") is the current latest stable release.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@server/repositories/book.repository.ts`:
- Around line 376-387: The module exports helper Effect wrappers for several
BookRepository methods but omits helpers for findByIsbn and
getUserBookWithDetails; add exported helper functions named findByIsbn and
getUserBookWithDetails that call Effect.flatMap(BookRepository, repo =>
repo.findByIsbn(isbn)) and Effect.flatMap(BookRepository, repo =>
repo.getUserBookWithDetails(userBookId, userId)) respectively so all interface
methods have consistent helpers and can be imported elsewhere.

In `@server/services/book.service.ts`:
- Around line 62-70: The mapping from repository userBook to LibraryBook is
duplicated in getUserLibrary and addBookToLibrary; extract a single helper
function (e.g., toLibraryBook(userBook): LibraryBook) that constructs { id,
bookId, title: userBook.book.title, author: userBook.book.author, isbn:
userBook.book.isbn, coverPath: userBook.book.coverPath, addedAt:
userBook.addedAt } and replace the inline mappings in both getUserLibrary and
addBookToLibrary to call toLibraryBook(userBook); declare the helper near the
top of the module with the correct repository input type (or export it if needed
for tests) to ensure type safety and reuse.
- Around line 163-169: The not-found handler inside
Effect.catchTag('OpenLibraryBookNotFoundError') returns the original isbn
instead of the normalizedISBN, causing inconsistency with successful lookups;
update the returned object in that handler to use normalizedISBN (the same
normalized value used on success) so the BookLookupResult's isbn field is
consistent with other responses.

In `@server/services/storage.service.ts`:
- Around line 36-54: The dynamic import of 'hub:blob' inside Effect.gen can
throw before Effect.tryPromise runs, so wrap the entire async operation (both
import('hub:blob') and the blob.put call) in a single Effect.tryPromise (or
perform a try/catch inside Effect.gen and convert any thrown error to a
StorageError) so import failures are caught and mapped to StorageError;
specifically, ensure the block that loads the module and calls blob.put (the
import('hub:blob') line and the blob.put(pathname, data, { contentType:
options?.contentType, prefix: options?.prefix })) returns via Effect.tryPromise
or rethrows as new StorageError so callers of Effect.gen receive a consistent
StorageError on failure.
♻️ Duplicate comments (13)
server/utils/effect.ts (3)

1-5: Missing explicit imports for service layers and utilities.

The file uses DbServiceLive, StorageServiceLive, AuthServiceLive, OpenLibraryRepositoryLive, BookRepositoryLive, BookServiceLive, DbService, StorageService, AuthService, BookRepository, OpenLibraryRepository, BookService, createError, and isError without explicit imports. While Nuxt/Nitro may auto-import these, explicit imports improve clarity and catch errors at compile time.


36-40: Simplify MainLive to avoid redundant layer provisioning.

BaseServicesLive is already provided via RepositoriesLive (line 25-28), which is included in ServicesLive. Providing it again here is redundant.

Proposed simplification
 // Combined live layer for all services
-export const MainLive = Layer.provideMerge(
-  ServicesLive,
-  BaseServicesLive
-)
+export const MainLive = ServicesLive

88-93: Consider using isError from H3 for consistent error detection.

The handleError function uses duck-typing ('statusCode' in error) while runEffect (line 126) uses the isError utility. For consistency and robustness, consider using isError in both places.

server/repositories/openLibrary.repository.ts (3)

1-5: Missing import: putBlob is called but not imported.

The downloadCover implementation uses putBlob on line 274 but it's not imported. This will cause a runtime error.

Proposed fix
 import { Context, Effect, Layer, Data, Duration } from 'effect'
 import { HttpClient } from '@effect/platform'
 import * as HCError from '@effect/platform/HttpClientError'
 import type { HttpClient as HttpClientType } from '@effect/platform'
 import sharp from 'sharp'
+import { putBlob, type StorageService } from '../services/storage.service'

45-46: notes field may be an object, not just a string.

OpenLibrary API can return notes as either a string or { type, value } object (similar to description handling on lines 181-185). This could cause the description to display as [object Object].

Proposed fix

Update the interface at line 45:

-    notes?: string
+    notes?: string | { type: string, value: string }

And update line 160:

-      let description = bookData.notes || bookData.excerpts?.[0]?.text
+      let description = (
+        typeof bookData.notes === 'string'
+          ? bookData.notes
+          : (bookData.notes as { value?: string })?.value
+      ) || bookData.excerpts?.[0]?.text

Also applies to: 159-160


219-260: Remove debug console.log statements before merging.

The downloadCover function contains multiple console.log statements (lines 221, 231, 239, 244, 253, 260) that appear to be debugging artifacts. Replace them with Effect.log / Effect.logDebug for consistent observability, or remove them entirely.

Proposed cleanup
   downloadCover: (isbn, size = 'L') =>
     Effect.gen(function* () {
-      console.log('downloadCover', isbn, size)
       const normalizedISBN = normalizeISBN(isbn)
       const coverUrl = `https://covers.openlibrary.org/b/isbn/${normalizedISBN}-${size}.jpg?default=false`

       const imageBuffer = yield* HttpClient.get(coverUrl).pipe(
         Effect.timeout(Duration.seconds(10)),
         Effect.flatMap((response) => {
           if (response.status < 200 || response.status >= 300) {
-            console.log('No cover available for ISBN', normalizedISBN)
             return Effect.succeed(null as ArrayBuffer | null)
           }

           const contentLength = response.headers['content-length']
           if (contentLength && parseInt(contentLength) < 1000) {
-            console.log('Too small, probably the placeholder image', normalizedISBN)
             return Effect.succeed(null as ArrayBuffer | null)
           }

-          console.log('Read the body within the same scope as the request', normalizedISBN)
           return response.arrayBuffer
         }),
         // ...
       )

-      console.log('image Buffer', imageBuffer?.byteLength)

       if (!imageBuffer) {
         yield* Effect.log(`[OpenLibrary] No cover found for ISBN ${normalizedISBN}`)
         return null
       }

-      console.log('image Buffer', imageBuffer.byteLength)
server/repositories/book.repository.ts (5)

184-197: Returned Book object is missing optional fields.

The return value omits description, subjects, publishDate, publishers, numberOfPages, and workKey fields that are defined in the Book interface and available in the book variable.

Proposed fix
           return {
             id: userBookId,
             bookId: book.id,
             book: {
               id: book.id,
               isbn: book.isbn,
               title: book.title,
               author: book.author,
               coverPath: book.coverPath,
               openLibraryKey: book.openLibraryKey,
-              createdAt: book.createdAt
+              createdAt: book.createdAt,
+              description: book.description ?? null,
+              subjects: book.subjects ?? null,
+              publishDate: book.publishDate ?? null,
+              publishers: book.publishers ?? null,
+              numberOfPages: book.numberOfPages ?? null,
+              workKey: book.workKey ?? null
             },
             addedAt
           }

238-251: Incomplete Book mapping in getLibrary results.

Similar to addBookByISBN, the Book objects are missing optional fields (description, subjects, publishDate, publishers, numberOfPages, workKey).


285-293: Incomplete Book mapping: missing optional fields.

The getBookById method returns a Book type but omits optional fields like description, subjects, publishDate, publishers, numberOfPages, and workKey.

Proposed fix
           return {
             id: book.id,
             isbn: book.isbn,
             title: book.title,
             author: book.author,
             coverPath: book.coverPath,
             openLibraryKey: book.openLibraryKey,
-            createdAt: book.createdAt instanceof Date ? book.createdAt : new Date(book.createdAt as unknown as string)
+            createdAt: book.createdAt instanceof Date ? book.createdAt : new Date(book.createdAt as unknown as string),
+            description: book.description ?? null,
+            subjects: book.subjects ?? null,
+            publishDate: book.publishDate ?? null,
+            publishers: book.publishers ?? null,
+            numberOfPages: book.numberOfPages ?? null,
+            workKey: book.workKey ?? null
           }

363-363: Unsafe JSON.parse may throw on malformed data.

If bookData.subjects contains malformed JSON, JSON.parse will throw an unhandled exception, crashing the request.

Proposed fix: Add safe JSON parsing
+// Helper for safe JSON parsing
+function safeJsonParse<T>(json: string | null): T | null {
+  if (!json) return null
+  try {
+    return JSON.parse(json)
+  } catch {
+    return null
+  }
+}

 // In getUserBookWithDetails:
-            subjects: bookData.subjects ? JSON.parse(bookData.subjects) : null,
+            subjects: safeJsonParse<string[]>(bookData.subjects),

1-5: Fix missing imports preventing compilation.

The import from 'hub:db:schema' does not support destructuring in this context. Additionally, the following types and functions are used but not imported: DbService, StorageService, OpenLibraryRepository, PaginationParams, PaginatedResult, BookDetails, OpenLibraryBookNotFoundError, OpenLibraryApiError, lookupByISBN, and downloadCover.

Required imports
 import { Context, Effect, Layer, Data } from 'effect'
 import type { HttpClient } from '@effect/platform'
 import { eq, and, count, desc } from 'drizzle-orm'
 import type { InferSelectModel } from 'drizzle-orm'
-import { books, userBooks } from 'hub:db:schema'
+import { books, userBooks } from '~/server/db/schema/domain'
+import { DbService } from '~/server/services/db.service'
+import { StorageService } from '~/server/services/storage.service'
+import {
+  OpenLibraryRepository,
+  OpenLibraryBookNotFoundError,
+  OpenLibraryApiError,
+  lookupByISBN,
+  downloadCover
+} from '~/server/repositories/openLibrary.repository'
+import type { PaginationParams, PaginatedResult } from '~/shared/types/pagination'
+import type { BookDetails } from '~/shared/types/book'
server/services/book.service.ts (2)

1-2: Missing imports for types and services used throughout the file.

The file references numerous types, error classes, services, and repository items that are not imported. This will cause TypeScript compilation errors.

Missing imports include:

  • Types: PaginationParams, PaginatedResult, LibraryBook, BookLookupResult, BatchDeleteResult, BookDetails
  • Errors: DatabaseError, BookCreateError, BookAlreadyOwnedError, OpenLibraryBookNotFoundError, OpenLibraryApiError, BookNotFoundError
  • Services: DbService, StorageService
  • Repository: BookRepository, OpenLibraryRepository, lookupByISBN, OpenLibraryBookData
Proposed imports
 import { Context, Effect, Layer, Either } from 'effect'
 import type { HttpClient } from '@effect/platform'
+import { DbService, type DatabaseError } from '~/server/services/db.service'
+import { StorageService } from '~/server/services/storage.service'
+import {
+  OpenLibraryRepository,
+  lookupByISBN,
+  type OpenLibraryBookData,
+  OpenLibraryBookNotFoundError,
+  OpenLibraryApiError
+} from '~/server/repositories/openLibrary.repository'
+import {
+  BookRepository,
+  BookNotFoundError,
+  BookCreateError,
+  BookAlreadyOwnedError
+} from '~/server/repositories/book.repository'
+import type { PaginationParams, PaginatedResult } from '~/shared/types/pagination'
+import type { LibraryBook, BookLookupResult, BatchDeleteResult, BookDetails } from '~/shared/types/book'

140-140: JSON.parse can throw on malformed data.

If localBook.subjects contains invalid JSON, this will throw an unhandled exception. Consider using a safe parsing approach.

Proposed fix using try-catch
-              subjects: localBook.subjects ? JSON.parse(localBook.subjects) : null,
+              subjects: localBook.subjects ? (() => {
+                try {
+                  return JSON.parse(localBook.subjects)
+                } catch {
+                  return null
+                }
+              })() : null,

Or better, extract a reusable safe parse utility:

const safeParseJson = (str: string | null): unknown | null => {
  if (!str) return null
  try {
    return JSON.parse(str)
  } catch {
    return null
  }
}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbafecd and 6a0c1e7.

📒 Files selected for processing (6)
  • server/repositories/book.repository.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/book.service.ts
  • server/services/storage.service.ts
  • server/utils/effect.ts
  • server/utils/effectHandler.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-13T21:08:13.933Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/utils/effect.ts:30-38
Timestamp: 2026-01-13T21:08:13.933Z
Learning: In Effect-TS code, when combining service tags (Context.Tag) for multiple services, use a union type (Context1.Tag | Context2.Tag) rather than an intersection (&). Service tags are nominal types and cannot be meaningfully intersected; a union correctly represents combined service requirements in the Effect<R> parameter, while runtime Layer composition handles provisioning. Apply this guidance to TypeScript files that define or compose Effect-TS service contexts (e.g., server/utils/effect.ts and similar files).

Applied to files:

  • server/utils/effectHandler.ts
  • server/utils/effect.ts
  • server/services/storage.service.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/book.service.ts
  • server/repositories/book.repository.ts
📚 Learning: 2026-01-13T21:24:30.028Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: shared/utils/schemas.ts:11-13
Timestamp: 2026-01-13T21:24:30.028Z
Learning: In TypeScript files using Zod v4.3.5 or newer, use the error option for both schema-level configuration and validators (min, max, etc.). The message parameter is deprecated. Example: z.string({ error: 'Required' }).min(10, { error: 'Too short' }). Apply this pattern to all Zod schema definitions in TS files to avoid deprecated API usage.

Applied to files:

  • server/utils/effectHandler.ts
  • server/utils/effect.ts
  • server/services/storage.service.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/book.service.ts
  • server/repositories/book.repository.ts
🧬 Code graph analysis (4)
server/utils/effectHandler.ts (2)
server/utils/effect.ts (3)
  • Effect (134-134)
  • MainServices (43-50)
  • runEffect (116-131)
server/services/auth.service.ts (1)
  • requireAuth (75-76)
server/utils/effect.ts (6)
server/services/db.service.ts (2)
  • DbServiceLive (13-15)
  • DbService (10-10)
server/services/storage.service.ts (2)
  • StorageServiceLive (34-97)
  • StorageService (31-31)
server/services/auth.service.ts (2)
  • AuthServiceLive (41-69)
  • AuthService (38-38)
server/repositories/openLibrary.repository.ts (2)
  • OpenLibraryRepositoryLive (118-290)
  • OpenLibraryRepository (76-79)
server/repositories/book.repository.ts (2)
  • BookRepositoryLive (92-374)
  • BookRepository (84-84)
server/services/book.service.ts (2)
  • BookServiceLive (51-176)
  • BookService (47-47)
server/repositories/openLibrary.repository.ts (2)
server/utils/effect.ts (4)
  • Data (134-134)
  • Effect (134-134)
  • Context (134-134)
  • Layer (134-134)
server/services/storage.service.ts (2)
  • StorageService (31-31)
  • putBlob (100-101)
server/repositories/book.repository.ts (7)
server/utils/effect.ts (4)
  • Data (134-134)
  • Effect (134-134)
  • Context (134-134)
  • Layer (134-134)
server/repositories/openLibrary.repository.ts (5)
  • OpenLibraryBookNotFoundError (8-11)
  • OpenLibraryApiError (13-15)
  • OpenLibraryRepository (76-79)
  • lookupByISBN (293-294)
  • downloadCover (296-297)
server/services/db.service.ts (1)
  • DbService (10-10)
server/services/storage.service.ts (1)
  • StorageService (31-31)
shared/types/pagination.ts (2)
  • PaginationParams (1-4)
  • PaginatedResult (6-15)
server/db/schema/domain.ts (2)
  • books (8-25)
  • userBooks (29-34)
shared/types/book.ts (1)
  • BookDetails (31-46)
🪛 GitHub Actions: Typecheck and Lint
server/repositories/book.repository.ts

[error] 5-5: TypeScript TS2305: Module '"hub:db:schema"' has no exported member 'books'.

🪛 GitHub Check: typecheck
server/repositories/book.repository.ts

[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'books'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'userBooks'.


[failure] 5-5:
Module '"hub:db:schema"' has no exported member 'books'.

🔇 Additional comments (10)
server/utils/effect.ts (2)

42-51: LGTM!

The union type for MainServices is correct for Effect-TS service tags, as service tags are nominal types that cannot be intersected. Based on learnings from previous review.


115-131: LGTM!

The runEffect function properly handles errors by converting them to H3 errors via handleError, then throwing if the result is an error. This integrates well with H3/Nitro's error handling.

server/utils/effectHandler.ts (1)

23-41: LGTM!

The effectHandler cleanly wraps handlers with authentication and delegates to runEffect for error handling. The implementation correctly enforces authentication and provides proper error handling through the centralized utilities.

server/services/storage.service.ts (2)

3-28: LGTM!

The error type and interfaces are well-structured with clear semantics. The StorageError properly captures the operation context for debugging.


99-110: LGTM!

The helper effects follow the standard Effect-TS pattern for service delegation.

server/repositories/openLibrary.repository.ts (1)

292-297: LGTM!

The helper effects follow the standard Effect-TS delegation pattern.

server/repositories/book.repository.ts (2)

7-24: LGTM!

The error types are well-defined with distinct tags. BookNotFoundError and OpenLibraryBookNotFoundError have different _tag values, so there's no collision.


26-48: LGTM!

The Book and UserBook interfaces are well-structured. The subjects field is correctly typed as string | null to represent the JSON-stringified array stored in the database.

server/services/book.service.ts (2)

6-43: Well-structured Effect-TS service interface.

The interface properly declares explicit error channels and dependency requirements for each method. The use of union types for errors and dependencies follows Effect-TS conventions correctly.


178-196: LGTM!

The helper effects follow the standard Effect-TS accessor pattern, providing convenient service access via Effect.flatMap. This is idiomatic and improves ergonomics for consumers.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Line 6: The workflow job currently named "typecheck" is misleading because it
only runs the lint script; rename the job key from typecheck to lint in the CI
workflow (update the "typecheck:" job header to "lint:") and adjust any
references or dependencies that reference the old job name (e.g., needs:, job
outputs or badges) so they point to "lint" instead to keep the job name
consistent with its behavior.

In `@server/services/storage.service.ts`:
- Around line 55-92: The get, delete, and list methods reference an undefined
blob variable; fix them the same way as the put method by performing the dynamic
import/initialization of the storage client inside each Effect.gen before
calling blob (e.g., obtain the blob client with the same async helper or import
used in put), then use that local blob to call get/delete/list and preserve the
current StorageError wrapping if the client is unavailable or the call fails;
ensure uploadedAt normalization in list remains and throw/return a StorageError
if the dynamic import or client resolution fails.

In `@server/utils/auth.ts`:
- Around line 37-52: getAuthUrl currently silently falls back to
'http://localhost:3000'; update it to emit a clear production warning when no
explicit URL is configured: detect production via process.env.NODE_ENV ===
'production' (and/or config.isProd if available), and if in production and
neither process.env.BETTER_AUTH_URL nor config.betterAuthUrl were found, call
console.warn (or a module logger if available) with a message referencing
getAuthUrl and advising to set BETTER_AUTH_URL; keep the existing fallback for
non-production but do not suppress the warning in production.

In `@server/utils/effect.ts`:
- Around line 111-127: runEffect uses Effect.catchAll(handleError) which
converts errors into a successful value (an H3Error) so the function can inspect
it and re-throw; add a short clarifying comment above runEffect (and/or just
above the Effect.catchAll line) explaining that handleError returns
Effect.succeed(h3Error) intentionally to surface H3Error as a return value that
is then detected via isError and thrown, referencing the runEffect function, the
Effect.catchAll(handleError) usage, and the isError check to help future
maintainers understand this control flow.
♻️ Duplicate comments (13)
nuxt.config.ts (1)

3-11: @nuxt/test-utils should be conditionally loaded for non-production environments.

This module is intended for development/testing only and should not be included in production builds.

package.json (1)

23-23: Move @nuxt/test-utils to devDependencies.

Test utilities should not be in production dependencies. This was flagged in a previous review and appears to still be unaddressed.

♻️ Suggested fix
  "dependencies": {
    ...
-   "@nuxt/test-utils": "3.22.0",
    ...
  },
  "devDependencies": {
    "@nuxt/eslint": "^1.12.1",
+   "@nuxt/test-utils": "3.22.0",
    "drizzle-kit": "^0.31.8",
    ...
  },
server/repositories/openLibrary.repository.ts (3)

37-56: notes field type is inconsistent with actual API response.

The notes field on line 45 is typed as string, but OpenLibrary API can return notes as either a string or { type, value } object (similar to description in OpenLibraryWorksApiResponse on line 62). This type mismatch could cause runtime issues when accessing bookData.notes on line 160.

🐛 Proposed fix
 interface OpenLibraryBooksApiResponse {
   [key: string]: {
     title: string
     authors?: Array<{ name: string, url?: string }>
     publishers?: Array<{ name: string }>
     publish_date?: string
     number_of_pages?: number
-    notes?: string
+    notes?: string | { type: string, value: string }
     excerpts?: Array<{ text: string }>

Then update line 160:

-      let description = bookData.notes || bookData.excerpts?.[0]?.text
+      let description = (
+        typeof bookData.notes === 'string'
+          ? bookData.notes
+          : bookData.notes?.value
+      ) || bookData.excerpts?.[0]?.text

263-273: Critical: putBlob is called but not imported.

The putBlob function is used on line 266 but is not imported from storage.service.ts. This will cause a runtime error when attempting to download and store covers.

🐛 Proposed fix

Add to imports at the top of the file:

 import { Context, Effect, Layer, Data, Duration } from 'effect'
 import { HttpClient } from '@effect/platform'
 import * as HCError from '@effect/platform/HttpClientError'
 import type { HttpClient as HttpClientType } from '@effect/platform'
 import sharp from 'sharp'
+import { putBlob } from '../services/storage.service'

1-5: Missing import for StorageService type.

The StorageService type is referenced on line 72 in the interface definition but is not imported. This will cause a TypeScript compilation error.

🐛 Proposed fix
 import { Context, Effect, Layer, Data, Duration } from 'effect'
 import { HttpClient } from '@effect/platform'
 import * as HCError from '@effect/platform/HttpClientError'
 import type { HttpClient as HttpClientType } from '@effect/platform'
 import sharp from 'sharp'
+import { putBlob, type StorageService } from '../services/storage.service'
server/repositories/book.repository.ts (5)

1-5: Missing imports for types and functions used throughout the file.

The file references DbService, StorageService, OpenLibraryRepository, PaginationParams, PaginatedResult, BookDetails, OpenLibraryBookNotFoundError, OpenLibraryApiError, lookupByISBN, and downloadCover without importing them. This will cause TypeScript compilation errors.

🐛 Proposed fix
 import { Context, Effect, Layer, Data } from 'effect'
 import type { HttpClient } from '@effect/platform'
 import { eq, and, count, desc } from 'drizzle-orm'
 import type { InferSelectModel } from 'drizzle-orm'
 import { books, userBooks } from 'hub:db:schema'
+import { DbService } from '../services/db.service'
+import { StorageService } from '../services/storage.service'
+import {
+  OpenLibraryRepository,
+  OpenLibraryBookNotFoundError,
+  OpenLibraryApiError,
+  lookupByISBN,
+  downloadCover
+} from './openLibrary.repository'
+import type { PaginationParams, PaginatedResult } from '~/shared/types/pagination'
+import type { BookDetails } from '~/shared/types/book'

184-198: Returned Book object is missing optional fields.

The returned book object omits description, subjects, publishDate, publishers, numberOfPages, and workKey fields that are part of the Book interface. This could cause issues if consumers expect these fields.

🐛 Proposed fix
           return {
             id: userBookId,
             bookId: book.id,
             book: {
               id: book.id,
               isbn: book.isbn,
               title: book.title,
               author: book.author,
               coverPath: book.coverPath,
               openLibraryKey: book.openLibraryKey,
-              createdAt: book.createdAt
+              createdAt: book.createdAt,
+              description: book.description ?? null,
+              subjects: book.subjects ?? null,
+              publishDate: book.publishDate ?? null,
+              publishers: book.publishers ?? null,
+              numberOfPages: book.numberOfPages ?? null,
+              workKey: book.workKey ?? null
             },
             addedAt
           }

238-251: Incomplete Book mapping in getLibrary results.

Similar to addBookByISBN, the Book objects in getLibrary results are missing optional fields (description, subjects, publishDate, publishers, numberOfPages, workKey).

🐛 Proposed fix
           const items = result.map(row => ({
             id: row.user_books.id,
             bookId: row.books.id,
             book: {
               id: row.books.id,
               isbn: row.books.isbn,
               title: row.books.title,
               author: row.books.author,
               coverPath: row.books.coverPath,
               openLibraryKey: row.books.openLibraryKey,
-              createdAt: row.books.createdAt
+              createdAt: row.books.createdAt,
+              description: row.books.description ?? null,
+              subjects: row.books.subjects ?? null,
+              publishDate: row.books.publishDate ?? null,
+              publishers: row.books.publishers ?? null,
+              numberOfPages: row.books.numberOfPages ?? null,
+              workKey: row.books.workKey ?? null
             },
             addedAt: row.user_books.addedAt
           }))

285-294: Incomplete Book mapping in getBookById.

The returned object is missing optional Book fields.

🐛 Proposed fix
           return {
             id: book.id,
             isbn: book.isbn,
             title: book.title,
             author: book.author,
             coverPath: book.coverPath,
             openLibraryKey: book.openLibraryKey,
-            createdAt: book.createdAt instanceof Date ? book.createdAt : new Date(book.createdAt as unknown as string)
+            createdAt: book.createdAt instanceof Date ? book.createdAt : new Date(book.createdAt as unknown as string),
+            description: book.description ?? null,
+            subjects: book.subjects ?? null,
+            publishDate: book.publishDate ?? null,
+            publishers: book.publishers ?? null,
+            numberOfPages: book.numberOfPages ?? null,
+            workKey: book.workKey ?? null
           }

355-371: Unsafe JSON.parse may throw on malformed data.

If bookData.subjects contains malformed JSON, JSON.parse on line 363 will throw an unhandled exception, crashing the request.

🐛 Proposed fix: Add safe JSON parsing
+// Helper for safe JSON parsing
+function safeJsonParse<T>(json: string | null): T | null {
+  if (!json) return null
+  try {
+    return JSON.parse(json)
+  } catch {
+    return null
+  }
+}

 // Then in getUserBookWithDetails:
           return {
             id: row.user_books.id,
             bookId: bookData.id,
             title: bookData.title,
             author: bookData.author,
             isbn: bookData.isbn,
             coverPath: bookData.coverPath,
             description: bookData.description ?? null,
-            subjects: bookData.subjects ? JSON.parse(bookData.subjects) : null,
+            subjects: safeJsonParse<string[]>(bookData.subjects),
             publishDate: bookData.publishDate ?? null,
server/services/book.service.ts (2)

1-13: Missing imports for types and dependencies.

The file uses LibraryBook, PaginationParams, PaginatedResult, DatabaseError, DbService, StorageService, OpenLibraryRepository, BookCreateError, BookAlreadyOwnedError, OpenLibraryBookNotFoundError, OpenLibraryApiError, BookNotFoundError, BatchDeleteResult, BookDetails, BookLookupResult, BookRepository, lookupByISBN, and OpenLibraryBookData without importing them.

🐛 Proposed imports
 import { Context, Effect, Layer, Either } from 'effect'
 import type { HttpClient } from '@effect/platform'
 import type { UserBook } from '../repositories/book.repository'
+import {
+  BookRepository,
+  BookNotFoundError,
+  BookCreateError,
+  BookAlreadyOwnedError,
+  DatabaseError
+} from '../repositories/book.repository'
+import {
+  OpenLibraryRepository,
+  OpenLibraryBookNotFoundError,
+  OpenLibraryApiError,
+  lookupByISBN,
+  type OpenLibraryBookData
+} from '../repositories/openLibrary.repository'
+import { DbService } from './db.service'
+import { StorageService } from './storage.service'
+import type { PaginationParams, PaginatedResult } from '~/shared/types/pagination'
+import type {
+  LibraryBook,
+  BookLookupResult,
+  BatchDeleteResult,
+  BookDetails
+} from '~/shared/types/book'

127-141: Unsafe JSON.parse on localBook.subjects.

The JSON.parse call on line 135 could throw if localBook.subjects contains malformed JSON. Apply the same safe parsing approach as suggested for the repository.

🐛 Proposed fix
+// Helper for safe JSON parsing (or import from shared utils)
+function safeJsonParse<T>(json: string | null): T | null {
+  if (!json) return null
+  try {
+    return JSON.parse(json)
+  } catch {
+    return null
+  }
+}

           if (localBook) {
             return {
               found: true,
               isbn: localBook.isbn || normalizedISBN,
               title: localBook.title,
               author: localBook.author,
               coverUrl: localBook.coverPath ? `/api/blob/${localBook.coverPath}` : null,
               description: localBook.description ?? undefined,
-              subjects: localBook.subjects ? JSON.parse(localBook.subjects) : null,
+              subjects: safeJsonParse<string[]>(localBook.subjects),
               publishDate: localBook.publishDate ?? undefined,
server/services/storage.service.ts (1)

33-53: Critical: blob is used but not defined or imported.

The StorageServiceLive implementation references blob (lines 38, 58, 69, 80) without importing or defining it. The comment on line 33 mentions "uses dynamic import to avoid build issues," but no dynamic import is present. This will cause a runtime error.

🐛 Proposed fix: Add dynamic import
 // Live implementation - uses dynamic import to avoid build issues
 export const StorageServiceLive = Layer.succeed(StorageService, {
   put: (pathname, data, options) =>
-    Effect.gen(function* () {
-      const result = yield* Effect.tryPromise({
-        try: () => blob.put(pathname, data, {
+    Effect.tryPromise({
+      try: async () => {
+        const { blob } = await import('hub:blob')
+        const result = await blob.put(pathname, data, {
           contentType: options?.contentType,
           prefix: options?.prefix
-        }),
-        catch: error => new StorageError({
-          message: `Failed to put blob: ${error}`,
-          operation: 'put'
         })
-      })
-      return {
-        pathname: result.pathname,
-        contentType: result.contentType,
-        size: result.size,
-        uploadedAt: new Date(result.uploadedAt)
-      }
+        return {
+          pathname: result.pathname,
+          contentType: result.contentType,
+          size: result.size,
+          uploadedAt: new Date(result.uploadedAt)
+        }
+      },
+      catch: error => new StorageError({
+        message: `Failed to put blob: ${error}`,
+        operation: 'put'
+      })
     }),

Apply similar pattern to get, delete, and list methods.

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a0c1e7 and 15ddf27.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • nuxt.config.ts
  • package.json
  • server/repositories/book.repository.ts
  • server/repositories/openLibrary.repository.ts
  • server/services/auth.service.ts
  • server/services/book.service.ts
  • server/services/storage.service.ts
  • server/utils/auth.ts
  • server/utils/effect.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-13T21:08:13.933Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/utils/effect.ts:30-38
Timestamp: 2026-01-13T21:08:13.933Z
Learning: In Effect-TS code, when combining service tags (Context.Tag) for multiple services, use a union type (Context1.Tag | Context2.Tag) rather than an intersection (&). Service tags are nominal types and cannot be meaningfully intersected; a union correctly represents combined service requirements in the Effect<R> parameter, while runtime Layer composition handles provisioning. Apply this guidance to TypeScript files that define or compose Effect-TS service contexts (e.g., server/utils/effect.ts and similar files).

Applied to files:

  • server/utils/auth.ts
  • server/repositories/openLibrary.repository.ts
  • nuxt.config.ts
  • server/services/auth.service.ts
  • server/repositories/book.repository.ts
  • server/utils/effect.ts
  • server/services/storage.service.ts
  • server/services/book.service.ts
📚 Learning: 2026-01-13T21:24:30.028Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: shared/utils/schemas.ts:11-13
Timestamp: 2026-01-13T21:24:30.028Z
Learning: In TypeScript files using Zod v4.3.5 or newer, use the error option for both schema-level configuration and validators (min, max, etc.). The message parameter is deprecated. Example: z.string({ error: 'Required' }).min(10, { error: 'Too short' }). Apply this pattern to all Zod schema definitions in TS files to avoid deprecated API usage.

Applied to files:

  • server/utils/auth.ts
  • server/repositories/openLibrary.repository.ts
  • nuxt.config.ts
  • server/services/auth.service.ts
  • server/repositories/book.repository.ts
  • server/utils/effect.ts
  • server/services/storage.service.ts
  • server/services/book.service.ts
🧬 Code graph analysis (4)
server/services/auth.service.ts (2)
server/utils/auth.ts (1)
  • auth (54-75)
server/db/schema/auth.ts (1)
  • session (16-25)
server/repositories/book.repository.ts (7)
server/utils/effect.ts (4)
  • Data (130-130)
  • Effect (130-130)
  • Context (130-130)
  • Layer (130-130)
server/repositories/openLibrary.repository.ts (5)
  • OpenLibraryBookNotFoundError (8-11)
  • OpenLibraryApiError (13-15)
  • OpenLibraryRepository (76-79)
  • lookupByISBN (285-286)
  • downloadCover (288-289)
server/services/db.service.ts (1)
  • DbService (10-10)
server/services/storage.service.ts (1)
  • StorageService (31-31)
shared/types/pagination.ts (2)
  • PaginationParams (1-4)
  • PaginatedResult (6-15)
server/db/schema/domain.ts (2)
  • books (8-25)
  • userBooks (29-34)
shared/types/book.ts (1)
  • BookDetails (31-46)
server/utils/effect.ts (6)
server/services/db.service.ts (2)
  • DbServiceLive (13-15)
  • DbService (10-10)
server/services/storage.service.ts (2)
  • StorageServiceLive (34-93)
  • StorageService (31-31)
server/services/auth.service.ts (2)
  • AuthServiceLive (40-49)
  • AuthService (22-22)
server/repositories/book.repository.ts (2)
  • BookRepositoryLive (92-374)
  • BookRepository (84-84)
server/repositories/openLibrary.repository.ts (1)
  • OpenLibraryRepository (76-79)
server/services/book.service.ts (2)
  • BookServiceLive (62-171)
  • BookService (58-58)
server/services/storage.service.ts (1)
server/utils/effect.ts (4)
  • Data (130-130)
  • Effect (130-130)
  • Context (130-130)
  • Layer (130-130)
🔇 Additional comments (30)
nuxt.config.ts (5)

30-37: LGTM!

The import directories align with the layered architecture (services/repositories) described in the PR.


39-42: LGTM!

NuxtHub configuration with SQLite and blob storage is properly set up for the deployment target.


44-53: LGTM!

ESLint stylistic rules are consistent and follow common conventions for TypeScript/JavaScript projects.


55-57: Verify intent: Image optimization is disabled.

Setting provider: 'none' disables all image optimization. This may be intentional for initial setup, but consider enabling a provider (e.g., ipx for self-hosted or a CDN provider) before production to optimize image delivery.


19-22: No action needed – configuration is correct.

betterAuthUrl correctly remains in server-only config. The client-side auth composable (createAuthClient() in app/composables/auth.ts) is initialized without requiring a URL, and the actual betterAuthUrl value is only accessed server-side in server/utils/auth.ts. Exposing it to the client would be unnecessary and create a potential security concern.

server/utils/auth.ts (2)

6-35: Security improvement: Production guard for auth secret is well implemented.

The getAuthSecret helper properly validates the secret is present in production and fails fast with a clear error message. The fallback to a dev secret is only used in non-production environments.


54-77: LGTM!

The auth configuration is properly set up with the helper functions, SQLite adapter, and email/password authentication enabled. The placeholder for social providers is appropriate for the initial setup.

package.json (1)

41-42: LGTM: Package manager updated to safe version.

The pnpm@10.28.0 version addresses the previously flagged CVE-2025-69262 vulnerability (fixed in 10.27.0+).

server/services/auth.service.ts (2)

24-37: Well-structured session fetching helper.

The fetchSession helper correctly extracts the shared session-fetching logic, addressing the previous review comment about code duplication. The implementation properly uses Effect.tryPromise with appropriate error handling.


39-56: LGTM!

The AuthServiceLive implementation is clean and leverages the fetchSession helper effectively. The helper effects at the bottom provide a convenient API for consuming the service.

server/utils/effect.ts (3)

7-13: Good implementation of redirect-following HTTP client.

The HttpClientLive layer correctly configures the HTTP client to follow up to 10 redirects, which is necessary for OpenLibrary cover URLs. The layer composition with NodeHttpClient.layer is properly structured.


15-36: Clean layer composition hierarchy.

The layer structure follows a clear dependency hierarchy (Base → Repositories → Services → Main) without redundant provisioning. This addresses the previous review feedback.


1-4: Reliance on Nuxt auto-imports is acceptable.

The file uses DbServiceLive, StorageServiceLive, AuthServiceLive, isError, createError, and other symbols without explicit imports. This is valid in Nuxt/Nitro projects where server utilities are auto-imported. While explicit imports would improve IDE support in some setups, this is a standard Nuxt pattern.

.github/workflows/ci.yml (1)

11-19: No issues found. Both actions/checkout@v6 and actions/setup-node@v6 are released and stable versions. The checkout action is the current stable major release, and setup-node v6 has been released and maintained (v6.1.0 as of Dec 2025). Using these versions is appropriate.

server/repositories/openLibrary.repository.ts (5)

86-95: LGTM!

The fetchJson helper properly implements timeout handling with Duration.seconds(5) and maps HTTP errors to domain-specific OpenLibraryApiError.


97-115: LGTM!

The checkCoverExists helper correctly handles timeouts and gracefully catches all errors to return false, preventing cover availability checks from failing the overall operation.


117-217: LGTM!

The lookupByISBN implementation has a well-structured flow: ISBN normalization, primary API fetch, cover validation, and optional enrichment from works data with proper error handling via Effect.catchAll and debug logging.


274-281: LGTM!

Good defensive error handling — the catchTag on OpenLibraryCoverError ensures cover download failures don't fail the entire operation, logging a warning and returning null instead.


284-289: LGTM!

Helper effect exports follow the standard Effect-TS pattern for ergonomic service method access.

server/repositories/book.repository.ts (5)

7-24: LGTM!

Error types are properly defined using Effect's Data.TaggedError pattern with distinct tags (BookNotFoundError, BookCreateError, BookAlreadyOwnedError, DatabaseError). The BookNotFoundError tag is distinct from OpenLibraryBookNotFoundError in the OpenLibrary repository.


26-48: LGTM!

The Book and UserBook interfaces properly define the domain types with appropriate optional fields matching the database schema.


50-84: LGTM!

The interface properly defines Effect-based method signatures with explicit error and dependency types. Based on learnings, the union types for service requirements are correctly used.


296-330: LGTM!

The removeFromLibrary and findByIsbn implementations are correctly structured with proper error handling.


376-393: LGTM!

All repository methods now have corresponding helper effect exports following the standard Effect-TS pattern.

server/services/storage.service.ts (1)

95-106: LGTM!

Helper effect exports follow the standard Effect-TS pattern for ergonomic service access.

server/services/book.service.ts (5)

5-13: LGTM!

Good refactor extracting toLibraryBook helper to eliminate duplicate mapping logic. This addresses the previous review feedback.


15-58: LGTM!

The BookServiceInterface properly defines Effect-based method signatures with explicit error and dependency types following Effect-TS patterns.


88-115: LGTM!

The batchRemoveFromLibrary implementation properly uses bounded concurrency ({ concurrency: 10 }) and correctly logs errors by yielding Effect.logError. This addresses previous review feedback.


120-168: LGTM!

The lookupBook implementation correctly uses normalizedISBN for both the local lookup and OpenLibrary fallback, and the not-found response consistently uses the normalized ISBN.


173-191: LGTM!

Helper effect exports provide ergonomic access to all service methods.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@server/utils/auth.ts`:
- Around line 37-59: In getAuthUrl, enhance the production warning to treat
empty-string values as unset by checking url.trim() as well as null/undefined;
update the condition that currently tests for !url to instead consider
url?.trim() === '' (or compute an isEmpty = !url || url.trim() === '' and use
that) so the warning and fallback to 'http://localhost:3000' behave consistently
with getAuthSecret.

In `@server/utils/effect.ts`:
- Around line 149-150: The module re-exports Effect, Layer, Context, and Data
but omits pipe; if consumers need pipe for composing effects, add pipe to the
export list so it’s publicly available (i.e., update the export statement that
currently exports Effect, Layer, Context, Data to also export pipe from
'effect'). Ensure the symbol name is exactly pipe to match imports elsewhere.
- Around line 37-46: The union type declaration for MainServices is
mis-indented; adjust continuation lines so binary-op continuation (the '=' and
subsequent '|' lines) use 4-space indentation to satisfy
`@stylistic/indent-binary-ops`. Specifically, align the "=" line and each union
member line (DbService, StorageService, AuthService, BookRepository,
OpenLibraryRepository, BookService, HttpClient.HttpClient) with a 4-space
continuation indent relative to the "export type MainServices" line so the '|'
tokens are indented correctly.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15ddf27 and f6ce7b1.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • server/utils/auth.ts
  • server/utils/effect.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T21:08:13.933Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/utils/effect.ts:30-38
Timestamp: 2026-01-13T21:08:13.933Z
Learning: In Effect-TS code, when combining service tags (Context.Tag) for multiple services, use a union type (Context1.Tag | Context2.Tag) rather than an intersection (&). Service tags are nominal types and cannot be meaningfully intersected; a union correctly represents combined service requirements in the Effect<R> parameter, while runtime Layer composition handles provisioning. Apply this guidance to TypeScript files that define or compose Effect-TS service contexts (e.g., server/utils/effect.ts and similar files).

Applied to files:

  • server/utils/auth.ts
  • server/utils/effect.ts
📚 Learning: 2026-01-13T21:24:30.028Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: shared/utils/schemas.ts:11-13
Timestamp: 2026-01-13T21:24:30.028Z
Learning: In TypeScript files using Zod v4.3.5 or newer, use the error option for both schema-level configuration and validators (min, max, etc.). The message parameter is deprecated. Example: z.string({ error: 'Required' }).min(10, { error: 'Too short' }). Apply this pattern to all Zod schema definitions in TS files to avoid deprecated API usage.

Applied to files:

  • server/utils/auth.ts
  • server/utils/effect.ts
📚 Learning: 2026-01-15T15:10:03.761Z
Learnt from: niklhut
Repo: niklhut/libroo PR: 1
File: server/services/storage.service.ts:55-92
Timestamp: 2026-01-15T15:10:03.761Z
Learning: In NuxtHub server-side code, the blob API from Cloudflare R2 is provided via the virtual module hub:blob and is auto-imported. In files under server/ (e.g., server/services/storage.service.ts), you can call blob.put(), blob.get(), blob.delete(), blob.list(), etc., without an explicit import. Do not add import statements for blob in server code. Ensure the files are under the server directory and that TypeScript paths resolve to the virtual module in NuxtHub.

Applied to files:

  • server/utils/auth.ts
  • server/utils/effect.ts
🧬 Code graph analysis (1)
server/utils/effect.ts (6)
server/services/db.service.ts (2)
  • DbServiceLive (13-15)
  • DbService (10-10)
server/services/storage.service.ts (2)
  • StorageServiceLive (34-93)
  • StorageService (31-31)
server/services/auth.service.ts (2)
  • AuthServiceLive (40-49)
  • AuthService (22-22)
server/repositories/book.repository.ts (2)
  • BookRepositoryLive (92-374)
  • BookRepository (84-84)
server/repositories/openLibrary.repository.ts (2)
  • OpenLibraryRepositoryLive (118-282)
  • OpenLibraryRepository (76-79)
server/services/book.service.ts (2)
  • BookServiceLive (62-171)
  • BookService (58-58)
🪛 GitHub Actions: Lint
server/utils/effect.ts

[error] 40-40: Expected indentation of 4 spaces @stylistic/indent-binary-ops

🪛 GitHub Check: lint
server/utils/effect.ts

[failure] 45-45:
Expected indentation of 4 spaces


[failure] 44-44:
Expected indentation of 4 spaces


[failure] 43-43:
Expected indentation of 4 spaces


[failure] 42-42:
Expected indentation of 4 spaces


[failure] 41-41:
Expected indentation of 4 spaces


[failure] 40-40:
Expected indentation of 4 spaces

🔇 Additional comments (12)
server/utils/auth.ts (4)

1-4: LGTM!

The imports correctly use NuxtHub virtual modules (hub:db and hub:db:schema) along with the better-auth library and its drizzle adapter.


6-35: Well implemented production security guard.

The function properly validates the auth secret and fails fast in production with a clear, actionable error message. The defensive try/catch around useRuntimeConfig handles CLI usage gracefully, and the empty string check on line 24 is thorough.


61-82: LGTM!

The auth configuration correctly uses the helper functions and is properly configured for the SQLite migration. The commented social providers placeholder is reasonable for the initial setup.


83-84: LGTM!

The type export enables proper type inference for consumers of the auth instance.

.github/workflows/ci.yml (2)

1-7: LGTM! Workflow and job naming are now consistent.

The workflow correctly uses actions/checkout@v6 and the job name lint accurately reflects its purpose. The previous review comment about the misleading typecheck job name has been addressed.


11-26: Action and Node.js versions are current.

The actions/checkout latest version is v6.0.1, released December 2, 2025. The actions/setup-node@v6 with node-version: 24 is the documented configuration, and Node.js 24.x is now in Active LTS (Krypton).

The workflow is appropriately streamlined for linting. If type checking is still needed in the project, consider whether a separate workflow or step should be added.

server/utils/effect.ts (6)

1-12: LGTM!

Clean import structure and well-documented HttpClientLive layer. The redirect-following behavior with a 10-redirect limit is appropriate for handling OpenLibrary cover URL redirects.


14-35: LGTM!

The layer composition correctly follows the dependency hierarchy: base services (DB, Storage, Auth, HttpClient) → repositories (Book, OpenLibrary) → services (Book). The simplified MainLive = ServicesLive avoids the redundant double-provisioning that was previously flagged.


47-53: LGTM!

The getProp helper provides safe property extraction from unknown objects with proper type guards. This is appropriate for extracting properties from tagged error objects.


55-77: LGTM!

Comprehensive error-to-status-code mapping with appropriate HTTP codes. The custom message formatters for BookNotFoundError and BookAlreadyOwnedError provide user-friendly context by extracting the ISBN.


79-111: LGTM!

The handleError function cleanly transforms errors into H3Errors using Effect.die(). The documentation clearly explains the pattern, and the use of isError() is now consistent throughout the file.


113-147: LGTM!

The runEffect function provides a clean execution wrapper with comprehensive documentation explaining the error-handling flow. The use of Effect.runPromiseExit with Exit.match and defect extraction from Cause.defects is idiomatic Effect-TS.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@niklhut niklhut merged commit 5bfdf22 into main Jan 15, 2026
2 checks passed
@niklhut niklhut deleted the feat/initial-setup branch January 15, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants