@furystack/mongodb-store
Version:
MongoDB Store implementation for FuryStack
148 lines • 5.64 kB
JavaScript
import { EventHub } from '@furystack/utils';
import { ObjectId } from 'mongodb';
import { Lock } from 'semaphore-async-await';
// Improved type safety for hasObjectId
const hasObjectId = (value) => {
return value && typeof value === 'object' && value._id instanceof ObjectId;
};
/**
* MongoDB Store implementation for FuryStack
*/
export class MongodbStore extends EventHub {
options;
primaryKey;
model;
initLock = new Lock();
collection;
createIdFilter(...values) {
// If primaryKey is _id, convert string values to ObjectId
return {
[this.primaryKey]: {
$in: this.primaryKey === '_id' ? values.map((value) => new ObjectId(value)) : values,
},
};
}
stringifyResultId(item) {
// If _id is ObjectId, convert to string
if (this.primaryKey === '_id' && hasObjectId(item)) {
return {
...item,
_id: item._id.toHexString(),
};
}
return item;
}
parseFilter(filter) {
if (!filter) {
return {};
}
// Only handle _id conversion if present
if (Object.prototype.hasOwnProperty.call(filter, '_id')) {
const f = { ...filter };
if (typeof f._id === 'string') {
f._id = new ObjectId(f._id);
}
else if (typeof f._id === 'object' && f._id !== null) {
const idObj = f._id;
if (idObj.$eq && typeof idObj.$eq === 'string') {
idObj.$eq = new ObjectId(idObj.$eq);
}
if (Array.isArray(idObj.$in)) {
idObj.$in = idObj.$in.map((id) => new ObjectId(id));
}
if (Array.isArray(idObj.$nin)) {
idObj.$nin = idObj.$nin.map((id) => new ObjectId(id));
}
}
return f;
}
return filter;
}
async getCollection() {
if (this.collection) {
return this.collection;
}
await this.initLock.acquire();
if (this.collection) {
return this.collection;
}
try {
const client = this.options.mongoClient();
const collection = client.db(this.options.db).collection(this.options.collection);
if (this.primaryKey !== '_id') {
await collection.createIndex({ [this.primaryKey]: 1 }, { unique: true });
}
this.collection = collection;
return collection;
}
finally {
this.initLock.release();
}
}
constructor(options) {
super();
this.options = options;
this.primaryKey = options.primaryKey;
this.model = options.model;
}
async add(...entries) {
const collection = await this.getCollection();
const result = await collection.insertMany(entries.map((e) => ({ ...e })));
const created = this.primaryKey === '_id'
? Object.values(result.insertedIds).map((insertedId, index) =>
// Use 'unknown' as intermediate cast to satisfy TypeScript
this.stringifyResultId({ _id: insertedId, ...entries[index] }))
: Object.values(result.insertedIds).map((insertedId, index) => {
// Use 'unknown' as intermediate cast to satisfy TypeScript
const entity = { _id: insertedId, ...entries[index] };
const { _id, ...r } = entity;
return r;
});
created.forEach((entity) => this.emit('onEntityAdded', { entity }));
return { created };
}
async update(id, data) {
const collection = await this.getCollection();
const updateResult = await collection.updateOne(this.createIdFilter(id), { $set: data });
if (updateResult.matchedCount < 1) {
throw Error(`Entity not found with id '${String(id)}', cannot update!`);
}
this.emit('onEntityUpdated', { id, change: data });
}
async count(filter) {
const collection = await this.getCollection();
return await collection.countDocuments(this.parseFilter(filter), {});
}
async find(filter) {
const collection = await this.getCollection();
const sort = filter.order
? Object.fromEntries(Object.entries(filter.order).map(([key, value]) => [key, value === 'ASC' ? 1 : -1]))
: {};
const result = await collection
.find(this.parseFilter(filter.filter))
.project(this.getProjection(filter.select))
.skip(filter.skip || 0)
.limit(filter.top || Number.MAX_SAFE_INTEGER)
.sort(sort)
.toArray();
return result.map((entity) => this.stringifyResultId(entity));
}
getProjection(fields) {
return {
...(this.primaryKey !== '_id' ? { _id: 0 } : {}),
...(fields ? Object.fromEntries(fields.map((field) => [field, 1])) : {}),
};
}
async get(key, select) {
const collection = await this.getCollection();
const projection = this.getProjection(select);
const result = await collection.findOne(this.createIdFilter(key), { projection });
return result ? this.stringifyResultId(result) : undefined;
}
async remove(...keys) {
const collection = await this.getCollection();
await collection.deleteMany(this.createIdFilter(...keys));
keys.forEach((key) => this.emit('onEntityRemoved', { key }));
}
}
//# sourceMappingURL=mongodb-store.js.map