ts-cache-mongoose 1.7.7 → 2.1.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.
package/src/ms.ts ADDED
@@ -0,0 +1,66 @@
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
+ }
@@ -0,0 +1,79 @@
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
+ }
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,4 @@
1
+ export { CacheModule } from './cache.module'
2
+ export { CACHE_OPTIONS, CacheService } from './cache.service'
3
+
4
+ export type { CacheModuleAsyncOptions, CacheModuleOptions, CacheOptionsFactory } from './interfaces'
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,38 @@
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 CHANGED
@@ -1,20 +1,20 @@
1
1
  import type { RedisOptions } from 'ioredis'
2
- import type { StringValue } from 'ms'
2
+ import type { Duration } from './ms'
3
3
 
4
- export type CacheTTL = number | StringValue
4
+ export type { Duration } from './ms'
5
5
 
6
6
  export type CacheData = Record<string, unknown> | Record<string, unknown>[] | unknown[] | number | undefined
7
7
 
8
8
  export type CacheOptions = {
9
9
  engine: 'memory' | 'redis'
10
10
  engineOptions?: RedisOptions
11
- defaultTTL?: CacheTTL
11
+ defaultTTL?: Duration
12
12
  debug?: boolean
13
13
  }
14
14
 
15
15
  export interface CacheEngine {
16
16
  get: (key: string) => Promise<CacheData> | CacheData
17
- set: (key: string, value: CacheData, ttl?: CacheTTL) => Promise<void> | void
17
+ set: (key: string, value: CacheData, ttl?: Duration) => Promise<void> | void
18
18
  del: (key: string) => Promise<void> | void
19
19
  clear: () => Promise<void> | void
20
20
  close: () => Promise<void> | void
package/src/version.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import mongoose from 'mongoose'
2
- import { satisfies } from 'semver'
3
2
 
4
3
  import type { CacheData } from './types'
5
4
 
6
- export const isMongooseLessThan7 = satisfies(mongoose.version, '<7')
5
+ export const isMongooseLessThan7 = Number.parseInt(mongoose.version, 10) < 7
7
6
 
8
7
  export const convertToObject = <T>(value: (T & { toObject?: () => CacheData }) | undefined): CacheData => {
9
8
  if (isMongooseLessThan7) {
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { ms, UNITS } from '../src/ms'
4
+
5
+ const { s, m, h, d, w, mo, y } = UNITS
6
+
7
+ describe('ms', () => {
8
+ it('should parse milliseconds', () => {
9
+ expect(ms('100ms')).toBe(100)
10
+ expect(ms('500 milliseconds')).toBe(500)
11
+ expect(ms('1 millisecond')).toBe(1)
12
+ expect(ms('200 msec')).toBe(200)
13
+ expect(ms('300 msecs')).toBe(300)
14
+ })
15
+
16
+ it('should parse seconds', () => {
17
+ expect(ms('1s')).toBe(s)
18
+ expect(ms('5 seconds')).toBe(5 * s)
19
+ expect(ms('30 sec')).toBe(30 * s)
20
+ expect(ms('1 second')).toBe(s)
21
+ expect(ms('2 secs')).toBe(2 * s)
22
+ })
23
+
24
+ it('should parse minutes', () => {
25
+ expect(ms('1m')).toBe(m)
26
+ expect(ms('5 minutes')).toBe(5 * m)
27
+ expect(ms('1 minute')).toBe(m)
28
+ expect(ms('2 min')).toBe(2 * m)
29
+ expect(ms('3 mins')).toBe(3 * m)
30
+ })
31
+
32
+ it('should parse hours', () => {
33
+ expect(ms('1h')).toBe(h)
34
+ expect(ms('2 hours')).toBe(2 * h)
35
+ expect(ms('1 hour')).toBe(h)
36
+ expect(ms('3 hr')).toBe(3 * h)
37
+ expect(ms('4 hrs')).toBe(4 * h)
38
+ })
39
+
40
+ it('should parse days', () => {
41
+ expect(ms('1d')).toBe(d)
42
+ expect(ms('2 days')).toBe(2 * d)
43
+ expect(ms('1 day')).toBe(d)
44
+ })
45
+
46
+ it('should parse weeks', () => {
47
+ expect(ms('1w')).toBe(w)
48
+ expect(ms('2 weeks')).toBe(2 * w)
49
+ expect(ms('1 week')).toBe(w)
50
+ })
51
+
52
+ it('should parse months', () => {
53
+ expect(ms('1mo')).toBe(mo)
54
+ expect(ms('1 month')).toBe(mo)
55
+ expect(ms('2 months')).toBe(2 * mo)
56
+ expect(ms('6mo')).toBe(6 * mo)
57
+ expect(ms('0.5mo')).toBe(0.5 * mo)
58
+ })
59
+
60
+ it('should parse years', () => {
61
+ expect(ms('1y')).toBe(y)
62
+ expect(ms('1 year')).toBe(y)
63
+ expect(ms('2 yrs')).toBe(2 * y)
64
+ expect(ms('1 yr')).toBe(y)
65
+ })
66
+
67
+ it('should parse decimal values', () => {
68
+ expect(ms('1.5h')).toBe(1.5 * h)
69
+ expect(ms('0.5d')).toBe(0.5 * d)
70
+ expect(ms('.5s')).toBe(0.5 * s)
71
+ })
72
+
73
+ it('should parse negative values', () => {
74
+ expect(ms('-1s')).toBe(-s)
75
+ expect(ms('-3m')).toBe(-3 * m)
76
+ })
77
+
78
+ it('should default to milliseconds without unit', () => {
79
+ expect(ms('100')).toBe(100)
80
+ expect(ms('0')).toBe(0)
81
+ })
82
+
83
+ it('should be case insensitive', () => {
84
+ // @ts-expect-error runtime check
85
+ expect(ms('1S')).toBe(s)
86
+ // @ts-expect-error runtime check
87
+ expect(ms('1M')).toBe(m)
88
+ // @ts-expect-error runtime check
89
+ expect(ms('1H')).toBe(h)
90
+ })
91
+
92
+ it('should return NaN for invalid strings', () => {
93
+ // @ts-expect-error testing invalid input
94
+ expect(ms('invalid')).toBeNaN()
95
+ // @ts-expect-error testing invalid input
96
+ expect(ms('')).toBeNaN()
97
+ // @ts-expect-error testing invalid input
98
+ expect(ms('abc123')).toBeNaN()
99
+ })
100
+
101
+ it('should return NaN for strings longer than 100 characters', () => {
102
+ // @ts-expect-error testing invalid input
103
+ expect(ms('a'.repeat(101))).toBeNaN()
104
+ })
105
+
106
+ it('should handle whitespace between number and unit', () => {
107
+ expect(ms('1 s')).toBe(s)
108
+ expect(ms('5 minutes')).toBe(5 * m)
109
+ expect(ms('1 mo')).toBe(mo)
110
+ expect(ms('1 week')).toBe(w)
111
+ expect(ms('1 year')).toBe(y)
112
+ })
113
+ })
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { CacheModule } from '../src/nest/cache.module'
4
+ import { CACHE_OPTIONS, CacheService } from '../src/nest/cache.service'
5
+
6
+ vi.mock('@nestjs/common', () => {
7
+ const LoggerMock = class {
8
+ log = vi.fn()
9
+ }
10
+ return {
11
+ Module: () => () => {},
12
+ Logger: LoggerMock,
13
+ }
14
+ })
15
+
16
+ vi.mock('../src/index', () => ({
17
+ default: {
18
+ init: vi.fn().mockReturnValue({
19
+ clear: vi.fn(),
20
+ close: vi.fn(),
21
+ }),
22
+ },
23
+ }))
24
+
25
+ const defaultOptions = { engine: 'memory' as const, defaultTTL: '60 seconds' }
26
+
27
+ describe('CacheModule', () => {
28
+ describe('forRoot', () => {
29
+ it('should return a dynamic module with providers', () => {
30
+ const result = CacheModule.forRoot(defaultOptions)
31
+ expect(result.module).toBe(CacheModule)
32
+ expect(result.providers).toBeDefined()
33
+ expect(result.exports).toContain(CacheService)
34
+ })
35
+
36
+ it('should set global to false by default', () => {
37
+ const result = CacheModule.forRoot(defaultOptions)
38
+ expect(result.global).toBe(false)
39
+ })
40
+
41
+ it('should set global when isGlobal is true', () => {
42
+ const result = CacheModule.forRoot({ ...defaultOptions, isGlobal: true })
43
+ expect(result.global).toBe(true)
44
+ })
45
+
46
+ it('should provide CACHE_OPTIONS with useValue', () => {
47
+ const result = CacheModule.forRoot(defaultOptions)
48
+ const optionsProvider = (result.providers as { provide: symbol; useValue: unknown }[]).find((p) => p.provide === CACHE_OPTIONS)
49
+ expect(optionsProvider).toBeDefined()
50
+ expect(optionsProvider?.useValue).toEqual(defaultOptions)
51
+ })
52
+ })
53
+
54
+ describe('forRootAsync', () => {
55
+ it('should return a dynamic module with useFactory', () => {
56
+ const result = CacheModule.forRootAsync({
57
+ useFactory: () => defaultOptions,
58
+ })
59
+ expect(result.module).toBe(CacheModule)
60
+ expect(result.providers).toBeDefined()
61
+ expect(result.exports).toContain(CacheService)
62
+ })
63
+
64
+ it('should support useClass', () => {
65
+ class TestFactory {
66
+ createCacheOptions() {
67
+ return defaultOptions
68
+ }
69
+ }
70
+ const result = CacheModule.forRootAsync({ useClass: TestFactory })
71
+ expect(result.providers).toBeDefined()
72
+ expect(result.providers?.length).toBeGreaterThan(1)
73
+ })
74
+
75
+ it('should support useExisting', () => {
76
+ class TestFactory {
77
+ createCacheOptions() {
78
+ return defaultOptions
79
+ }
80
+ }
81
+ const result = CacheModule.forRootAsync({ useExisting: TestFactory })
82
+ expect(result.providers).toBeDefined()
83
+ })
84
+
85
+ it('should return empty providers when no factory method', () => {
86
+ const result = CacheModule.forRootAsync({})
87
+ const providers = result.providers as unknown[]
88
+ const serviceProvider = providers.find((p) => typeof p === 'object' && p !== null && 'provide' in p && (p as { provide: unknown }).provide === CacheService)
89
+ expect(serviceProvider).toBeDefined()
90
+ })
91
+
92
+ it('should pass imports through', () => {
93
+ const result = CacheModule.forRootAsync({
94
+ imports: [],
95
+ useFactory: () => defaultOptions,
96
+ })
97
+ expect(result.imports).toEqual([])
98
+ })
99
+
100
+ it('should set global when isGlobal is true', () => {
101
+ const result = CacheModule.forRootAsync({
102
+ isGlobal: true,
103
+ useFactory: () => defaultOptions,
104
+ })
105
+ expect(result.global).toBe(true)
106
+ })
107
+ })
108
+ })
109
+
110
+ describe('CacheService', () => {
111
+ it('should store options', () => {
112
+ const service = new CacheService(defaultOptions)
113
+ expect(service).toBeDefined()
114
+ })
115
+
116
+ it('should initialize cache on bootstrap', async () => {
117
+ const service = new CacheService(defaultOptions)
118
+ await service.onApplicationBootstrap()
119
+ expect(service.instance).toBeDefined()
120
+ })
121
+
122
+ it('should close cache on shutdown', async () => {
123
+ const service = new CacheService(defaultOptions)
124
+ await service.onApplicationBootstrap()
125
+ const closeSpy = vi.spyOn(service.instance, 'close')
126
+ await service.onApplicationShutdown()
127
+ expect(closeSpy).toHaveBeenCalled()
128
+ })
129
+
130
+ it('should not throw on shutdown when not initialized', async () => {
131
+ const service = new CacheService(defaultOptions)
132
+ await expect(service.onApplicationShutdown()).resolves.not.toThrow()
133
+ })
134
+
135
+ it('should clear cache with custom key', async () => {
136
+ const service = new CacheService(defaultOptions)
137
+ await service.onApplicationBootstrap()
138
+ const clearSpy = vi.spyOn(service.instance, 'clear')
139
+ await service.clear('test-key')
140
+ expect(clearSpy).toHaveBeenCalledWith('test-key')
141
+ })
142
+
143
+ it('should clear all cache without key', async () => {
144
+ const service = new CacheService(defaultOptions)
145
+ await service.onApplicationBootstrap()
146
+ const clearSpy = vi.spyOn(service.instance, 'clear')
147
+ await service.clear()
148
+ expect(clearSpy).toHaveBeenCalledWith(undefined)
149
+ })
150
+
151
+ it('should expose instance getter', async () => {
152
+ const service = new CacheService(defaultOptions)
153
+ await service.onApplicationBootstrap()
154
+ expect(service.instance).toBeDefined()
155
+ expect(service.instance.clear).toBeDefined()
156
+ expect(service.instance.close).toBeDefined()
157
+ })
158
+ })
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { sortKeys } from '../src/sort-keys'
4
+
5
+ describe('sortKeys', () => {
6
+ it('should sort object keys alphabetically', () => {
7
+ expect(sortKeys({ c: 1, a: 2, b: 3 })).toEqual({ a: 2, b: 3, c: 1 })
8
+ })
9
+
10
+ it('should sort nested objects deeply', () => {
11
+ expect(sortKeys({ b: { d: 1, c: 2 }, a: 1 })).toEqual({ a: 1, b: { c: 2, d: 1 } })
12
+ })
13
+
14
+ it('should sort arrays of objects', () => {
15
+ const input = [
16
+ { b: 1, a: 2 },
17
+ { d: 3, c: 4 },
18
+ ]
19
+ const expected = [
20
+ { a: 2, b: 1 },
21
+ { c: 4, d: 3 },
22
+ ]
23
+ expect(sortKeys(input)).toEqual(expected)
24
+ })
25
+
26
+ it('should handle nested arrays', () => {
27
+ const input = { items: [{ z: 1, a: 2 }] }
28
+ expect(sortKeys(input)).toEqual({ items: [{ a: 2, z: 1 }] })
29
+ })
30
+
31
+ it('should handle nested arrays of arrays', () => {
32
+ const input = { data: [[{ b: 1, a: 2 }]] }
33
+ expect(sortKeys(input)).toEqual({ data: [[{ a: 2, b: 1 }]] })
34
+ })
35
+
36
+ it('should preserve non-object values in arrays', () => {
37
+ const input = { items: [1, 'string', true, null] }
38
+ expect(sortKeys(input)).toEqual({ items: [1, 'string', true, null] })
39
+ })
40
+
41
+ it('should handle empty objects', () => {
42
+ expect(sortKeys({})).toEqual({})
43
+ })
44
+
45
+ it('should handle empty arrays', () => {
46
+ expect(sortKeys([])).toEqual([])
47
+ })
48
+
49
+ it('should handle deeply nested structures', () => {
50
+ const input = { c: { b: { a: { z: 1, y: 2 } } } }
51
+ expect(sortKeys(input)).toEqual({ c: { b: { a: { y: 2, z: 1 } } } })
52
+ })
53
+
54
+ it('should not modify non-plain objects', () => {
55
+ const date = new Date()
56
+ const input = { b: date, a: 1 }
57
+ const result = sortKeys(input) as Record<string, unknown>
58
+ expect(result.a).toBe(1)
59
+ expect(result.b).toBe(date)
60
+ })
61
+
62
+ it('should handle circular references without infinite loop', () => {
63
+ const obj: Record<string, unknown> = { a: 1 }
64
+ obj.self = obj
65
+ const result = sortKeys(obj)
66
+ expect(result).toBeDefined()
67
+ })
68
+
69
+ it('should produce deterministic output regardless of input order', () => {
70
+ const a = sortKeys({ z: 1, a: 2, m: 3 })
71
+ const b = sortKeys({ a: 2, m: 3, z: 1 })
72
+ expect(JSON.stringify(a)).toBe(JSON.stringify(b))
73
+ })
74
+
75
+ it('should handle top-level array input', () => {
76
+ const input = [{ b: 1, a: 2 }] as Record<string, unknown>[]
77
+ const result = sortKeys(input)
78
+ expect(result).toEqual([{ a: 2, b: 1 }])
79
+ })
80
+ })
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "include": ["src"],
3
3
  "compilerOptions": {
4
- "target": "ES2021",
5
- "lib": ["ES2021"],
4
+ "target": "ES2022",
5
+ "lib": ["ES2022"],
6
6
  "types": ["node"],
7
7
  "module": "Preserve",
8
8
  "moduleResolution": "bundler",
@@ -15,9 +15,9 @@
15
15
  "declaration": true,
16
16
  "declarationMap": true,
17
17
  "esModuleInterop": true,
18
+ "experimentalDecorators": true,
18
19
  "exactOptionalPropertyTypes": true,
19
20
  "forceConsistentCasingInFileNames": true,
20
- "importHelpers": true,
21
21
  "isolatedModules": true,
22
22
  "noEmitOnError": true,
23
23
  "noFallthroughCasesInSwitch": true,