|
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(); |
|
} |
|
} |