Commit 4481d274012a59b55cff9a51f1ae3c89c4782126
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.
Showing
3 changed files
with
113 additions
and
0 deletions
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 | +}); | ... | ... |