metal-orm 1.1.2 → 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/README.md CHANGED
@@ -131,7 +131,7 @@ On top of the query builder, MetalORM ships a focused runtime managed by `Orm` a
131
131
  - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
132
132
  - **Scoped transactions**: `session.transaction(async s => { ... })` wraps `begin/commit/rollback` on the existing executor; `Orm.transaction` remains available when you want a fresh transactional executor per call.
133
133
  - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
134
- - **Caching**: Flexible caching with `MemoryCacheAdapter` (dev) or `KeyvCacheAdapter` (Redis, etc.). Features human-readable TTL (`'30m'`, `'2h'`), tag-based invalidation, and multi-tenant cache isolation.
134
+ - **Caching**: Flexible caching with `MemoryCacheAdapter` (dev), `KeyvCacheAdapter` (simple production), or `RedisCacheAdapter` (full-featured with tag support). Features human-readable TTL (`'30m'`, `'2h'`), tag-based invalidation, and multi-tenant cache isolation.
135
135
  - **Tree Behavior (Nested Set/MPTT)**: hierarchical data with `TreeManager`, `treeQuery()`, and `@Tree` decorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation.
136
136
  - **DTO/OpenAPI helpers**: the `metal-orm/dto` module generates DTOs and OpenAPI schemas, including tree schemas (`TreeNode`, `TreeNodeResult`, threaded trees).
137
137
  - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/wiki/Unit_of_work).
@@ -195,15 +195,24 @@ Pick the matching dialect (`MySqlDialect`, `SQLiteDialect`, `PostgresDialect`, `
195
195
 
196
196
  > Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
197
197
 
198
- **Optional: Caching Backend**
198
+ **Optional: Caching Backends**
199
199
 
200
- For production caching with Redis or other stores:
200
+ For production caching, choose based on your needs:
201
+
202
+ | Adapter | Tags | Install | Use Case |
203
+ |---------|------|---------|----------|
204
+ | `RedisCacheAdapter` | ✅ Full support | `npm install ioredis` | Production with tag invalidation |
205
+ | `KeyvCacheAdapter` | ❌ Not supported | `npm install keyv @keyv/redis` | Simple production setups |
201
206
 
202
207
  ```bash
208
+ # For full-featured Redis (recommended)
209
+ npm install ioredis
210
+
211
+ # For simple Keyv-based caching
203
212
  npm install keyv @keyv/redis
204
213
  ```
205
214
 
206
- > The `keyv` package is optional. MetalORM includes `MemoryCacheAdapter` for development without external dependencies.
215
+ > Caching packages are optional peer dependencies. MetalORM includes `MemoryCacheAdapter` for development without external dependencies.
207
216
 
208
217
  ### Playground (optional) 🧪
209
218
 
package/dist/index.cjs CHANGED
@@ -89,6 +89,7 @@ __export(index_exports, {
89
89
  PrimaryKey: () => PrimaryKey,
90
90
  PrototypeMaterializationStrategy: () => PrototypeMaterializationStrategy,
91
91
  QueryCacheManager: () => QueryCacheManager,
92
+ RedisCacheAdapter: () => RedisCacheAdapter,
92
93
  RelationKinds: () => RelationKinds,
93
94
  STANDARD_COLUMN_TYPES: () => STANDARD_COLUMN_TYPES,
94
95
  SelectQueryBuilder: () => SelectQueryBuilder,
@@ -14009,6 +14010,11 @@ function isValidDuration(value) {
14009
14010
  // src/cache/adapters/memory-cache-adapter.ts
14010
14011
  var MemoryCacheAdapter = class {
14011
14012
  name = "memory";
14013
+ capabilities = {
14014
+ tags: true,
14015
+ prefix: true,
14016
+ ttl: true
14017
+ };
14012
14018
  storage = /* @__PURE__ */ new Map();
14013
14019
  tagIndex = /* @__PURE__ */ new Map();
14014
14020
  async get(key) {
@@ -18862,6 +18868,11 @@ var KeyvCacheAdapter = class {
18862
18868
  this.keyv = keyv;
18863
18869
  }
18864
18870
  name = "keyv";
18871
+ capabilities = {
18872
+ tags: false,
18873
+ prefix: true,
18874
+ ttl: true
18875
+ };
18865
18876
  async get(key) {
18866
18877
  return this.keyv.get(key);
18867
18878
  }
@@ -18905,6 +18916,152 @@ var KeyvCacheAdapter = class {
18905
18916
  }
18906
18917
  };
18907
18918
 
18919
+ // src/cache/adapters/redis-cache-adapter.ts
18920
+ var RedisCacheAdapter = class {
18921
+ name = "redis";
18922
+ capabilities = {
18923
+ tags: true,
18924
+ prefix: true,
18925
+ ttl: true
18926
+ };
18927
+ redis;
18928
+ ownsConnection;
18929
+ tagPrefix;
18930
+ /**
18931
+ * Cria um adapter Redis
18932
+ *
18933
+ * @param redis - Instância do ioredis OU opções de conexão
18934
+ * @param options - Opções adicionais
18935
+ * @param options.tagPrefix - Prefixo para chaves de tag (default: 'tag:')
18936
+ *
18937
+ * Exemplos:
18938
+ *
18939
+ * // Com instância existente (recomendado para connection pooling):
18940
+ * const redis = new Redis({ host: 'localhost', port: 6379 });
18941
+ * const adapter = new RedisCacheAdapter(redis);
18942
+ *
18943
+ * // Com opções (adapter gerencia conexão):
18944
+ * const adapter = new RedisCacheAdapter({ host: 'localhost', port: 6379 });
18945
+ *
18946
+ * // Para testes com ioredis-mock:
18947
+ * import Redis from 'ioredis-mock';
18948
+ * const adapter = new RedisCacheAdapter(new Redis());
18949
+ */
18950
+ constructor(redis, options) {
18951
+ this.tagPrefix = options?.tagPrefix ?? "tag:";
18952
+ if (this.isRedisInstance(redis)) {
18953
+ this.redis = redis;
18954
+ this.ownsConnection = false;
18955
+ } else {
18956
+ this.redis = this.createRedis(redis);
18957
+ this.ownsConnection = true;
18958
+ }
18959
+ }
18960
+ isRedisInstance(obj) {
18961
+ return typeof obj === "object" && obj !== null && "get" in obj && "set" in obj && "del" in obj && typeof obj.get === "function";
18962
+ }
18963
+ createRedis(options) {
18964
+ try {
18965
+ const Redis = require("ioredis");
18966
+ return new Redis(options);
18967
+ } catch {
18968
+ throw new Error(
18969
+ "ioredis is required for RedisCacheAdapter. Install it with: npm install ioredis"
18970
+ );
18971
+ }
18972
+ }
18973
+ async get(key) {
18974
+ const value = await this.redis.get(key);
18975
+ if (value === null) {
18976
+ return void 0;
18977
+ }
18978
+ try {
18979
+ return JSON.parse(value);
18980
+ } catch {
18981
+ return void 0;
18982
+ }
18983
+ }
18984
+ async has(key) {
18985
+ const value = await this.redis.get(key);
18986
+ return value !== null;
18987
+ }
18988
+ async set(key, value, ttlMs, tags) {
18989
+ const serialized = JSON.stringify(value);
18990
+ if (ttlMs) {
18991
+ await this.redis.set(key, serialized, "PX", ttlMs);
18992
+ } else {
18993
+ await this.redis.set(key, serialized);
18994
+ }
18995
+ if (tags && tags.length > 0) {
18996
+ await this.registerTags(key, tags);
18997
+ }
18998
+ }
18999
+ async delete(key) {
19000
+ await this.redis.del(key);
19001
+ }
19002
+ async invalidate(key) {
19003
+ await this.delete(key);
19004
+ }
19005
+ async invalidateTags(tags) {
19006
+ const keysToDelete = /* @__PURE__ */ new Set();
19007
+ for (const tag of tags) {
19008
+ const tagKey = `${this.tagPrefix}${tag}`;
19009
+ const keys = await this.redis.smembers(tagKey);
19010
+ for (const key of keys) {
19011
+ keysToDelete.add(key);
19012
+ }
19013
+ await this.redis.del(tagKey);
19014
+ }
19015
+ if (keysToDelete.size > 0) {
19016
+ await this.redis.del(...Array.from(keysToDelete));
19017
+ }
19018
+ }
19019
+ async invalidatePrefix(prefix) {
19020
+ const keysToDelete = [];
19021
+ let cursor = "0";
19022
+ do {
19023
+ const [nextCursor, keys] = await this.redis.scan(
19024
+ cursor,
19025
+ "MATCH",
19026
+ `${prefix}*`,
19027
+ "COUNT",
19028
+ 100
19029
+ );
19030
+ cursor = nextCursor;
19031
+ keysToDelete.push(...keys);
19032
+ } while (cursor !== "0");
19033
+ if (keysToDelete.length > 0) {
19034
+ const batchSize = 1e3;
19035
+ for (let i = 0; i < keysToDelete.length; i += batchSize) {
19036
+ const batch = keysToDelete.slice(i, i + batchSize);
19037
+ await this.redis.del(...batch);
19038
+ }
19039
+ }
19040
+ }
19041
+ async registerTags(key, tags) {
19042
+ for (const tag of tags) {
19043
+ const tagKey = `${this.tagPrefix}${tag}`;
19044
+ await this.redis.sadd(tagKey, key);
19045
+ }
19046
+ }
19047
+ async dispose() {
19048
+ if (this.ownsConnection) {
19049
+ try {
19050
+ await this.redis.quit();
19051
+ } catch {
19052
+ this.redis.disconnect?.();
19053
+ }
19054
+ }
19055
+ }
19056
+ /**
19057
+ * Retorna a instância Redis subjacente
19058
+ * Útil para operações avançadas ou health checks
19059
+ */
19060
+ getRedis() {
19061
+ return this.redis;
19062
+ }
19063
+ };
19064
+
18908
19065
  // src/cache/tag-index.ts
18909
19066
  var TagIndex = class {
18910
19067
  tagToKeys = /* @__PURE__ */ new Map();
@@ -19059,6 +19216,7 @@ var TagIndex = class {
19059
19216
  PrimaryKey,
19060
19217
  PrototypeMaterializationStrategy,
19061
19218
  QueryCacheManager,
19219
+ RedisCacheAdapter,
19062
19220
  RelationKinds,
19063
19221
  STANDARD_COLUMN_TYPES,
19064
19222
  SelectQueryBuilder,