UNPKG

jdb-lite

Version:

A lightweight schema-based JSON database library with validation

407 lines 13.7 kB
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