mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-10-07 08:19:05 -05:00
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:
173
scripts/seed-storage.mjs
Normal file
173
scripts/seed-storage.mjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user