generated from wiz4apps/templates.OnlineShop
99 lines
4.3 KiB
TypeScript
99 lines
4.3 KiB
TypeScript
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { imagePath, readImages } from '../src/db/images';
|
|
|
|
/**
|
|
* File-side acceptance for the product gallery (docs/PLAN.md §11, card #64):
|
|
* • db/seed/images.tsv covers 93 products, each with a position = 1 (main photo)
|
|
* • every derived url exists on disk as a non-empty file, and file count = row count
|
|
* • generated db/seed/0001_seed.guardsafe.sql carries every product_image url
|
|
* and no `ON CONFLICT` (the /db-forms guard rejects it)
|
|
*
|
|
* Wired into `pnpm test` via scripts/verify.ts; can also be run directly.
|
|
*/
|
|
|
|
const PRODUCTS_DIR = fileURLToPath(new URL('../public/images/products', import.meta.url));
|
|
const GUARDSAFE_SQL = fileURLToPath(new URL('../db/seed/0001_seed.guardsafe.sql', import.meta.url));
|
|
|
|
export interface ImageArtifactReport {
|
|
rows: number;
|
|
products: number;
|
|
productsWithMain: number;
|
|
files: number;
|
|
bytes: number;
|
|
missing: string[];
|
|
extra: string[];
|
|
guardsafeHasAllRows: boolean;
|
|
guardsafeHasOnConflict: boolean;
|
|
}
|
|
|
|
function listJpgFiles(dir: string, prefix = ''): string[] {
|
|
const out: string[] = [];
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
if (entry.isDirectory()) out.push(...listJpgFiles(join(dir, entry.name), relative));
|
|
else if (entry.name.endsWith('.jpg')) out.push(relative);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function verifyImageArtifacts(): ImageArtifactReport {
|
|
const rows = readImages();
|
|
const products = new Set(rows.map((row) => row.slug));
|
|
const productsWithMain = new Set(rows.filter((row) => row.position === 1).map((row) => row.slug));
|
|
const expected = new Set(rows.map((row) => imagePath(row.slug, row.position)));
|
|
|
|
const missing: string[] = [];
|
|
let bytes = 0;
|
|
for (const row of rows) {
|
|
const relative = `${row.slug}/${row.position}.jpg`;
|
|
const info = statSync(join(PRODUCTS_DIR, relative), { throwIfNoEntry: false });
|
|
if (!info || info.size === 0) missing.push(imagePath(row.slug, row.position));
|
|
else bytes += info.size;
|
|
}
|
|
|
|
const files = listJpgFiles(PRODUCTS_DIR);
|
|
const extra = files
|
|
.map((file) => `/images/products/${file}`)
|
|
.filter((url) => !expected.has(url))
|
|
.sort();
|
|
|
|
const guardsafe = readFileSync(GUARDSAFE_SQL, 'utf8');
|
|
return {
|
|
rows: rows.length,
|
|
products: products.size,
|
|
productsWithMain: productsWithMain.size,
|
|
files: files.length,
|
|
bytes,
|
|
missing,
|
|
extra,
|
|
guardsafeHasAllRows: [...expected].every((url) => guardsafe.includes(`'${url}'`)),
|
|
guardsafeHasOnConflict: /ON\s+CONFLICT/i.test(guardsafe),
|
|
};
|
|
}
|
|
|
|
if ((process.argv[1] ?? '').endsWith('verify-images.ts')) {
|
|
const report = verifyImageArtifacts();
|
|
const problems: string[] = [];
|
|
if (report.products !== 93) problems.push(`products = ${report.products}, expected 93`);
|
|
if (report.productsWithMain !== 93) problems.push(`products with position 1 = ${report.productsWithMain}, expected 93`);
|
|
if (report.missing.length > 0) problems.push(`missing files = ${report.missing.length} (${report.missing.slice(0, 5).join(', ')})`);
|
|
if (report.rows !== report.files) problems.push(`db/seed/images.tsv rows = ${report.rows}, files = ${report.files}`);
|
|
if (report.extra.length > 0) problems.push(`orphan files = ${report.extra.length} (${report.extra.slice(0, 5).join(', ')})`);
|
|
if (!report.guardsafeHasAllRows) problems.push('0001_seed.guardsafe.sql is missing product_image rows');
|
|
if (report.guardsafeHasOnConflict) problems.push('0001_seed.guardsafe.sql contains ON CONFLICT');
|
|
|
|
console.log('verify-images: OK');
|
|
console.log(` products = ${report.products}/93 (position 1: ${report.productsWithMain}/93)`);
|
|
console.log(` image rows = ${report.rows}`);
|
|
console.log(` files on disk = ${report.files} (${(report.bytes / (1024 * 1024)).toFixed(1)} MiB)`);
|
|
console.log(` missing files = ${report.missing.length}`);
|
|
console.log(` orphan files = ${report.extra.length}`);
|
|
console.log(` guard-safe: rows = ${report.guardsafeHasAllRows}, on-conflict = ${report.guardsafeHasOnConflict}`);
|
|
if (problems.length > 0) {
|
|
console.error(`verify-images: FAILED — ${problems.join('; ')}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|