Last active
March 4, 2026 21:39
-
-
Save mandiwise/20c0925f3de2e757d67e919410026111 to your computer and use it in GitHub Desktop.
Write data to JSON files as a simple persistence layer for Node.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import jsonStore from "json-store"; | |
| const store = jsonStore(); | |
| const data = await store.list(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Inspiration: https://github.com/alexkwolfe/json-fs-store | |
| import fs from "fs/promises"; | |
| import path from "path"; | |
| import { v4 as uuidv4 } from "uuid"; | |
| export default function (dir = path.join(process.cwd(), "store")) { | |
| async function ensureDir() { | |
| await fs.mkdir(dir, { recursive: true }); | |
| } | |
| async function readdirFull(dir) { | |
| const files = await fs.readdir(dir); | |
| return files.map(f => path.join(dir, f)); | |
| } | |
| async function loadFile(filePath) { | |
| try { | |
| const code = await fs.readFile(filePath, "utf8"); | |
| return JSON.parse(code); | |
| } catch (error) { | |
| throw new Error(`Error loading/parsing ${filePath}: ${error.message}`); | |
| } | |
| } | |
| async function atomicWriteFile(targetPath, data, encoding = "utf8") { | |
| const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`; | |
| // Open explicitly so we can fsync | |
| const handle = await fs.open(tmpPath, "w"); | |
| try { | |
| await handle.writeFile(data, { encoding }); | |
| await handle.sync(); // fsync file contents | |
| } finally { | |
| await handle.close(); | |
| } | |
| // Atomic replace | |
| await fs.rename(tmpPath, targetPath); | |
| } | |
| async function getUniqueName(name) { | |
| await ensureDir(); | |
| const files = await readdirFull(dir); | |
| const jsonFiles = files.filter(f => f.endsWith(".json")); | |
| const objects = await Promise.all(jsonFiles.map(loadFile)); | |
| const existingNames = new Set(objects.map(o => o.name).filter(Boolean)); | |
| if (!existingNames.has(name)) { | |
| return name; | |
| } | |
| let nameBase = name; | |
| let counter = 1; | |
| const suffixRegex = /-(\d+)$/; | |
| const match = name.match(suffixRegex); | |
| if (match) { | |
| counter = parseInt(match[1], 10); | |
| nameBase = name.replace(suffixRegex, ""); | |
| } | |
| while (existingNames.has(`${nameBase}-${counter}`)) { | |
| counter++; | |
| } | |
| return `${nameBase}-${counter}`; | |
| } | |
| return { | |
| dir, | |
| // List all stored objects | |
| async list() { | |
| await ensureDir(); | |
| const files = await readdirFull(dir); | |
| const jsonFiles = files.filter(f => f.endsWith(".json")); | |
| const objects = await Promise.all(jsonFiles.map(loadFile)); | |
| return objects.sort((a, b) => (a.name || "").localeCompare(b.name || "")); | |
| }, | |
| // Store an object in a file (atomic) | |
| async add(obj, enforceUniqueName = false) { | |
| await ensureDir(); | |
| let json; | |
| obj.id = obj.id || uuidv4(); | |
| if (enforceUniqueName) { | |
| const name = obj.name || "Untitled"; | |
| const uniqueName = await getUniqueName(name); | |
| obj.name = uniqueName; | |
| } | |
| try { | |
| json = JSON.stringify(obj, null, 2); | |
| } catch (error) { | |
| throw error; | |
| } | |
| const filePath = path.join(dir, `${obj.id}.json`); | |
| await atomicWriteFile(filePath, json); | |
| return json; | |
| }, | |
| // Update a stored object | |
| async update(obj, enforceUniqueName = false) { | |
| await ensureDir(); | |
| const { id, name: newName } = obj; | |
| const filePath = path.join(dir, `${id}.json`); | |
| const existingFile = await loadFile(filePath); | |
| if (!existingFile) { | |
| throw new Error("File not found"); | |
| } | |
| if (enforceUniqueName && existingFile.name !== newName) { | |
| const uniqueName = await getUniqueName(newName); | |
| if (newName !== uniqueName) { | |
| throw new Error("Name already in use"); | |
| } | |
| } | |
| let json; | |
| try { | |
| json = JSON.stringify(obj, null, 2); | |
| } catch (error) { | |
| throw error; | |
| } | |
| await atomicWriteFile(filePath, json); | |
| return json; | |
| }, | |
| // Delete an object's file | |
| async remove(id) { | |
| await ensureDir(); | |
| const filePath = path.join(dir, `${id}.json`); | |
| await fs.unlink(filePath); | |
| }, | |
| // Load an object from file | |
| async load(id) { | |
| await ensureDir(); | |
| const filePath = path.join(dir, `${id}.json`); | |
| return loadFile(filePath); | |
| } | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment