fix: persist product archive cover and provenance
This commit is contained in:
@@ -54,7 +54,8 @@ class ProductArchiveStore:
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
purge_after TEXT
|
||||
purge_after TEXT,
|
||||
cover_image_id TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_products_source_external_id
|
||||
ON products(source, external_product_id);
|
||||
@@ -77,6 +78,7 @@ class ProductArchiveStore:
|
||||
version_id TEXT NOT NULL,
|
||||
image_path TEXT NOT NULL DEFAULT '',
|
||||
image_type TEXT NOT NULL DEFAULT '',
|
||||
is_cover INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_images_product_id
|
||||
@@ -89,6 +91,9 @@ class ProductArchiveStore:
|
||||
product_id TEXT NOT NULL,
|
||||
version_id TEXT NOT NULL,
|
||||
archive_path TEXT NOT NULL DEFAULT '',
|
||||
source_job_id TEXT NOT NULL DEFAULT '',
|
||||
source_asset_id TEXT NOT NULL DEFAULT '',
|
||||
db_checksum TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_wordcloud_archives_product_id
|
||||
@@ -110,6 +115,17 @@ class ProductArchiveStore:
|
||||
ON cleanup_records(purge_after);
|
||||
"""
|
||||
)
|
||||
self._ensure_column(conn, "products", "cover_image_id", "TEXT")
|
||||
self._ensure_column(conn, "product_images", "is_cover", "INTEGER NOT NULL DEFAULT 0")
|
||||
self._ensure_column(conn, "product_wordcloud_archives", "source_job_id", "TEXT NOT NULL DEFAULT ''")
|
||||
self._ensure_column(conn, "product_wordcloud_archives", "source_asset_id", "TEXT NOT NULL DEFAULT ''")
|
||||
self._ensure_column(conn, "product_wordcloud_archives", "db_checksum", "TEXT NOT NULL DEFAULT ''")
|
||||
|
||||
@staticmethod
|
||||
def _ensure_column(conn: sqlite3.Connection, table: str, column: str, declaration: str) -> None:
|
||||
columns = {row["name"] for row in conn.execute(f"PRAGMA table_info({table})")}
|
||||
if column not in columns:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {declaration}")
|
||||
|
||||
def _execute(self, sql: str, parameters: tuple[Any, ...] = ()) -> None:
|
||||
with self._lock, self._connect() as conn:
|
||||
@@ -132,6 +148,7 @@ class ProductArchiveStore:
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
purge_after=datetime.fromisoformat(row["purge_after"]) if row["purge_after"] else None,
|
||||
cover_image_id=row["cover_image_id"],
|
||||
)
|
||||
|
||||
def upsert_product(self, product: ProductInput) -> ProductRecord:
|
||||
@@ -198,6 +215,65 @@ class ProductArchiveStore:
|
||||
)
|
||||
return record
|
||||
|
||||
def _before_archive_commit(self) -> None:
|
||||
"""Test seam: runs inside the archive metadata transaction before commit."""
|
||||
|
||||
def write_archive_version(
|
||||
self,
|
||||
product_id: str,
|
||||
version_id: str,
|
||||
metadata: dict[str, Any],
|
||||
preview_path: str,
|
||||
archives: list[dict[str, str]],
|
||||
now: datetime | None = None,
|
||||
) -> tuple[ProductVersionRecord, ProductImageRecord, list[ProductWordcloudArchiveRecord]]:
|
||||
"""Persist one complete archive version and make its preview the sole cover."""
|
||||
created_at = now or _now()
|
||||
version = ProductVersionRecord(
|
||||
version_id=version_id, product_id=product_id, metadata=metadata, created_at=created_at
|
||||
)
|
||||
image = ProductImageRecord(
|
||||
image_id=f"img_{uuid.uuid4().hex}", product_id=product_id, version_id=version_id,
|
||||
image_path=preview_path, image_type="design_preview", is_cover=True, created_at=created_at,
|
||||
)
|
||||
records = [
|
||||
ProductWordcloudArchiveRecord(
|
||||
archive_id=f"wca_{uuid.uuid4().hex}", product_id=product_id, version_id=version_id,
|
||||
archive_path=item["archive_path"], source_job_id=item["source_job_id"],
|
||||
source_asset_id=item["source_asset_id"], db_checksum=item["db_checksum"], created_at=created_at,
|
||||
)
|
||||
for item in archives
|
||||
]
|
||||
with self._lock, self._connect() as conn:
|
||||
product = conn.execute("SELECT product_id FROM products WHERE product_id = ?", (product_id,)).fetchone()
|
||||
if product is None:
|
||||
raise ValueError(f"unknown product_id: {product_id}")
|
||||
conn.execute("BEGIN")
|
||||
conn.execute(
|
||||
"INSERT INTO product_versions (version_id, product_id, version, metadata, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(version.version_id, version.product_id, version.version, json.dumps(version.metadata, ensure_ascii=False), version.created_at.isoformat()),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE product_images SET is_cover = 0 WHERE product_id = ? AND is_cover = 1", (product_id,)
|
||||
)
|
||||
conn.execute(
|
||||
"""INSERT INTO product_images (image_id, product_id, version_id, image_path, image_type, is_cover, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
(image.image_id, image.product_id, image.version_id, image.image_path, image.image_type, 1, image.created_at.isoformat()),
|
||||
)
|
||||
for record in records:
|
||||
conn.execute(
|
||||
"""INSERT INTO product_wordcloud_archives
|
||||
(archive_id, product_id, version_id, archive_path, source_job_id, source_asset_id, db_checksum, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(record.archive_id, record.product_id, record.version_id, record.archive_path,
|
||||
record.source_job_id, record.source_asset_id, record.db_checksum, record.created_at.isoformat()),
|
||||
)
|
||||
conn.execute("UPDATE products SET cover_image_id = ?, updated_at = ? WHERE product_id = ?", (image.image_id, created_at.isoformat(), product_id))
|
||||
self._before_archive_commit()
|
||||
conn.commit()
|
||||
return version, image, records
|
||||
|
||||
def add_image(
|
||||
self, product_id: str, version_id: str, image_path: str = "", image_type: str = "",
|
||||
now: datetime | None = None,
|
||||
|
||||
Reference in New Issue
Block a user