@rx-ted/packages-honest-plugins 1.0.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 (266) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +45 -0
  3. package/api-doc/.turbo/turbo-build.log +1 -0
  4. package/api-doc/.turbo/turbo-test.log +11 -0
  5. package/api-doc/.turbo/turbo-typecheck.log +1 -0
  6. package/api-doc/CHANGELOG.md +22 -0
  7. package/api-doc/README.md +80 -0
  8. package/api-doc/dist/api-doc.plugin.d.ts +53 -0
  9. package/api-doc/dist/api-doc.plugin.js +452 -0
  10. package/api-doc/dist/index.d.ts +5 -0
  11. package/api-doc/dist/index.js +3 -0
  12. package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
  13. package/api-doc/dist/interfaces/renderer.interface.js +1 -0
  14. package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
  15. package/api-doc/dist/renderers/scalar.renderer.js +76 -0
  16. package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
  17. package/api-doc/dist/renderers/swagger.renderer.js +83 -0
  18. package/api-doc/node_modules/.bin/esbuild +22 -0
  19. package/api-doc/node_modules/.bin/jiti +22 -0
  20. package/api-doc/node_modules/.bin/sass +22 -0
  21. package/api-doc/node_modules/.bin/tsx +22 -0
  22. package/api-doc/node_modules/.bin/vite +22 -0
  23. package/api-doc/node_modules/.bin/vitest +22 -0
  24. package/api-doc/node_modules/.bin/yaml +22 -0
  25. package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  26. package/api-doc/package.json +48 -0
  27. package/api-doc/src/api-doc.plugin.ts +554 -0
  28. package/api-doc/src/index.ts +5 -0
  29. package/api-doc/src/interfaces/renderer.interface.ts +5 -0
  30. package/api-doc/src/plugin.test.ts +740 -0
  31. package/api-doc/src/renderers/scalar.renderer.ts +82 -0
  32. package/api-doc/src/renderers/swagger.renderer.ts +89 -0
  33. package/api-doc/tsconfig.json +21 -0
  34. package/api-doc/vitest.config.ts +13 -0
  35. package/cache/.turbo/turbo-build.log +1 -0
  36. package/cache/.turbo/turbo-test.log +16 -0
  37. package/cache/CHANGELOG.md +23 -0
  38. package/cache/README.md +79 -0
  39. package/cache/dist/cache-service.d.ts +8 -0
  40. package/cache/dist/cache-service.js +21 -0
  41. package/cache/dist/cacheable.d.ts +2 -0
  42. package/cache/dist/cacheable.js +10 -0
  43. package/cache/dist/cloudflare/driver.d.ts +23 -0
  44. package/cache/dist/cloudflare/driver.js +102 -0
  45. package/cache/dist/cloudflare/index.d.ts +3 -0
  46. package/cache/dist/cloudflare/index.js +2 -0
  47. package/cache/dist/cloudflare/plugin.d.ts +19 -0
  48. package/cache/dist/cloudflare/plugin.js +58 -0
  49. package/cache/dist/index.d.ts +11 -0
  50. package/cache/dist/index.js +14 -0
  51. package/cache/dist/local/driver.d.ts +2 -0
  52. package/cache/dist/local/driver.js +98 -0
  53. package/cache/dist/local/index.d.ts +2 -0
  54. package/cache/dist/local/index.js +2 -0
  55. package/cache/dist/local/plugin.d.ts +14 -0
  56. package/cache/dist/local/plugin.js +24 -0
  57. package/cache/dist/redis/driver.d.ts +10 -0
  58. package/cache/dist/redis/driver.js +102 -0
  59. package/cache/dist/redis/index.d.ts +4 -0
  60. package/cache/dist/redis/index.js +2 -0
  61. package/cache/dist/redis/plugin.d.ts +25 -0
  62. package/cache/dist/redis/plugin.js +61 -0
  63. package/cache/dist/resolve.d.ts +24 -0
  64. package/cache/dist/resolve.js +114 -0
  65. package/cache/dist/types.d.ts +18 -0
  66. package/cache/dist/types.js +1 -0
  67. package/cache/node_modules/.bin/esbuild +22 -0
  68. package/cache/node_modules/.bin/jiti +22 -0
  69. package/cache/node_modules/.bin/sass +22 -0
  70. package/cache/node_modules/.bin/tsx +22 -0
  71. package/cache/node_modules/.bin/vite +22 -0
  72. package/cache/node_modules/.bin/vitest +22 -0
  73. package/cache/node_modules/.bin/yaml +22 -0
  74. package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  75. package/cache/package.json +46 -0
  76. package/cache/src/cache-service.test.ts +16 -0
  77. package/cache/src/cache-service.ts +15 -0
  78. package/cache/src/cacheable.test.ts +78 -0
  79. package/cache/src/cacheable.ts +16 -0
  80. package/cache/src/cloudflare/driver.test.ts +188 -0
  81. package/cache/src/cloudflare/driver.ts +118 -0
  82. package/cache/src/cloudflare/index.ts +3 -0
  83. package/cache/src/cloudflare/plugin.ts +71 -0
  84. package/cache/src/index.test.ts +68 -0
  85. package/cache/src/index.ts +20 -0
  86. package/cache/src/local/driver.ts +117 -0
  87. package/cache/src/local/index.ts +2 -0
  88. package/cache/src/local/plugin.ts +34 -0
  89. package/cache/src/redis/driver.test.ts +71 -0
  90. package/cache/src/redis/driver.ts +124 -0
  91. package/cache/src/redis/index.ts +4 -0
  92. package/cache/src/redis/plugin.ts +84 -0
  93. package/cache/src/resolve.test.ts +257 -0
  94. package/cache/src/resolve.ts +140 -0
  95. package/cache/src/types.ts +14 -0
  96. package/cache/tsconfig.json +21 -0
  97. package/cache/vitest.config.ts +13 -0
  98. package/db/.turbo/turbo-build.log +1 -0
  99. package/db/.turbo/turbo-test.log +21 -0
  100. package/db/CHANGELOG.md +26 -0
  101. package/db/README.md +99 -0
  102. package/db/dist/constants.d.ts +1 -0
  103. package/db/dist/constants.js +1 -0
  104. package/db/dist/d1/index.d.ts +3 -0
  105. package/db/dist/d1/index.js +2 -0
  106. package/db/dist/d1/plugin.d.ts +18 -0
  107. package/db/dist/d1/plugin.js +104 -0
  108. package/db/dist/db-service.d.ts +7 -0
  109. package/db/dist/db-service.js +21 -0
  110. package/db/dist/index.d.ts +15 -0
  111. package/db/dist/index.js +20 -0
  112. package/db/dist/mysql/client.d.ts +36 -0
  113. package/db/dist/mysql/client.js +84 -0
  114. package/db/dist/mysql/driver.d.ts +18 -0
  115. package/db/dist/mysql/driver.js +107 -0
  116. package/db/dist/mysql/index.d.ts +11 -0
  117. package/db/dist/mysql/index.js +11 -0
  118. package/db/dist/mysql/plugin.d.ts +51 -0
  119. package/db/dist/mysql/plugin.js +94 -0
  120. package/db/dist/mysql/types.d.ts +38 -0
  121. package/db/dist/mysql/types.js +1 -0
  122. package/db/dist/postgres/index.d.ts +2 -0
  123. package/db/dist/postgres/index.js +3 -0
  124. package/db/dist/repository.d.ts +14 -0
  125. package/db/dist/repository.js +79 -0
  126. package/db/dist/resolve.d.ts +6 -0
  127. package/db/dist/resolve.js +81 -0
  128. package/db/dist/schema-builder/column-builder.d.ts +26 -0
  129. package/db/dist/schema-builder/column-builder.js +53 -0
  130. package/db/dist/schema-builder/compile-d1.d.ts +5 -0
  131. package/db/dist/schema-builder/compile-d1.js +67 -0
  132. package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
  133. package/db/dist/schema-builder/compile-mysql.js +78 -0
  134. package/db/dist/schema-builder/index.d.ts +8 -0
  135. package/db/dist/schema-builder/index.js +22 -0
  136. package/db/dist/schema-builder/table-builder.d.ts +16 -0
  137. package/db/dist/schema-builder/table-builder.js +38 -0
  138. package/db/dist/schema-builder/types.d.ts +26 -0
  139. package/db/dist/schema-builder/types.js +1 -0
  140. package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
  141. package/db/dist/schema-builder/zod-bridge.js +53 -0
  142. package/db/dist/sqlite/index.d.ts +4 -0
  143. package/db/dist/sqlite/index.js +2 -0
  144. package/db/dist/sqlite/plugin.d.ts +17 -0
  145. package/db/dist/sqlite/plugin.js +33 -0
  146. package/db/dist/types.d.ts +42 -0
  147. package/db/dist/types.js +1 -0
  148. package/db/node_modules/.bin/esbuild +22 -0
  149. package/db/node_modules/.bin/jiti +22 -0
  150. package/db/node_modules/.bin/sass +22 -0
  151. package/db/node_modules/.bin/tsx +22 -0
  152. package/db/node_modules/.bin/vite +22 -0
  153. package/db/node_modules/.bin/vitest +22 -0
  154. package/db/node_modules/.bin/yaml +22 -0
  155. package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  156. package/db/package.json +59 -0
  157. package/db/src/constants.test.ts +8 -0
  158. package/db/src/constants.ts +1 -0
  159. package/db/src/d1/index.ts +13 -0
  160. package/db/src/d1/plugin.ts +135 -0
  161. package/db/src/db-service.ts +14 -0
  162. package/db/src/index.test.ts +63 -0
  163. package/db/src/index.ts +57 -0
  164. package/db/src/mysql/client.ts +112 -0
  165. package/db/src/mysql/driver.test.ts +190 -0
  166. package/db/src/mysql/driver.ts +116 -0
  167. package/db/src/mysql/index.ts +51 -0
  168. package/db/src/mysql/plugin.test.ts +228 -0
  169. package/db/src/mysql/plugin.ts +128 -0
  170. package/db/src/mysql/types.ts +32 -0
  171. package/db/src/postgres/index.ts +15 -0
  172. package/db/src/repository.test.ts +195 -0
  173. package/db/src/repository.ts +95 -0
  174. package/db/src/resolve.ts +105 -0
  175. package/db/src/schema-builder/column-builder.test.ts +55 -0
  176. package/db/src/schema-builder/column-builder.ts +87 -0
  177. package/db/src/schema-builder/compile-d1.test.ts +60 -0
  178. package/db/src/schema-builder/compile-d1.ts +70 -0
  179. package/db/src/schema-builder/compile-mysql.test.ts +64 -0
  180. package/db/src/schema-builder/compile-mysql.ts +102 -0
  181. package/db/src/schema-builder/index.test.ts +36 -0
  182. package/db/src/schema-builder/index.ts +41 -0
  183. package/db/src/schema-builder/table-builder.test.ts +55 -0
  184. package/db/src/schema-builder/table-builder.ts +50 -0
  185. package/db/src/schema-builder/types.test.ts +68 -0
  186. package/db/src/schema-builder/types.ts +30 -0
  187. package/db/src/schema-builder/zod-bridge.ts +89 -0
  188. package/db/src/sqlite/index.ts +14 -0
  189. package/db/src/sqlite/plugin.ts +46 -0
  190. package/db/src/types.ts +45 -0
  191. package/db/tsconfig.json +21 -0
  192. package/db/vitest.config.ts +13 -0
  193. package/mail/.turbo/turbo-build.log +1 -0
  194. package/mail/.turbo/turbo-test.log +12 -0
  195. package/mail/.turbo/turbo-typecheck.log +1 -0
  196. package/mail/CHANGELOG.md +25 -0
  197. package/mail/README.md +214 -0
  198. package/mail/dist/index.d.ts +4 -0
  199. package/mail/dist/index.js +2 -0
  200. package/mail/dist/plugin.d.ts +64 -0
  201. package/mail/dist/plugin.js +232 -0
  202. package/mail/dist/providers/brevo.provider.d.ts +15 -0
  203. package/mail/dist/providers/brevo.provider.js +75 -0
  204. package/mail/dist/providers/custom.provider.d.ts +9 -0
  205. package/mail/dist/providers/custom.provider.js +22 -0
  206. package/mail/dist/providers/resend.provider.d.ts +16 -0
  207. package/mail/dist/providers/resend.provider.js +77 -0
  208. package/mail/dist/providers/smtp.provider.d.ts +21 -0
  209. package/mail/dist/providers/smtp.provider.js +86 -0
  210. package/mail/dist/rate-limiter.d.ts +16 -0
  211. package/mail/dist/rate-limiter.js +49 -0
  212. package/mail/dist/types.d.ts +68 -0
  213. package/mail/dist/types.js +6 -0
  214. package/mail/node_modules/.bin/esbuild +22 -0
  215. package/mail/node_modules/.bin/jiti +22 -0
  216. package/mail/node_modules/.bin/sass +22 -0
  217. package/mail/node_modules/.bin/tsx +22 -0
  218. package/mail/node_modules/.bin/vite +22 -0
  219. package/mail/node_modules/.bin/vitest +22 -0
  220. package/mail/node_modules/.bin/yaml +22 -0
  221. package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  222. package/mail/package.json +52 -0
  223. package/mail/src/index.ts +4 -0
  224. package/mail/src/plugin.test.ts +219 -0
  225. package/mail/src/plugin.ts +313 -0
  226. package/mail/src/providers/brevo.provider.ts +78 -0
  227. package/mail/src/providers/custom.provider.ts +29 -0
  228. package/mail/src/providers/resend.provider.ts +74 -0
  229. package/mail/src/providers/smtp.provider.ts +85 -0
  230. package/mail/src/rate-limiter.test.ts +81 -0
  231. package/mail/src/rate-limiter.ts +54 -0
  232. package/mail/src/types.ts +81 -0
  233. package/mail/tsconfig.json +21 -0
  234. package/mail/vitest.config.ts +13 -0
  235. package/package.json +36 -0
  236. package/s3/.turbo/turbo-build.log +1 -0
  237. package/s3/.turbo/turbo-test.log +11 -0
  238. package/s3/CHANGELOG.md +20 -0
  239. package/s3/README.md +90 -0
  240. package/s3/dist/index.d.ts +6 -0
  241. package/s3/dist/index.js +4 -0
  242. package/s3/dist/s3-service.d.ts +7 -0
  243. package/s3/dist/s3-service.js +21 -0
  244. package/s3/dist/s3.driver.d.ts +6 -0
  245. package/s3/dist/s3.driver.js +221 -0
  246. package/s3/dist/s3.plugin.d.ts +24 -0
  247. package/s3/dist/s3.plugin.js +58 -0
  248. package/s3/dist/types.d.ts +30 -0
  249. package/s3/dist/types.js +1 -0
  250. package/s3/node_modules/.bin/esbuild +22 -0
  251. package/s3/node_modules/.bin/jiti +22 -0
  252. package/s3/node_modules/.bin/sass +22 -0
  253. package/s3/node_modules/.bin/tsx +22 -0
  254. package/s3/node_modules/.bin/vite +22 -0
  255. package/s3/node_modules/.bin/vitest +22 -0
  256. package/s3/node_modules/.bin/yaml +22 -0
  257. package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  258. package/s3/package.json +47 -0
  259. package/s3/src/index.test.ts +20 -0
  260. package/s3/src/index.ts +13 -0
  261. package/s3/src/s3-service.ts +14 -0
  262. package/s3/src/s3.driver.ts +259 -0
  263. package/s3/src/s3.plugin.ts +80 -0
  264. package/s3/src/types.ts +37 -0
  265. package/s3/tsconfig.json +21 -0
  266. package/s3/vitest.config.ts +13 -0
@@ -0,0 +1,117 @@
1
+ import type { CacheDriver } from '../types';
2
+
3
+ interface CacheEntry {
4
+ value: unknown;
5
+ expiresAt: number | null;
6
+ }
7
+
8
+ export function createLocalCacheDriver(): CacheDriver {
9
+ const store = new Map<string, CacheEntry>();
10
+
11
+ function isExpired(entry: CacheEntry): boolean {
12
+ return entry.expiresAt !== null && Date.now() > entry.expiresAt;
13
+ }
14
+
15
+ function prune(): void {
16
+ for (const [key, entry] of store) {
17
+ if (isExpired(entry)) store.delete(key);
18
+ }
19
+ }
20
+
21
+ return {
22
+ async get<T>(key: string): Promise<T | null> {
23
+ const entry = store.get(key);
24
+ if (!entry || isExpired(entry)) {
25
+ store.delete(key);
26
+ return null;
27
+ }
28
+ return entry.value as T;
29
+ },
30
+
31
+ async set(key: string, value: unknown, ttl?: number): Promise<boolean> {
32
+ store.set(key, {
33
+ value,
34
+ expiresAt: ttl ? Date.now() + ttl * 1000 : null,
35
+ });
36
+ return true;
37
+ },
38
+
39
+ async delete(key: string): Promise<boolean> {
40
+ return store.delete(key);
41
+ },
42
+
43
+ async exists(key: string): Promise<boolean> {
44
+ const entry = store.get(key);
45
+ if (!entry || isExpired(entry)) {
46
+ store.delete(key);
47
+ return false;
48
+ }
49
+ return true;
50
+ },
51
+
52
+ async mget<T>(keys: string[]): Promise<(T | null)[]> {
53
+ return keys.map((key) => {
54
+ const entry = store.get(key);
55
+ if (!entry || isExpired(entry)) {
56
+ store.delete(key);
57
+ return null;
58
+ }
59
+ return entry.value as T;
60
+ });
61
+ },
62
+
63
+ async mset(items: Array<{ key: string; value: unknown; ttl?: number }>): Promise<boolean> {
64
+ for (const item of items) {
65
+ store.set(item.key, {
66
+ value: item.value,
67
+ expiresAt: item.ttl ? Date.now() + item.ttl * 1000 : null,
68
+ });
69
+ }
70
+ return true;
71
+ },
72
+
73
+ async deleteByPattern(pattern: string): Promise<number> {
74
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
75
+ let count = 0;
76
+ for (const key of store.keys()) {
77
+ if (regex.test(key)) {
78
+ store.delete(key);
79
+ count++;
80
+ }
81
+ }
82
+ return count;
83
+ },
84
+
85
+ async incr(key: string): Promise<number> {
86
+ const entry = store.get(key);
87
+ const current = entry && !isExpired(entry) ? (entry.value as number) : 0;
88
+ const next = current + 1;
89
+ store.set(key, { value: next, expiresAt: null });
90
+ return next;
91
+ },
92
+
93
+ async decr(key: string): Promise<number> {
94
+ const entry = store.get(key);
95
+ const current = entry && !isExpired(entry) ? (entry.value as number) : 0;
96
+ const next = current - 1;
97
+ store.set(key, { value: next, expiresAt: null });
98
+ return next;
99
+ },
100
+
101
+ async expire(key: string, ttl: number): Promise<boolean> {
102
+ const entry = store.get(key);
103
+ if (!entry) return false;
104
+ entry.expiresAt = Date.now() + ttl * 1000;
105
+ return true;
106
+ },
107
+
108
+ async close(): Promise<void> {
109
+ store.clear();
110
+ },
111
+
112
+ async healthCheck(): Promise<boolean> {
113
+ prune();
114
+ return true;
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,2 @@
1
+ export { LocalCachePlugin, LOCAL_CACHE_CONTEXT_KEY } from './plugin';
2
+ export { createLocalCacheDriver } from './driver';
@@ -0,0 +1,34 @@
1
+ import type { Hono } from 'hono';
2
+ import type { ILogger } from '@rx-ted/packages-core';
3
+ import type { IPlugin, Application } from '@rx-ted/packages-honest';
4
+ import { ComponentManager, resolvePluginLogger } from '@rx-ted/packages-honest';
5
+ import { createLocalCacheDriver } from './driver';
6
+ import type { CacheDriver } from '../types';
7
+ import { CACHE_GLOBAL_KEY } from '../cache-service';
8
+
9
+ export const LOCAL_CACHE_CONTEXT_KEY = 'honest:local-cache';
10
+
11
+ export class LocalCachePlugin implements IPlugin {
12
+ static readonly globalKey = 'local-cache';
13
+ readonly name = 'local-cache-plugin';
14
+ readonly version = '1.0.0';
15
+
16
+ logger?: ILogger;
17
+ private driver: CacheDriver;
18
+
19
+ constructor() {
20
+ this.driver = createLocalCacheDriver();
21
+ }
22
+
23
+ async beforeModulesRegistered(app: Application, _hono: Hono): Promise<void> {
24
+ this.logger ??= resolvePluginLogger(this.name);
25
+ this.logger.info('Local cache: initializing in-memory cache...');
26
+ app.getContext().set(LOCAL_CACHE_CONTEXT_KEY, this.driver);
27
+ ComponentManager.registerPlugin(CACHE_GLOBAL_KEY, this.driver);
28
+ this.logger.info('Local cache: ready');
29
+ }
30
+
31
+ async close(): Promise<void> {
32
+ await this.driver.close();
33
+ }
34
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createRedisDriver } from './driver';
3
+ import type { RedisClientOptions } from './driver';
4
+
5
+ vi.mock('redis', () => {
6
+ const mockClient = {
7
+ on: vi.fn(),
8
+ connect: vi.fn().mockResolvedValue(undefined),
9
+ get: vi.fn(),
10
+ set: vi.fn(),
11
+ setEx: vi.fn(),
12
+ del: vi.fn(),
13
+ exists: vi.fn(),
14
+ mGet: vi.fn(),
15
+ multi: vi.fn(() => mockMulti),
16
+ incr: vi.fn(),
17
+ decr: vi.fn(),
18
+ expire: vi.fn(),
19
+ scan: vi.fn(),
20
+ ping: vi.fn(),
21
+ quit: vi.fn().mockResolvedValue(undefined),
22
+ };
23
+
24
+ const mockMulti = {
25
+ set: vi.fn(() => mockMulti),
26
+ setEx: vi.fn(() => mockMulti),
27
+ exec: vi.fn(),
28
+ };
29
+
30
+ return {
31
+ createClient: vi.fn(() => mockClient),
32
+ };
33
+ });
34
+
35
+ describe('RedisDriver', () => {
36
+ const options: RedisClientOptions = {
37
+ host: '127.0.0.1',
38
+ port: 6379,
39
+ db: 0,
40
+ };
41
+
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ });
45
+
46
+ it('creates driver with url option', async () => {
47
+ const { createClient } = await import('redis');
48
+ await createRedisDriver({ url: 'redis://localhost:6379' });
49
+ expect(createClient).toHaveBeenCalledWith({ url: 'redis://localhost:6379' });
50
+ });
51
+
52
+ it('creates driver with host/port options', async () => {
53
+ const { createClient } = await import('redis');
54
+ await createRedisDriver(options);
55
+ expect(createClient).toHaveBeenCalledWith({
56
+ socket: { host: '127.0.0.1', port: 6379 },
57
+ username: undefined,
58
+ password: undefined,
59
+ database: 0,
60
+ });
61
+ });
62
+
63
+ it('connects to redis', async () => {
64
+ const redis = await import('redis');
65
+ const createClient = redis.createClient as any;
66
+
67
+ await createRedisDriver(options);
68
+ expect(createClient).toHaveBeenCalledOnce();
69
+ expect(createClient().connect).toHaveBeenCalled();
70
+ });
71
+ });
@@ -0,0 +1,124 @@
1
+ import { createClient } from 'redis';
2
+ import type { CacheDriver } from '../types';
3
+
4
+ type RedisClientInstance = ReturnType<typeof createClient>;
5
+
6
+ export interface RedisClientOptions {
7
+ url?: string;
8
+ host?: string;
9
+ port?: number;
10
+ username?: string;
11
+ password?: string;
12
+ db?: number;
13
+ }
14
+
15
+ export async function createRedisDriver(options: RedisClientOptions): Promise<CacheDriver> {
16
+ const client = createClient(
17
+ options.url
18
+ ? { url: options.url }
19
+ : {
20
+ socket: {
21
+ host: options.host ?? '127.0.0.1',
22
+ port: options.port ?? 6379,
23
+ },
24
+ username: options.username,
25
+ password: options.password,
26
+ database: options.db ?? 0,
27
+ },
28
+ );
29
+
30
+ await client.connect();
31
+
32
+ return new RedisDriver(client);
33
+ }
34
+
35
+ class RedisDriver implements CacheDriver {
36
+ constructor(private client: RedisClientInstance) {}
37
+
38
+ async get<T = unknown>(key: string): Promise<T | null> {
39
+ const value = await this.client.get(key);
40
+ if (value === null) return null;
41
+ return JSON.parse(value) as T;
42
+ }
43
+
44
+ async set(key: string, value: unknown, ttl?: number): Promise<boolean> {
45
+ const serialized = JSON.stringify(value);
46
+ let result: string;
47
+ if (ttl) {
48
+ result = await this.client.setEx(key, ttl, serialized);
49
+ } else {
50
+ result = (await this.client.set(key, serialized)) ?? '';
51
+ }
52
+ return result === 'OK';
53
+ }
54
+
55
+ async delete(key: string): Promise<boolean> {
56
+ const result = await this.client.del(key);
57
+ return result > 0;
58
+ }
59
+
60
+ async deleteByPattern(pattern: string): Promise<number> {
61
+ let cursor = 0;
62
+ let total = 0;
63
+ do {
64
+ const result = await this.client.scan(cursor, { MATCH: pattern, COUNT: 100 });
65
+ cursor = result.cursor;
66
+ if (result.keys.length > 0) {
67
+ const count = await this.client.del(result.keys);
68
+ total += count;
69
+ }
70
+ } while (cursor !== 0);
71
+ return total;
72
+ }
73
+
74
+ async exists(key: string): Promise<boolean> {
75
+ const result = await this.client.exists(key);
76
+ return result > 0;
77
+ }
78
+
79
+ async mget<T = unknown>(keys: string[]): Promise<(T | null)[]> {
80
+ const values = await this.client.mGet(keys);
81
+ return values.map((v: string | null) => (v ? (JSON.parse(v) as T) : null));
82
+ }
83
+
84
+ async mset(items: Array<{ key: string; value: unknown; ttl?: number }>): Promise<boolean> {
85
+ const pipeline = this.client.multi();
86
+ for (const item of items) {
87
+ const serialized = JSON.stringify(item.value);
88
+ if (item.ttl) {
89
+ pipeline.setEx(item.key, item.ttl, serialized);
90
+ } else {
91
+ pipeline.set(item.key, serialized);
92
+ }
93
+ }
94
+ const results = await pipeline.exec();
95
+ if (!results) return false;
96
+ return results.every((r: unknown) => r === 'OK');
97
+ }
98
+
99
+ async incr(key: string): Promise<number> {
100
+ return this.client.incr(key);
101
+ }
102
+
103
+ async decr(key: string): Promise<number> {
104
+ return this.client.decr(key);
105
+ }
106
+
107
+ async expire(key: string, ttl: number): Promise<boolean> {
108
+ const result = (await this.client.expire(key, ttl)) as unknown as number;
109
+ return result === 1;
110
+ }
111
+
112
+ async close(): Promise<void> {
113
+ await this.client.quit();
114
+ }
115
+
116
+ async healthCheck(): Promise<boolean> {
117
+ try {
118
+ await this.client.ping();
119
+ return true;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,4 @@
1
+ export { RedisPlugin, REDIS_CONTEXT_KEY, REDIS_GLOBAL_KEY } from './plugin';
2
+ export type { RedisPluginOptions } from './plugin';
3
+ export { createRedisDriver } from './driver';
4
+ export type { RedisClientOptions } from './driver';
@@ -0,0 +1,84 @@
1
+ import type { Hono } from 'hono';
2
+ import type { ILogger } from '@rx-ted/packages-core';
3
+ import { ComponentManager, maskSensitive, resolvePluginLogger } from '@rx-ted/packages-honest';
4
+ import type { Application, IPlugin } from '@rx-ted/packages-honest';
5
+ import { createRedisDriver } from './driver';
6
+ import type { RedisClientOptions } from './driver';
7
+ import type { CacheDriver } from '../types';
8
+ import { CACHE_GLOBAL_KEY } from '../cache-service';
9
+
10
+ export const REDIS_CONTEXT_KEY = 'honest:redis';
11
+ export const REDIS_GLOBAL_KEY = 'redis';
12
+
13
+ export interface RedisPluginOptions {
14
+ connection: RedisClientOptions;
15
+ contextKey?: string;
16
+ }
17
+
18
+ export class RedisPlugin implements IPlugin {
19
+ static readonly globalKey = REDIS_GLOBAL_KEY;
20
+ readonly name = 'redis-plugin';
21
+ readonly version = '1.0.0';
22
+
23
+ logger?: ILogger;
24
+
25
+ private driver: CacheDriver | null = null;
26
+ private readonly connection: RedisClientOptions;
27
+ private readonly contextKey: string;
28
+
29
+ constructor(options: RedisPluginOptions) {
30
+ this.connection = options.connection;
31
+ this.contextKey = options.contextKey ?? REDIS_CONTEXT_KEY;
32
+ }
33
+
34
+ getClient(): CacheDriver {
35
+ if (!this.driver) {
36
+ throw new Error('Redis not connected. Ensure beforeModulesRegistered has run.');
37
+ }
38
+ return this.driver;
39
+ }
40
+
41
+ async beforeModulesRegistered(app: Application, _hono: Hono): Promise<void> {
42
+ this.logger ??= resolvePluginLogger(this.name);
43
+ this.logger.info('Redis: connecting...');
44
+
45
+ const connInfo: Record<string, unknown> = {
46
+ category: 'plugins',
47
+ host: this.connection.host,
48
+ port: this.connection.port,
49
+ db: this.connection.db,
50
+ username: this.connection.username,
51
+ password: maskSensitive(this.connection.password),
52
+ };
53
+
54
+ try {
55
+ this.driver = await createRedisDriver(this.connection);
56
+ app.getContext().set(this.contextKey, this.driver);
57
+ ComponentManager.registerPlugin(CACHE_GLOBAL_KEY, this.driver);
58
+
59
+ const ok = await this.driver.healthCheck();
60
+ if (!ok) throw new Error('Redis: health check failed');
61
+ this.logger.info(connInfo, 'success to redis');
62
+ } catch (error) {
63
+ this.logger.error(
64
+ { ...connInfo, error: error instanceof Error ? error.message : String(error) },
65
+ 'fail to connect redis',
66
+ );
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ async afterModulesRegistered(_app: Application, _hono: Hono): Promise<void> {
72
+ if (this.driver) {
73
+ const ok = await this.driver.healthCheck();
74
+ if (ok) {
75
+ this.logger?.info('Redis: health check passed');
76
+ }
77
+ }
78
+ }
79
+
80
+ async close(): Promise<void> {
81
+ await this.driver?.close();
82
+ this.driver = null;
83
+ }
84
+ }
@@ -0,0 +1,257 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const mockKvNs = vi.hoisted(() => ({ get: vi.fn(), put: vi.fn(), delete: vi.fn() }));
4
+
5
+ const mockHasPlugin = vi.hoisted(() => vi.fn());
6
+ const mockGetPlugin = vi.hoisted(() => vi.fn());
7
+ const mockRegisterPlugin = vi.hoisted(() => vi.fn());
8
+ const mockRuntimeEnv = vi.hoisted(() => vi.fn().mockReturnValue({}));
9
+
10
+ const mockKvDriver = vi.hoisted(() => ({
11
+ name: 'cloudflare-kv',
12
+ close: vi.fn().mockResolvedValue(undefined),
13
+ healthCheck: vi.fn().mockResolvedValue(true),
14
+ }));
15
+ const mockRedisDriver = vi.hoisted(() => ({
16
+ name: 'redis',
17
+ close: vi.fn().mockResolvedValue(undefined),
18
+ healthCheck: vi.fn().mockResolvedValue(true),
19
+ }));
20
+ const mockLocalDriver = vi.hoisted(() => ({
21
+ name: 'local-cache',
22
+ close: vi.fn().mockResolvedValue(undefined),
23
+ healthCheck: vi.fn().mockResolvedValue(true),
24
+ }));
25
+
26
+ const mockCreateCloudflareKvDriver = vi.hoisted(() => vi.fn().mockReturnValue(mockKvDriver));
27
+ const mockCreateRedisDriver = vi.hoisted(() => vi.fn().mockResolvedValue(mockRedisDriver));
28
+ const mockCreateLocalCacheDriver = vi.hoisted(() => vi.fn().mockReturnValue(mockLocalDriver));
29
+
30
+ vi.mock('@rx-ted/packages-core', () => {
31
+ function resolveBinding(name: string) {
32
+ return (
33
+ (globalThis as any)[name] ??
34
+ (globalThis as any).env?.[name] ??
35
+ (typeof process !== 'undefined' ? (process.env as any)?.[name] : undefined) ??
36
+ mockRuntimeEnv()?.[name]
37
+ );
38
+ }
39
+ return {
40
+ ENV_SYMBOL: Symbol('app:env'),
41
+ Platform: { env: mockRuntimeEnv },
42
+ resolveBinding,
43
+ };
44
+ });
45
+
46
+ vi.mock('@rx-ted/packages-honest', () => ({
47
+ ComponentManager: {
48
+ hasPlugin: mockHasPlugin,
49
+ getPlugin: mockGetPlugin,
50
+ registerPlugin: mockRegisterPlugin,
51
+ },
52
+ resolvePluginLogger: vi.fn().mockReturnValue({
53
+ child: vi.fn().mockReturnThis(),
54
+ info: vi.fn(),
55
+ error: vi.fn(),
56
+ warn: vi.fn(),
57
+ debug: vi.fn(),
58
+ }),
59
+ }));
60
+
61
+ vi.mock('./cloudflare/driver', () => ({ createCloudflareKvDriver: mockCreateCloudflareKvDriver }));
62
+ vi.mock('./redis/driver', () => ({ createRedisDriver: mockCreateRedisDriver }));
63
+ vi.mock('./local/driver', () => ({ createLocalCacheDriver: mockCreateLocalCacheDriver }));
64
+ vi.mock('./cache-service', () => ({ CACHE_GLOBAL_KEY: 'app:cache' }));
65
+
66
+ import { CachePlugin, findKvBinding, resolveBinding } from './resolve';
67
+
68
+ function makeEnv(platform: string, vars: Record<string, string> = {}) {
69
+ return {
70
+ platform,
71
+ has: (key: string) => key in vars,
72
+ get: (key: string, _type?: string) => vars[key],
73
+ toObject: () => ({ ...vars }),
74
+ };
75
+ }
76
+
77
+ function mockApp() {
78
+ return { getContext: () => ({ set: vi.fn() }) };
79
+ }
80
+
81
+ describe('CachePlugin', () => {
82
+ beforeEach(() => {
83
+ vi.clearAllMocks();
84
+ mockHasPlugin.mockReturnValue(true);
85
+ mockGetPlugin.mockReturnValue(makeEnv('node'));
86
+ mockRuntimeEnv.mockReturnValue({});
87
+ mockCreateRedisDriver.mockResolvedValue(mockRedisDriver);
88
+ });
89
+
90
+ describe('auto-detect on cloudflare', () => {
91
+ beforeEach(() => {
92
+ mockGetPlugin.mockReturnValue(makeEnv('cloudflare'));
93
+ for (const key of Object.keys(globalThis as any)) {
94
+ if ((globalThis as any)[key] === mockKvNs) {
95
+ delete (globalThis as any)[key];
96
+ }
97
+ }
98
+ });
99
+
100
+ it('uses Cloudflare KV when binding on globalThis', async () => {
101
+ (globalThis as any).MY_KV = mockKvNs;
102
+ const plugin = new CachePlugin();
103
+ await plugin.beforeModulesRegistered(mockApp() as any);
104
+
105
+ expect(mockCreateCloudflareKvDriver).toHaveBeenCalledWith(mockKvNs);
106
+ expect(mockCreateLocalCacheDriver).not.toHaveBeenCalled();
107
+ expect(mockRegisterPlugin).toHaveBeenCalledWith('app:cache', mockKvDriver);
108
+ delete (globalThis as any).MY_KV;
109
+ });
110
+
111
+ it('throws when no KV binding on cloudflare', async () => {
112
+ const plugin = new CachePlugin();
113
+ await expect(plugin.beforeModulesRegistered(mockApp() as any)).rejects.toThrow(
114
+ 'Cloudflare KV binding not found',
115
+ );
116
+ expect(mockCreateLocalCacheDriver).not.toHaveBeenCalled();
117
+ expect(mockCreateCloudflareKvDriver).not.toHaveBeenCalled();
118
+ });
119
+ });
120
+
121
+ describe('auto-detect on node', () => {
122
+ it('uses Redis when env has REDIS_HOST', async () => {
123
+ mockGetPlugin.mockReturnValue(
124
+ makeEnv('node', { REDIS_HOST: 'localhost', REDIS_PORT: '6379' }),
125
+ );
126
+ const plugin = new CachePlugin();
127
+ await plugin.beforeModulesRegistered(mockApp() as any);
128
+
129
+ expect(mockCreateRedisDriver).toHaveBeenCalled();
130
+ expect(mockCreateLocalCacheDriver).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it('falls back to local when no Redis config', async () => {
134
+ const plugin = new CachePlugin();
135
+ await plugin.beforeModulesRegistered(mockApp() as any);
136
+
137
+ expect(mockCreateLocalCacheDriver).toHaveBeenCalled();
138
+ expect(mockCreateRedisDriver).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it('falls back to local when Redis connection fails', async () => {
142
+ mockGetPlugin.mockReturnValue(makeEnv('node', { REDIS_HOST: 'bad-host' }));
143
+ mockCreateRedisDriver.mockRejectedValue(new Error('connection refused'));
144
+ const plugin = new CachePlugin();
145
+ await plugin.beforeModulesRegistered(mockApp() as any);
146
+
147
+ expect(mockCreateRedisDriver).toHaveBeenCalled();
148
+ expect(mockCreateLocalCacheDriver).toHaveBeenCalled();
149
+ });
150
+ });
151
+
152
+ describe('explicit driver option', () => {
153
+ it('uses KV when driver: "kv" with binding on globalThis', async () => {
154
+ (globalThis as any).MY_KV = mockKvNs;
155
+ const plugin = new CachePlugin({ driver: 'kv' });
156
+ await plugin.beforeModulesRegistered(mockApp() as any);
157
+
158
+ expect(mockCreateCloudflareKvDriver).toHaveBeenCalled();
159
+ expect(mockCreateLocalCacheDriver).not.toHaveBeenCalled();
160
+ delete (globalThis as any).MY_KV;
161
+ });
162
+
163
+ it('throws when driver: "kv" without binding', async () => {
164
+ const plugin = new CachePlugin({ driver: 'kv' });
165
+ await expect(plugin.beforeModulesRegistered(mockApp() as any)).rejects.toThrow(
166
+ 'Cloudflare KV binding not found',
167
+ );
168
+ });
169
+
170
+ it('uses Redis when driver: "redis"', async () => {
171
+ const plugin = new CachePlugin({ driver: 'redis', redis: { host: 'explicit-host' } });
172
+ await plugin.beforeModulesRegistered(mockApp() as any);
173
+
174
+ expect(mockCreateRedisDriver).toHaveBeenCalled();
175
+ expect(mockCreateLocalCacheDriver).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('uses local when driver: "local"', async () => {
179
+ const plugin = new CachePlugin({ driver: 'local' });
180
+ await plugin.beforeModulesRegistered(mockApp() as any);
181
+
182
+ expect(mockCreateLocalCacheDriver).toHaveBeenCalled();
183
+ expect(mockCreateCloudflareKvDriver).not.toHaveBeenCalled();
184
+ expect(mockCreateRedisDriver).not.toHaveBeenCalled();
185
+ });
186
+ });
187
+
188
+ describe('explicit redis options', () => {
189
+ it('uses explicit redis config over env vars', async () => {
190
+ const env = makeEnv('node', { REDIS_HOST: 'ignored' });
191
+ mockGetPlugin.mockReturnValue(env);
192
+ const plugin = new CachePlugin({ redis: { host: 'explicit-host', port: 9999 } });
193
+ await plugin.beforeModulesRegistered(mockApp() as any);
194
+
195
+ expect(mockCreateRedisDriver).toHaveBeenCalledWith({ host: 'explicit-host', port: 9999 });
196
+ });
197
+ });
198
+
199
+ describe('API', () => {
200
+ it('getClient returns driver after init', async () => {
201
+ const plugin = new CachePlugin();
202
+ await plugin.beforeModulesRegistered(mockApp() as any);
203
+ expect(plugin.getClient()).toBe(mockLocalDriver);
204
+ });
205
+
206
+ it('getClient throws before init', () => {
207
+ const plugin = new CachePlugin();
208
+ expect(() => plugin.getClient()).toThrow('not initialized');
209
+ });
210
+
211
+ it('close calls driver.close and clears driver', async () => {
212
+ const plugin = new CachePlugin();
213
+ await plugin.beforeModulesRegistered(mockApp() as any);
214
+ await plugin.close();
215
+
216
+ expect(mockLocalDriver.close).toHaveBeenCalled();
217
+ expect(() => plugin.getClient()).toThrow('not initialized');
218
+ });
219
+ });
220
+ });
221
+
222
+ describe('findKvBinding', () => {
223
+ beforeEach(() => {
224
+ // Clean up any test bindings on globalThis
225
+ for (const key of Object.keys(globalThis as any)) {
226
+ if ((globalThis as any)[key] === mockKvNs) {
227
+ delete (globalThis as any)[key];
228
+ }
229
+ }
230
+ });
231
+
232
+ it('finds binding on globalThis by duck-type (get+put+delete)', () => {
233
+ (globalThis as any).MY_KV = mockKvNs;
234
+ expect(findKvBinding()).toBe('MY_KV');
235
+ });
236
+
237
+ it('finds binding on globalThis.env by duck-type', () => {
238
+ (globalThis as any).env = { MY_KV: mockKvNs };
239
+ expect(findKvBinding()).toBe('MY_KV');
240
+ delete (globalThis as any).env;
241
+ });
242
+
243
+ it('returns empty string when no binding found', () => {
244
+ expect(findKvBinding()).toBe('');
245
+ });
246
+
247
+ it('accepts optional Env parameter (for consistency)', () => {
248
+ const fakeEnv = { toObject: () => ({}) };
249
+ expect(findKvBinding(fakeEnv as any)).toBe('');
250
+ });
251
+ });
252
+
253
+ describe('resolveBinding', () => {
254
+ it('finds binding from env.get fallback', () => {
255
+ expect(resolveBinding('NONEXISTENT')).toBeUndefined();
256
+ });
257
+ });