metal-orm 1.0.118 → 1.1.1
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/scripts/generate-entities/cli.mjs +7 -0
- package/scripts/generate-entities/generate.mjs +10 -6
- package/scripts/inspect-schema.mjs +181 -0
- package/scripts/naming-strategy.mjs +17 -7
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexa chaves por tags para invalidação em massa
|
|
3
|
+
* Implementação em memória (pode ser persistida no Redis)
|
|
4
|
+
*/
|
|
5
|
+
export class TagIndex {
|
|
6
|
+
private tagToKeys: Map<string, Set<string>> = new Map();
|
|
7
|
+
private keyToTags: Map<string, Set<string>> = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registra que uma chave pertence a determinadas tags
|
|
11
|
+
*/
|
|
12
|
+
register(key: string, tags: string[]): void {
|
|
13
|
+
// Mapeia tag -> chaves
|
|
14
|
+
for (const tag of tags) {
|
|
15
|
+
if (!this.tagToKeys.has(tag)) {
|
|
16
|
+
this.tagToKeys.set(tag, new Set());
|
|
17
|
+
}
|
|
18
|
+
this.tagToKeys.get(tag)!.add(key);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Mapeia chave -> tags (para limpeza futura)
|
|
22
|
+
const existingTags = this.keyToTags.get(key) ?? new Set();
|
|
23
|
+
tags.forEach(tag => existingTags.add(tag));
|
|
24
|
+
this.keyToTags.set(key, existingTags);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Remove uma chave do índice
|
|
29
|
+
*/
|
|
30
|
+
unregister(key: string): void {
|
|
31
|
+
const tags = this.keyToTags.get(key);
|
|
32
|
+
if (tags) {
|
|
33
|
+
for (const tag of tags) {
|
|
34
|
+
this.tagToKeys.get(tag)?.delete(key);
|
|
35
|
+
// Limpa tags vazias
|
|
36
|
+
if (this.tagToKeys.get(tag)?.size === 0) {
|
|
37
|
+
this.tagToKeys.delete(tag);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.keyToTags.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Obtém todas as chaves de uma tag
|
|
46
|
+
*/
|
|
47
|
+
getKeysByTag(tag: string): string[] {
|
|
48
|
+
return Array.from(this.tagToKeys.get(tag) ?? []);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Obtém todas as tags de uma chave
|
|
53
|
+
*/
|
|
54
|
+
getTagsByKey(key: string): string[] {
|
|
55
|
+
return Array.from(this.keyToTags.get(key) ?? []);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Invalida todas as chaves de um conjunto de tags
|
|
60
|
+
* Retorna as chaves afetadas
|
|
61
|
+
*/
|
|
62
|
+
invalidateTags(tags: string[]): string[] {
|
|
63
|
+
const keysToInvalidate = new Set<string>();
|
|
64
|
+
|
|
65
|
+
for (const tag of tags) {
|
|
66
|
+
const keys = this.tagToKeys.get(tag);
|
|
67
|
+
if (keys) {
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
keysToInvalidate.add(key);
|
|
70
|
+
this.unregister(key); // Remove do índice
|
|
71
|
+
}
|
|
72
|
+
// Remove a tag completamente
|
|
73
|
+
this.tagToKeys.delete(tag);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Array.from(keysToInvalidate);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Invalida por prefixo (útil para multi-tenancy)
|
|
82
|
+
* Retorna as chaves afetadas
|
|
83
|
+
*/
|
|
84
|
+
invalidatePrefix(prefix: string): string[] {
|
|
85
|
+
const keysToInvalidate: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (const key of this.keyToTags.keys()) {
|
|
88
|
+
if (key.startsWith(prefix)) {
|
|
89
|
+
keysToInvalidate.push(key);
|
|
90
|
+
this.unregister(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return keysToInvalidate;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Retorna todas as tags registradas
|
|
99
|
+
*/
|
|
100
|
+
getAllTags(): string[] {
|
|
101
|
+
return Array.from(this.tagToKeys.keys());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retorna todas as chaves registradas
|
|
106
|
+
*/
|
|
107
|
+
getAllKeys(): string[] {
|
|
108
|
+
return Array.from(this.keyToTags.keys());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Limpa todo o índice
|
|
113
|
+
*/
|
|
114
|
+
clear(): void {
|
|
115
|
+
this.tagToKeys.clear();
|
|
116
|
+
this.keyToTags.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retorna estatísticas do índice
|
|
121
|
+
*/
|
|
122
|
+
getStats(): { tags: number; keys: number } {
|
|
123
|
+
return {
|
|
124
|
+
tags: this.tagToKeys.size,
|
|
125
|
+
keys: this.keyToTags.size,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|