@engjts/nexus 0.1.8 → 0.1.9
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 +1 -1
- package/BENCHMARK_REPORT.md +0 -343
- package/documentation/01-getting-started.md +0 -240
- package/documentation/02-context.md +0 -335
- package/documentation/03-routing.md +0 -397
- package/documentation/04-middleware.md +0 -483
- package/documentation/05-validation.md +0 -514
- package/documentation/06-error-handling.md +0 -465
- package/documentation/07-performance.md +0 -364
- package/documentation/08-adapters.md +0 -470
- package/documentation/09-api-reference.md +0 -548
- package/documentation/10-examples.md +0 -582
- package/documentation/11-deployment.md +0 -477
- package/documentation/12-sentry.md +0 -620
- package/documentation/13-sentry-data-storage.md +0 -996
- package/documentation/14-sentry-data-reference.md +0 -457
- package/documentation/15-sentry-summary.md +0 -409
- package/documentation/16-alerts-system.md +0 -745
- package/documentation/17-alert-adapters.md +0 -696
- package/documentation/18-alerts-implementation-summary.md +0 -385
- package/documentation/19-class-based-routing.md +0 -840
- package/documentation/20-websocket-realtime.md +0 -813
- package/documentation/21-cache-system.md +0 -510
- package/documentation/22-job-queue.md +0 -772
- package/documentation/23-sentry-plugin.md +0 -551
- package/documentation/24-testing-utilities.md +0 -1287
- package/documentation/25-api-versioning.md +0 -533
- package/documentation/26-context-store.md +0 -607
- package/documentation/27-dependency-injection.md +0 -329
- package/documentation/28-lifecycle-hooks.md +0 -521
- package/documentation/29-package-structure.md +0 -196
- package/documentation/30-plugin-system.md +0 -414
- package/documentation/31-jwt-authentication.md +0 -597
- package/documentation/32-cli.md +0 -268
- package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
- package/documentation/ALERTS-INDEX.md +0 -330
- package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
- package/documentation/README.md +0 -178
- package/documentation/index.html +0 -34
- package/modern_framework_paper.md +0 -1870
- package/public/css/style.css +0 -87
- package/public/index.html +0 -34
- package/public/js/app.js +0 -27
- package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
- package/src/advanced/cache/MultiTierCache.ts +0 -194
- package/src/advanced/cache/RedisCacheStore.ts +0 -341
- package/src/advanced/cache/index.ts +0 -5
- package/src/advanced/cache/types.ts +0 -40
- package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
- package/src/advanced/graphql/index.ts +0 -22
- package/src/advanced/graphql/server.ts +0 -252
- package/src/advanced/graphql/types.ts +0 -42
- package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
- package/src/advanced/jobs/JobQueue.ts +0 -556
- package/src/advanced/jobs/RedisQueueStore.ts +0 -367
- package/src/advanced/jobs/index.ts +0 -5
- package/src/advanced/jobs/types.ts +0 -70
- package/src/advanced/observability/APMManager.ts +0 -163
- package/src/advanced/observability/AlertManager.ts +0 -109
- package/src/advanced/observability/MetricRegistry.ts +0 -151
- package/src/advanced/observability/ObservabilityCenter.ts +0 -304
- package/src/advanced/observability/StructuredLogger.ts +0 -154
- package/src/advanced/observability/TracingManager.ts +0 -117
- package/src/advanced/observability/adapters.ts +0 -304
- package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
- package/src/advanced/observability/index.ts +0 -11
- package/src/advanced/observability/types.ts +0 -174
- package/src/advanced/playground/extractPathParams.ts +0 -6
- package/src/advanced/playground/generateFieldExample.ts +0 -31
- package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
- package/src/advanced/playground/generateSummary.ts +0 -19
- package/src/advanced/playground/getTagFromPath.ts +0 -9
- package/src/advanced/playground/index.ts +0 -8
- package/src/advanced/playground/playground.ts +0 -250
- package/src/advanced/playground/types.ts +0 -49
- package/src/advanced/playground/zodToExample.ts +0 -16
- package/src/advanced/playground/zodToParams.ts +0 -15
- package/src/advanced/postman/buildAuth.ts +0 -31
- package/src/advanced/postman/buildBody.ts +0 -15
- package/src/advanced/postman/buildQueryParams.ts +0 -27
- package/src/advanced/postman/buildRequestItem.ts +0 -36
- package/src/advanced/postman/buildResponses.ts +0 -11
- package/src/advanced/postman/buildUrl.ts +0 -33
- package/src/advanced/postman/capitalize.ts +0 -4
- package/src/advanced/postman/generateCollection.ts +0 -59
- package/src/advanced/postman/generateEnvironment.ts +0 -34
- package/src/advanced/postman/generateExampleFromZod.ts +0 -21
- package/src/advanced/postman/generateFieldExample.ts +0 -45
- package/src/advanced/postman/generateName.ts +0 -20
- package/src/advanced/postman/generateUUID.ts +0 -11
- package/src/advanced/postman/getTagFromPath.ts +0 -10
- package/src/advanced/postman/index.ts +0 -28
- package/src/advanced/postman/postman.ts +0 -156
- package/src/advanced/postman/slugify.ts +0 -7
- package/src/advanced/postman/types.ts +0 -140
- package/src/advanced/realtime/index.ts +0 -18
- package/src/advanced/realtime/websocket.ts +0 -231
- package/src/advanced/sentry/index.ts +0 -1236
- package/src/advanced/sentry/types.ts +0 -355
- package/src/advanced/static/generateDirectoryListing.ts +0 -47
- package/src/advanced/static/generateETag.ts +0 -7
- package/src/advanced/static/getMimeType.ts +0 -9
- package/src/advanced/static/index.ts +0 -32
- package/src/advanced/static/isSafePath.ts +0 -13
- package/src/advanced/static/publicDir.ts +0 -21
- package/src/advanced/static/serveStatic.ts +0 -225
- package/src/advanced/static/spa.ts +0 -24
- package/src/advanced/static/types.ts +0 -159
- package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
- package/src/advanced/swagger/buildOperation.ts +0 -61
- package/src/advanced/swagger/buildParameters.ts +0 -61
- package/src/advanced/swagger/buildRequestBody.ts +0 -21
- package/src/advanced/swagger/buildResponses.ts +0 -54
- package/src/advanced/swagger/capitalize.ts +0 -5
- package/src/advanced/swagger/convertPath.ts +0 -9
- package/src/advanced/swagger/createSwagger.ts +0 -12
- package/src/advanced/swagger/generateOperationId.ts +0 -21
- package/src/advanced/swagger/generateSpec.ts +0 -105
- package/src/advanced/swagger/generateSummary.ts +0 -24
- package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
- package/src/advanced/swagger/generateThemeCss.ts +0 -53
- package/src/advanced/swagger/index.ts +0 -25
- package/src/advanced/swagger/swagger.ts +0 -237
- package/src/advanced/swagger/types.ts +0 -206
- package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
- package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
- package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
- package/src/advanced/testing/factory.ts +0 -509
- package/src/advanced/testing/harness.ts +0 -612
- package/src/advanced/testing/index.ts +0 -430
- package/src/advanced/testing/load-test.ts +0 -618
- package/src/advanced/testing/mock-server.ts +0 -498
- package/src/advanced/testing/mock.ts +0 -670
- package/src/cli/bin.ts +0 -9
- package/src/cli/cli.ts +0 -158
- package/src/cli/commands/add.ts +0 -178
- package/src/cli/commands/build.ts +0 -73
- package/src/cli/commands/create.ts +0 -166
- package/src/cli/commands/dev.ts +0 -85
- package/src/cli/commands/generate.ts +0 -99
- package/src/cli/commands/help.ts +0 -95
- package/src/cli/commands/init.ts +0 -91
- package/src/cli/commands/version.ts +0 -38
- package/src/cli/index.ts +0 -6
- package/src/cli/templates/generators.ts +0 -359
- package/src/cli/templates/index.ts +0 -680
- package/src/cli/utils/exec.ts +0 -52
- package/src/cli/utils/file-system.ts +0 -78
- package/src/cli/utils/logger.ts +0 -111
- package/src/core/adapter.ts +0 -88
- package/src/core/application.ts +0 -1453
- package/src/core/context-pool.ts +0 -79
- package/src/core/context.ts +0 -856
- package/src/core/index.ts +0 -94
- package/src/core/middleware.ts +0 -272
- package/src/core/performance/buffer-pool.ts +0 -108
- package/src/core/performance/middleware-optimizer.ts +0 -162
- package/src/core/plugin/PluginManager.ts +0 -435
- package/src/core/plugin/builder.ts +0 -358
- package/src/core/plugin/index.ts +0 -50
- package/src/core/plugin/types.ts +0 -214
- package/src/core/router/file-router.ts +0 -623
- package/src/core/router/index.ts +0 -260
- package/src/core/router/radix-tree.ts +0 -242
- package/src/core/serializer.ts +0 -397
- package/src/core/store/index.ts +0 -30
- package/src/core/store/registry.ts +0 -178
- package/src/core/store/request-store.ts +0 -240
- package/src/core/store/types.ts +0 -233
- package/src/core/types.ts +0 -616
- package/src/database/adapter.ts +0 -35
- package/src/database/adapters/index.ts +0 -1
- package/src/database/adapters/mysql.ts +0 -669
- package/src/database/database.ts +0 -70
- package/src/database/dialect.ts +0 -388
- package/src/database/index.ts +0 -12
- package/src/database/migrations.ts +0 -86
- package/src/database/optimizer.ts +0 -125
- package/src/database/query-builder.ts +0 -404
- package/src/database/realtime.ts +0 -53
- package/src/database/schema.ts +0 -71
- package/src/database/transactions.ts +0 -56
- package/src/database/types.ts +0 -87
- package/src/deployment/cluster.ts +0 -471
- package/src/deployment/config.ts +0 -454
- package/src/deployment/docker.ts +0 -599
- package/src/deployment/graceful-shutdown.ts +0 -373
- package/src/deployment/index.ts +0 -56
- package/src/index.ts +0 -281
- package/src/security/adapter.ts +0 -318
- package/src/security/auth/JWTPlugin.ts +0 -234
- package/src/security/auth/JWTProvider.ts +0 -316
- package/src/security/auth/adapter.ts +0 -12
- package/src/security/auth/jwt.ts +0 -234
- package/src/security/auth/middleware.ts +0 -188
- package/src/security/csrf.ts +0 -220
- package/src/security/headers.ts +0 -108
- package/src/security/index.ts +0 -60
- package/src/security/rate-limit/adapter.ts +0 -7
- package/src/security/rate-limit/memory.ts +0 -108
- package/src/security/rate-limit/middleware.ts +0 -181
- package/src/security/sanitization.ts +0 -75
- package/src/security/types.ts +0 -240
- package/src/security/utils.ts +0 -52
- package/tsconfig.json +0 -39
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { CacheStore, CacheEntry } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Redis cache store configuration
|
|
6
|
-
*/
|
|
7
|
-
export interface RedisCacheConfig {
|
|
8
|
-
/**
|
|
9
|
-
* Redis connection URL
|
|
10
|
-
* @example 'redis://localhost:6379'
|
|
11
|
-
*/
|
|
12
|
-
url?: string;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Redis host
|
|
16
|
-
* @default 'localhost'
|
|
17
|
-
*/
|
|
18
|
-
host?: string;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Redis port
|
|
22
|
-
* @default 6379
|
|
23
|
-
*/
|
|
24
|
-
port?: number;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Redis password
|
|
28
|
-
*/
|
|
29
|
-
password?: string;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Redis database number
|
|
33
|
-
* @default 0
|
|
34
|
-
*/
|
|
35
|
-
db?: number;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Key prefix for namespacing
|
|
39
|
-
* @default 'nexus:'
|
|
40
|
-
*/
|
|
41
|
-
prefix?: string;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Connection timeout in milliseconds
|
|
45
|
-
* @default 5000
|
|
46
|
-
*/
|
|
47
|
-
connectTimeout?: number;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Enable TLS/SSL
|
|
51
|
-
* @default false
|
|
52
|
-
*/
|
|
53
|
-
tls?: boolean;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Lazy connect - don't connect immediately
|
|
57
|
-
* @default false
|
|
58
|
-
*/
|
|
59
|
-
lazyConnect?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Redis client interface
|
|
64
|
-
* Supports both 'redis' and 'ioredis' packages
|
|
65
|
-
*/
|
|
66
|
-
export interface RedisClientLike {
|
|
67
|
-
get(key: string): Promise<string | null>;
|
|
68
|
-
set(key: string, value: string, options?: { EX?: number; PX?: number }): Promise<any>;
|
|
69
|
-
setex?(key: string, seconds: number, value: string): Promise<any>;
|
|
70
|
-
del(key: string | string[]): Promise<number>;
|
|
71
|
-
keys(pattern: string): Promise<string[]>;
|
|
72
|
-
flushdb?(): Promise<any>;
|
|
73
|
-
flushDb?(): Promise<any>;
|
|
74
|
-
quit?(): Promise<any>;
|
|
75
|
-
disconnect?(): Promise<any>;
|
|
76
|
-
on?(event: string, listener: (...args: any[]) => void): void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Redis cache store with full feature support
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```typescript
|
|
84
|
-
* // Using with ioredis
|
|
85
|
-
* import Redis from 'ioredis';
|
|
86
|
-
*
|
|
87
|
-
* const redis = new Redis({ host: 'localhost', port: 6379 });
|
|
88
|
-
* const cache = new RedisCacheStore('sessions', { client: redis });
|
|
89
|
-
*
|
|
90
|
-
* // Using with node-redis
|
|
91
|
-
* import { createClient } from 'redis';
|
|
92
|
-
*
|
|
93
|
-
* const client = createClient({ url: 'redis://localhost:6379' });
|
|
94
|
-
* await client.connect();
|
|
95
|
-
* const cache = new RedisCacheStore('sessions', { client });
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
export class RedisCacheStore<Value = unknown> implements CacheStore<Value> {
|
|
99
|
-
readonly name: string;
|
|
100
|
-
private client: RedisClientLike;
|
|
101
|
-
private prefix: string;
|
|
102
|
-
private emitter = new EventEmitter();
|
|
103
|
-
private connected: boolean = false;
|
|
104
|
-
|
|
105
|
-
constructor(
|
|
106
|
-
name: string,
|
|
107
|
-
options: RedisCacheConfig & { client: RedisClientLike }
|
|
108
|
-
) {
|
|
109
|
-
this.name = name;
|
|
110
|
-
this.client = options.client;
|
|
111
|
-
this.prefix = options.prefix ?? 'nexus:';
|
|
112
|
-
|
|
113
|
-
// Listen for connection events if supported
|
|
114
|
-
if (this.client.on) {
|
|
115
|
-
this.client.on('connect', () => {
|
|
116
|
-
this.connected = true;
|
|
117
|
-
this.emitter.emit('connect');
|
|
118
|
-
});
|
|
119
|
-
this.client.on('error', (err) => {
|
|
120
|
-
this.emitter.emit('error', err);
|
|
121
|
-
});
|
|
122
|
-
this.client.on('close', () => {
|
|
123
|
-
this.connected = false;
|
|
124
|
-
this.emitter.emit('disconnect');
|
|
125
|
-
});
|
|
126
|
-
} else {
|
|
127
|
-
this.connected = true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Build the full Redis key with prefix
|
|
133
|
-
*/
|
|
134
|
-
private buildKey(key: string): string {
|
|
135
|
-
return `${this.prefix}${this.name}:${key}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get a value from cache
|
|
140
|
-
*/
|
|
141
|
-
async get(key: string): Promise<CacheEntry<Value> | undefined> {
|
|
142
|
-
try {
|
|
143
|
-
const data = await this.client.get(this.buildKey(key));
|
|
144
|
-
if (!data) {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const entry = JSON.parse(data) as CacheEntry<Value>;
|
|
149
|
-
|
|
150
|
-
// Check expiration (Redis handles TTL, but we double-check for safety)
|
|
151
|
-
if (entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
152
|
-
await this.delete(key);
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return entry;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
this.emitter.emit('error', error);
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Set a value in cache
|
|
165
|
-
*/
|
|
166
|
-
async set(key: string, entry: CacheEntry<Value>): Promise<void> {
|
|
167
|
-
try {
|
|
168
|
-
const fullKey = this.buildKey(key);
|
|
169
|
-
const data = JSON.stringify(entry);
|
|
170
|
-
|
|
171
|
-
if (entry.expiresAt) {
|
|
172
|
-
const ttlMs = entry.expiresAt - Date.now();
|
|
173
|
-
if (ttlMs > 0) {
|
|
174
|
-
const ttlSec = Math.ceil(ttlMs / 1000);
|
|
175
|
-
// Try different methods for compatibility
|
|
176
|
-
if (this.client.setex) {
|
|
177
|
-
// ioredis style
|
|
178
|
-
await this.client.setex(fullKey, ttlSec, data);
|
|
179
|
-
} else {
|
|
180
|
-
// node-redis style
|
|
181
|
-
await this.client.set(fullKey, data, { EX: ttlSec });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
await this.client.set(fullKey, data);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.emitter.emit('set', key, entry);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
this.emitter.emit('error', error);
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Delete a key from cache
|
|
197
|
-
*/
|
|
198
|
-
async delete(key: string): Promise<void> {
|
|
199
|
-
try {
|
|
200
|
-
await this.client.del(this.buildKey(key));
|
|
201
|
-
this.emitter.emit('delete', key);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
this.emitter.emit('error', error);
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Clear all keys in this cache namespace
|
|
210
|
-
*/
|
|
211
|
-
async clear(): Promise<void> {
|
|
212
|
-
try {
|
|
213
|
-
const pattern = `${this.prefix}${this.name}:*`;
|
|
214
|
-
const keys = await this.client.keys(pattern);
|
|
215
|
-
|
|
216
|
-
if (keys.length > 0) {
|
|
217
|
-
await this.client.del(keys);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
this.emitter.emit('clear');
|
|
221
|
-
} catch (error) {
|
|
222
|
-
this.emitter.emit('error', error);
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Get all keys matching a pattern
|
|
229
|
-
*/
|
|
230
|
-
async keys(pattern?: RegExp): Promise<string[]> {
|
|
231
|
-
try {
|
|
232
|
-
const redisPattern = `${this.prefix}${this.name}:*`;
|
|
233
|
-
const allKeys = await this.client.keys(redisPattern);
|
|
234
|
-
|
|
235
|
-
// Remove prefix to get clean keys
|
|
236
|
-
const prefixLength = `${this.prefix}${this.name}:`.length;
|
|
237
|
-
const cleanKeys = allKeys.map(k => k.slice(prefixLength));
|
|
238
|
-
|
|
239
|
-
if (!pattern) {
|
|
240
|
-
return cleanKeys;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return cleanKeys.filter(key => pattern.test(key));
|
|
244
|
-
} catch (error) {
|
|
245
|
-
this.emitter.emit('error', error);
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Check if connected to Redis
|
|
252
|
-
*/
|
|
253
|
-
isConnected(): boolean {
|
|
254
|
-
return this.connected;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Subscribe to cache events
|
|
259
|
-
*/
|
|
260
|
-
on(event: 'set' | 'delete' | 'clear' | 'connect' | 'disconnect' | 'error', listener: (...args: any[]) => void): void {
|
|
261
|
-
this.emitter.on(event, listener);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Disconnect from Redis
|
|
266
|
-
*/
|
|
267
|
-
async disconnect(): Promise<void> {
|
|
268
|
-
if (this.client.quit) {
|
|
269
|
-
await this.client.quit();
|
|
270
|
-
} else if (this.client.disconnect) {
|
|
271
|
-
await this.client.disconnect();
|
|
272
|
-
}
|
|
273
|
-
this.connected = false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Create a Redis cache store with automatic client creation
|
|
279
|
-
* Requires 'ioredis' or 'redis' package to be installed
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```typescript
|
|
283
|
-
* // Auto-create Redis client
|
|
284
|
-
* const cache = await createRedisCache('sessions', {
|
|
285
|
-
* host: 'localhost',
|
|
286
|
-
* port: 6379,
|
|
287
|
-
* prefix: 'myapp:'
|
|
288
|
-
* });
|
|
289
|
-
*
|
|
290
|
-
* await cache.set('user:123', { value: { name: 'John' }, expiresAt: Date.now() + 3600000 });
|
|
291
|
-
* const user = await cache.get('user:123');
|
|
292
|
-
* ```
|
|
293
|
-
*/
|
|
294
|
-
export async function createRedisCache<Value = unknown>(
|
|
295
|
-
name: string,
|
|
296
|
-
config: RedisCacheConfig = {}
|
|
297
|
-
): Promise<RedisCacheStore<Value>> {
|
|
298
|
-
let client: RedisClientLike;
|
|
299
|
-
|
|
300
|
-
// Try ioredis first
|
|
301
|
-
try {
|
|
302
|
-
const Redis = require('ioredis');
|
|
303
|
-
client = new Redis({
|
|
304
|
-
host: config.host ?? 'localhost',
|
|
305
|
-
port: config.port ?? 6379,
|
|
306
|
-
password: config.password,
|
|
307
|
-
db: config.db ?? 0,
|
|
308
|
-
connectTimeout: config.connectTimeout ?? 5000,
|
|
309
|
-
tls: config.tls ? {} : undefined,
|
|
310
|
-
lazyConnect: config.lazyConnect ?? false
|
|
311
|
-
});
|
|
312
|
-
} catch {
|
|
313
|
-
// Try node-redis
|
|
314
|
-
try {
|
|
315
|
-
const { createClient } = require('redis');
|
|
316
|
-
const url = config.url ?? `redis://${config.host ?? 'localhost'}:${config.port ?? 6379}`;
|
|
317
|
-
|
|
318
|
-
client = createClient({
|
|
319
|
-
url,
|
|
320
|
-
password: config.password,
|
|
321
|
-
database: config.db ?? 0,
|
|
322
|
-
socket: {
|
|
323
|
-
connectTimeout: config.connectTimeout ?? 5000,
|
|
324
|
-
tls: config.tls ?? false
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// node-redis requires explicit connect
|
|
329
|
-
await (client as any).connect();
|
|
330
|
-
} catch {
|
|
331
|
-
throw new Error(
|
|
332
|
-
'Redis client not found. Please install either "ioredis" or "redis" package:\n' +
|
|
333
|
-
' npm install ioredis\n' +
|
|
334
|
-
' # or\n' +
|
|
335
|
-
' npm install redis'
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return new RedisCacheStore<Value>(name, { ...config, client });
|
|
341
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { InMemoryCacheStore } from './InMemoryCacheStore';
|
|
2
|
-
export { MultiTierCache } from './MultiTierCache';
|
|
3
|
-
export { RedisCacheStore, createRedisCache } from './RedisCacheStore';
|
|
4
|
-
export type { RedisCacheConfig, RedisClientLike } from './RedisCacheStore';
|
|
5
|
-
export * from './types';
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface CacheEntry<Value = unknown> {
|
|
2
|
-
value: Value;
|
|
3
|
-
expiresAt?: number;
|
|
4
|
-
tags?: string[];
|
|
5
|
-
meta?: Record<string, any>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface CacheStore<Value = unknown> {
|
|
9
|
-
readonly name: string;
|
|
10
|
-
get(key: string): Promise<CacheEntry<Value> | undefined>;
|
|
11
|
-
set(key: string, entry: CacheEntry<Value>): Promise<void>;
|
|
12
|
-
delete(key: string): Promise<void>;
|
|
13
|
-
clear(): Promise<void>;
|
|
14
|
-
keys?(pattern?: RegExp): Promise<string[]>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CacheTierConfig<Value = unknown> {
|
|
18
|
-
store: CacheStore<Value>;
|
|
19
|
-
ttl?: number;
|
|
20
|
-
maxSize?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CacheSetOptions<Value = unknown> {
|
|
24
|
-
ttl?: number;
|
|
25
|
-
tags?: string[];
|
|
26
|
-
meta?: CacheEntry<Value>['meta'];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface CacheWrapOptions<Value = unknown> extends CacheSetOptions<Value> {
|
|
30
|
-
refresh?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface MemoizeOptions<Value = unknown> extends CacheSetOptions<Value> {
|
|
34
|
-
keyResolver?: (...args: any[]) => string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface TagIndexEntry {
|
|
38
|
-
keys: Set<string>;
|
|
39
|
-
expiresAt?: number;
|
|
40
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
interface SimpleDataLoaderBatch<Key, Value> {
|
|
2
|
-
keys: Key[];
|
|
3
|
-
resolvers: Array<{ resolve: (value: Value) => void; reject: (error: Error) => void }>;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export class SimpleDataLoader<Key = any, Value = any> {
|
|
7
|
-
private batch: SimpleDataLoaderBatch<Key, Value> | null = null;
|
|
8
|
-
|
|
9
|
-
constructor(private batchLoadFn: (keys: Key[]) => Promise<Map<Key, Value>>) { }
|
|
10
|
-
|
|
11
|
-
load(key: Key): Promise<Value> {
|
|
12
|
-
if (!this.batch) {
|
|
13
|
-
this.batch = { keys: [], resolvers: [] };
|
|
14
|
-
queueMicrotask(() => this.dispatch());
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return new Promise<Value>((resolve, reject) => {
|
|
18
|
-
this.batch!.keys.push(key);
|
|
19
|
-
this.batch!.resolvers.push({ resolve, reject });
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private async dispatch() {
|
|
24
|
-
if (!this.batch) return;
|
|
25
|
-
const current = this.batch;
|
|
26
|
-
this.batch = null;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const result = await this.batchLoadFn(current.keys);
|
|
30
|
-
current.keys.forEach((key, index) => {
|
|
31
|
-
const value = result.get(key);
|
|
32
|
-
if (value === undefined) {
|
|
33
|
-
current.resolvers[index].reject(new Error(`DataLoader missing key "${String(key)}"`));
|
|
34
|
-
} else {
|
|
35
|
-
current.resolvers[index].resolve(value);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
} catch (error) {
|
|
39
|
-
current.resolvers.forEach(resolver => resolver.reject(error as Error));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nexus GraphQL Module
|
|
3
|
-
*
|
|
4
|
-
* Optional GraphQL integration for Nexus framework.
|
|
5
|
-
* Requires `graphql` as peer dependency.
|
|
6
|
-
*
|
|
7
|
-
* Install: npm install graphql
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* import { GraphQLServer, SimpleDataLoader } from '@engjts/server/graphql';
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export { GraphQLServer } from './server';
|
|
14
|
-
export { SimpleDataLoader } from './SimpleDataLoader';
|
|
15
|
-
export type {
|
|
16
|
-
GraphQLComplexityOptions,
|
|
17
|
-
GraphQLCacheOptions,
|
|
18
|
-
GraphQLRequestPayload,
|
|
19
|
-
GraphQLServerOptions,
|
|
20
|
-
SimpleDataLoaderBatch,
|
|
21
|
-
DataLoaderFactory
|
|
22
|
-
} from './types';
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DocumentNode,
|
|
3
|
-
ExecutionResult,
|
|
4
|
-
GraphQLSchema,
|
|
5
|
-
Kind,
|
|
6
|
-
OperationDefinitionNode,
|
|
7
|
-
execute,
|
|
8
|
-
getOperationAST,
|
|
9
|
-
parse,
|
|
10
|
-
validate
|
|
11
|
-
} from 'graphql';
|
|
12
|
-
import { Context, Handler, Response } from '../../core/types';
|
|
13
|
-
import { GraphQLComplexityOptions, GraphQLRequestPayload, GraphQLServerOptions } from './types';
|
|
14
|
-
|
|
15
|
-
export class GraphQLServer {
|
|
16
|
-
private schema: GraphQLSchema;
|
|
17
|
-
private options: GraphQLServerOptions;
|
|
18
|
-
|
|
19
|
-
constructor(options: GraphQLServerOptions) {
|
|
20
|
-
this.schema = options.schema;
|
|
21
|
-
this.options = {
|
|
22
|
-
playground: options.playground ?? false,
|
|
23
|
-
introspection: options.introspection ?? process.env.NODE_ENV !== 'production',
|
|
24
|
-
dataloaders: options.dataloaders ?? true,
|
|
25
|
-
...options
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Returns a framework route handler
|
|
31
|
-
*/
|
|
32
|
-
handler(): Handler {
|
|
33
|
-
return async (ctx: Context): Promise<Response> => {
|
|
34
|
-
if (ctx.method === 'GET' && this.shouldRenderPlayground(ctx)) {
|
|
35
|
-
return ctx.html(this.renderPlayground());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const payload = await this.parseRequest(ctx);
|
|
39
|
-
const document = this.parseDocument(payload.query);
|
|
40
|
-
this.enforceRules(document, payload);
|
|
41
|
-
|
|
42
|
-
const cacheKey = await this.computeCacheKey(payload);
|
|
43
|
-
if (cacheKey) {
|
|
44
|
-
const cached = await this.options.cache!.instance.get(cacheKey);
|
|
45
|
-
if (cached) {
|
|
46
|
-
return this.buildResponse(cached as ExecutionResult);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const contextValue = await this.buildContext(ctx);
|
|
51
|
-
const result = await execute({
|
|
52
|
-
schema: this.schema,
|
|
53
|
-
document,
|
|
54
|
-
variableValues: payload.variables,
|
|
55
|
-
operationName: payload.operationName,
|
|
56
|
-
contextValue
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (cacheKey && !result.errors) {
|
|
60
|
-
await this.options.cache!.instance.set(cacheKey, result, { ttl: this.options.cache?.ttl });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return this.buildResponse(result);
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private shouldRenderPlayground(ctx: Context): boolean {
|
|
68
|
-
if (!this.options.playground) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
const accept = ctx.headers['accept'];
|
|
72
|
-
if (!accept) return false;
|
|
73
|
-
const acceptHeader = Array.isArray(accept) ? accept.join(',') : accept;
|
|
74
|
-
return acceptHeader.includes('text/html');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async parseRequest(ctx: Context): Promise<GraphQLRequestPayload> {
|
|
78
|
-
if (ctx.method === 'GET') {
|
|
79
|
-
return {
|
|
80
|
-
query: ctx.query.query,
|
|
81
|
-
variables: this.safeParseJSON(ctx.query.variables),
|
|
82
|
-
operationName: ctx.query.operationName as string | undefined
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (typeof ctx.body === 'string') {
|
|
87
|
-
return JSON.parse(ctx.body);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return ctx.body ?? {};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private parseDocument(query?: string): DocumentNode {
|
|
94
|
-
if (!query) {
|
|
95
|
-
throw new Error('GraphQL query is required');
|
|
96
|
-
}
|
|
97
|
-
return parse(query);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private enforceRules(document: DocumentNode, payload: GraphQLRequestPayload) {
|
|
101
|
-
if (!this.options.introspection) {
|
|
102
|
-
const operation = getOperationAST(document, payload.operationName || undefined);
|
|
103
|
-
if (operation?.operation === 'query' && operation.name?.value === '__schema') {
|
|
104
|
-
throw new Error('Introspection is disabled');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const validationErrors = validate(this.schema, document);
|
|
109
|
-
if (validationErrors.length > 0) {
|
|
110
|
-
throw validationErrors[0];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (this.options.depthLimit !== undefined) {
|
|
114
|
-
const depth = this.calculateDepth(document);
|
|
115
|
-
if (depth > this.options.depthLimit) {
|
|
116
|
-
throw new Error(`Query depth ${depth} exceeds limit of ${this.options.depthLimit}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (this.options.complexity) {
|
|
121
|
-
const complexity = this.calculateComplexity(document, this.options.complexity);
|
|
122
|
-
if (complexity > this.options.complexity.limit) {
|
|
123
|
-
throw new Error(`Query complexity ${complexity} exceeds limit of ${this.options.complexity.limit}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async buildContext(ctx: Context) {
|
|
129
|
-
const base = typeof this.options.context === 'function'
|
|
130
|
-
? await this.options.context({ ctx })
|
|
131
|
-
: this.options.context ?? {};
|
|
132
|
-
|
|
133
|
-
if (this.options.dataloaders) {
|
|
134
|
-
Object.assign(base, { loaders: {} });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { ...base, request: ctx };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private calculateDepth(document: DocumentNode): number {
|
|
141
|
-
let maxDepth = 0;
|
|
142
|
-
|
|
143
|
-
const traverse = (node: any, depth: number) => {
|
|
144
|
-
if (!node.selectionSet) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
depth += 1;
|
|
148
|
-
maxDepth = Math.max(maxDepth, depth);
|
|
149
|
-
for (const selection of node.selectionSet.selections) {
|
|
150
|
-
traverse(selection, depth);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
for (const definition of document.definitions) {
|
|
155
|
-
if (definition.kind === Kind.OPERATION_DEFINITION) {
|
|
156
|
-
traverse(definition, 0);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return maxDepth;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private calculateComplexity(
|
|
164
|
-
document: DocumentNode,
|
|
165
|
-
options: GraphQLComplexityOptions
|
|
166
|
-
): number {
|
|
167
|
-
const costs = options.cost || {};
|
|
168
|
-
const defaultCost = options.defaultCost ?? 1;
|
|
169
|
-
let complexity = 0;
|
|
170
|
-
|
|
171
|
-
const visitNode = (node: OperationDefinitionNode | any) => {
|
|
172
|
-
if (!node.selectionSet) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
for (const selection of node.selectionSet.selections) {
|
|
177
|
-
if (selection.kind === Kind.FIELD) {
|
|
178
|
-
const fieldName = selection.name.value;
|
|
179
|
-
complexity += costs[fieldName] ?? defaultCost;
|
|
180
|
-
}
|
|
181
|
-
visitNode(selection);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
for (const definition of document.definitions) {
|
|
186
|
-
if (definition.kind === Kind.OPERATION_DEFINITION) {
|
|
187
|
-
visitNode(definition);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return complexity;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private async computeCacheKey(payload: GraphQLRequestPayload): Promise<string | null> {
|
|
195
|
-
if (!this.options.cache) {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const generator = this.options.cache.keyGenerator ??
|
|
200
|
-
((data: GraphQLRequestPayload) => JSON.stringify(data));
|
|
201
|
-
|
|
202
|
-
return generator(payload);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private buildResponse(result: ExecutionResult): Response {
|
|
206
|
-
if (result.errors && this.options.formatError) {
|
|
207
|
-
result = {
|
|
208
|
-
...result,
|
|
209
|
-
errors: result.errors.map(err => this.options.formatError!(err))
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
statusCode: result.errors ? 400 : 200,
|
|
215
|
-
headers: { 'Content-Type': 'application/json' },
|
|
216
|
-
body: JSON.stringify(result)
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private safeParseJSON(value: any) {
|
|
221
|
-
if (!value) return undefined;
|
|
222
|
-
try {
|
|
223
|
-
if (typeof value === 'string') {
|
|
224
|
-
return JSON.parse(value);
|
|
225
|
-
}
|
|
226
|
-
return value;
|
|
227
|
-
} catch {
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private renderPlayground() {
|
|
233
|
-
return `
|
|
234
|
-
<!DOCTYPE html>
|
|
235
|
-
<html>
|
|
236
|
-
<head>
|
|
237
|
-
<meta charset=utf-8/>
|
|
238
|
-
<title>GraphQL Playground</title>
|
|
239
|
-
<link rel="stylesheet" href="https://unpkg.com/graphql-playground-react/build/static/css/index.css" />
|
|
240
|
-
<link rel="shortcut icon" href="https://raw.githubusercontent.com/graphql/graphql-playground/main/packages/graphql-playground-react/public/favicon.png" />
|
|
241
|
-
<script src="https://unpkg.com/graphql-playground-react/build/static/js/middleware.js"></script>
|
|
242
|
-
</head>
|
|
243
|
-
<body>
|
|
244
|
-
<div id="root" />
|
|
245
|
-
<script>window.addEventListener('load', function () {
|
|
246
|
-
GraphQLPlayground.init(document.getElementById('root'), { endpoint: '/graphql' })
|
|
247
|
-
})</script>
|
|
248
|
-
</body>
|
|
249
|
-
</html>`;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|