mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add Hardcover shelf sync & unify book mappings
Introduce Hardcover provider support and consolidate per-provider book mapping tables into a unified BookMapping model. Adds two Prisma migrations (add_hardcover_shelves, unify_book_mappings), new backend services (hardcover-api, shelf-sync-core), and provider-specific sync logic and API routes for hardcover shelves with token/list validation. Frontend: new HardcoverForm component, refactor AddShelfModal to support Hardcover, hook updates, and small UI/accessibility tweaks. Also add documentation for Goodreads and Hardcover sync flows and update tests to cover scheduler/prisma helpers.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "hardcover_shelves" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"list_id" TEXT NOT NULL,
|
||||
"api_token" TEXT NOT NULL,
|
||||
"last_sync_at" TIMESTAMP(3),
|
||||
"book_count" INTEGER,
|
||||
"cover_urls" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "hardcover_shelves_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "hardcover_book_mappings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hardcover_book_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"author" TEXT NOT NULL,
|
||||
"audible_asin" TEXT,
|
||||
"cover_url" TEXT,
|
||||
"no_match" BOOLEAN NOT NULL DEFAULT false,
|
||||
"last_search_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "hardcover_book_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "hardcover_shelves_user_id_idx" ON "hardcover_shelves"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "hardcover_shelves_user_id_list_id_key" ON "hardcover_shelves"("user_id", "list_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "hardcover_book_mappings_hardcover_book_id_key" ON "hardcover_book_mappings"("hardcover_book_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "hardcover_book_mappings_hardcover_book_id_idx" ON "hardcover_book_mappings"("hardcover_book_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "hardcover_book_mappings_audible_asin_idx" ON "hardcover_book_mappings"("audible_asin");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "hardcover_shelves" ADD CONSTRAINT "hardcover_shelves_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "book_mappings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"external_book_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"author" TEXT NOT NULL,
|
||||
"audible_asin" TEXT,
|
||||
"cover_url" TEXT,
|
||||
"no_match" BOOLEAN NOT NULL DEFAULT false,
|
||||
"last_search_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "book_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Migrate data from goodreads_book_mappings
|
||||
INSERT INTO "book_mappings" ("id", "provider", "external_book_id", "title", "author", "audible_asin", "cover_url", "no_match", "last_search_at", "created_at", "updated_at")
|
||||
SELECT "id", 'goodreads', "goodreads_book_id", "title", "author", "audible_asin", "cover_url", "no_match", "last_search_at", "created_at", "updated_at"
|
||||
FROM "goodreads_book_mappings";
|
||||
|
||||
-- Migrate data from hardcover_book_mappings
|
||||
INSERT INTO "book_mappings" ("id", "provider", "external_book_id", "title", "author", "audible_asin", "cover_url", "no_match", "last_search_at", "created_at", "updated_at")
|
||||
SELECT "id", 'hardcover', "hardcover_book_id", "title", "author", "audible_asin", "cover_url", "no_match", "last_search_at", "created_at", "updated_at"
|
||||
FROM "hardcover_book_mappings";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "goodreads_book_mappings";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "hardcover_book_mappings";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "book_mappings_provider_external_book_id_key" ON "book_mappings"("provider", "external_book_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "book_mappings_provider_external_book_id_idx" ON "book_mappings"("provider", "external_book_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "book_mappings_audible_asin_idx" ON "book_mappings"("audible_asin");
|
||||
+22
-31
@@ -518,26 +518,34 @@ model GoodreadsShelf {
|
||||
@@map("goodreads_shelves")
|
||||
}
|
||||
|
||||
model GoodreadsBookMapping {
|
||||
id String @id @default(uuid())
|
||||
goodreadsBookId String @unique @map("goodreads_book_id")
|
||||
title String
|
||||
author String
|
||||
audibleAsin String? @map("audible_asin")
|
||||
coverUrl String? @map("cover_url") @db.Text
|
||||
noMatch Boolean @default(false) @map("no_match")
|
||||
lastSearchAt DateTime? @map("last_search_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
// ============================================================================
|
||||
// UNIFIED BOOK MAPPING TABLE
|
||||
// Global book-to-ASIN mapping cache shared across all shelf providers.
|
||||
// Uses provider + externalBookId composite key for cross-provider dedup.
|
||||
// ============================================================================
|
||||
|
||||
@@index([goodreadsBookId])
|
||||
model BookMapping {
|
||||
id String @id @default(uuid())
|
||||
provider String // "goodreads", "hardcover", etc.
|
||||
externalBookId String @map("external_book_id")
|
||||
title String
|
||||
author String
|
||||
audibleAsin String? @map("audible_asin")
|
||||
coverUrl String? @map("cover_url") @db.Text
|
||||
noMatch Boolean @default(false) @map("no_match")
|
||||
lastSearchAt DateTime? @map("last_search_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([provider, externalBookId])
|
||||
@@index([provider, externalBookId])
|
||||
@@index([audibleAsin])
|
||||
@@map("goodreads_book_mappings")
|
||||
@@map("book_mappings")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HARDCOVER SYNC TABLES
|
||||
// Per-user Hardcover list subscriptions + global book-to-ASIN mapping cache
|
||||
// Per-user Hardcover list subscriptions
|
||||
// ============================================================================
|
||||
|
||||
model HardcoverShelf {
|
||||
@@ -560,23 +568,6 @@ model HardcoverShelf {
|
||||
@@map("hardcover_shelves")
|
||||
}
|
||||
|
||||
model HardcoverBookMapping {
|
||||
id String @id @default(uuid())
|
||||
hardcoverBookId String @unique @map("hardcover_book_id") // Internal ID from Hardcover
|
||||
title String
|
||||
author String
|
||||
audibleAsin String? @map("audible_asin")
|
||||
coverUrl String? @map("cover_url") @db.Text
|
||||
noMatch Boolean @default(false) @map("no_match")
|
||||
lastSearchAt DateTime? @map("last_search_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([hardcoverBookId])
|
||||
@@index([audibleAsin])
|
||||
@@map("hardcover_book_mappings")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKS TABLE
|
||||
// Cross-ASIN audiobook identity mapping — links multiple Audible ASINs
|
||||
|
||||
Reference in New Issue
Block a user