ts-cache-mongoose 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,72 +0,0 @@
1
- import { ms } from '../ms'
2
- import { MemoryCacheEngine } from './engine/MemoryCacheEngine'
3
- import { RedisCacheEngine } from './engine/RedisCacheEngine'
4
-
5
- import type { CacheData, CacheEngine, CacheOptions, Duration } from '../types'
6
-
7
- export class Cache {
8
- readonly #engine!: CacheEngine
9
- readonly #defaultTTL: number
10
- readonly #debug: boolean
11
- readonly #engines = ['memory', 'redis'] as const
12
-
13
- constructor(cacheOptions: CacheOptions) {
14
- if (!this.#engines.includes(cacheOptions.engine)) {
15
- throw new Error(`Invalid engine name: ${cacheOptions.engine}`)
16
- }
17
-
18
- if (cacheOptions.engine === 'redis' && !cacheOptions.engineOptions) {
19
- throw new Error(`Engine options are required for ${cacheOptions.engine} engine`)
20
- }
21
-
22
- cacheOptions.defaultTTL ??= '1 minute'
23
-
24
- this.#defaultTTL = ms(cacheOptions.defaultTTL)
25
-
26
- if (cacheOptions.engine === 'redis' && cacheOptions.engineOptions) {
27
- this.#engine = new RedisCacheEngine(cacheOptions.engineOptions)
28
- }
29
-
30
- if (cacheOptions.engine === 'memory') {
31
- this.#engine = new MemoryCacheEngine()
32
- }
33
-
34
- this.#debug = cacheOptions.debug === true
35
- }
36
-
37
- async get(key: string): Promise<CacheData> {
38
- const cacheEntry = await this.#engine.get(key)
39
- if (this.#debug) {
40
- const cacheHit = cacheEntry == null ? 'MISS' : 'HIT'
41
- console.log(`[ts-cache-mongoose] GET '${key}' - ${cacheHit}`)
42
- }
43
- return cacheEntry
44
- }
45
-
46
- async set(key: string, value: CacheData, ttl: Duration | null): Promise<void> {
47
- const givenTTL = ttl == null ? null : ms(ttl)
48
- const actualTTL = givenTTL ?? this.#defaultTTL
49
- await this.#engine.set(key, value, actualTTL)
50
- if (this.#debug) {
51
- console.log(`[ts-cache-mongoose] SET '${key}' - ttl: ${actualTTL.toFixed(0)} ms`)
52
- }
53
- }
54
-
55
- async del(key: string): Promise<void> {
56
- await this.#engine.del(key)
57
- if (this.#debug) {
58
- console.log(`[ts-cache-mongoose] DEL '${key}'`)
59
- }
60
- }
61
-
62
- async clear(): Promise<void> {
63
- await this.#engine.clear()
64
- if (this.#debug) {
65
- console.log('[ts-cache-mongoose] CLEAR')
66
- }
67
- }
68
-
69
- async close(): Promise<void> {
70
- return this.#engine.close()
71
- }
72
- }
@@ -1,41 +0,0 @@
1
- import { ms } from '../../ms'
2
-
3
- import type { CacheData, CacheEngine, Duration } from '../../types'
4
-
5
- export class MemoryCacheEngine implements CacheEngine {
6
- readonly #cache: Map<string, { value: CacheData; expiresAt: number } | undefined>
7
-
8
- constructor() {
9
- this.#cache = new Map()
10
- }
11
-
12
- get(key: string): CacheData {
13
- const item = this.#cache.get(key)
14
- if (!item || item.expiresAt < Date.now()) {
15
- this.del(key)
16
- return undefined
17
- }
18
- return item.value
19
- }
20
-
21
- set(key: string, value: CacheData, ttl?: Duration): void {
22
- const givenTTL = ttl == null ? undefined : ms(ttl)
23
- const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY
24
- this.#cache.set(key, {
25
- value,
26
- expiresAt: Date.now() + actualTTL,
27
- })
28
- }
29
-
30
- del(key: string): void {
31
- this.#cache.delete(key)
32
- }
33
-
34
- clear(): void {
35
- this.#cache.clear()
36
- }
37
-
38
- close(): void {
39
- // do nothing
40
- }
41
- }
@@ -1,52 +0,0 @@
1
- import { EJSON } from 'bson'
2
- import IORedis from 'ioredis'
3
- import { ms } from '../../ms'
4
- import { convertToObject } from '../../version'
5
-
6
- import type { Redis, RedisOptions } from 'ioredis'
7
- import type { CacheData, CacheEngine, Duration } from '../../types'
8
-
9
- export class RedisCacheEngine implements CacheEngine {
10
- readonly #client: Redis
11
-
12
- constructor(options: RedisOptions) {
13
- options.keyPrefix ??= 'cache-mongoose:'
14
- this.#client = new IORedis(options)
15
- }
16
-
17
- async get(key: string): Promise<CacheData> {
18
- try {
19
- const value = await this.#client.get(key)
20
- if (value === null) {
21
- return undefined
22
- }
23
- return EJSON.parse(value) as CacheData
24
- } catch (err) {
25
- console.error(err)
26
- return undefined
27
- }
28
- }
29
-
30
- async set(key: string, value: CacheData, ttl?: Duration): Promise<void> {
31
- try {
32
- const givenTTL = ttl == null ? undefined : ms(ttl)
33
- const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY
34
- const serializedValue = EJSON.stringify(convertToObject(value))
35
- await this.#client.setex(key, Math.ceil(actualTTL / 1000), serializedValue)
36
- } catch (err) {
37
- console.error(err)
38
- }
39
- }
40
-
41
- async del(key: string): Promise<void> {
42
- await this.#client.del(key)
43
- }
44
-
45
- async clear(): Promise<void> {
46
- await this.#client.flushdb()
47
- }
48
-
49
- async close(): Promise<void> {
50
- await this.#client.quit()
51
- }
52
- }
@@ -1,51 +0,0 @@
1
- import { getKey } from '../key'
2
-
3
- import type { Mongoose } from 'mongoose'
4
- import type { Cache } from '../cache/Cache'
5
- import type { Duration } from '../types'
6
-
7
- export function extendAggregate(mongoose: Mongoose, cache: Cache): void {
8
- const mongooseExec = mongoose.Aggregate.prototype.exec
9
-
10
- mongoose.Aggregate.prototype.getCacheKey = function (): string {
11
- if (this._key != null) return this._key
12
-
13
- return getKey({
14
- pipeline: this.pipeline(),
15
- })
16
- }
17
-
18
- mongoose.Aggregate.prototype.getDuration = function (): Duration | null {
19
- return this._ttl
20
- }
21
-
22
- mongoose.Aggregate.prototype.cache = function (ttl?: Duration, customKey?: string) {
23
- this._ttl = ttl ?? null
24
- this._key = customKey ?? null
25
- return this
26
- }
27
-
28
- mongoose.Aggregate.prototype.exec = async function (...args: []) {
29
- if (!Object.hasOwn(this, '_ttl')) {
30
- return mongooseExec.apply(this, args)
31
- }
32
-
33
- const key = this.getCacheKey()
34
- const ttl = this.getDuration()
35
-
36
- const resultCache = await cache.get(key).catch((err: unknown) => {
37
- console.error(err)
38
- })
39
-
40
- if (resultCache) {
41
- return resultCache
42
- }
43
-
44
- const result = (await mongooseExec.call(this)) as Record<string, unknown>[] | Record<string, unknown>
45
- await cache.set(key, result, ttl).catch((err: unknown) => {
46
- console.error(err)
47
- })
48
-
49
- return result
50
- }
51
- }
@@ -1,81 +0,0 @@
1
- import { getKey } from '../key'
2
-
3
- import type { Mongoose } from 'mongoose'
4
- import type { Cache } from '../cache/Cache'
5
- import type { Duration } from '../types'
6
-
7
- export function extendQuery(mongoose: Mongoose, cache: Cache): void {
8
- const mongooseExec = mongoose.Query.prototype.exec
9
-
10
- mongoose.Query.prototype.getCacheKey = function (): string {
11
- if (this._key != null) return this._key
12
-
13
- const filter = this.getFilter()
14
- const update = this.getUpdate()
15
- const options = this.getOptions()
16
- const mongooseOptions = this.mongooseOptions()
17
-
18
- return getKey({
19
- model: this.model.modelName,
20
- op: this.op,
21
- filter,
22
- update,
23
- options,
24
- mongooseOptions,
25
- _path: this._path,
26
- _fields: this._fields,
27
- _distinct: this._distinct,
28
- _conditions: this._conditions,
29
- })
30
- }
31
-
32
- mongoose.Query.prototype.getDuration = function (): Duration | null {
33
- return this._ttl
34
- }
35
-
36
- mongoose.Query.prototype.cache = function (ttl?: Duration, customKey?: string) {
37
- this._ttl = ttl ?? null
38
- this._key = customKey ?? null
39
- return this
40
- }
41
-
42
- mongoose.Query.prototype.exec = async function (...args: []) {
43
- if (!Object.hasOwn(this, '_ttl')) {
44
- return mongooseExec.apply(this, args)
45
- }
46
-
47
- const key = this.getCacheKey()
48
- const ttl = this.getDuration()
49
- const mongooseOptions = this.mongooseOptions()
50
-
51
- const isCount = this.op?.includes('count') ?? false
52
- const isDistinct = this.op === 'distinct'
53
- const model = this.model.modelName
54
-
55
- const resultCache = await cache.get(key).catch((err: unknown) => {
56
- console.error(err)
57
- })
58
-
59
- if (resultCache) {
60
- if (isCount || isDistinct || mongooseOptions.lean) {
61
- return resultCache
62
- }
63
-
64
- const modelConstructor = mongoose.model<unknown>(model)
65
-
66
- if (Array.isArray(resultCache)) {
67
- return resultCache.map((item) => {
68
- return modelConstructor.hydrate(item)
69
- })
70
- }
71
- return modelConstructor.hydrate(resultCache)
72
- }
73
-
74
- const result = (await mongooseExec.call(this)) as Record<string, unknown>[] | Record<string, unknown>
75
- await cache.set(key, result, ttl).catch((err: unknown) => {
76
- console.error(err)
77
- })
78
-
79
- return result
80
- }
81
- }
package/src/index.ts DELETED
@@ -1,68 +0,0 @@
1
- import { Cache } from './cache/Cache'
2
- import { extendAggregate } from './extend/aggregate'
3
- import { extendQuery } from './extend/query'
4
-
5
- import type { Mongoose } from 'mongoose'
6
- import type { CacheOptions, Duration } from './types'
7
-
8
- export * from './types'
9
-
10
- declare module 'mongoose' {
11
- interface Query<ResultType, DocType, THelpers, RawDocType> {
12
- cache: (this: Query<ResultType, DocType, THelpers, RawDocType>, ttl?: Duration, customKey?: string) => this
13
- _key: string | null
14
- getCacheKey: (this: Query<ResultType, DocType, THelpers, RawDocType>) => string
15
- _ttl: Duration | null
16
- getDuration: (this: Query<ResultType, DocType, THelpers, RawDocType>) => Duration | null
17
- op?: string
18
- _path?: unknown
19
- _fields?: unknown
20
- _distinct?: unknown
21
- _conditions?: unknown
22
- }
23
-
24
- interface Aggregate<ResultType> {
25
- cache: (this: Aggregate<ResultType>, ttl?: Duration, customKey?: string) => this
26
- _key: string | null
27
- getCacheKey: (this: Aggregate<ResultType>) => string
28
- _ttl: Duration | null
29
- getDuration: (this: Aggregate<ResultType>) => Duration | null
30
- }
31
- }
32
-
33
- class CacheMongoose {
34
- static #instance: CacheMongoose | undefined
35
- private cache!: Cache
36
-
37
- private constructor() {
38
- // Private constructor to prevent external instantiation
39
- }
40
-
41
- public static init(mongoose: Mongoose, cacheOptions: CacheOptions): CacheMongoose {
42
- if (!CacheMongoose.#instance) {
43
- CacheMongoose.#instance = new CacheMongoose()
44
- CacheMongoose.#instance.cache = new Cache(cacheOptions)
45
-
46
- const cache = CacheMongoose.#instance.cache
47
-
48
- extendQuery(mongoose, cache)
49
- extendAggregate(mongoose, cache)
50
- }
51
-
52
- return CacheMongoose.#instance
53
- }
54
-
55
- public async clear(customKey?: string): Promise<void> {
56
- if (customKey == null) {
57
- await this.cache.clear()
58
- } else {
59
- await this.cache.del(customKey)
60
- }
61
- }
62
-
63
- public async close(): Promise<void> {
64
- await this.cache.close()
65
- }
66
- }
67
-
68
- export default CacheMongoose
package/src/key.ts DELETED
@@ -1,10 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { sortKeys } from './sort-keys'
3
-
4
- export function getKey(data: Record<string, unknown>[] | Record<string, unknown>): string {
5
- const sortedObj = sortKeys(data)
6
- const sortedStr = JSON.stringify(sortedObj, (_, val: unknown) => {
7
- return val instanceof RegExp ? String(val) : val
8
- })
9
- return createHash('sha1').update(sortedStr).digest('hex')
10
- }
package/src/ms.ts DELETED
@@ -1,66 +0,0 @@
1
- const s = 1000
2
- const m = s * 60
3
- const h = m * 60
4
- const d = h * 24
5
- const w = d * 7
6
- const y = d * 365.25
7
- const mo = y / 12
8
-
9
- export const UNITS = {
10
- milliseconds: 1,
11
- millisecond: 1,
12
- msecs: 1,
13
- msec: 1,
14
- ms: 1,
15
- seconds: s,
16
- second: s,
17
- secs: s,
18
- sec: s,
19
- s,
20
- minutes: m,
21
- minute: m,
22
- mins: m,
23
- min: m,
24
- m,
25
- hours: h,
26
- hour: h,
27
- hrs: h,
28
- hr: h,
29
- h,
30
- days: d,
31
- day: d,
32
- d,
33
- weeks: w,
34
- week: w,
35
- w,
36
- months: mo,
37
- month: mo,
38
- mo,
39
- years: y,
40
- year: y,
41
- yrs: y,
42
- yr: y,
43
- y,
44
- } as const satisfies Record<string, number>
45
-
46
- export type Unit = keyof typeof UNITS
47
-
48
- export type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`
49
-
50
- const unitPattern = Object.keys(UNITS)
51
- .sort((a, b) => b.length - a.length)
52
- .join('|')
53
-
54
- const RE = new RegExp(String.raw`^(-?(?:\d+)?\.?\d+)\s*(${unitPattern})?$`, 'i')
55
-
56
- export const ms = (val: Duration): number => {
57
- const str = String(val)
58
- if (str.length > 100) return Number.NaN
59
-
60
- const match = RE.exec(str)
61
- if (!match) return Number.NaN
62
-
63
- const n = Number.parseFloat(match[1] ?? '')
64
- const type = (match[2] ?? 'ms').toLowerCase()
65
- return n * (UNITS[type as Unit] ?? 0)
66
- }
@@ -1,79 +0,0 @@
1
- /** biome-ignore-all lint/complexity/noStaticOnlyClass: nest */
2
- import { Module } from '@nestjs/common'
3
- import { CACHE_OPTIONS, CacheService } from './cache.service'
4
-
5
- import type { DynamicModule, Provider } from '@nestjs/common'
6
- import type { CacheModuleAsyncOptions, CacheModuleOptions, CacheOptionsFactory } from './interfaces'
7
-
8
- @Module({})
9
- export class CacheModule {
10
- static forRoot(options: CacheModuleOptions & { isGlobal?: boolean }): DynamicModule {
11
- return {
12
- module: CacheModule,
13
- global: options.isGlobal ?? false,
14
- providers: [
15
- { provide: CACHE_OPTIONS, useValue: options },
16
- {
17
- provide: CacheService,
18
- useFactory: (opts: CacheModuleOptions) => new CacheService(opts),
19
- inject: [CACHE_OPTIONS],
20
- },
21
- ],
22
- exports: [CacheService],
23
- }
24
- }
25
-
26
- static forRootAsync(options: CacheModuleAsyncOptions): DynamicModule {
27
- const asyncProviders = CacheModule.createAsyncProviders(options)
28
-
29
- return {
30
- module: CacheModule,
31
- global: options.isGlobal ?? false,
32
- imports: options.imports ?? [],
33
- providers: [
34
- ...asyncProviders,
35
- {
36
- provide: CacheService,
37
- useFactory: (opts: CacheModuleOptions) => new CacheService(opts),
38
- inject: [CACHE_OPTIONS],
39
- },
40
- ],
41
- exports: [CacheService],
42
- }
43
- }
44
-
45
- private static createAsyncProviders(options: CacheModuleAsyncOptions): Provider[] {
46
- if (options.useFactory) {
47
- return [
48
- {
49
- provide: CACHE_OPTIONS,
50
- useFactory: options.useFactory,
51
- inject: options.inject ?? [],
52
- },
53
- ]
54
- }
55
-
56
- if (options.useClass) {
57
- return [
58
- { provide: options.useClass, useClass: options.useClass },
59
- {
60
- provide: CACHE_OPTIONS,
61
- useFactory: (factory: CacheOptionsFactory) => factory.createCacheOptions(),
62
- inject: [options.useClass],
63
- },
64
- ]
65
- }
66
-
67
- if (options.useExisting) {
68
- return [
69
- {
70
- provide: CACHE_OPTIONS,
71
- useFactory: (factory: CacheOptionsFactory) => factory.createCacheOptions(),
72
- inject: [options.useExisting],
73
- },
74
- ]
75
- }
76
-
77
- return []
78
- }
79
- }
@@ -1,37 +0,0 @@
1
- import { Logger } from '@nestjs/common'
2
- import mongoose from 'mongoose'
3
- import CacheMongoose from '../index'
4
-
5
- import type { OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common'
6
- import type { CacheModuleOptions } from './interfaces'
7
-
8
- export const CACHE_OPTIONS = Symbol('CACHE_OPTIONS')
9
-
10
- export class CacheService implements OnApplicationBootstrap, OnApplicationShutdown {
11
- private readonly logger = new Logger(CacheService.name)
12
- private readonly options: CacheModuleOptions
13
- private cacheMongoose!: CacheMongoose
14
-
15
- constructor(options: CacheModuleOptions) {
16
- this.options = options
17
- }
18
-
19
- get instance(): CacheMongoose {
20
- return this.cacheMongoose
21
- }
22
-
23
- async onApplicationBootstrap(): Promise<void> {
24
- this.cacheMongoose = CacheMongoose.init(mongoose, this.options)
25
- this.logger.log(`Cache initialized with ${this.options.engine} engine`)
26
- }
27
-
28
- async onApplicationShutdown(): Promise<void> {
29
- if (this.cacheMongoose) {
30
- await this.cacheMongoose.close()
31
- }
32
- }
33
-
34
- async clear(customKey?: string): Promise<void> {
35
- await this.cacheMongoose.clear(customKey)
36
- }
37
- }
package/src/nest/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { CacheModule } from './cache.module'
2
- export { CACHE_OPTIONS, CacheService } from './cache.service'
3
-
4
- export type { CacheModuleAsyncOptions, CacheModuleOptions, CacheOptionsFactory } from './interfaces'
@@ -1,17 +0,0 @@
1
- import type { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common'
2
- import type { CacheOptions } from '../types'
3
-
4
- export type CacheModuleOptions = CacheOptions
5
-
6
- export interface CacheOptionsFactory {
7
- createCacheOptions(): CacheModuleOptions | Promise<CacheModuleOptions>
8
- }
9
-
10
- export interface CacheModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
11
- isGlobal?: boolean
12
- inject?: (InjectionToken | OptionalFactoryDependency)[]
13
- useClass?: Type<CacheOptionsFactory>
14
- useExisting?: Type<CacheOptionsFactory>
15
- // biome-ignore lint/suspicious/noExplicitAny: NestJS convention for factory args
16
- useFactory?: (...args: any[]) => CacheModuleOptions | Promise<CacheModuleOptions>
17
- }
package/src/sort-keys.ts DELETED
@@ -1,38 +0,0 @@
1
- const isPlainObject = (value: unknown): value is Record<string, unknown> => {
2
- if (typeof value !== 'object' || value === null) return false
3
- const proto = Object.getPrototypeOf(value) as unknown
4
- return proto === Object.prototype || proto === null
5
- }
6
-
7
- export const sortKeys = (input: Record<string, unknown> | Record<string, unknown>[]): Record<string, unknown> | Record<string, unknown>[] => {
8
- const seen = new WeakSet<object>()
9
-
10
- const sortObject = (obj: Record<string, unknown>): Record<string, unknown> => {
11
- if (seen.has(obj)) return obj
12
- seen.add(obj)
13
-
14
- const sorted: Record<string, unknown> = {}
15
- for (const key of Object.keys(obj).sort((a, b) => a.localeCompare(b))) {
16
- const value = obj[key]
17
- if (Array.isArray(value)) {
18
- sorted[key] = sortArray(value)
19
- } else if (isPlainObject(value)) {
20
- sorted[key] = sortObject(value)
21
- } else {
22
- sorted[key] = value
23
- }
24
- }
25
- return sorted
26
- }
27
-
28
- const sortArray = (arr: unknown[]): unknown[] => {
29
- return arr.map((item) => {
30
- if (Array.isArray(item)) return sortArray(item)
31
- if (isPlainObject(item)) return sortObject(item)
32
- return item
33
- })
34
- }
35
-
36
- if (Array.isArray(input)) return sortArray(input) as Record<string, unknown>[]
37
- return sortObject(input)
38
- }
package/src/types.ts DELETED
@@ -1,21 +0,0 @@
1
- import type { RedisOptions } from 'ioredis'
2
- import type { Duration } from './ms'
3
-
4
- export type { Duration } from './ms'
5
-
6
- export type CacheData = Record<string, unknown> | Record<string, unknown>[] | unknown[] | number | undefined
7
-
8
- export type CacheOptions = {
9
- engine: 'memory' | 'redis'
10
- engineOptions?: RedisOptions
11
- defaultTTL?: Duration
12
- debug?: boolean
13
- }
14
-
15
- export interface CacheEngine {
16
- get: (key: string) => Promise<CacheData> | CacheData
17
- set: (key: string, value: CacheData, ttl?: Duration) => Promise<void> | void
18
- del: (key: string) => Promise<void> | void
19
- clear: () => Promise<void> | void
20
- close: () => Promise<void> | void
21
- }
package/src/version.ts DELETED
@@ -1,18 +0,0 @@
1
- import mongoose from 'mongoose'
2
-
3
- import type { CacheData } from './types'
4
-
5
- export const isMongooseLessThan7 = Number.parseInt(mongoose.version, 10) < 7
6
-
7
- export const convertToObject = <T>(value: (T & { toObject?: () => CacheData }) | undefined): CacheData => {
8
- if (isMongooseLessThan7) {
9
- if (value != null && typeof value === 'object' && !Array.isArray(value) && value.toObject) {
10
- return value.toObject()
11
- }
12
- if (Array.isArray(value)) {
13
- return value.map((doc) => convertToObject(doc))
14
- }
15
- }
16
-
17
- return value
18
- }