Add local seed data so a db reset covers current features (#91)

* Add local db seed

* Expand local db seed for main schema and dev testing fixtures.

Seed the production tag catalog, 19 hack permutations for download/version/archive flows, shared MinIO patch storage, and placehold.co cover uploads.

* Update local seed fixtures for current schema and xdelta patches.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jared Schoeny
2026-09-04 13:31:30 -06:00
committed by GitHub
parent f84350be82
commit fbe25aa4cd
5 changed files with 768 additions and 0 deletions

173
scripts/seed-storage.mjs Normal file
View File

@@ -0,0 +1,173 @@
/**
* Upload dev seed fixtures to MinIO.
*
* Shared constants (keep in sync with supabase/seed.sql):
* SEED_SHARED_BPS = seed-shared.bps
* SEED_SHARED_XDELTA = seed-shared.xdelta
* SEED_APPROVED_SLUG = seed-emerald-demo
* SEED_PENDING_SLUG = seed-pending-demo
*
* Usage:
* npm run seed:storage
*
* Requires MinIO/S3 env vars (S3_ENDPOINT, S3_PORT, etc.) and PATCHES_BUCKET.
* Cover seeding also requires COVERS_BUCKET and network access to placehold.co.
*/
import { existsSync } from "node:fs";
import { stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "minio";
export const SEED_SHARED_BPS = "seed-shared.bps";
export const SEED_SHARED_XDELTA = "seed-shared.xdelta";
const PATCHES_BUCKET = process.env.PATCHES_BUCKET || "patches";
const COVERS_BUCKET = process.env.COVERS_BUCKET || "covers";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BPS_FIXTURE_PATH = path.resolve(__dirname, "../public/patches/example_patch.bps");
const XDELTA_FIXTURE_PATH = path.resolve(__dirname, "../public/patches/example_patch.xdelta");
/** Hacks that get ≥3 cover images (all complete seeded hacks). */
const COMPLETE_HACK_SLUGS = [
"seed-emerald-demo",
"seed-pending-ready",
"seed-patcher-only-admin-1",
"seed-patcher-only-admin-2",
"seed-patcher-only-admin-3",
"seed-patcher-only-creator-1",
"seed-patcher-only-creator-2",
"seed-patcher-only-creator-3",
"seed-patcher-only-creator2-1",
"seed-patcher-only-creator2-2",
"seed-patcher-only-creator2-3",
"seed-all-downloads",
"seed-current-only",
"seed-draft-version",
"seed-archived-version",
"seed-third-party",
"seed-xdelta-demo",
];
const COVER_PALETTES = [
{ bg: "1a1a2e", text: "ffffff" },
{ bg: "16213e", text: "e94560" },
{ bg: "0f3460", text: "ffffff" },
{ bg: "2d6a4f", text: "ffffff" },
{ bg: "40916c", text: "d8f3dc" },
{ bg: "5c4d7d", text: "ffffff" },
];
function requireEnv(name) {
const value = process.env[name];
if (!value) {
console.error(`Missing required environment variable: ${name}`);
console.error("Load env via npm run seed:storage (uses --env-file flags) or export vars manually.");
process.exit(1);
}
return value;
}
async function ensureBucket(client, bucket) {
const exists = await client.bucketExists(bucket);
if (!exists) {
console.log(`Creating bucket "${bucket}"...`);
await client.makeBucket(bucket, "");
}
}
function coverObjectKey(slug, index) {
return `${slug}/cover-${index}.png`;
}
function placeholdUrl(slug, index) {
const palette = COVER_PALETTES[(slug.length + index) % COVER_PALETTES.length];
const label = slug.replace(/^seed-/, "").replace(/-/g, " ");
const text = encodeURIComponent(`Cover ${index}\n${label}`);
return `https://placehold.co/240x160/${palette.bg}/${palette.text}/png?text=${text}`;
}
async function uploadPatchFixture(client, fixturePath, objectKey, expectedHint) {
if (!existsSync(fixturePath)) {
console.error(`Fixture not found: ${fixturePath}`);
console.error(expectedHint);
process.exit(1);
}
const { size } = await stat(fixturePath);
console.log(`Uploading ${fixturePath} (${(size / 1024 / 1024).toFixed(1)} MB)`);
console.log(` → ${PATCHES_BUCKET}/${objectKey}`);
await client.fPutObject(PATCHES_BUCKET, objectKey, fixturePath, {
"Content-Type": "application/octet-stream",
});
}
async function uploadSharedPatches(client) {
await uploadPatchFixture(
client,
BPS_FIXTURE_PATH,
SEED_SHARED_BPS,
"Expected public/patches/example_patch.bps (Pokémon Emerald dev patch).",
);
await uploadPatchFixture(
client,
XDELTA_FIXTURE_PATH,
SEED_SHARED_XDELTA,
"Expected public/patches/example_patch.xdelta (Pokémon Emerald xdelta fixture).",
);
}
async function uploadCovers(client) {
let uploaded = 0;
for (const slug of COMPLETE_HACK_SLUGS) {
for (let i = 1; i <= 3; i++) {
const objectKey = coverObjectKey(slug, i);
const url = placeholdUrl(slug, i);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`placehold.co fetch failed for ${slug} cover ${i}: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
await client.putObject(COVERS_BUCKET, objectKey, buffer, buffer.length, {
"Content-Type": "image/png",
});
uploaded++;
}
}
console.log(`Uploaded ${uploaded} cover images to ${COVERS_BUCKET}/`);
}
async function main() {
const endPoint = requireEnv("S3_ENDPOINT");
const port = parseInt(requireEnv("S3_PORT"), 10);
const accessKey = requireEnv("S3_ACCESS_KEY_ID");
const secretKey = requireEnv("S3_SECRET_ACCESS_KEY");
const useSSL = process.env.S3_USE_SSL === "true";
const client = new Client({ endPoint, port, accessKey, secretKey, useSSL });
await ensureBucket(client, PATCHES_BUCKET);
await ensureBucket(client, COVERS_BUCKET);
await uploadSharedPatches(client);
await uploadCovers(client);
console.log("Done.");
console.log("");
console.log("To test in-browser patching:");
console.log(" 1. Open /hack/seed-emerald-demo");
console.log(" 2. Link a local Pokémon Emerald ROM (CRC32: 1f1c08fb)");
console.log(" 3. Patch and download");
}
main().catch((err) => {
console.error("seed:storage failed:", err.message || err);
if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
console.error("Is MinIO running and reachable at S3_ENDPOINT:S3_PORT?");
}
process.exit(1);
});