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.
Files changed (35) hide show
  1. package/README.md +26 -14
  2. package/dist/index.cjs +728 -17
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +419 -7
  5. package/dist/index.d.ts +419 -7
  6. package/dist/index.js +720 -17
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -2
  9. package/scripts/generate-entities/cli.mjs +7 -0
  10. package/scripts/generate-entities/generate.mjs +10 -6
  11. package/scripts/inspect-schema.mjs +181 -0
  12. package/scripts/naming-strategy.mjs +17 -7
  13. package/src/cache/adapters/index.ts +2 -0
  14. package/src/cache/adapters/keyv-cache-adapter.ts +81 -0
  15. package/src/cache/adapters/memory-cache-adapter.ts +127 -0
  16. package/src/cache/cache-interfaces.ts +70 -0
  17. package/src/cache/duration-utils.ts +82 -0
  18. package/src/cache/index.ts +28 -0
  19. package/src/cache/query-cache-manager.ts +130 -0
  20. package/src/cache/strategies/cache-strategy.ts +29 -0
  21. package/src/cache/strategies/default-cache-strategy.ts +96 -0
  22. package/src/cache/strategies/index.ts +2 -0
  23. package/src/cache/tag-index.ts +128 -0
  24. package/src/core/dialect/abstract.ts +565 -565
  25. package/src/core/dialect/mssql/index.ts +68 -3
  26. package/src/core/dialect/postgres/index.ts +1 -1
  27. package/src/core/dialect/sqlite/index.ts +1 -1
  28. package/src/core/execution/db-executor.ts +107 -103
  29. package/src/core/execution/executors/mysql-executor.ts +9 -2
  30. package/src/index.ts +3 -0
  31. package/src/orm/orm-session.ts +616 -563
  32. package/src/orm/orm.ts +108 -71
  33. package/src/orm/unit-of-work.ts +22 -4
  34. package/src/query-builder/select/cache-facet.ts +67 -0
  35. 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,2 @@
1
+ export type { CacheStrategy } from './cache-strategy.js';
2
+ export { DefaultCacheStrategy } from './default-cache-strategy.js';
@@ -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
+ }