Skip to content

Instantly share code, notes, and snippets.

@trvswgnr
Created August 25, 2023 21:22
Show Gist options
  • Save trvswgnr/330028eb0d9048894ea0078e7b7167f1 to your computer and use it in GitHub Desktop.
Save trvswgnr/330028eb0d9048894ea0078e7b7167f1 to your computer and use it in GitHub Desktop.
nosql db with files
import { promises as fs } from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { Mutex } from 'async-mutex';
interface Document {
[key: string]: any;
}
interface Index {
[key: string]: { [value: string]: string[] };
}
interface Condition {
$eq?: any;
$gt?: number;
$gte?: number;
$lt?: number;
$lte?: number;
$ne?: any;
$in?: any[];
$nin?: any[];
}
interface Query {
[key: string]: any | Condition;
}
class Collection {
private path: string;
private index: Index = {};
private mutex: Mutex = new Mutex();
constructor(path: string) {
this.path = path;
this.loadIndex();
}
private async loadIndex(): Promise<void> {
try {
const data = await fs.readFile(`${this.path}/index.json`, 'utf-8');
this.index = JSON.parse(data);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
private async saveIndex(): Promise<void> {
await fs.writeFile(`${this.path}/index.json`, JSON.stringify(this.index));
}
private updateIndex(id: string, doc: Document): void {
for (const key in doc) {
const value = doc[key];
if (!this.index[key]) {
this.index[key] = {};
}
if (!this.index[key][value]) {
this.index[key][value] = [];
}
this.index[key][value].push(id);
}
}
private removeFromIndex(id: string, doc: Document): void {
for (const key in doc) {
const value = doc[key];
if (this.index[key] && this.index[key][value]) {
this.index[key][value] = this.index[key][value].filter(docId => docId !== id);
}
}
}
async create(doc: Document): Promise<string> {
const release = await this.mutex.acquire();
try {
const id = uuidv4();
await fs.writeFile(`${this.path}/${id}.json`, JSON.stringify(doc));
this.updateIndex(id, doc);
await this.saveIndex();
return id;
} finally {
release();
}
}
async read(id: string): Promise<Document | null> {
try {
const data = await fs.readFile(`${this.path}/${id}.json`, 'utf-8');
return JSON.parse(data);
} catch (err) {
if (err.code === 'ENOENT') {
return null;
} else {
throw err;
}
}
}
async update(id: string, doc: Document): Promise<void> {
const release = await this.mutex.acquire();
try {
const oldDoc = await this.read(id);
if (oldDoc) {
this.removeFromIndex(id, oldDoc);
}
await fs.writeFile(`${this.path}/${id}.json`, JSON.stringify(doc));
this.updateIndex(id, doc);
await this.saveIndex();
} finally {
release();
}
}
async delete(id: string): Promise<void> {
const release = await this.mutex.acquire();
try {
const doc = await this.read(id);
if (doc) {
this.removeFromIndex(id, doc);
}
await fs.unlink(`${this.path}/${id}.json`);
await this.saveIndex();
} finally {
release();
}
}
private match(doc: Document, query: Query): boolean {
for (const key in query) {
const value = doc[key];
const condition = query[key];
if (typeof condition === 'object' && condition !== null) {
if (condition.$eq !== undefined && value !== condition.$eq) return false;
if (condition.$gt !== undefined && value <= condition.$gt) return false;
if (condition.$gte !== undefined && value < condition.$gte) return false;
if (condition.$lt !== undefined && value >= condition.$lt) return false;
if (condition.$lte !== undefined && value > condition.$lte) return false;
if (condition.$ne !== undefined && value === condition.$ne) return false;
if (condition.$in !== undefined && !condition.$in.includes(value)) return false;
if (condition.$nin !== undefined && condition.$nin.includes(value)) return false;
} else {
if (value !== condition) return false;
}
}
return true;
}
async query(query: Query): Promise<Document[]> {
const ids = new Set<string>();
for (const key in query) {
const condition = query[key];
if (typeof condition === 'object' && condition !== null) {
if (condition.$eq !== undefined) {
const eqIds = this.index[key] && this.index[key][condition.$eq];
if (eqIds) {
for (const id of eqIds) {
ids.add(id);
}
}
}
// Handle other conditions...
} else {
const eqIds = this.index[key] && this.index[key][condition];
if (eqIds) {
for (const id of eqIds) {
ids.add(id);
}
}
}
}
const docs = await Promise.all(Array.from(ids).map(id => this.read(id)));
return docs.filter(doc => doc && this.match(doc, query));
}
async backup(): Promise<void> {
await fs.copyFile(`${this.path}/index.json`, `${this.path}/index.json.bak`);
}
async restore(): Promise<void> {
await fs.copyFile(`${this.path}/index.json.bak`, `${this.path}/index.json`);
await this.loadIndex();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment