@daiso-tech/core
Version:
The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.
272 lines • 8.81 kB
JavaScript
/**
* @module Cache
*/
import { TypeCacheError, UnexpectedCacheError, } from "../../../../cache/contracts/_module-exports.js";
import { MongodbCacheAdapterSerde } from "../../../../cache/implementations/adapters/mongodb-cache-adapter/mongodb-cache-adapter-serde.js";
import { MongoServerError } from "mongodb";
import {} from "mongodb";
import escapeStringRegexp from "escape-string-regexp";
/**
* To utilize the `MongodbCacheAdapter`, you must install the `"mongodb"` package and supply a {@link ISerde | `ISerde<string>`}, with an adapter like {@link SuperJsonSerdeAdapter | `SuperJsonSerdeAdapter `}.
*
* IMPORT_PATH: `"@daiso-tech/core/cache/adapters"`
* @group Adapters
*/
export class MongodbCacheAdapter {
static filterUnexpiredKeys(keys) {
const hasNoExpiration = {
expiration: {
$eq: null,
},
};
const hasExpiration = {
expiration: {
$ne: null,
},
};
const hasNotExpired = {
expiration: {
$gt: new Date(),
},
};
const keysMatch = {
key: {
$in: keys,
},
};
return {
$and: [
keysMatch,
{
$or: [
hasNoExpiration,
{
$and: [hasExpiration, hasNotExpired],
},
],
},
],
};
}
static isMongodbIncrementError(value) {
return (value instanceof MongoServerError &&
value.code !== undefined &&
(typeof value.code === "string" ||
typeof value.code === "number") &&
String(value.code) === "14");
}
serde;
collection;
/**
* @example
* ```ts
* import { MongodbCacheAdapter } from "@daiso-tech/core/cache/adapters";
* import { Serde } from "@daiso-tech/core/serde";
* import { SuperJsonSerdeAdapter } from "@daiso-tech/core/serde/adapters"
* import { MongoClient } from "mongodb";
*
* const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING");
* const database = client.db("database");
* const serde = new Serde(new SuperJsonSerdeAdapter());
* const cacheAdapter = new MongodbCacheAdapter({
* database,
* serde,
* });
* // You need initialize the adapter once before using it.
* await cacheAdapter.init();
* ```
*/
constructor(settings) {
const { collectionName = "cache", collectionSettings, database, serde, } = settings;
this.collection = database.collection(collectionName, collectionSettings);
this.serde = new MongodbCacheAdapterSerde(serde);
}
/**
* Creates all related indexes.
* Note the `init` method needs to be called before using the adapter.
*/
async init() {
try {
await this.collection.createIndex({
key: 1,
}, {
unique: true,
});
await this.collection.createIndex("expiration", {
expireAfterSeconds: 0,
});
}
catch {
/* Empty */
}
}
/**
* Removes the collection where the cache values are stored and all it's related indexes.
* Note all cache data will be removed.
*/
async deInit() {
await this.collection.dropIndexes();
await this.collection.drop();
}
getDocValue(document) {
if (document === null) {
return null;
}
const { expiration, value } = document;
if (expiration === null) {
return this.serde.deserialize(value);
}
const hasExpired = expiration.getTime() <= new Date().getTime();
if (hasExpired) {
return null;
}
return this.serde.deserialize(value);
}
async get(key) {
const document = await this.collection.findOne({
key,
}, {
projection: {
_id: 0,
expiration: 1,
value: 1,
},
});
return this.getDocValue(document);
}
async getAndRemove(key) {
const document = await this.collection.findOneAndDelete({
key,
}, {
projection: {
_id: 0,
expiration: 1,
value: 1,
},
});
return this.getDocValue(document);
}
isDocExpired(document) {
if (document === null) {
return true;
}
const { expiration } = document;
if (expiration === null) {
return false;
}
const hasExpired = expiration.getTime() <= new Date().getTime();
return hasExpired;
}
async add(key, value, ttl) {
const hasExpirationQuery = {
$ne: ["$expiration", null],
};
const hasExpiredQuery = {
$lte: ["$expiration", new Date()],
};
const hasExpirationAndExpiredQuery = {
$and: [hasExpirationQuery, hasExpiredQuery],
};
const serializedValue = this.serde.serialize(value);
const document = await this.collection.findOneAndUpdate({
key,
}, [
{
$set: {
value: {
$cond: {
if: hasExpirationAndExpiredQuery,
then: serializedValue,
else: "$value",
},
},
expiration: {
$cond: {
if: hasExpirationAndExpiredQuery,
then: ttl?.toEndDate() ?? null,
else: "$expiration",
},
},
},
},
], {
upsert: true,
projection: {
_id: 0,
expiration: 1,
},
});
return this.isDocExpired(document);
}
async put(key, value, ttl) {
const document = await this.collection.findOneAndUpdate({
key,
}, {
$set: {
value: this.serde.serialize(value),
expiration: ttl?.toEndDate() ?? null,
},
}, {
upsert: true,
projection: {
_id: 0,
expiration: 1,
},
});
return !this.isDocExpired(document);
}
async update(key, value) {
const updateResult = await this.collection.updateOne(MongodbCacheAdapter.filterUnexpiredKeys([key]), {
$set: {
value: this.serde.serialize(value),
},
});
if (!updateResult.acknowledged) {
throw new UnexpectedCacheError("Mongodb update was not acknowledged");
}
return updateResult.modifiedCount > 0;
}
async increment(key, value) {
try {
const updateResult = await this.collection.updateOne(MongodbCacheAdapter.filterUnexpiredKeys([key]), {
$inc: {
value,
},
});
if (!updateResult.acknowledged) {
throw new UnexpectedCacheError("Mongodb update was not acknowledged");
}
return updateResult.modifiedCount > 0;
}
catch (error) {
if (MongodbCacheAdapter.isMongodbIncrementError(error)) {
throw new TypeCacheError(`Unable to increment or decrement none number type key "${key}"`);
}
throw error;
}
}
async removeMany(keys) {
const deleteResult = await this.collection.deleteMany(MongodbCacheAdapter.filterUnexpiredKeys(keys));
if (!deleteResult.acknowledged) {
throw new UnexpectedCacheError("Mongodb deletion was not acknowledged");
}
return deleteResult.deletedCount > 0;
}
async removeAll() {
const mongodbResult = await this.collection.deleteMany();
if (!mongodbResult.acknowledged) {
throw new UnexpectedCacheError("Mongodb deletion was not acknowledged");
}
}
async removeByKeyPrefix(prefix) {
const mongodbResult = await this.collection.deleteMany({
key: {
$regex: new RegExp(`^${escapeStringRegexp(prefix)}`),
},
});
if (!mongodbResult.acknowledged) {
throw new UnexpectedCacheError("Mongodb deletion was not acknowledged");
}
}
}
//# sourceMappingURL=mongodb-cache-adapter.js.map