@aequum/in-memory
Version:
aequum in-memory stuff
247 lines (246 loc) • 9.04 kB
JavaScript
"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;