jdb-lite
Version:
A lightweight schema-based JSON database library with validation
407 lines • 13.7 kB
JavaScript
import { ObjectId } from './objectid';
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
return this;
}
emit(event, ...args) {
if (!this.events[event]) {
return false;
}
this.events[event].forEach(listener => listener(...args));
return true;
}
off(event, listener) {
if (!this.events[event]) {
return this;
}
this.events[event] = this.events[event].filter(l => l !== listener);
return this;
}
}
export class QueryBuilder {
constructor(db, collectionName) {
this._filter = {};
this._sort = {};
this._limit = 0;
this._skip = 0;
this._select = {};
this._populate = [];
this.db = db;
this.collectionName = collectionName;
}
// Make the query thenable so it can be awaited directly
then(onfulfilled, onrejected) {
return this.exec().then(onfulfilled, onrejected);
}
catch(onrejected) {
return this.exec().catch(onrejected);
}
async find(filter) {
if (filter) {
this._filter = { ...this._filter, ...filter };
}
return this.exec();
}
async findOne(filter) {
if (filter) {
this._filter = { ...this._filter, ...filter };
}
const results = await this.limit(1).exec();
return results[0] || null;
}
async findById(id) {
return this.db.findById(this.collectionName, id);
}
async findByIdAndUpdate(id, update, options) {
const result = await this.db.updateOne(this.collectionName, { _id: id }, update);
if (result.modifiedCount === 0) {
return null;
}
return options?.new ? this.db.findById(this.collectionName, id) : null;
}
async findByIdAndDelete(id) {
const doc = await this.db.findById(this.collectionName, id);
if (!doc)
return null;
await this.db.deleteOne(this.collectionName, { _id: id });
return doc;
}
async findOneAndUpdate(filter, update, options) {
const doc = await this.db.findOne(this.collectionName, filter);
if (!doc)
return null;
await this.db.updateOne(this.collectionName, filter, update);
return options?.new ? this.db.findOne(this.collectionName, filter) : doc;
}
async findOneAndDelete(filter) {
const doc = await this.db.findOne(this.collectionName, filter);
if (!doc)
return null;
await this.db.deleteOne(this.collectionName, filter);
return doc;
}
async updateOne(filter, update) {
return this.db.updateOne(this.collectionName, filter, update);
}
async updateMany(filter, update) {
return this.db.updateMany(this.collectionName, filter, update);
}
async deleteOne(filter) {
return this.db.deleteOne(this.collectionName, filter);
}
async deleteMany(filter) {
return this.db.deleteMany(this.collectionName, filter);
}
async countDocuments(filter) {
return this.db.countDocuments(this.collectionName, filter || this._filter);
}
sort(sortBy) {
if (typeof sortBy === 'string') {
const direction = sortBy.startsWith('-') ? -1 : 1;
const field = sortBy.replace(/^-/, '');
this._sort = { [field]: direction };
}
else {
this._sort = sortBy;
}
return this;
}
limit(limit) {
this._limit = limit;
return this;
}
skip(skip) {
this._skip = skip;
return this;
}
select(fields) {
if (typeof fields === 'string') {
const fieldList = fields.split(' ');
this._select = fieldList.reduce((acc, field) => {
acc[field] = 1;
return acc;
}, {});
}
else if (Array.isArray(fields)) {
this._select = fields.reduce((acc, field) => {
acc[field] = 1;
return acc;
}, {});
}
else {
this._select = fields;
}
return this;
}
populate(path) {
if (typeof path === 'string') {
this._populate.push(path);
}
else {
this._populate.push(...path);
}
return this;
}
async exec() {
let results = await this.db.find(this.collectionName, this._filter);
// Apply sorting
if (Object.keys(this._sort).length > 0) {
results.sort((a, b) => {
for (const [field, direction] of Object.entries(this._sort)) {
const aVal = a[field];
const bVal = b[field];
if (aVal < bVal)
return -1 * direction;
if (aVal > bVal)
return 1 * direction;
}
return 0;
});
}
// Apply skip and limit
if (this._skip > 0) {
results = results.slice(this._skip);
}
if (this._limit > 0) {
results = results.slice(0, this._limit);
}
// Apply field selection
if (Object.keys(this._select).length > 0) {
results = results.map(doc => {
const selected = {};
for (const [field, include] of Object.entries(this._select)) {
if (include === 1) {
selected[field] = doc[field];
}
}
return selected;
});
}
return results;
}
}
export class DocumentInstance {
constructor(doc, schema, isNew = false) {
this._isNew = false;
this._modified = new Set();
this._doc = doc;
this._schema = schema;
this._isNew = isNew;
}
get isNew() {
return this._isNew;
}
isModified(path) {
if (path) {
return this._modified.has(path);
}
return this._modified.size > 0;
}
markModified(path) {
this._modified.add(path);
}
async save() {
// This would be implemented by the model
throw new Error('Save method should be implemented by model');
}
async remove() {
// This would be implemented by the model
throw new Error('Remove method should be implemented by model');
}
toObject() {
return { ...this._doc };
}
toJSON() {
return this.toObject();
}
async populate(path) {
// Basic population implementation
return this._doc;
}
depopulate(path) {
// Remove populated fields
return this._doc;
}
async validate() {
const errors = this._schema.validate(this._doc);
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
}
}
export function createModel(name, schema, db) {
const collectionName = schema.options.collection || name.toLowerCase() + 's';
// Ensure collection is initialized when model is created
db.ensureCollection(collectionName);
const ModelConstructor = function (doc) {
if (doc) {
return createInstance(doc, true);
}
return this;
};
function createInstance(doc, isNew = false) {
const fullDoc = { ...doc };
// Generate _id if not provided and is new
if (isNew && !fullDoc._id) {
fullDoc._id = new ObjectId().toString();
}
// Apply defaults
for (const [key, schemaDef] of Object.entries(schema.definition)) {
if (fullDoc[key] === undefined && !Array.isArray(schemaDef)) {
const typeDef = schemaDef;
if (typeDef.default !== undefined) {
fullDoc[key] = typeof typeDef.default === 'function' ? typeDef.default() : typeDef.default;
}
}
}
// Create a clean instance without circular references
const instance = {
// Document data
...fullDoc,
// Instance properties
isNew,
_modified: new Set(),
// Instance methods
isModified: function (path) {
if (path) {
return this._modified.has(path);
}
return this._modified.size > 0;
},
markModified: function (path) {
this._modified.add(path);
},
toObject: function () {
const obj = { ...fullDoc };
// Remove instance-specific properties
delete obj.isNew;
delete obj._modified;
delete obj.save;
delete obj.remove;
delete obj.toObject;
delete obj.toJSON;
delete obj.isModified;
delete obj.markModified;
delete obj.populate;
delete obj.depopulate;
delete obj.validate;
return obj;
},
toJSON: function () {
return this.toObject();
},
save: async function () {
// Validate first
const errors = schema.validate(this.toObject());
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
const plainData = this.toObject();
if (this.isNew) {
const result = await db.insertOne(collectionName, plainData);
this.isNew = false;
return result;
}
else {
await db.updateOne(collectionName, { _id: plainData._id }, plainData);
return plainData;
}
},
remove: async function () {
const plainData = this.toObject();
await db.deleteOne(collectionName, { _id: plainData._id });
return plainData;
},
populate: async function (path) {
return this;
},
depopulate: function (path) {
return this;
},
validate: async function () {
const errors = schema.validate(this.toObject());
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
}
};
return instance;
}
// Add static properties and methods
ModelConstructor.collection = { name: collectionName };
// Static methods
ModelConstructor.find = function (filter) {
const query = new QueryBuilder(db, collectionName);
if (filter) {
query._filter = filter;
}
return query;
};
ModelConstructor.findOne = async function (filter) {
return db.findOne(collectionName, filter);
};
ModelConstructor.findById = async function (id) {
return db.findById(collectionName, id);
};
ModelConstructor.findByIdAndUpdate = async function (id, update, options) {
const query = new QueryBuilder(db, collectionName);
return query.findByIdAndUpdate(id, update, options);
};
ModelConstructor.findByIdAndDelete = async function (id) {
const query = new QueryBuilder(db, collectionName);
return query.findByIdAndDelete(id);
};
ModelConstructor.findOneAndUpdate = async function (filter, update, options) {
const query = new QueryBuilder(db, collectionName);
return query.findOneAndUpdate(filter, update, options);
};
ModelConstructor.findOneAndDelete = async function (filter) {
const query = new QueryBuilder(db, collectionName);
return query.findOneAndDelete(filter);
};
ModelConstructor.updateOne = async function (filter, update) {
return db.updateOne(collectionName, filter, update);
};
ModelConstructor.updateMany = async function (filter, update) {
return db.updateMany(collectionName, filter, update);
};
ModelConstructor.deleteOne = async function (filter) {
return db.deleteOne(collectionName, filter);
};
ModelConstructor.deleteMany = async function (filter) {
return db.deleteMany(collectionName, filter);
};
ModelConstructor.countDocuments = async function (filter) {
return db.countDocuments(collectionName, filter);
};
ModelConstructor.create = async function (doc) {
const instance = createInstance(doc, true);
const result = await instance.save();
return result;
};
ModelConstructor.insertMany = async function (docs) {
const validatedDocs = docs.map(doc => {
const instance = createInstance(doc, true);
return instance.toObject();
});
return db.insertMany(collectionName, validatedDocs);
};
ModelConstructor.watch = function () {
return new EventEmitter();
};
// Add event emitter functionality
const emitter = new EventEmitter();
Object.assign(ModelConstructor, emitter);
// Add schema methods and statics
Object.assign(ModelConstructor.prototype, schema.methods);
Object.assign(ModelConstructor, schema.statics);
return ModelConstructor;
}
//# sourceMappingURL=model.js.map