csv-database
Version:
lightweight CSV database
118 lines (117 loc) • 3.74 kB
JavaScript
const fastcsv = require("fast-csv");
const fs = require("fs");
const lockfile = require("proper-lockfile");
const tempy = require("tempy");
// Map SIGINT & SIGTERM to process exit
// so that lockfile removes the lockfile automatically
process
.once("SIGINT", () => process.exit(1))
.once("SIGTERM", () => process.exit(1));
// lock middleware to ensure async/thread safety
const lock = async (file, next) => new Promise((resolve, reject) => {
lockfile.lock(file, {
retries: {
retries: 500,
factor: 3,
minTimeout: 1 * 10,
maxTimeout: 60 * 1000,
randomize: true
}
}, async (err, release) => {
if (err) {
reject(err);
}
try {
await next();
}
catch (err) {
reject(err);
}
release();
resolve();
});
});
/** file i/o helpers */
const getCsvStream = (file, delimiter) => {
// append flag : we only work with empty files or adding data
const fileStream = fs.createWriteStream(file, { flags: "a" });
const csvStream = fastcsv.createWriteStream({
headers: true,
delimiter
});
csvStream.pipe(fileStream);
return csvStream;
};
const copyCsv = (from, to) => new Promise((resolve, reject) => {
const fromStream = fs.createReadStream(from);
fromStream.on("error", reject);
const toStream = fs.createWriteStream(to);
toStream.on("error", reject).on("close", resolve);
fromStream.pipe(toStream);
});
/** */
const read = async (filename, delimiter, events) => new Promise((resolve, reject) => {
fastcsv
.fromPath(filename, { delimiter, headers: true })
.on("data", events.onData)
.on("error", reject)
.on("end", resolve);
});
const edit = (filename, delimiter, events) => new Promise((resolve, reject) => {
const copy = tempy.file();
const tempStream = getCsvStream(copy, delimiter);
fastcsv
.fromPath(filename, { delimiter, headers: true })
.on("data", data => {
const newData = events.onEdit(data);
// handling deletion case when editing returns nothing
if (newData) {
tempStream.write(newData);
}
})
.on("error", reject)
.on("end", () => {
tempStream.end();
});
tempStream.on("end", () => {
// copy data from tempfile to original file
copyCsv(copy, filename)
.then(resolve)
.catch(reject);
});
});
const add = (filename, delimiter, data) => new Promise((resolve, reject) => {
const copy = tempy.file();
const tempStream = getCsvStream(copy, delimiter);
fastcsv
.fromPath(filename, { delimiter, headers: true })
.on("error", reject)
.on("end", () => {
// appending data at end of file
for (const row of data) {
tempStream.write(row);
}
tempStream.end();
})
.pipe(tempStream);
tempStream.on("end", () => {
// copy data from tempfile to original file
copyCsv(copy, filename)
.then(resolve)
.catch(reject);
});
});
const lockedEdit = async (filename, delimiter, events) => {
const func = async () => edit(filename, delimiter, events);
return lock(filename, func);
};
const lockedAdd = async (filename, delimiter, data) => {
const func = async () => add(filename, delimiter, data);
return lock(filename, func);
};
const csvEditor = (filename, delimiter) => ({
read: events => read(filename, delimiter, events),
add: data => lockedAdd(filename, delimiter, data),
edit: events => lockedEdit(filename, delimiter, events)
});
module.exports = csvEditor;