Commit 4481d274012a59b55cff9a51f1ae3c89c4782126

Authored by yibo
1 parent ab6176a4

Add JSON CRUD store for dressup dance room asset libraries (T1)

Generic createStore(filePath) shared by characters.json/garments.json/
dance-refs.json, same full-read/full-write pattern as library.json.
Not yet wired into server.js routes — that's T3-T5.
dressup-store.js 0 → 100644
  1 +// The three cold-start asset libraries for the dressup dance room
  2 +// (dev-plan.md T1; schemas: dressup-dance-room-mvcp.md M.3/M.4/M.5).
  3 +const path = require("path");
  4 +const { createStore } = require("./store.js");
  5 +
  6 +const characters = createStore(path.join(__dirname, "characters.json"));
  7 +const garments = createStore(path.join(__dirname, "garments.json"));
  8 +const danceRefs = createStore(path.join(__dirname, "dance-refs.json"));
  9 +
  10 +module.exports = { characters, garments, danceRefs };
... ...
store.js 0 → 100644
  1 +// Generic JSON-file CRUD store (dev-plan.md T1), shared by characters.json/
  2 +// garments.json/dance-refs.json. Same full-read/full-write pattern as
  3 +// server.js's library.json — no in-memory cache, so a process restart just
  4 +// re-reads the file.
  5 +const fs = require("fs");
  6 +const crypto = require("crypto");
  7 +
  8 +function createStore(filePath) {
  9 + function all() {
  10 + try {
  11 + return JSON.parse(fs.readFileSync(filePath, "utf8"));
  12 + } catch {
  13 + return [];
  14 + }
  15 + }
  16 + function save(items) {
  17 + fs.writeFileSync(filePath, JSON.stringify(items, null, 2));
  18 + }
  19 +
  20 + return {
  21 + list() {
  22 + return all();
  23 + },
  24 + get(id) {
  25 + return all().find((item) => item.id === id) || null;
  26 + },
  27 + create(data) {
  28 + const items = all();
  29 + const item = { id: crypto.randomUUID(), createdAt: Date.now(), ...data };
  30 + items.push(item);
  31 + save(items);
  32 + return item;
  33 + },
  34 + update(id, patch) {
  35 + const items = all();
  36 + const idx = items.findIndex((item) => item.id === id);
  37 + if (idx === -1) return null;
  38 + items[idx] = { ...items[idx], ...patch, id };
  39 + save(items);
  40 + return items[idx];
  41 + },
  42 + remove(id) {
  43 + const items = all();
  44 + const next = items.filter((item) => item.id !== id);
  45 + save(next);
  46 + return next.length !== items.length;
  47 + },
  48 + };
  49 +}
  50 +
  51 +module.exports = { createStore };
... ...
store.test.js 0 → 100644
  1 +// RED/GREEN check for the JSON CRUD store shared by characters/garments/
  2 +// dance-refs (dev-plan.md T1). Run: node --test store.test.js
  3 +const test = require("node:test");
  4 +const assert = require("node:assert/strict");
  5 +const fs = require("node:fs");
  6 +const os = require("node:os");
  7 +const path = require("node:path");
  8 +const { createStore } = require("./store.js");
  9 +
  10 +function tempFile() {
  11 + return path.join(os.tmpdir(), `store-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
  12 +}
  13 +
  14 +test("create/list/get/update/remove round-trip", () => {
  15 + const file = tempFile();
  16 + const store = createStore(file);
  17 +
  18 + const created = store.create({ name: "test" });
  19 + assert.equal(typeof created.id, "string");
  20 + assert.equal(created.name, "test");
  21 + assert.equal(typeof created.createdAt, "number");
  22 +
  23 + assert.deepEqual(store.list(), [created]);
  24 + assert.deepEqual(store.get(created.id), created);
  25 +
  26 + const updated = store.update(created.id, { name: "updated" });
  27 + assert.equal(updated.name, "updated");
  28 + assert.equal(updated.id, created.id);
  29 +
  30 + assert.equal(store.remove(created.id), true);
  31 + assert.deepEqual(store.list(), []);
  32 + assert.equal(store.remove(created.id), false);
  33 +
  34 + fs.unlinkSync(file);
  35 +});
  36 +
  37 +test("data survives a fresh store instance pointed at the same file (simulated process restart)", () => {
  38 + const file = tempFile();
  39 + const store1 = createStore(file);
  40 + const created = store1.create({ name: "persisted" });
  41 +
  42 + const store2 = createStore(file); // new instance, no shared in-memory state
  43 + assert.deepEqual(store2.get(created.id), created);
  44 +
  45 + fs.unlinkSync(file);
  46 +});
  47 +
  48 +test("list() on a missing file returns an empty array, not a throw", () => {
  49 + const file = tempFile(); // never created
  50 + const store = createStore(file);
  51 + assert.deepEqual(store.list(), []);
  52 +});
... ...