metal-orm 1.0.118 → 1.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/README.md +26 -14
- package/dist/index.cjs +728 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +419 -7
- package/dist/index.d.ts +419 -7
- package/dist/index.js +720 -17
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cache/adapters/index.ts +2 -0
- package/src/cache/adapters/keyv-cache-adapter.ts +81 -0
- package/src/cache/adapters/memory-cache-adapter.ts +127 -0
- package/src/cache/cache-interfaces.ts +70 -0
- package/src/cache/duration-utils.ts +82 -0
- package/src/cache/index.ts +28 -0
- package/src/cache/query-cache-manager.ts +130 -0
- package/src/cache/strategies/cache-strategy.ts +29 -0
- package/src/cache/strategies/default-cache-strategy.ts +96 -0
- package/src/cache/strategies/index.ts +2 -0
- package/src/cache/tag-index.ts +128 -0
- package/src/core/dialect/abstract.ts +565 -565
- package/src/core/dialect/mssql/index.ts +68 -3
- package/src/core/dialect/postgres/index.ts +1 -1
- package/src/core/dialect/sqlite/index.ts +1 -1
- package/src/core/execution/db-executor.ts +107 -103
- package/src/core/execution/executors/mysql-executor.ts +9 -2
- package/src/index.ts +3 -0
- package/src/orm/orm-session.ts +616 -563
- package/src/orm/orm.ts +108 -71
- package/src/orm/unit-of-work.ts +22 -4
- package/src/query-builder/select/cache-facet.ts +67 -0
- package/src/query-builder/select.ts +125 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"mysql2": "^3.9.0",
|
|
44
44
|
"pg": "^8.0.0",
|
|
45
45
|
"sqlite3": "^5.1.6",
|
|
46
|
-
"tedious": "^19.0.0"
|
|
46
|
+
"tedious": "^19.0.0",
|
|
47
|
+
"keyv": "^5.6.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"mysql2": {
|
|
@@ -57,6 +58,9 @@
|
|
|
57
58
|
},
|
|
58
59
|
"tedious": {
|
|
59
60
|
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"keyv": {
|
|
63
|
+
"optional": true
|
|
60
64
|
}
|
|
61
65
|
},
|
|
62
66
|
"devDependencies": {
|
|
@@ -66,6 +70,7 @@
|
|
|
66
70
|
"@vitest/ui": "^4.0.18",
|
|
67
71
|
"eslint": "^9.39.2",
|
|
68
72
|
"express": "^5.2.1",
|
|
73
|
+
"keyv": "^5.6.0",
|
|
69
74
|
"mysql-memory-server": "^1.14.0",
|
|
70
75
|
"mysql2": "^3.16.2",
|
|
71
76
|
"pg": "^8.17.2",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CacheProvider } from '../cache-interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface mínima para Keyv
|
|
5
|
+
* Usa unknown para permitir diferentes versões do Keyv
|
|
6
|
+
*/
|
|
7
|
+
interface KeyvInstance {
|
|
8
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
9
|
+
set<T>(key: string, value: T, ttl?: number): Promise<boolean | void>;
|
|
10
|
+
delete(key: string): Promise<boolean>;
|
|
11
|
+
iterator?: unknown;
|
|
12
|
+
disconnect?(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adapter para Keyv (Redis, SQLite, etc.)
|
|
17
|
+
* Keyv deve ser instalado separadamente:
|
|
18
|
+
* npm install keyv @keyv/redis
|
|
19
|
+
*/
|
|
20
|
+
export class KeyvCacheAdapter implements CacheProvider {
|
|
21
|
+
readonly name = 'keyv';
|
|
22
|
+
|
|
23
|
+
constructor(private keyv: KeyvInstance) {}
|
|
24
|
+
|
|
25
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
26
|
+
return this.keyv.get(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async has(key: string): Promise<boolean> {
|
|
30
|
+
const value = await this.keyv.get(key);
|
|
31
|
+
return value !== undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
35
|
+
await this.keyv.set(key, value, ttlMs);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async delete(key: string): Promise<void> {
|
|
39
|
+
await this.keyv.delete(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async invalidate(key: string): Promise<void> {
|
|
43
|
+
await this.delete(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async invalidateTags(_tags: string[]): Promise<void> {
|
|
47
|
+
// Keyv não suporta invalidação por tags nativamente
|
|
48
|
+
// Para suporte completo, usar RedisTagProvider
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Keyv adapter does not support tag invalidation. ' +
|
|
51
|
+
'Use MemoryCacheAdapter for testing or implement a custom Redis provider.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async invalidatePrefix(prefix: string): Promise<void> {
|
|
56
|
+
// Tenta usar iterador se disponível (Redis)
|
|
57
|
+
if (typeof this.keyv.iterator === 'function') {
|
|
58
|
+
const keys: string[] = [];
|
|
59
|
+
for await (const [key] of this.keyv.iterator()) {
|
|
60
|
+
if (key.startsWith(prefix)) {
|
|
61
|
+
keys.push(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (keys.length > 0) {
|
|
66
|
+
await Promise.all(keys.map(k => this.keyv.delete(k)));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fallback: não suportado
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Keyv adapter does not support prefix invalidation in this store. ' +
|
|
74
|
+
'Consider using a store with iterator support.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async dispose(): Promise<void> {
|
|
79
|
+
await this.keyv.disconnect?.();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { CacheProvider } from '../cache-interfaces.js';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
expiresAt?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Implementação em memória do cache provider
|
|
10
|
+
* Útil para testes e ambientes de desenvolvimento
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryCacheAdapter implements CacheProvider {
|
|
13
|
+
readonly name = 'memory';
|
|
14
|
+
private storage: Map<string, CacheEntry<unknown>> = new Map();
|
|
15
|
+
private tagIndex: Map<string, Set<string>> = new Map();
|
|
16
|
+
|
|
17
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
18
|
+
const entry = this.storage.get(key);
|
|
19
|
+
|
|
20
|
+
if (!entry) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verifica expiração
|
|
25
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
26
|
+
await this.delete(key);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return entry.value as T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async has(key: string): Promise<boolean> {
|
|
34
|
+
const value = await this.get(key);
|
|
35
|
+
return value !== undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
39
|
+
const entry: CacheEntry<T> = {
|
|
40
|
+
value,
|
|
41
|
+
expiresAt: ttlMs ? Date.now() + ttlMs : undefined,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.storage.set(key, entry);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async delete(key: string): Promise<void> {
|
|
48
|
+
this.storage.delete(key);
|
|
49
|
+
// Remove do índice de tags
|
|
50
|
+
for (const [tag, keys] of this.tagIndex) {
|
|
51
|
+
keys.delete(key);
|
|
52
|
+
if (keys.size === 0) {
|
|
53
|
+
this.tagIndex.delete(tag);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async invalidate(key: string): Promise<void> {
|
|
59
|
+
await this.delete(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async invalidateTags(tags: string[]): Promise<void> {
|
|
63
|
+
const keysToDelete = new Set<string>();
|
|
64
|
+
|
|
65
|
+
for (const tag of tags) {
|
|
66
|
+
const keys = this.tagIndex.get(tag);
|
|
67
|
+
if (keys) {
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
keysToDelete.add(key);
|
|
70
|
+
}
|
|
71
|
+
this.tagIndex.delete(tag);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const key of keysToDelete) {
|
|
76
|
+
this.storage.delete(key);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async invalidatePrefix(prefix: string): Promise<void> {
|
|
81
|
+
const keysToDelete: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const key of this.storage.keys()) {
|
|
84
|
+
if (key.startsWith(prefix)) {
|
|
85
|
+
keysToDelete.push(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const key of keysToDelete) {
|
|
90
|
+
await this.delete(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Registra uma chave com tags (para invalidação)
|
|
96
|
+
*/
|
|
97
|
+
registerTags(key: string, tags: string[]): void {
|
|
98
|
+
for (const tag of tags) {
|
|
99
|
+
if (!this.tagIndex.has(tag)) {
|
|
100
|
+
this.tagIndex.set(tag, new Set());
|
|
101
|
+
}
|
|
102
|
+
this.tagIndex.get(tag)!.add(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Limpa todo o cache
|
|
108
|
+
*/
|
|
109
|
+
clear(): void {
|
|
110
|
+
this.storage.clear();
|
|
111
|
+
this.tagIndex.clear();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Retorna estatísticas do cache
|
|
116
|
+
*/
|
|
117
|
+
getStats(): { size: number; tags: number } {
|
|
118
|
+
return {
|
|
119
|
+
size: this.storage.size,
|
|
120
|
+
tags: this.tagIndex.size,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async dispose(): Promise<void> {
|
|
125
|
+
this.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interfaces segregadas para cache (ISP - Interface Segregation Principle)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Leitura de cache - pode ser implementada por read-replicas
|
|
7
|
+
*/
|
|
8
|
+
export interface CacheReader {
|
|
9
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
10
|
+
has(key: string): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Escrita em cache - pode ser implementada por write-nodes
|
|
15
|
+
*/
|
|
16
|
+
export interface CacheWriter {
|
|
17
|
+
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
18
|
+
delete(key: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Invalidação em massa - separada pois nem todo cache suporta tags
|
|
23
|
+
*/
|
|
24
|
+
export interface CacheInvalidator {
|
|
25
|
+
invalidate(key: string): Promise<void>;
|
|
26
|
+
invalidateTags(tags: string[]): Promise<void>;
|
|
27
|
+
invalidatePrefix(prefix: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interface completa para implementações full-featured
|
|
32
|
+
*/
|
|
33
|
+
export interface CacheProvider extends CacheReader, CacheWriter, CacheInvalidator {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
dispose?(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* TTL human-readable: '1d', '2h', '30m', '15s', '1w'
|
|
40
|
+
* Ou número em milissegundos
|
|
41
|
+
*/
|
|
42
|
+
export type Duration = number | `${number}${'s'|'m'|'h'|'d'|'w'}`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Opções de cache para uma query
|
|
46
|
+
*/
|
|
47
|
+
export interface CacheOptions {
|
|
48
|
+
key: string;
|
|
49
|
+
ttl: Duration;
|
|
50
|
+
tags?: string[];
|
|
51
|
+
autoInvalidate?: boolean;
|
|
52
|
+
condition?: (result: unknown) => boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Estratégias de invalidação disponíveis
|
|
57
|
+
*/
|
|
58
|
+
export type InvalidationStrategy =
|
|
59
|
+
| 'tags'
|
|
60
|
+
| 'entity'
|
|
61
|
+
| 'prefix'
|
|
62
|
+
| 'key'
|
|
63
|
+
| 'ttl';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Estado interno do cache no query builder
|
|
67
|
+
*/
|
|
68
|
+
export interface CacheState {
|
|
69
|
+
options?: CacheOptions;
|
|
70
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Duration } from './cache-interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiplicadores para converter unidades de tempo em milissegundos
|
|
5
|
+
*/
|
|
6
|
+
const DURATION_MULTIPLIERS = {
|
|
7
|
+
s: 1000, // segundos
|
|
8
|
+
m: 60000, // minutos
|
|
9
|
+
h: 3600000, // horas
|
|
10
|
+
d: 86400000, // dias
|
|
11
|
+
w: 604800000, // semanas
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse de duração human-readable para milissegundos
|
|
16
|
+
* @param duration - Número (ms) ou string no formato '30s', '10m', '2h', '1d', '1w'
|
|
17
|
+
* @returns Tempo em milissegundos
|
|
18
|
+
* @throws Error se o formato for inválido
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* parseDuration(60000) // 60000
|
|
22
|
+
* parseDuration('30s') // 30000
|
|
23
|
+
* parseDuration('10m') // 600000
|
|
24
|
+
* parseDuration('2h') // 7200000
|
|
25
|
+
* parseDuration('1d') // 86400000
|
|
26
|
+
* parseDuration('1w') // 604800000
|
|
27
|
+
*/
|
|
28
|
+
export function parseDuration(duration: Duration): number {
|
|
29
|
+
// Se já é número, retorna diretamente
|
|
30
|
+
if (typeof duration === 'number') {
|
|
31
|
+
return duration;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Regex para extrair número e unidade
|
|
35
|
+
const match = duration.match(/^(\d+)([smhdw])$/);
|
|
36
|
+
|
|
37
|
+
if (!match) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Invalid duration format: "${duration}". ` +
|
|
40
|
+
`Use formats like '30s', '10m', '2h', '1d', '1w' or a number in milliseconds.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const value = parseInt(match[1], 10);
|
|
45
|
+
const unit = match[2] as keyof typeof DURATION_MULTIPLIERS;
|
|
46
|
+
|
|
47
|
+
return value * DURATION_MULTIPLIERS[unit];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converte milissegundos para formato human-readable
|
|
52
|
+
* @param ms - Tempo em milissegundos
|
|
53
|
+
* @returns String no formato mais apropriado
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* formatDuration(30000) // '30s'
|
|
57
|
+
* formatDuration(600000) // '10m'
|
|
58
|
+
* formatDuration(7200000) // '2h'
|
|
59
|
+
*/
|
|
60
|
+
export function formatDuration(ms: number): string {
|
|
61
|
+
if (ms < 1000) return `${ms}ms`;
|
|
62
|
+
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
|
63
|
+
if (ms < 3600000) return `${Math.floor(ms / 60000)}m`;
|
|
64
|
+
if (ms < 86400000) return `${Math.floor(ms / 3600000)}h`;
|
|
65
|
+
if (ms < 604800000) return `${Math.floor(ms / 86400000)}d`;
|
|
66
|
+
return `${Math.floor(ms / 604800000)}w`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Valida se uma string é uma duração válida
|
|
71
|
+
* @param value - Valor a ser validado
|
|
72
|
+
* @returns true se for uma duração válida
|
|
73
|
+
*/
|
|
74
|
+
export function isValidDuration(value: unknown): value is Duration {
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
return value >= 0;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
return /^\d+[smhdw]$/.test(value);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Interfaces
|
|
2
|
+
export type {
|
|
3
|
+
CacheReader,
|
|
4
|
+
CacheWriter,
|
|
5
|
+
CacheInvalidator,
|
|
6
|
+
CacheProvider,
|
|
7
|
+
Duration,
|
|
8
|
+
CacheOptions,
|
|
9
|
+
InvalidationStrategy,
|
|
10
|
+
CacheState,
|
|
11
|
+
} from './cache-interfaces.js';
|
|
12
|
+
|
|
13
|
+
// Utils
|
|
14
|
+
export { parseDuration, formatDuration, isValidDuration } from './duration-utils.js';
|
|
15
|
+
|
|
16
|
+
// Strategies
|
|
17
|
+
export type { CacheStrategy } from './strategies/cache-strategy.js';
|
|
18
|
+
export { DefaultCacheStrategy } from './strategies/default-cache-strategy.js';
|
|
19
|
+
|
|
20
|
+
// Adapters
|
|
21
|
+
export { MemoryCacheAdapter } from './adapters/memory-cache-adapter.js';
|
|
22
|
+
export { KeyvCacheAdapter } from './adapters/keyv-cache-adapter.js';
|
|
23
|
+
|
|
24
|
+
// Manager
|
|
25
|
+
export { QueryCacheManager } from './query-cache-manager.js';
|
|
26
|
+
|
|
27
|
+
// Tag Index
|
|
28
|
+
export { TagIndex } from './tag-index.js';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CacheProvider,
|
|
3
|
+
CacheOptions,
|
|
4
|
+
Duration
|
|
5
|
+
} from './cache-interfaces.js';
|
|
6
|
+
import type { CacheStrategy } from './strategies/cache-strategy.js';
|
|
7
|
+
import { DefaultCacheStrategy } from './strategies/default-cache-strategy.js';
|
|
8
|
+
import { parseDuration } from './duration-utils.js';
|
|
9
|
+
import { MemoryCacheAdapter } from './adapters/memory-cache-adapter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gerenciador de cache para queries
|
|
13
|
+
* Responsabilidade única: orquestrar leitura/escrita no cache (SRP)
|
|
14
|
+
*/
|
|
15
|
+
export class QueryCacheManager {
|
|
16
|
+
constructor(
|
|
17
|
+
private provider: CacheProvider = new MemoryCacheAdapter(),
|
|
18
|
+
private strategy: CacheStrategy = new DefaultCacheStrategy(),
|
|
19
|
+
private defaultTtl: Duration = '1h'
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Executa com cache - padrão execute-around
|
|
24
|
+
* @returns Resultado da execução (do cache ou da função)
|
|
25
|
+
*/
|
|
26
|
+
async getOrExecute<T>(
|
|
27
|
+
options: CacheOptions,
|
|
28
|
+
executor: () => Promise<T>,
|
|
29
|
+
tenantId?: string | number
|
|
30
|
+
): Promise<T> {
|
|
31
|
+
const key = this.strategy.generateKey(options.key, tenantId);
|
|
32
|
+
const ttlMs = this.parseDuration(options.ttl ?? this.defaultTtl);
|
|
33
|
+
|
|
34
|
+
// Tenta obter do cache
|
|
35
|
+
const cached = await this.provider.get<T>(key);
|
|
36
|
+
if (cached !== undefined) {
|
|
37
|
+
return this.strategy.deserialize(cached);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Executa a query
|
|
41
|
+
const result = await executor();
|
|
42
|
+
|
|
43
|
+
// Verifica se deve cachear
|
|
44
|
+
if (!this.strategy.shouldCache(result, options)) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Serializa e salva no cache
|
|
49
|
+
const serialized = this.strategy.serialize(result);
|
|
50
|
+
await this.provider.set(key, serialized, ttlMs);
|
|
51
|
+
|
|
52
|
+
// Registra tags se disponível
|
|
53
|
+
if (options.tags) {
|
|
54
|
+
await this.registerTags(key, options.tags);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Invalida uma chave específica
|
|
62
|
+
*/
|
|
63
|
+
async invalidateKey(key: string, tenantId?: string | number): Promise<void> {
|
|
64
|
+
const fullKey = this.strategy.generateKey(key, tenantId);
|
|
65
|
+
await this.provider.invalidate(fullKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Invalida por tags
|
|
70
|
+
*/
|
|
71
|
+
async invalidateTags(tags: string[]): Promise<void> {
|
|
72
|
+
await this.provider.invalidateTags(tags);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Invalida por prefixo (útil para multi-tenancy)
|
|
77
|
+
*/
|
|
78
|
+
async invalidatePrefix(prefix: string): Promise<void> {
|
|
79
|
+
await this.provider.invalidatePrefix(prefix);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Limpa todo o cache (cuidado!)
|
|
84
|
+
*/
|
|
85
|
+
async clear(): Promise<void> {
|
|
86
|
+
const provider = this.provider as CacheProvider & { clear?: () => void };
|
|
87
|
+
if (typeof provider.clear === 'function') {
|
|
88
|
+
provider.clear();
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error('Cache provider does not support clear operation');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Retorna estatísticas do cache (se disponível)
|
|
96
|
+
*/
|
|
97
|
+
getStats(): { size: number; tags: number } | undefined {
|
|
98
|
+
const provider = this.provider as CacheProvider & { getStats?: () => { size: number; tags: number } };
|
|
99
|
+
if (typeof provider.getStats === 'function') {
|
|
100
|
+
return provider.getStats();
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Libera recursos do cache
|
|
107
|
+
*/
|
|
108
|
+
async dispose(): Promise<void> {
|
|
109
|
+
await this.provider.dispose?.();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Registra tags para uma chave
|
|
114
|
+
*/
|
|
115
|
+
private async registerTags(key: string, tags: string[]): Promise<void> {
|
|
116
|
+
// Se o provider tem suporte nativo a tags
|
|
117
|
+
const provider = this.provider as CacheProvider & { registerTags?: (key: string, tags: string[]) => void };
|
|
118
|
+
if (typeof provider.registerTags === 'function') {
|
|
119
|
+
provider.registerTags(key, tags);
|
|
120
|
+
}
|
|
121
|
+
// Caso contrário, as tags são usadas apenas na invalidação
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Converte duração para milissegundos
|
|
126
|
+
*/
|
|
127
|
+
private parseDuration(d: Duration): number {
|
|
128
|
+
return parseDuration(d);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CacheOptions } from '../cache-interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Estratégia de cache - define como chaves são geradas,
|
|
5
|
+
* dados são serializados/desserializados e se devem ser cacheados
|
|
6
|
+
*/
|
|
7
|
+
export interface CacheStrategy {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gera chave de cache considerando tenant
|
|
12
|
+
*/
|
|
13
|
+
generateKey(queryKey: string, tenantId?: string | number): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decide se o resultado deve ser cacheado
|
|
17
|
+
*/
|
|
18
|
+
shouldCache(result: unknown, options: CacheOptions): boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serializa dados para armazenamento
|
|
22
|
+
*/
|
|
23
|
+
serialize<T>(data: T): unknown;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Desserializa dados do cache
|
|
27
|
+
*/
|
|
28
|
+
deserialize<T>(data: unknown): T;
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CacheStrategy } from './cache-strategy.js';
|
|
2
|
+
import type { CacheOptions } from '../cache-interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implementação padrão da estratégia de cache
|
|
6
|
+
* Suporta serialização de Date, BigInt e multi-tenancy
|
|
7
|
+
*/
|
|
8
|
+
export class DefaultCacheStrategy implements CacheStrategy {
|
|
9
|
+
readonly name = 'default';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gera chave de cache com prefixo de tenant se houver
|
|
13
|
+
*/
|
|
14
|
+
generateKey(queryKey: string, tenantId?: string | number): string {
|
|
15
|
+
if (tenantId !== undefined) {
|
|
16
|
+
return `tenant:${tenantId}:${queryKey}`;
|
|
17
|
+
}
|
|
18
|
+
return queryKey;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Verifica se deve cachear baseado na condição configurada
|
|
23
|
+
*/
|
|
24
|
+
shouldCache(result: unknown, options: CacheOptions): boolean {
|
|
25
|
+
if (options.condition) {
|
|
26
|
+
return options.condition(result);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serializa com suporte a tipos especiais
|
|
33
|
+
*/
|
|
34
|
+
serialize<T>(data: T): unknown {
|
|
35
|
+
return JSON.stringify(data, (key, value) => {
|
|
36
|
+
// Serializa Date
|
|
37
|
+
if (value instanceof Date) {
|
|
38
|
+
return { __type: 'Date', value: value.toISOString() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Serializa BigInt
|
|
42
|
+
if (typeof value === 'bigint') {
|
|
43
|
+
return { __type: 'BigInt', value: value.toString() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Serializa Map
|
|
47
|
+
if (value instanceof Map) {
|
|
48
|
+
return { __type: 'Map', value: Array.from(value.entries()) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Serializa Set
|
|
52
|
+
if (value instanceof Set) {
|
|
53
|
+
return { __type: 'Set', value: Array.from(value) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return value;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Desserializa restaurando tipos especiais
|
|
62
|
+
*/
|
|
63
|
+
deserialize<T>(data: unknown): T {
|
|
64
|
+
if (typeof data !== 'string') {
|
|
65
|
+
return data as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return JSON.parse(data, (key, value) => {
|
|
69
|
+
if (!value || typeof value !== 'object') {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Restaura Date
|
|
74
|
+
if (value.__type === 'Date') {
|
|
75
|
+
return new Date(value.value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Restaura BigInt
|
|
79
|
+
if (value.__type === 'BigInt') {
|
|
80
|
+
return BigInt(value.value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Restaura Map
|
|
84
|
+
if (value.__type === 'Map') {
|
|
85
|
+
return new Map(value.value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Restaura Set
|
|
89
|
+
if (value.__type === 'Set') {
|
|
90
|
+
return new Set(value.value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
}) as T;
|
|
95
|
+
}
|
|
96
|
+
}
|