Skip to content

Instantly share code, notes, and snippets.

@michaelrambeau
Last active September 12, 2024 10:13
Show Gist options
  • Save michaelrambeau/b3c266031fe105b891198c0122ce455b to your computer and use it in GitHub Desktop.
Save michaelrambeau/b3c266031fe105b891198c0122ce455b to your computer and use it in GitHub Desktop.
A simple MongoDB adapter for keyv

Simple MongoDB Keyv Adapter

Why?

The MongoDB adapter for keyv is fine but I needed a way to pass an existing connection to the adapter.

In the current implementation, the constructor connects to the MongoDB by itself, I couldn't make it work by extending the current class.

So I created my own MongoDB adapter, based on the existing one, removing the feature about MongoDB GridFS I didn't need.

How to use it

import { MongoClient as mongoClient } from "mongodb";

import { getCache } from "./cache";

const uri = process.env.MONGODB_URI;
if (!uri) throw new Error("MONGODB_URI is not set");

const client = new mongoClient(uri);
await client.connect();

console.log("Connected to MongoDB!");

const cache = getCache(client);

cache.set("user1", { name: "Mike" }, 10_000);
cache.set("user2", { name: "Larry" }, 60_000);
import Keyv from "keyv";
import { MongoClient as mongoClient } from "mongodb";
import { SimpleKeyvMongo } from "./simple-keyv-mongo";
export function getCache(client: mongoClient, collection = "cache") {
const db = client.db();
const connection = Promise.resolve({
mongoClient: client,
db: db,
store: db.collection(collection),
});
const cache = new Keyv(new SimpleKeyvMongo(connection, { collection }));
return cache;
}
import EventEmitter from "events";
import { type Collection, type Db } from "mongodb";
import {
MongoClient as mongoClient,
type WithId,
type Document,
} from "mongodb";
import { type KeyvStoreAdapter, type StoredData } from "keyv";
type Options = {
collection?: string;
};
type Connection = {
mongoClient: mongoClient;
db: Db;
store: Collection;
};
export class SimpleKeyvMongo extends EventEmitter implements KeyvStoreAdapter {
ttlSupport = false;
opts: Options & { url: string };
connect: Promise<Connection>;
namespace?: string;
constructor(connection: Promise<Connection>, options?: Options) {
super();
this.opts = {
collection: "keyv",
...options,
url: "", // does not make sense but needed to avoid run time errors
};
this.connect = connection;
}
async get<Value>(key: string): Promise<StoredData<Value>> {
const client = await this.connect;
const document = await client.store.findOne({ key: { $eq: key } });
if (!document) {
return undefined;
}
return document.value as StoredData<Value>;
}
async getMany<Value>(keys: string[]) {
const connect = await this.connect;
const values: Array<{ key: string; value: StoredData<Value> }> =
await connect.store.s.db
.collection(this.opts.collection!)
.find({ key: { $in: keys } })
.project({ _id: 0, value: 1, key: 1 })
.toArray();
const results = [...keys];
let i = 0;
for (const key of keys) {
const rowIndex = values.findIndex(
(row: { key: string; value: unknown }) => row.key === key
);
// @ts-expect-error - results type
results[i] = rowIndex > -1 ? values[rowIndex].value : undefined;
i++;
}
return results as Array<StoredData<Value>>;
}
async set(key: string, value: any, ttl?: number) {
const expiresAt =
typeof ttl === "number" ? new Date(Date.now() + ttl) : null;
const client = await this.connect;
await client.store.updateOne(
{ key: { $eq: key } },
{ $set: { key, value, expiresAt } },
{ upsert: true }
);
}
async delete(key: string) {
if (typeof key !== "string") {
return false;
}
const client = await this.connect;
const object = await client.store.deleteOne({ key: { $eq: key } });
return object.deletedCount > 0;
}
async deleteMany(keys: string[]) {
const client = await this.connect;
const object = await client.store.deleteMany({ key: { $in: keys } });
return object.deletedCount > 0;
}
async clear() {
const client = await this.connect;
await client.store.deleteMany({
key: { $regex: this.namespace ? `^${this.namespace}:*` : "" },
});
}
async *iterator(namespace?: string) {
const client = await this.connect;
const regexp = new RegExp(`^${namespace ? namespace + ":" : ".*"}`);
const iterator = client.store
.find({
key: regexp,
})
.map((x: WithId<Document>) => [x.key, x.value]);
yield* iterator;
}
async has(key: string) {
const client = await this.connect;
const filter = { ["key"]: { $eq: key } };
const document = await client.store.count(filter);
return document !== 0;
}
async disconnect(): Promise<void> {
const client = await this.connect;
await client.mongoClient.close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment