metal-orm 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -44,7 +44,8 @@
44
44
  "pg": "^8.0.0",
45
45
  "sqlite3": "^5.1.6",
46
46
  "tedious": "^19.0.0",
47
- "keyv": "^5.6.0"
47
+ "keyv": "^5.6.0",
48
+ "ioredis": "^5.0.0"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "mysql2": {
@@ -61,6 +62,9 @@
61
62
  },
62
63
  "keyv": {
63
64
  "optional": true
65
+ },
66
+ "ioredis": {
67
+ "optional": true
64
68
  }
65
69
  },
66
70
  "devDependencies": {
@@ -70,6 +74,8 @@
70
74
  "@vitest/ui": "^4.0.18",
71
75
  "eslint": "^9.39.2",
72
76
  "express": "^5.2.1",
77
+ "ioredis": "^5.6.1",
78
+ "ioredis-mock": "^8.9.0",
73
79
  "keyv": "^5.6.0",
74
80
  "mysql-memory-server": "^1.14.0",
75
81
  "mysql2": "^3.16.2",
@@ -19,6 +19,11 @@ interface KeyvInstance {
19
19
  */
20
20
  export class KeyvCacheAdapter implements CacheProvider {
21
21
  readonly name = 'keyv';
22
+ readonly capabilities = {
23
+ tags: false,
24
+ prefix: true,
25
+ ttl: true,
26
+ };
22
27
 
23
28
  constructor(private keyv: KeyvInstance) {}
24
29
 
@@ -11,6 +11,11 @@ interface CacheEntry<T> {
11
11
  */
12
12
  export class MemoryCacheAdapter implements CacheProvider {
13
13
  readonly name = 'memory';
14
+ readonly capabilities = {
15
+ tags: true,
16
+ prefix: true,
17
+ ttl: true,
18
+ };
14
19
  private storage: Map<string, CacheEntry<unknown>> = new Map();
15
20
  private tagIndex: Map<string, Set<string>> = new Map();
16
21
 
@@ -0,0 +1,233 @@
1
+ import type { CacheProvider } from '../cache-interfaces.js';
2
+
3
+ // Tipos mínimos para ioredis (para evitar dependência obrigatória)
4
+ interface RedisLike {
5
+ get(key: string): Promise<string | null>;
6
+ set(key: string, value: string, ...args: (string | number)[]): Promise<string | null>;
7
+ del(...keys: string[]): Promise<number>;
8
+ sadd(key: string, ...members: string[]): Promise<number>;
9
+ smembers(key: string): Promise<string[]>;
10
+ srem(key: string, ...members: string[]): Promise<number>;
11
+ scan(cursor: string | number, ...args: (string | number)[]): Promise<[string, string[]]>;
12
+ quit(): Promise<string>;
13
+ disconnect(): void;
14
+ isReady?: boolean;
15
+ status?: string;
16
+ }
17
+
18
+ interface RedisOptions {
19
+ host?: string;
20
+ port?: number;
21
+ password?: string;
22
+ db?: number;
23
+ keyPrefix?: string;
24
+ lazyConnect?: boolean;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ /**
29
+ * Adapter para Redis usando ioredis
30
+ *
31
+ * Suporta:
32
+ * - Tags via Redis Sets (SADD, SMEMBERS, SREM)
33
+ * - Prefix invalidation via SCAN
34
+ * - TTL nativo do Redis
35
+ *
36
+ * Instalação:
37
+ * npm install ioredis
38
+ *
39
+ * Para testes (dev):
40
+ * npm install --save-dev ioredis-mock
41
+ */
42
+ export class RedisCacheAdapter implements CacheProvider {
43
+ readonly name = 'redis';
44
+ readonly capabilities = {
45
+ tags: true,
46
+ prefix: true,
47
+ ttl: true,
48
+ };
49
+
50
+ private redis: RedisLike;
51
+ private ownsConnection: boolean;
52
+ private tagPrefix: string;
53
+
54
+ /**
55
+ * Cria um adapter Redis
56
+ *
57
+ * @param redis - Instância do ioredis OU opções de conexão
58
+ * @param options - Opções adicionais
59
+ * @param options.tagPrefix - Prefixo para chaves de tag (default: 'tag:')
60
+ *
61
+ * Exemplos:
62
+ *
63
+ * // Com instância existente (recomendado para connection pooling):
64
+ * const redis = new Redis({ host: 'localhost', port: 6379 });
65
+ * const adapter = new RedisCacheAdapter(redis);
66
+ *
67
+ * // Com opções (adapter gerencia conexão):
68
+ * const adapter = new RedisCacheAdapter({ host: 'localhost', port: 6379 });
69
+ *
70
+ * // Para testes com ioredis-mock:
71
+ * import Redis from 'ioredis-mock';
72
+ * const adapter = new RedisCacheAdapter(new Redis());
73
+ */
74
+ constructor(
75
+ redis: RedisLike | RedisOptions,
76
+ options?: { tagPrefix?: string }
77
+ ) {
78
+ this.tagPrefix = options?.tagPrefix ?? 'tag:';
79
+
80
+ if (this.isRedisInstance(redis)) {
81
+ // Recebeu uma instância existente
82
+ this.redis = redis;
83
+ this.ownsConnection = false;
84
+ } else {
85
+ // Recebeu opções, precisa criar a conexão
86
+ this.redis = this.createRedis(redis);
87
+ this.ownsConnection = true;
88
+ }
89
+ }
90
+
91
+ private isRedisInstance(obj: unknown): obj is RedisLike {
92
+ return (
93
+ typeof obj === 'object' &&
94
+ obj !== null &&
95
+ 'get' in obj &&
96
+ 'set' in obj &&
97
+ 'del' in obj &&
98
+ typeof (obj as RedisLike).get === 'function'
99
+ );
100
+ }
101
+
102
+ private createRedis(options: RedisOptions): RedisLike {
103
+ // Dynamic import para evitar dependência obrigatória
104
+ try {
105
+ const Redis = require('ioredis');
106
+ return new Redis(options) as RedisLike;
107
+ } catch {
108
+ throw new Error(
109
+ 'ioredis is required for RedisCacheAdapter. ' +
110
+ 'Install it with: npm install ioredis'
111
+ );
112
+ }
113
+ }
114
+
115
+ async get<T>(key: string): Promise<T | undefined> {
116
+ const value = await this.redis.get(key);
117
+ if (value === null) {
118
+ return undefined;
119
+ }
120
+ try {
121
+ return JSON.parse(value) as T;
122
+ } catch {
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ async has(key: string): Promise<boolean> {
128
+ const value = await this.redis.get(key);
129
+ return value !== null;
130
+ }
131
+
132
+ async set<T>(
133
+ key: string,
134
+ value: T,
135
+ ttlMs?: number,
136
+ tags?: string[]
137
+ ): Promise<void> {
138
+ const serialized = JSON.stringify(value);
139
+
140
+ if (ttlMs) {
141
+ // EX = seconds, PX = milliseconds
142
+ await this.redis.set(key, serialized, 'PX', ttlMs);
143
+ } else {
144
+ await this.redis.set(key, serialized);
145
+ }
146
+
147
+ // Registra tags se fornecidas
148
+ if (tags && tags.length > 0) {
149
+ await this.registerTags(key, tags);
150
+ }
151
+ }
152
+
153
+ async delete(key: string): Promise<void> {
154
+ await this.redis.del(key);
155
+ }
156
+
157
+ async invalidate(key: string): Promise<void> {
158
+ await this.delete(key);
159
+ }
160
+
161
+ async invalidateTags(tags: string[]): Promise<void> {
162
+ const keysToDelete = new Set<string>();
163
+
164
+ for (const tag of tags) {
165
+ const tagKey = `${this.tagPrefix}${tag}`;
166
+ const keys = await this.redis.smembers(tagKey);
167
+
168
+ for (const key of keys) {
169
+ keysToDelete.add(key);
170
+ }
171
+
172
+ // Deleta o set da tag
173
+ await this.redis.del(tagKey);
174
+ }
175
+
176
+ // Deleta todas as chaves associadas
177
+ if (keysToDelete.size > 0) {
178
+ await this.redis.del(...Array.from(keysToDelete));
179
+ }
180
+ }
181
+
182
+ async invalidatePrefix(prefix: string): Promise<void> {
183
+ const keysToDelete: string[] = [];
184
+ let cursor = '0';
185
+
186
+ do {
187
+ const [nextCursor, keys] = await this.redis.scan(
188
+ cursor,
189
+ 'MATCH',
190
+ `${prefix}*`,
191
+ 'COUNT',
192
+ 100
193
+ );
194
+ cursor = nextCursor;
195
+ keysToDelete.push(...keys);
196
+ } while (cursor !== '0');
197
+
198
+ if (keysToDelete.length > 0) {
199
+ // Deleta em batches de 1000 para evitar bloqueio
200
+ const batchSize = 1000;
201
+ for (let i = 0; i < keysToDelete.length; i += batchSize) {
202
+ const batch = keysToDelete.slice(i, i + batchSize);
203
+ await this.redis.del(...batch);
204
+ }
205
+ }
206
+ }
207
+
208
+ private async registerTags(key: string, tags: string[]): Promise<void> {
209
+ for (const tag of tags) {
210
+ const tagKey = `${this.tagPrefix}${tag}`;
211
+ await this.redis.sadd(tagKey, key);
212
+ }
213
+ }
214
+
215
+ async dispose(): Promise<void> {
216
+ if (this.ownsConnection) {
217
+ try {
218
+ await this.redis.quit();
219
+ } catch {
220
+ // Se quit falhar, tenta disconnect
221
+ this.redis.disconnect?.();
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Retorna a instância Redis subjacente
228
+ * Útil para operações avançadas ou health checks
229
+ */
230
+ getRedis(): RedisLike {
231
+ return this.redis;
232
+ }
233
+ }
@@ -27,11 +27,22 @@ export interface CacheInvalidator {
27
27
  invalidatePrefix(prefix: string): Promise<void>;
28
28
  }
29
29
 
30
+ /**
31
+ * Capabilities de um cache provider
32
+ * Permite detectar funcionalidades suportadas em runtime
33
+ */
34
+ export interface CacheCapabilities {
35
+ tags: boolean;
36
+ prefix: boolean;
37
+ ttl: boolean;
38
+ }
39
+
30
40
  /**
31
41
  * Interface completa para implementações full-featured
32
42
  */
33
43
  export interface CacheProvider extends CacheReader, CacheWriter, CacheInvalidator {
34
44
  readonly name: string;
45
+ readonly capabilities: CacheCapabilities;
35
46
  dispose?(): Promise<void>;
36
47
  }
37
48
 
@@ -4,6 +4,7 @@ export type {
4
4
  CacheWriter,
5
5
  CacheInvalidator,
6
6
  CacheProvider,
7
+ CacheCapabilities,
7
8
  Duration,
8
9
  CacheOptions,
9
10
  InvalidationStrategy,
@@ -20,6 +21,7 @@ export { DefaultCacheStrategy } from './strategies/default-cache-strategy.js';
20
21
  // Adapters
21
22
  export { MemoryCacheAdapter } from './adapters/memory-cache-adapter.js';
22
23
  export { KeyvCacheAdapter } from './adapters/keyv-cache-adapter.js';
24
+ export { RedisCacheAdapter } from './adapters/redis-cache-adapter.js';
23
25
 
24
26
  // Manager
25
27
  export { QueryCacheManager } from './query-cache-manager.js';
@@ -24,10 +24,13 @@ import {
24
24
  CollateExpressionNode
25
25
  } from './expression-nodes.js';
26
26
 
27
- export type LiteralValue = LiteralNode['value'];
28
- export type ValueOperandInput = OperandNode | LiteralValue;
29
-
30
- export type TypedLike<T> = { tsType?: T } | { __tsType: T };
27
+ export type LiteralValue = LiteralNode['value'];
28
+ export type ValueOperandInput = OperandNode | LiteralValue;
29
+ type ScalarComparable = string | number | boolean | Date | Buffer | null;
30
+ type OrderedComparable = string | number | Date | Buffer;
31
+ type NotArray<T> = T extends readonly unknown[] ? never : T;
32
+
33
+ export type TypedLike<T> = { tsType?: T } | { __tsType: T };
31
34
 
32
35
  /**
33
36
  * Type guard to check if a value is a literal value
@@ -68,20 +71,27 @@ const columnRefToNode = (col: ColumnRef): ColumnNode => {
68
71
  * @param value - Value to convert (OperandNode, ColumnRef, or literal value)
69
72
  * @returns OperandNode representing the value
70
73
  */
71
- const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
72
- // Already an operand node
73
- if (isOperandNode(value)) {
74
- return value;
75
- }
74
+ const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
75
+ // Already an operand node
76
+ if (isOperandNode(value)) {
77
+ return value;
78
+ }
76
79
 
77
80
  // Literal value
78
- if (isLiteralValue(value)) {
79
- return toLiteralNode(value);
80
- }
81
-
82
- // Must be ColumnRef
83
- return columnRefToNode(value as ColumnRef);
84
- };
81
+ if (isLiteralValue(value)) {
82
+ return toLiteralNode(value);
83
+ }
84
+
85
+ if (Array.isArray(value)) {
86
+ throw new Error(
87
+ 'Array operands are not supported in scalar comparisons. ' +
88
+ 'Use inList/notInList for array matching.'
89
+ );
90
+ }
91
+
92
+ // Must be ColumnRef
93
+ return columnRefToNode(value as ColumnRef);
94
+ };
85
95
 
86
96
  /**
87
97
  * Converts a primitive or existing operand into an operand node
@@ -151,12 +161,12 @@ export const aliasRef = (name: string): AliasRefNode => ({
151
161
  */
152
162
  export const correlateBy = (table: string, column: string): ColumnNode => outerRef({ name: column, table });
153
163
 
154
- const createBinaryExpression = (
155
- operator: SqlOperator,
156
- left: OperandNode | ColumnRef,
157
- right: OperandNode | ColumnRef | string | number | boolean | null,
158
- escape?: string
159
- ): BinaryExpressionNode => {
164
+ const createBinaryExpression = (
165
+ operator: SqlOperator,
166
+ left: OperandNode | ColumnRef,
167
+ right: OperandNode | ColumnRef | LiteralValue,
168
+ escape?: string
169
+ ): BinaryExpressionNode => {
160
170
  const node: BinaryExpressionNode = {
161
171
  type: 'BinaryExpression',
162
172
  left: toOperandNode(left),
@@ -187,11 +197,11 @@ const createBinaryExpression = (
187
197
  * // With strict typing (typescript will error if types mismatch)
188
198
  * eq(users.firstName, 'Ada');
189
199
  */
190
- export function eq<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
191
- export function eq(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number | boolean): BinaryExpressionNode;
192
- export function eq(left: OperandNode | ColumnRef | TypedLike<unknown>, right: OperandNode | ColumnRef | string | number | boolean | TypedLike<unknown>): BinaryExpressionNode {
193
- return createBinaryExpression('=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number | boolean);
194
- }
200
+ export function eq<T extends ScalarComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
201
+ export function eq<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
202
+ export function eq(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
203
+ return createBinaryExpression('=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
204
+ }
195
205
 
196
206
  /**
197
207
  * Creates a not equal expression (`left != right`).
@@ -203,14 +213,11 @@ export function eq(left: OperandNode | ColumnRef | TypedLike<unknown>, right: Op
203
213
  * @example
204
214
  * neq(users.status, 'inactive');
205
215
  */
206
- export function neq<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
207
- export function neq(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number | boolean): BinaryExpressionNode;
208
- export function neq(
209
- left: OperandNode | ColumnRef | TypedLike<unknown>,
210
- right: OperandNode | ColumnRef | string | number | boolean | TypedLike<unknown>
211
- ): BinaryExpressionNode {
212
- return createBinaryExpression('!=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number | boolean);
213
- }
216
+ export function neq<T extends ScalarComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
217
+ export function neq<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
218
+ export function neq(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
219
+ return createBinaryExpression('!=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
220
+ }
214
221
 
215
222
  /**
216
223
  * Creates a greater-than expression (`left > right`).
@@ -222,11 +229,11 @@ export function neq(
222
229
  * @example
223
230
  * gt(users.age, 18);
224
231
  */
225
- export function gt<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
226
- export function gt(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number): BinaryExpressionNode;
227
- export function gt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: OperandNode | ColumnRef | string | number | TypedLike<unknown>): BinaryExpressionNode {
228
- return createBinaryExpression('>', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number);
229
- }
232
+ export function gt<T extends OrderedComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
233
+ export function gt<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
234
+ export function gt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
235
+ return createBinaryExpression('>', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
236
+ }
230
237
 
231
238
  /**
232
239
  * Creates a greater-than-or-equal expression (`left >= right`).
@@ -238,11 +245,11 @@ export function gt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: Op
238
245
  * @example
239
246
  * gte(users.score, 100);
240
247
  */
241
- export function gte<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
242
- export function gte(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number): BinaryExpressionNode;
243
- export function gte(left: OperandNode | ColumnRef | TypedLike<unknown>, right: OperandNode | ColumnRef | string | number | TypedLike<unknown>): BinaryExpressionNode {
244
- return createBinaryExpression('>=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number);
245
- }
248
+ export function gte<T extends OrderedComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
249
+ export function gte<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
250
+ export function gte(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
251
+ return createBinaryExpression('>=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
252
+ }
246
253
 
247
254
  /**
248
255
  * Creates a less-than expression (`left < right`).
@@ -254,11 +261,11 @@ export function gte(left: OperandNode | ColumnRef | TypedLike<unknown>, right: O
254
261
  * @example
255
262
  * lt(inventory.stock, 5);
256
263
  */
257
- export function lt<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
258
- export function lt(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number): BinaryExpressionNode;
259
- export function lt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: OperandNode | ColumnRef | string | number | TypedLike<unknown>): BinaryExpressionNode {
260
- return createBinaryExpression('<', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number);
261
- }
264
+ export function lt<T extends OrderedComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
265
+ export function lt<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
266
+ export function lt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
267
+ return createBinaryExpression('<', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
268
+ }
262
269
 
263
270
  /**
264
271
  * Creates a less-than-or-equal expression (`left <= right`).
@@ -270,11 +277,11 @@ export function lt(left: OperandNode | ColumnRef | TypedLike<unknown>, right: Op
270
277
  * @example
271
278
  * lte(products.price, 50.00);
272
279
  */
273
- export function lte<T>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
274
- export function lte(left: OperandNode | ColumnRef, right: OperandNode | ColumnRef | string | number): BinaryExpressionNode;
275
- export function lte(left: OperandNode | ColumnRef | TypedLike<unknown>, right: OperandNode | ColumnRef | string | number | TypedLike<unknown>): BinaryExpressionNode {
276
- return createBinaryExpression('<=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | string | number);
277
- }
280
+ export function lte<T extends OrderedComparable>(left: TypedLike<T>, right: T | TypedLike<T>): BinaryExpressionNode;
281
+ export function lte<T>(left: OperandNode | ColumnRef, right: T & NotArray<T>): BinaryExpressionNode;
282
+ export function lte(left: OperandNode | ColumnRef | TypedLike<unknown>, right: unknown): BinaryExpressionNode {
283
+ return createBinaryExpression('<=', left as OperandNode | ColumnRef, right as OperandNode | ColumnRef | LiteralValue);
284
+ }
278
285
 
279
286
  /**
280
287
  * Creates a `LIKE` pattern matching expression.