node-micro-db v1.0.8
micro-db
micro-db is a lightweight, (by default) json-based, file-based, zero config database for nodejs.
Installation
npm i node-micro-db
yarn add node-micro-db
🪄 Quickstart
type User = {
name: string;
age: number;
};
const db = new MicroDB<User>();
const idOfJohn = db.create({ name: 'john', age: 23 });
// john's birthday
db.update(idOfJohn, { age: 24 });
// select with id
const john = db.select(idOfJohn);
// john.name = 'john'
// john.age = 24
// select without id
const john1 = db.selectWhere(user => user.name === 'john');
// john1.name = 'john'
// john1.age = 24
For more information about available methods, check out the API
Contents
Why micro-db
- ☁️ Lightweight
- ⚡️ Preformat
- ⌛️ Instant Persistent
- 🔎 Debuggable
- ✨ Typescript Support
- ⚙️ Zero Config
- 🛠 Expandable
- 🔌 Easily Replaceable
Why micro-db is outstanding
The difference between micro-db and most json-based databases is, that most json-based databases serialize the whole data object before it can be stored into a file. micro-db appends data snapshots in key-value pairs directly into the file. This has the huge advantage of instant-persistency without a big workload, because only the stored object has to be serialized.
micro-db stores data records with an internal database id as the key. This means there is no need for worrying about such things as primary keys, because micro-db does the job. If you need to use the generated id in your code, you can make your records id-aware
Due to the fact, that micro-db only has to serialize the changed object micro-db can handle large collections of data records. The collection size only affects database startup and janitor clean cycle times. The only impact on database operation performance is the size of the data record itself. The bigger the record, the slower the database operations will be.
Features
Typescript Support
micro-db supports static typing with typescript.
To use the full power of typescript you should use micro-db as a sql database. Although, if type safety isn't a matter for you, you can use micro-db also as a nosql database (by using typescripts Union Types and Type Guards your nosql database can also be type safe).
Debuggable
micro-db's default json serializer stores all records in a human readable format in your database file. Since every database operation results is a new stored record, all operations an their results are traceable in the database file.
Expandable
If the json format used for serialization doesn't suits you needs, you can implement an own MicroDBSerializer
yourself and pass it with your config.
This technique can be used for things as encryption or data compression.
Built in Janitor
micro-db has a janitor built in. The main task of the MicroDBJanitor
is to clean redundant / outdated snapshots from your database file.
Easily Replaceable
When used right (with the MicroDBFacade
), you hide the actual database operations from the rest of your app. Which means, that you can easily change your database later on, without even noticing it anywhere else in the app.
When to use micro-db
micro-db loves an environment, where performance and instant-persistence matters, but space don't.
When NOT to use micro-db
When space (database file size) is a heavy constraint for your nodejs app, you should consider using another database.
How to deal with space constraints
The MicroDBJanitor
is responsible for cleaning up the database file for redundant records. The MicroDBJanitor
is configured with a cronjob, which determines when and how often the janitor has to run. The more traffic or changes your database has, the more often the janitor should run to prevent huge overhead in file size.
Gotchas
undefined
vs null
values
In micro-db world a data value of undefined
deletes the record. If you want to store optional records anyhow, you can use null
for that.
$watchPropertyNext()
Making watching a property working correct, requires the currentValue and a lastValue
(value before the change) to be passed into the predicate. Because of how Javascript works, while the SubscriptionManager gets constructed the derived class (the one you want to watch) isn't fully constructed yet, so the lastValue
gets initialized as undefined
. To take care of that, the lastValue
gets its value within an setImmediate()
.
This results in the fact, that all $watchProperty()
and $watchPropertyNext()
subscriptions made within the iteration of the event loop while the constructor of the derived class (the one you are want to watch) is run, will be triggered whenever the first value change in the class appears.
See example in tests
Patterns
Facade Pattern
micro-db encourages you, to hide bare-bones database operations, like select
or update
statements from the rest of you application using the Facade pattern
To implement this in your code, you can extend the MicroDBFacade
class. The MicroDBFacade
provides the same api as a MicroDBDriver
, but all methods are protected, which means they are inaccessible from outside of the class.
The example below shows how straight forward this approach is:
Example
export class UserDB extends MicroDBFacade<UserDBEntry> {
// 1. create the db instance
private static db = new UserDB({ fileName: 'db/users.db' });
// 2. expose shutdown function
static shutdown = UserDB.db.shutdown;
// 3. define complex database operations
static logout = (userId: string) => {
const userRecord = UserDB.db.select(userId);
// ...
UserDB.db.update(userId, {
//...
});
};
}
// 4. usage
UserDB.logout('some-user-id');
Example (using instance methods rather than static ones)
export class UserDB extends MicroDBFacade<UserDBEntry> {
constructor() {
// 1. construct db instance
super({ fileName: 'db/users.db' });
}
// 2. expose shutdown function
shutdown = () => this.db.close();
// 3. define complex database operations
logout = (userId: string) => {
const userRecord = this.db.select(userId);
// ...
this..db.update(userId, {
//...
});
};
}
// 4. usage
const db = new UserDB();
db.logout('some-user-id');
id-aware Records
Sometimes, you need your database records to be aware of their id. This is mainly the case, when otherwise heavy select statements are needed to query some commonly used data. Implementing this with micro-db is very easy (and works best with the Facade Pattern).
Example
export class UserDB extends MicroDBFacade<UserDBEntry> {
// same as above
private static db = new UserDB({ name: 'users', fileName: 'db/users.db' });
static shutdown = UserDB.db.shutdown;
static createUser = (name: string, age: number) => {
// 1. user object with empty id
const data: UserDBEntry = {
id: '',
name,
age,
};
// 2. create database records and receive id
const id = UserDB.db.create(data);
// 3. update created record with received id
UserDB.db.update(id, { id });
};
}