UNPKG

@aequum/in-memory

Version:
247 lines (246 loc) 9.04 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InMemoryRepository = void 0; const data_1 = require("@aequum/exceptions/data"); const validation_1 = require("@aequum/exceptions/validation"); const utils_1 = require("@aequum/utils"); const node_crypto_1 = require("node:crypto"); /** * In memory (**volatile**) repository, implements basic * input/output operations. * * **NOTE**: If you want to change the primary key field * you must set the `primaryKeyField` property and pass the * `PrimeryKeyField` type parameter. * * @typeParam Model - The model type, must extend `AnyObject` * @typeParam PrimeryKeyField - The primary key field name, defaults to 'id' * if this changes you must set the `primaryKeyField` property */ class InMemoryRepository { data = []; primaryKeyField = 'id'; /** * Primary Key generator, by default uses `randomUUID()` * to generate a unique identifier. */ genPrimaryKey() { return (0, node_crypto_1.randomUUID)(); } /** * Check if an item matches the filter. * * @param item Repository item * @param filter Custom filter to match against the item */ matchesFilter(item, filter) { return Object.keys(filter).every((key) => { if (Array.isArray(item[key])) { if (!Array.isArray(filter[key])) return item[key].some(i => filter[key].includes(i)); return item[key].includes(filter[key]); } return item[key] === filter[key]; }); } /** * Find indexes of items that match the filter. * * @param filter Filter to match against the items */ matchIndexes(filter) { return this.data .map((item, idx) => this.matchesFilter(item, filter) ? idx : -1) .filter(idx => idx !== -1); } /** * Dot notation find, allows to find nested properties * in the item. */ dotNotationFind(item, property) { return property.split('.').reduce((acc, key) => { if (acc && typeof acc === 'object' && key in acc) { return acc[key]; } return undefined; }, item); } /** * Delete an item from the repository. * * @param filter */ async delete(filter) { for (const idx of this.matchIndexes(filter)) this.data.splice(Number(idx), 1); } /** * Delete all items that match the filter. * * @param filter */ async deleteMany(filter) { await this.delete(filter); } /** * Find items in the repository that match the filter. * * @param filter */ async find(filter) { return this.data.filter(item => this.matchesFilter(item, filter)); } /** * Get an item from the repository that matches the filter. * * @param filter */ async getOne(filter) { const item = this.data.find(item => this.matchesFilter(item, filter)); if (!item) throw new data_1.NotFoundException(`Item not found`); return item; } /** * Get an item by its primary key. * * @param id */ async getOneById(id) { const item = this.data.find(item => item[this.primaryKeyField] === id); if (!item) throw new data_1.NotFoundException(`Item not found`); return item; } /** * Put an item into the repository. */ async put(data) { const hasPrimaryKey = this.primaryKeyField in data; const existingIndex = (hasPrimaryKey ? this.data.findIndex(item => item[this.primaryKeyField] === data[this.primaryKeyField]) : -1); if (!hasPrimaryKey) data = { [this.primaryKeyField]: this.genPrimaryKey(), ...data }; if (existingIndex !== -1) this.data[existingIndex] = data; else this.data.push(data); return data; } /** * Put many items into the repository. */ async putMany(data) { return Promise.all(data.map(item => this.put(item))); } /** * Update one or more items in the repository that match the filter. * * @param filter Filter to match * @param data Data to update the items with */ async update(filter, data) { const idxs = this.matchIndexes(filter); if (idxs.length === 0) { throw new data_1.NotFoundException(`No items found`); } for (const idx of idxs) this.data[idx] = utils_1.data.mergeDeep(this.data[idx], data); if (idxs.length === 1) return this.data[idxs[0]]; return this.data.filter((_, idx) => idxs.includes(idx)); } /** * Push a value into an array property of items that match the filter. * * @param filter Filter to match the items * @param property The property to push the value into, can be a dot notation * string to access nested properties. * For example: 'nested.property.array' * @param value The value to push into the array property * @throws NotFoundException if no items match the filter * @throws ValidationException if the property is not a valid string * @throws NotFoundException if the property is not found in the item */ async pushOnArrayProperty(filter, property, value) { const idxs = this.matchIndexes(filter); if (idxs.length === 0) throw new data_1.NotFoundException(`No items found`); if (!property || typeof property !== 'string') throw new validation_1.ValidationException({ property: [[ 'ERR_INVALID_PROPERTY', `Property \`${property}\` is not a valid string`, [property], ]] }); for (const idx of idxs) { const item = this.data[idx]; const dotNotationChunks = property.split('.'); const lastProperty = dotNotationChunks.pop(); const dotNotationParent = dotNotationChunks.join('.'); const parentObject = (dotNotationChunks.length > 0 ? this.dotNotationFind(item, dotNotationParent) : item); if (!parentObject) throw new data_1.NotFoundException(`Property \`${dotNotationParent}\` not found in item`); if (parentObject[lastProperty] == undefined) parentObject[lastProperty] = []; parentObject[lastProperty].push(value); } if (idxs.length === 1) return this.data[idxs[0]]; return this.data.filter((_, idx) => idxs.includes(idx)); } /** * Pull a value from an array property of items that match the filter. * * @param filter Filter to match the items * @param property The property * string to access nested properties. * For example: 'nested.property.array' * @param value The value to push into the array property */ async pullFromArrayProperty(filter, property, value) { const idxs = this.matchIndexes(filter); if (idxs.length === 0) throw new data_1.NotFoundException(`No items found`); if (!property || typeof property !== 'string') throw new validation_1.ValidationException({ property: [[ 'ERR_INVALID_PROPERTY', `Property \`${property}\` is not a valid string`, [property], ]] }); for (const idx of idxs) { const item = this.data[idx]; const dotNotationChunks = property.split('.'); const lastProperty = dotNotationChunks.pop(); const dotNotationParent = dotNotationChunks.join('.'); const parentObject = (dotNotationChunks.length > 0 ? this.dotNotationFind(item, dotNotationParent) : item); if ((!parentObject) || parentObject[lastProperty] == undefined) throw new data_1.NotFoundException(`Property \`${property}\` not found in item`); if (!Array.isArray(parentObject[lastProperty])) throw new validation_1.ValidationException({ property: [[ 'ERR_INVALID_PROPERTY', `Property \`${property}\` is not an array`, [property], ]] }); const valueIndex = parentObject[lastProperty].indexOf(value); parentObject[lastProperty].splice(valueIndex, 1); } if (idxs.length === 1) return this.data[idxs[0]]; return this.data.filter((_, idx) => idxs.includes(idx)); } } exports.InMemoryRepository = InMemoryRepository;