ts-cache-mongoose 2.0.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.
@@ -3,14 +3,17 @@
3
3
  var common = require('@nestjs/common');
4
4
  var mongoose = require('mongoose');
5
5
  var index = require('../index.cjs');
6
+ require('node:v8');
6
7
  require('bson');
7
8
  require('ioredis');
8
9
  require('node:crypto');
9
10
 
10
11
  const CACHE_OPTIONS = Symbol("CACHE_OPTIONS");
11
12
  class CacheService {
13
+ logger = new common.Logger(CacheService.name);
14
+ options;
15
+ cacheMongoose;
12
16
  constructor(options) {
13
- this.logger = new common.Logger(CacheService.name);
14
17
  this.options = options;
15
18
  }
16
19
  get instance() {
@@ -1,14 +1,17 @@
1
1
  import { Logger, Module } from '@nestjs/common';
2
2
  import mongoose from 'mongoose';
3
3
  import CacheMongoose from '../index.mjs';
4
+ import 'node:v8';
4
5
  import 'bson';
5
6
  import 'ioredis';
6
7
  import 'node:crypto';
7
8
 
8
9
  const CACHE_OPTIONS = Symbol("CACHE_OPTIONS");
9
10
  class CacheService {
11
+ logger = new Logger(CacheService.name);
12
+ options;
13
+ cacheMongoose;
10
14
  constructor(options) {
11
- this.logger = new Logger(CacheService.name);
12
15
  this.options = options;
13
16
  }
14
17
  get instance() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-cache-mongoose",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Cache plugin for mongoose Queries and Aggregate (in-memory, redis)",
5
5
  "author": "ilovepixelart",
6
6
  "license": "MIT",
@@ -12,9 +12,6 @@
12
12
  "url": "https://github.com/ilovepixelart/ts-cache-mongoose/issues"
13
13
  },
14
14
  "homepage": "https://github.com/ilovepixelart/ts-cache-mongoose#readme",
15
- "directories": {
16
- "examples": "examples"
17
- },
18
15
  "keywords": [
19
16
  "backend",
20
17
  "mongo",
@@ -35,15 +32,10 @@
35
32
  "aggregate"
36
33
  ],
37
34
  "engines": {
38
- "node": ">=16"
35
+ "node": ">=20"
39
36
  },
40
37
  "files": [
41
- "dist",
42
- "src",
43
- "tests",
44
- "tsconfig.json",
45
- "vite.config.mts",
46
- "biome.json"
38
+ "dist"
47
39
  ],
48
40
  "type": "module",
49
41
  "exports": {
@@ -78,37 +70,44 @@
78
70
  "main": "./dist/index.cjs",
79
71
  "module": "./dist/index.mjs",
80
72
  "types": "./dist/index.d.cts",
73
+ "publishConfig": {
74
+ "access": "public",
75
+ "provenance": true
76
+ },
81
77
  "scripts": {
82
- "prepare": "simple-git-hooks",
83
78
  "biome": "npx @biomejs/biome check",
84
79
  "biome:fix": "npx @biomejs/biome check --write .",
85
80
  "test": "vitest run --coverage",
86
81
  "test:open": "vitest run --coverage && open-cli coverage/lcov-report/index.html",
87
82
  "clean": "rm -rf ./dist",
88
83
  "type:check": "tsc --noEmit",
84
+ "type:check:tests": "tsc --noEmit -p tests/tsconfig.json",
89
85
  "build": "pkgroll --clean-dist",
90
- "release": "npm install && npm run biome && npm run type:check && npm run build && np --no-publish"
86
+ "release": "npm install && npm run biome && npm run type:check && npm run type:check:tests && npm run build && np --no-publish"
91
87
  },
92
88
  "dependencies": {
93
89
  "ioredis": "5.10.0"
94
90
  },
95
91
  "devDependencies": {
96
- "@biomejs/biome": "2.4.7",
97
- "@nestjs/common": "11.1.16",
98
- "@types/node": "25.5.0",
99
- "@vitest/coverage-v8": "4.1.0",
92
+ "@biomejs/biome": "2.4.11",
93
+ "@nestjs/common": "11.1.18",
94
+ "@nestjs/core": "11.1.18",
95
+ "@nestjs/testing": "11.1.18",
96
+ "@types/node": "25.6.0",
97
+ "@vitest/coverage-v8": "4.1.4",
100
98
  "bson": "7.2.0",
99
+ "fast-check": "4.6.0",
101
100
  "mongodb-memory-server": "11.0.1",
102
- "mongoose": "9.3.0",
103
- "np": "11.0.2",
104
- "open-cli": "8.0.0",
101
+ "mongoose": "9.4.1",
102
+ "np": "11.0.3",
103
+ "open-cli": "9.0.0",
105
104
  "pkgroll": "2.27.0",
106
105
  "simple-git-hooks": "2.13.1",
107
106
  "typescript": "5.9.3",
108
- "vitest": "4.1.0"
107
+ "vitest": "4.1.4"
109
108
  },
110
109
  "peerDependencies": {
111
- "@nestjs/common": ">=9.0.0",
110
+ "@nestjs/common": ">=9.0.0 < 12",
112
111
  "bson": ">=4.7.2 < 8",
113
112
  "mongoose": ">=6.6.0 < 10"
114
113
  },
@@ -125,7 +124,6 @@
125
124
  "publish": false
126
125
  },
127
126
  "overrides": {
128
- "tmp": "0.2.5",
129
- "file-type": "21.3.2"
127
+ "tmp": "0.2.5"
130
128
  }
131
129
  }
package/biome.json DELETED
@@ -1,47 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
3
- "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
4
- "files": {
5
- "ignoreUnknown": false,
6
- "includes": ["src/**/*.ts", "tests/**/*.ts"]
7
- },
8
- "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
9
- "assist": {
10
- "actions": {
11
- "source": {
12
- "organizeImports": {
13
- "level": "on",
14
- "options": {
15
- "groups": [
16
- "vitest",
17
- ":BLANK_LINE:",
18
- ":NODE:",
19
- { "type": false },
20
- ":BLANK_LINE:"
21
- ]
22
- }
23
- }
24
- }
25
- }
26
- },
27
- "linter": {
28
- "enabled": true,
29
- "rules": {
30
- "recommended": true
31
- }
32
- },
33
- "javascript": {
34
- "formatter": {
35
- "trailingCommas": "all",
36
- "quoteStyle": "single",
37
- "semicolons": "asNeeded",
38
- "lineWidth": 320
39
- },
40
- "globals": ["Atomics", "SharedArrayBuffer"]
41
- },
42
- "json": {
43
- "formatter": {
44
- "trailingCommas": "none"
45
- }
46
- }
47
- }
@@ -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, CacheTTL } 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 = typeof cacheOptions.defaultTTL === 'string' ? ms(cacheOptions.defaultTTL) : 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: CacheTTL | null): Promise<void> {
47
- const givenTTL = typeof ttl === 'string' ? ms(ttl) : 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, CacheTTL } 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?: CacheTTL): void {
22
- const givenTTL = typeof ttl === 'string' ? ms(ttl) : 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, CacheTTL } 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?: CacheTTL): Promise<void> {
31
- try {
32
- const givenTTL = typeof ttl === 'string' ? ms(ttl) : 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,52 +0,0 @@
1
- import { getKey } from '../key'
2
-
3
- import type { Mongoose } from 'mongoose'
4
- import type { Cache } from '../cache/Cache'
5
- import type { CacheTTL } 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.getCacheTTL = function (): CacheTTL | null {
19
- return this._ttl
20
- }
21
-
22
- mongoose.Aggregate.prototype.cache = function (ttl?: CacheTTL, 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
- // biome-ignore lint/suspicious/noPrototypeBuiltins: to support node 16
30
- if (!Object.prototype.hasOwnProperty.call(this, '_ttl')) {
31
- return mongooseExec.apply(this, args)
32
- }
33
-
34
- const key = this.getCacheKey()
35
- const ttl = this.getCacheTTL()
36
-
37
- const resultCache = await cache.get(key).catch((err: unknown) => {
38
- console.error(err)
39
- })
40
-
41
- if (resultCache) {
42
- return resultCache
43
- }
44
-
45
- const result = (await mongooseExec.call(this)) as Record<string, unknown>[] | Record<string, unknown>
46
- await cache.set(key, result, ttl).catch((err: unknown) => {
47
- console.error(err)
48
- })
49
-
50
- return result
51
- }
52
- }
@@ -1,82 +0,0 @@
1
- import { getKey } from '../key'
2
-
3
- import type { Mongoose } from 'mongoose'
4
- import type { Cache } from '../cache/Cache'
5
- import type { CacheTTL } 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.getCacheTTL = function (): CacheTTL | null {
33
- return this._ttl
34
- }
35
-
36
- mongoose.Query.prototype.cache = function (ttl?: CacheTTL, 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
- // biome-ignore lint/suspicious/noPrototypeBuiltins: to support node 16
44
- if (!Object.prototype.hasOwnProperty.call(this, '_ttl')) {
45
- return mongooseExec.apply(this, args)
46
- }
47
-
48
- const key = this.getCacheKey()
49
- const ttl = this.getCacheTTL()
50
- const mongooseOptions = this.mongooseOptions()
51
-
52
- const isCount = this.op?.includes('count') ?? false
53
- const isDistinct = this.op === 'distinct'
54
- const model = this.model.modelName
55
-
56
- const resultCache = await cache.get(key).catch((err: unknown) => {
57
- console.error(err)
58
- })
59
-
60
- if (resultCache) {
61
- if (isCount || isDistinct || mongooseOptions.lean) {
62
- return resultCache
63
- }
64
-
65
- const modelConstructor = mongoose.model<unknown>(model)
66
-
67
- if (Array.isArray(resultCache)) {
68
- return resultCache.map((item) => {
69
- return modelConstructor.hydrate(item)
70
- })
71
- }
72
- return modelConstructor.hydrate(resultCache)
73
- }
74
-
75
- const result = (await mongooseExec.call(this)) as Record<string, unknown>[] | Record<string, unknown>
76
- await cache.set(key, result, ttl).catch((err: unknown) => {
77
- console.error(err)
78
- })
79
-
80
- return result
81
- }
82
- }
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, CacheTTL } 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?: CacheTTL, customKey?: string) => this
13
- _key: string | null
14
- getCacheKey: (this: Query<ResultType, DocType, THelpers, RawDocType>) => string
15
- _ttl: CacheTTL | null
16
- getCacheTTL: (this: Query<ResultType, DocType, THelpers, RawDocType>) => CacheTTL | 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?: CacheTTL, customKey?: string) => this
26
- _key: string | null
27
- getCacheKey: (this: Aggregate<ResultType>) => string
28
- _ttl: CacheTTL | null
29
- getCacheTTL: (this: Aggregate<ResultType>) => CacheTTL | 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,55 +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
-
8
- // NOSONAR — regex from ms package, intentionally covers all time unit aliases
9
- const RE = /^(-?(?:\d+)?\.?\d+)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i
10
-
11
- const UNITS: Record<string, number> = {
12
- years: y,
13
- year: y,
14
- yrs: y,
15
- yr: y,
16
- y,
17
- weeks: w,
18
- week: w,
19
- w,
20
- days: d,
21
- day: d,
22
- d,
23
- hours: h,
24
- hour: h,
25
- hrs: h,
26
- hr: h,
27
- h,
28
- minutes: m,
29
- minute: m,
30
- mins: m,
31
- min: m,
32
- m,
33
- seconds: s,
34
- second: s,
35
- secs: s,
36
- sec: s,
37
- s,
38
- milliseconds: 1,
39
- millisecond: 1,
40
- msecs: 1,
41
- msec: 1,
42
- ms: 1,
43
- }
44
-
45
- export const ms = (val: string): number => {
46
- const str = String(val)
47
- if (str.length > 100) return 0
48
-
49
- const match = RE.exec(str)
50
- if (!match) return 0
51
-
52
- const n = Number.parseFloat(match[1] ?? '')
53
- const type = (match[2] ?? 'ms').toLowerCase()
54
- return n * (UNITS[type] ?? 0)
55
- }