UNPKG

dsgg-qjs-db

Version:

A lightweight local NoSQL database

777 lines (583 loc) 20.4 kB
// ---------------------------------------------------------- // Name : collection // Author : Sam (Coding Samrat) // Description : ... // ---------------------------------------------------------- import { ObjectId } from 'bson' import { readJson, writeJson } from './fileIO.js' export default class Collection { #_database = null #_collection = null #_handler = null #_colName = '' constructor(colName, handler) { this.#_colName = colName.toLowerCase() this.#_handler = handler this.#_database = this.#_getDatabase() // Initiating Collection this.#_collection = this.#_getCollection() } /** * Find documents of collection * @param {Map} query * @param {Map} option * @returns {Map} collections */ async find(query = null, option = {}) { this.#_database = await this.#_getDatabase(); let limit = option?.limit; let sort = option?.sort; let result = []; // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query'); } // Ensure the limit is an Array if (limit && (!Array.isArray(limit) || limit.length !== 2)) { throw new Error('Invalid limit parameter'); } let [limitStart, limitEnd] = [0, 0]; if (limit) { [limitStart, limitEnd] = limit; if (limitStart > limitEnd) { throw new Error("limit[0] must be less than limit[1]"); } } // Fetch documents if (!query) { result = this.#_collection; } else { for (const doc of await this.#_collection) { if (this.#_matchesQuery(doc, query)) { result.push(doc); } } } // Apply sorting if needed if (sort && typeof sort.field === 'string' && ['asc', 'desc'].includes(sort.order)) { result.sort((a, b) => { const fieldA = a[sort.field]; const fieldB = b[sort.field]; if (fieldA === undefined || fieldB === undefined) { throw new Error(`Field ${sort.field} does not exist in the documents`); } if (sort.order === 'asc') { return fieldA > fieldB ? 1 : fieldA < fieldB ? -1 : 0; } else { return fieldA < fieldB ? 1 : fieldA > fieldB ? -1 : 0; } }); } // Apply limit if specified if (limit) { result = result.slice(limitStart, limitEnd); } return result; } /** * Find documents of collection like * @param {Map} query * @param {Map} option * @returns {Map} collections */ async findLike(query = null, option = {}) { this.#_database = await this.#_getDatabase(); let limit = option?.limit; let sort = option?.sort; let result = []; // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query'); } // Ensure the limit is an Array if (limit && (!Array.isArray(limit) || limit.length !== 2)) { throw new Error('Invalid limit parameter'); } let [limitStart, limitEnd] = [0, 0]; if (limit) { [limitStart, limitEnd] = limit; if (limitStart > limitEnd) { throw new Error("limit[0] must be less than limit[1]"); } } // Fetch documents if (!query) { result = this.#_collection; } else { for (const doc of await this.#_collection) { if (this.#_matchesQueryLike(doc, query)) { result.push(doc); } } } // Apply sorting if needed if (sort && typeof sort.field === 'string' && ['asc', 'desc'].includes(sort.order)) { result.sort((a, b) => { const fieldA = a[sort.field]; const fieldB = b[sort.field]; if (fieldA === undefined || fieldB === undefined) { throw new Error(`Field ${sort.field} does not exist in the documents`); } if (sort.order === 'asc') { return fieldA > fieldB ? 1 : fieldA < fieldB ? -1 : 0; } else { return fieldA < fieldB ? 1 : fieldA > fieldB ? -1 : 0; } }); } // Apply limit if specified if (limit) { result = result.slice(limitStart, limitEnd); } return result; } /** * Find a single document of collection * @param {Map} query * @returns document */ async findOne(query) { this.#_database = await this.#_getDatabase() let result = {} // Early return if no query if (!query) { throw new Error('Query cann\'t be null') } // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query') } for (const doc of await this.#_collection) { if (this.#_matchesQuery(doc, query)) { result = doc break } } return result } /** * * @param {ObjectId | String | Number} _id * @returns */ async findById(_id) { this.#_database = await this.#_getDatabase() for (const doc of await this.#_collection) { if (doc._id.toString() === _id.toString()) { return doc } } } /** * Insert a single document to the collection * @param {Map} document * @returns document */ async create(document) { this.#_database = await this.#_getDatabase() let _document = {} // Early return if no query if (!document) { throw new Error('Query cann\'t be null') } // Ensure the document is an object if (typeof document !== 'object' || Array.isArray(document)) { throw new Error('Invalid Document') } // console.log(document.hasOwnProperty("_id")) // Check if the document has key "_id" if (document.hasOwnProperty("_id")) { // Check if the document already exists if (await this.#_docExists(document._id)) { throw new Error(`A document with id \`${document._id}\` is already exists`) } _document = document } else { _document = { _id: new ObjectId(), ...document } } // Push document to the database const _col = await this.#_collection _col.push(_document) this.#_database[this.#_colName] = _col // Update database // Write to the database await this.#_handler.write(this.#_database) return _document } /** * Insert a single document to the collection * @param {Map} document * @returns document */ async insertOne(document) { this.#_database = await this.#_getDatabase() let _document = {} // Early return if no query if (!document) { throw new Error('Query cann\'t be null') } // Ensure the document is an object if (typeof document !== 'object' || Array.isArray(document)) { throw new Error('Invalid Document') } // console.log(document.hasOwnProperty("_id")) // Check if the document has key "_id" if (document.hasOwnProperty("_id")) { // Check if the document already exists if (await this.#_docExists(document._id)) { throw new Error(`A document with id \`${document._id}\` is already exists`) } _document = document } else { _document = { _id: new ObjectId(), ...document } } // Push document to the database const _col = await this.#_collection _col.push(_document) this.#_database[this.#_colName] = _col // Update database // Write to the database await this.#_handler.write(this.#_database) return _document } /** * * @param {[Map]} documents * @returns */ async insertMany(documents) { this.#_database = await this.#_getDatabase() const _col = await this.#_collection let docIds = [] for (let i = 0; i < documents.length; i++) { const document = documents[i] // Ensure the document is an object if (typeof document !== 'object' || Array.isArray(document)) { throw new Error('Invalid Document') } // ... let _document = {} // Check if the document has key "_id" if (document.hasOwnProperty("_id")) { // Check if the document already exists if (await this.#_docExists(document._id)) { throw new Error(`A document with id \`${document._id}\` is already exists`) } _document = document } else { _document = { _id: new ObjectId(), ...document } } // Push document to the database _col.push(_document) // Push docIds docIds.push(_document._id) } // Write to the database this.#_database[this.#_colName] = _col // Update database await this.#_handler.write(this.#_database) return docIds } /** * Delete only the first match document. * @param {Map} query * @returns document id */ async deleteOne(query) { this.#_database = await this.#_getDatabase() // Early return if no query if (!query) { throw new Error('Query cann\'t be null') } // Get full collection let coll = await this.#_collection // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query') } for (let i = 0; i < (await this.#_collection).length; i++) { const document = (await this.#_collection)[i] if (this.#_matchesQuery(document, query)) { await coll.splice(i, 1) this.#_collection = coll this.#_database[this.#_colName] = coll // Write the database await this.#_handler.write(this.#_database) return document } } } /** * Delete all document matches the query * @param {Map} query * @returns list of Objects */ async deleteMany(query) { this.#_database = await this.#_getDatabase(); let deletedDocs = []; let coll = await this.#_collection; if (query) { if (typeof query !== 'object' || Array.isArray(query)) { throw new Error('Invalid query'); } const newColl = coll.filter(document => { if (this.#_matchesQuery(document, query)) { deletedDocs.push(document); return false; } return true; }); this.#_collection = newColl; this.#_database[this.#_colName] = newColl; } else { deletedDocs = coll; this.#_collection = []; // Update the database this.#_database[this.#_colName] = []; } await this.#_handler.write(this.#_database); return deletedDocs; } /** * * @param {ObjectId} _id * @returns deleted document */ async findByIdAndDelete(_id) { // Early return if no query if (!_id) { throw new Error('Id is required') } // Query const query = { _id } return await this.deleteOne(query) } /** * Update the first match document * @param {Map} query * @param {Map} payload * @param {Map} option * @returns updated document */ async updateOne(query, payload, option = {}) { this.#_database = await this.#_getDatabase(); const hasQuery = Object.keys(query).length > 0 let coll = await this.#_collection if (!hasQuery) { throw new Error('Query is required') } // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query') } // Ensure the query is an object if (payload && (typeof payload !== 'object' || Array.isArray(payload))) { throw new Error('Invalid query') } for (let i = 0; i < coll.length; i++) { const document = coll[i]; if (this.#_matchesQuery(document, query)) { const newDocument = { ...document, ...payload } coll[i] = newDocument this.#_database[this.#_colName] = coll await this.#_handler.write(this.#_database) if (option?.new) { return newDocument } else { return document } } } } /** * Update the all matches documents * @param {Map} query * @param {Map} payload * @param {Map} option * @returns updated documents key(by option) */ async updateMany(query, payload, option = { key: "_id" }) { this.#_database = await this.#_getDatabase(); let docKeys = [] const hasQuery = Object.keys(query).length > 0 let coll = await this.#_collection // Ensure the query is an object if (query && (typeof query !== 'object' || Array.isArray(query))) { throw new Error('Invalid query') } // Ensure the query is an object if (payload && (typeof payload !== 'object' || Array.isArray(payload))) { throw new Error('Invalid query') } // Loop through all collection for (let i = 0; i < coll.length; i++) { const document = coll[i]; if (hasQuery) { if (this.#_matchesQuery(document, query)) { const newDocument = { ...document, ...payload } coll[i] = newDocument docKeys.push(document[option.key]) } } else { const newDocument = { ...document, ...payload } coll[i] = newDocument docKeys.push(document[option.key]) } } this.#_database[this.#_colName] = coll await this.#_handler.write(this.#_database) return docKeys } /** * Update the document matches by _id * @param {ObjectId} _id * @param {Map} payload * @param {Map} option * @returns updated document */ async findByIdAndUpdate(_id, payload, option = {}) { this.#_database = await this.#_getDatabase(); let coll = await this.#_collection if (!_id || !payload) { throw new Error('All args are required') } // Ensure the query is an object if (payload && (typeof payload !== 'object' || Array.isArray(payload))) { throw new Error('Invalid query') } // Ensure the query is an object if (option && (typeof option !== 'object' || Array.isArray(option))) { throw new Error('Invalid query') } for (let i = 0; i < coll.length; i++) { const document = coll[i]; if (this.#_matchesQuery(document, { _id })) { const newDocument = { ...document, ...payload } coll[i] = newDocument this.#_database[this.#_colName] = coll await this.#_handler.write(this.#_database) if (option?.new) { return newDocument } else { return document } } } } /** * Export the collection as JSON * @param {String} filename */ async export(filename = '') { const coll = await this.#_collection filename = filename === '' ? `${this.#_colName}.json` : filename const payload = {} payload[this.#_colName] = coll await writeJson(filename, payload) } /** * Import the collection as JSON * @param {String} filename */ async #import(filename) { let coll = await this.#_collection let data = await readJson(filename) data = data[this.#_colName] coll = [ ...coll, ...data ]; this.#_collection = coll; this.#_database[this.#_colName] = coll await this.#_handler.write(this.#_database) // don't use await } /** * Rename the collection * @param {String} newName * @returns new name */ async rename(newName) { let coll = await this.#_collection // Check if collection is empty or not if (coll.length > 0) { (await this.#_database)[newName] = coll; delete (await this.#_database)[this.#_colName] } await this.#_handler.write(await this.#_database) return newName } /** * Drop or Delete the collection from database */ async drop() { delete (await this.#_database)[this.#_colName] await this.#_handler.write(await this.#_database) } /** * * @returns length of collection */ async count() { // console.log(await this.#_database) return (await this.#_collection).length } // ------------------------------------------------------------------- async #_getDatabase() { // console.log(await this.#_handler.read()) return this.#_handler.read() } async #_getCollection() { if ((await this.#_database).hasOwnProperty(this.#_colName)) { return (await this.#_database)[this.#_colName] } else { (await this.#_database)[this.#_colName] = Array() return Array() } } async #_docExists(docId) { return (await this.#_collection).some(doc => doc._id === docId) } #_matchesQuery(doc, query) { return Object.entries(query).every(([key, value]) => { if (key === '_id') { // If the key is _id, compare it after converting it to string return doc[key].toString() === value.toString(); } if (key.endsWith('s') && Array.isArray(value)) { // 如果key以s结尾,并且value是数组,则判断doc[key.slice(0, -1)]是否包含value中的任意一个元素 return value.some(id => doc[key.slice(0, -1)].includes(id)); } return doc[key] === value; }); } #_matchesQueryLike(doc, query) { return Object.entries(query).some(([key, value]) => { return doc[key].toString().includes(value.toString()); }); } #_updateCollection(value) { // Update collection this.#_collection = value // Update database this.#_handler.read()[this.#_colName] = value } }