nodecore-kit 0.3.0 → 0.4.0
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 +530 -42
- package/dist/index.cjs +1534 -206
- package/dist/index.d.ts +919 -36
- package/dist/index.js +1510 -207
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,39 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nodecore-kit
|
|
2
2
|
|
|
3
3
|
**A modular backend SDK for Node.js services.**
|
|
4
4
|
|
|
5
5
|
Provides infrastructure helpers, utilities, and microservice building blocks in a clean, scalable, and framework-agnostic way.
|
|
6
6
|
|
|
7
|
+
[View on npm](https://www.npmjs.com/package/nodecore-kit)
|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## 📦 Features
|
|
10
12
|
|
|
11
|
-
### Infrastructure
|
|
12
|
-
- Redis
|
|
13
|
-
- SQS
|
|
13
|
+
### Infrastructure
|
|
14
|
+
- **Redis** — get/set/expire, scan, hash ops, auth cache helpers
|
|
15
|
+
- **SQS** — enqueue, long-poll dequeue, DLQ support, graceful stop
|
|
16
|
+
- **S3** — upload, download, stream, copy, signed URLs
|
|
17
|
+
- **Cron** — job scheduler with human-readable shorthands, overlap protection, per-job status tracking, and graceful shutdown
|
|
18
|
+
- **Mailer** — provider-agnostic email adapter with SMTP, Resend, and SendGrid support
|
|
14
19
|
- *(Future: Kafka, etc.)*
|
|
15
20
|
|
|
16
21
|
### HTTP Utilities
|
|
17
|
-
- `
|
|
22
|
+
- `makeRequest` — typed, generic fetch wrapper with retry and timeout
|
|
18
23
|
- Pagination helpers
|
|
19
24
|
|
|
20
25
|
### Core Utilities
|
|
21
|
-
- `uuid` — binary/string conversion, generation,
|
|
22
|
-
-
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
26
|
+
- `uuid` — binary/string conversion, generation, FIFO support, validation
|
|
27
|
+
- String utilities — `camelCase`, `snakeCase`, `kebabCase`, `pascalCase`, `truncate`, `maskString`, and more
|
|
28
|
+
- Validator utilities — `isEmail`, `isURL`, `isUUID`, `isEmpty`, `isNil`, and more
|
|
29
|
+
- Object utilities — `flattenObject`, `unflattenObject`
|
|
30
|
+
- Async utilities — `sleep`, `retry`, `timeout`, `debounce`, `throttle`, `memoize`, `once`
|
|
25
31
|
|
|
26
32
|
### Security
|
|
27
|
-
- JWT encode
|
|
33
|
+
- **JWT** — encode, decode, inspect, expiry helpers via `jwtService`
|
|
34
|
+
- **Hashing** — bcrypt passwords, HMAC signing, SHA fingerprinting, secure token generation via `hashService`
|
|
35
|
+
|
|
36
|
+
### Validation
|
|
37
|
+
- `joiMiddleware` — Express middleware for body/params/query/headers/files
|
|
38
|
+
- `joiValidate` — inline validator with full type inference
|
|
28
39
|
|
|
29
40
|
### Logging
|
|
30
|
-
-
|
|
31
|
-
-
|
|
41
|
+
- `WinstonLogger` — structured logging, file transports, child loggers, pretty dev output
|
|
42
|
+
- Logger interface compatible with `console` or any custom logger
|
|
32
43
|
|
|
33
44
|
### Error Handling
|
|
34
45
|
- `ValidationError`
|
|
35
|
-
- `
|
|
46
|
+
- `AuthenticationError`
|
|
36
47
|
- `NotFoundError`
|
|
48
|
+
- `ServerError`
|
|
37
49
|
|
|
38
50
|
---
|
|
39
51
|
|
|
@@ -52,65 +64,540 @@ yarn add nodecore-kit
|
|
|
52
64
|
```ts
|
|
53
65
|
import {
|
|
54
66
|
uuid,
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
hashService,
|
|
68
|
+
jwtService,
|
|
69
|
+
joiMiddleware,
|
|
70
|
+
joiValidate,
|
|
71
|
+
makeRequest,
|
|
72
|
+
retry,
|
|
73
|
+
debounce,
|
|
57
74
|
SQS,
|
|
58
|
-
|
|
75
|
+
S3,
|
|
76
|
+
Redis,
|
|
77
|
+
Cron,
|
|
78
|
+
Mailer,
|
|
79
|
+
SmtpProvider,
|
|
80
|
+
ResendProvider,
|
|
81
|
+
SendGridProvider,
|
|
59
82
|
WinstonLogger,
|
|
60
|
-
jwtService,
|
|
61
83
|
} from "nodecore-kit";
|
|
62
84
|
```
|
|
63
85
|
|
|
86
|
+
---
|
|
87
|
+
|
|
64
88
|
### UUID
|
|
65
89
|
|
|
66
90
|
```ts
|
|
67
|
-
|
|
91
|
+
// Generate
|
|
92
|
+
const id = uuid.get(); // v4 (default)
|
|
93
|
+
const id = uuid.get("v1"); // v1 time-based
|
|
94
|
+
|
|
95
|
+
// Binary conversion (optimised for MySQL storage)
|
|
96
|
+
const binary = uuid.toBinary("550e8400-e29b-41d4-a716-446655440000");
|
|
97
|
+
const str = uuid.toString(binary);
|
|
98
|
+
|
|
99
|
+
// Bulk conversion on objects
|
|
100
|
+
const record = uuid.manyToString(dbRow, ["id", "userId"]);
|
|
101
|
+
const row = uuid.manyToBinary(record, ["id", "userId"]);
|
|
102
|
+
|
|
103
|
+
// Validate
|
|
104
|
+
uuid.isValid("550e8400-e29b-41d4-a716-446655440000"); // true
|
|
68
105
|
```
|
|
69
106
|
|
|
70
|
-
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Hashing
|
|
71
110
|
|
|
72
111
|
```ts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
// Passwords — bcrypt
|
|
113
|
+
const hashed = await hashService.hash("myPassword");
|
|
114
|
+
const match = await hashService.compare("myPassword", hashed);
|
|
115
|
+
|
|
116
|
+
// Password reset / email verification tokens
|
|
117
|
+
const { token, hashed } = hashService.generateHashedToken();
|
|
118
|
+
await db.user.update({ resetToken: hashed }); // store hash
|
|
119
|
+
await email.send({ resetLink: `?token=${token}` }); // send raw token to user
|
|
77
120
|
|
|
78
|
-
|
|
121
|
+
// On reset — hash incoming token and compare
|
|
122
|
+
const incoming = hashService.sha256(req.body.token);
|
|
123
|
+
const isValid = incoming === user.resetToken;
|
|
124
|
+
|
|
125
|
+
// Webhook signature verification
|
|
126
|
+
const sig = hashService.hmac(payload, process.env.WEBHOOK_SECRET!);
|
|
127
|
+
const valid = hashService.verifyHmac(payload, secret, req.headers["x-signature"] as string);
|
|
128
|
+
|
|
129
|
+
// Content fingerprinting / cache keys (not for passwords)
|
|
130
|
+
const fingerprint = hashService.sha256("some content");
|
|
79
131
|
```
|
|
80
132
|
|
|
81
|
-
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### JWT Service
|
|
82
136
|
|
|
83
137
|
```ts
|
|
84
|
-
|
|
138
|
+
// Encode
|
|
139
|
+
const token = await jwtService.encode({
|
|
140
|
+
data: { userId: 123, role: "admin" },
|
|
141
|
+
secretKey: process.env.JWT_SECRET!,
|
|
142
|
+
expiresIn: "7d",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Decode + verify
|
|
146
|
+
const payload = await jwtService.decode<{ userId: number }>({
|
|
147
|
+
token,
|
|
148
|
+
secretKey: process.env.JWT_SECRET!,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Inspect without verifying (safe — never use for auth)
|
|
152
|
+
const claims = jwtService.inspect<{ userId: number }>(token);
|
|
153
|
+
|
|
154
|
+
// Expiry helpers
|
|
155
|
+
const expiry = jwtService.getExpiry(token); // Date | null
|
|
156
|
+
const isExpired = jwtService.isExpired(token); // boolean
|
|
157
|
+
|
|
158
|
+
// Multi-service validation with issuer/audience
|
|
159
|
+
const token = await jwtService.encode({
|
|
160
|
+
data: { userId: 1 },
|
|
161
|
+
secretKey,
|
|
162
|
+
issuer: "auth-service",
|
|
163
|
+
audience: "api-service",
|
|
164
|
+
});
|
|
85
165
|
```
|
|
86
166
|
|
|
87
|
-
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Joi Validation
|
|
88
170
|
|
|
89
171
|
```ts
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
172
|
+
import Joi from "joi";
|
|
173
|
+
|
|
174
|
+
const createUserSchema = Joi.object({
|
|
175
|
+
name: Joi.string().required(),
|
|
176
|
+
email: Joi.string().email().required(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// As Express middleware
|
|
180
|
+
router.post(
|
|
181
|
+
"/users",
|
|
182
|
+
joiMiddleware({
|
|
183
|
+
body: { schema: createUserSchema },
|
|
184
|
+
params: { schema: Joi.object({ id: Joi.string().uuid().required() }) },
|
|
185
|
+
query: { schema: paginationSchema, options: { allowUnknown: true } },
|
|
186
|
+
}),
|
|
187
|
+
createUser,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Inline / direct validation
|
|
191
|
+
const dto = joiValidate<CreateUserDto>({
|
|
192
|
+
schema: createUserSchema,
|
|
193
|
+
data: req.body,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
95
198
|
|
|
96
|
-
|
|
97
|
-
const logger = new WinstonLogger();
|
|
199
|
+
### HTTP Requests
|
|
98
200
|
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
```ts
|
|
202
|
+
// Simple GET
|
|
203
|
+
const user = await makeRequest<User>({ url: "/api/users/1" });
|
|
204
|
+
|
|
205
|
+
// POST with typed body
|
|
206
|
+
const post = await makeRequest<Post, CreatePostDto>({
|
|
207
|
+
url: "/api/posts",
|
|
208
|
+
method: "POST",
|
|
209
|
+
data: { title: "Hello", body: "World" },
|
|
210
|
+
token: "my-jwt-token",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// With retry + timeout
|
|
214
|
+
const data = await makeRequest<Data>({
|
|
215
|
+
url: "/api/slow-endpoint",
|
|
216
|
+
timeout: 5000,
|
|
217
|
+
retries: 3,
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### Async Utilities
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// sleep
|
|
227
|
+
await sleep(1000);
|
|
228
|
+
|
|
229
|
+
// retry with exponential backoff
|
|
230
|
+
const data = await retry(
|
|
231
|
+
() => fetchUser(id),
|
|
232
|
+
{ retries: 3, delay: 500, exponential: true, onError: (err, attempt) => logger.warn(`Attempt ${attempt} failed`, { err }) }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// timeout
|
|
236
|
+
const data = await timeout(fetchUser(id), 5000);
|
|
237
|
+
|
|
238
|
+
// debounce with cancel/flush
|
|
239
|
+
const search = debounce((query: string) => fetchResults(query), 300);
|
|
240
|
+
search("hello");
|
|
241
|
+
search.cancel();
|
|
242
|
+
search.flush("hello");
|
|
243
|
+
|
|
244
|
+
// throttle with trailing edge
|
|
245
|
+
const onScroll = throttle(() => updatePosition(), 100, { trailing: true });
|
|
246
|
+
|
|
247
|
+
// memoize (works with async functions too)
|
|
248
|
+
const getUser = memoize((id: number) => fetchUser(id));
|
|
249
|
+
await getUser(1); // fetches
|
|
250
|
+
await getUser(1); // returns cached — getUser.clear() to reset
|
|
251
|
+
|
|
252
|
+
// once — run exactly one time
|
|
253
|
+
const init = once(() => setupDatabase());
|
|
254
|
+
await init(); // runs
|
|
255
|
+
await init(); // returns cached result, does not run again
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### String Utilities
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
capitalize("hello world") // "Hello world"
|
|
264
|
+
camelCase("hello_world") // "helloWorld"
|
|
265
|
+
pascalCase("hello_world") // "HelloWorld"
|
|
266
|
+
snakeCase("helloWorld") // "hello_world"
|
|
267
|
+
kebabCase("helloWorld") // "hello-world"
|
|
268
|
+
splitWords("helloWorld") // ["hello", "world"]
|
|
269
|
+
truncate("Hello, world!", 8) // "Hello..."
|
|
270
|
+
truncate("Hello, world!", 8, " →") // "Hello →"
|
|
271
|
+
maskString("4111111111111234") // "************1234"
|
|
272
|
+
isBlank(" ") // true
|
|
273
|
+
reverse("hello") // "olleh"
|
|
274
|
+
countOccurrences("hello world", "l") // 3
|
|
275
|
+
normalizeWhitespace(" hello world") // "hello world"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Validator Utilities
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
isEmail("user@example.com") // true
|
|
284
|
+
isURL("https://example.com") // true (http/https only)
|
|
285
|
+
isUUID("550e8400-...") // true
|
|
286
|
+
isObject({ a: 1 }) // true
|
|
287
|
+
isArray([1, 2, 3]) // true
|
|
288
|
+
isString("hello") // true
|
|
289
|
+
isNumber(42) // true (excludes NaN, Infinity)
|
|
290
|
+
isInteger(3) // true
|
|
291
|
+
isPositive(5) // true
|
|
292
|
+
isNegative(-1) // true
|
|
293
|
+
isBoolean(false) // true
|
|
294
|
+
isDate(new Date()) // true
|
|
295
|
+
isJSON('{"a":1}') // true
|
|
296
|
+
isNil(null) // true
|
|
297
|
+
isEmpty([]) // true
|
|
298
|
+
isEmpty({}) // true
|
|
299
|
+
isEmpty(" ") // true
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Object Utilities
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// Flatten
|
|
308
|
+
flattenObject({ a: { b: { c: 1 } } })
|
|
309
|
+
// → { "a.b.c": 1 }
|
|
310
|
+
|
|
311
|
+
flattenObject({ a: { b: 1 } }, { separator: "_" })
|
|
312
|
+
// → { "a_b": 1 }
|
|
101
313
|
|
|
102
|
-
//
|
|
314
|
+
// Unflatten
|
|
315
|
+
unflattenObject({ "a.b.c": 1 })
|
|
316
|
+
// → { a: { b: { c: 1 } } }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### Redis
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const redis = new Redis("redis://localhost:6379");
|
|
325
|
+
await redis.start();
|
|
326
|
+
|
|
327
|
+
// Core ops
|
|
328
|
+
await redis.set("key", { foo: "bar" });
|
|
329
|
+
await redis.setEx("key", { foo: "bar" }, "1 hour");
|
|
330
|
+
const value = await redis.get<MyType>("key");
|
|
331
|
+
await redis.delete("key");
|
|
332
|
+
await redis.exists("key");
|
|
333
|
+
await redis.ttl("key");
|
|
334
|
+
await redis.expire("key", "30 minutes");
|
|
335
|
+
|
|
336
|
+
// Counters
|
|
337
|
+
await redis.increment("rate:user:123"); // 1, 2, 3...
|
|
338
|
+
await redis.increment("rate:user:123", "1 hour"); // sets TTL on first create
|
|
339
|
+
await redis.decrement("rate:user:123");
|
|
340
|
+
|
|
341
|
+
// Hash ops
|
|
342
|
+
await redis.hset("user:1", { name: "Alice", role: "admin" });
|
|
343
|
+
const name = await redis.hget("user:1", "name");
|
|
344
|
+
const all = await redis.hgetAll<User>("user:1");
|
|
345
|
+
await redis.hdel("user:1", "role");
|
|
346
|
+
|
|
347
|
+
// Pattern ops (safe — uses SCAN not KEYS)
|
|
348
|
+
const keys = await redis.scan("session:*");
|
|
349
|
+
const deleted = await redis.deleteByPattern("session:*");
|
|
350
|
+
|
|
351
|
+
// Auth cache helpers
|
|
352
|
+
await redis.cacheUser(user, "1 day");
|
|
353
|
+
const cached = await redis.getCachedUser(userId);
|
|
354
|
+
await redis.updateAuthData(userId, "permissions", "admin:write", "ADD");
|
|
355
|
+
await redis.updateAuthData(userId, "permissions", "admin:write", "REMOVE");
|
|
356
|
+
|
|
357
|
+
// Flush (throws in production unless forced)
|
|
358
|
+
await redis.flush(); // throws in production
|
|
359
|
+
await redis.flush(true); // override
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### SQS
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
const sqs = new SQS({
|
|
368
|
+
region: "us-east-1",
|
|
369
|
+
accessKeyId: process.env.AWS_KEY!,
|
|
370
|
+
secretAccessKey: process.env.AWS_SECRET!,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Enqueue
|
|
374
|
+
await sqs.enqueue({
|
|
375
|
+
queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue",
|
|
376
|
+
message: { event: "user.created", userId: 1 },
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// FIFO queue
|
|
103
380
|
await sqs.enqueue({
|
|
381
|
+
queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue.fifo",
|
|
382
|
+
message: { event: "order.placed" },
|
|
383
|
+
messageGroupId: "orders",
|
|
384
|
+
messageDeduplicationId: uuid.get(),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Dequeue (long-polls until stop() is called)
|
|
388
|
+
sqs.dequeue({
|
|
104
389
|
queueUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-queue",
|
|
105
|
-
|
|
390
|
+
consumerFunction: async (message) => {
|
|
391
|
+
await processMessage(message);
|
|
392
|
+
},
|
|
393
|
+
dlqUrl: "https://sqs.us-east-1.amazonaws.com/1234/my-dead-letter-queue",
|
|
394
|
+
useRedrivePolicy: false,
|
|
106
395
|
});
|
|
396
|
+
|
|
397
|
+
// Graceful shutdown
|
|
398
|
+
process.on("SIGTERM", () => sqs.stop());
|
|
107
399
|
```
|
|
108
400
|
|
|
109
|
-
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
### S3
|
|
110
404
|
|
|
111
405
|
```ts
|
|
112
|
-
const
|
|
113
|
-
|
|
406
|
+
const s3 = new S3({
|
|
407
|
+
region: "us-east-1",
|
|
408
|
+
accessKeyId: process.env.AWS_KEY!,
|
|
409
|
+
secretAccessKey: process.env.AWS_SECRET!,
|
|
410
|
+
defaultBucket: "my-default-bucket",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Upload — returns { bucket, key, url }
|
|
414
|
+
const result = await s3.upload({
|
|
415
|
+
key: "avatars/user-1.png",
|
|
416
|
+
body: buffer,
|
|
417
|
+
contentType: "image/png",
|
|
418
|
+
metadata: { uploadedBy: "user-1" },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Download as Buffer
|
|
422
|
+
const buffer = await s3.download({ key: "avatars/user-1.png" });
|
|
423
|
+
|
|
424
|
+
// Stream (preferred for large files)
|
|
425
|
+
const stream = await s3.stream({ key: "videos/clip.mp4" });
|
|
426
|
+
|
|
427
|
+
// Copy within or across buckets
|
|
428
|
+
await s3.copy({
|
|
429
|
+
sourceKey: "uploads/tmp.png",
|
|
430
|
+
destinationKey: "avatars/user-1.png",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Delete / exists
|
|
434
|
+
await s3.delete({ key: "avatars/user-1.png" });
|
|
435
|
+
const exists = await s3.exists({ key: "avatars/user-1.png" });
|
|
436
|
+
|
|
437
|
+
// Signed URLs
|
|
438
|
+
const downloadUrl = await s3.getSignedDownloadUrl({ key: "report.pdf", expiresIn: 3600 });
|
|
439
|
+
const uploadUrl = await s3.getSignedUploadUrl({ key: "avatar.png", contentType: "image/png" });
|
|
440
|
+
|
|
441
|
+
// Bucket preset — great for scoping per feature
|
|
442
|
+
const avatars = s3.bucket("user-avatars");
|
|
443
|
+
await avatars.upload({ key: "user-1.png", body: buffer });
|
|
444
|
+
await avatars.getSignedDownloadUrl({ key: "user-1.png" });
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
### Cron
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
const cron = new Cron(logger); // logger is optional
|
|
453
|
+
|
|
454
|
+
// Register with human shorthand
|
|
455
|
+
cron.register({
|
|
456
|
+
name: "send-digest",
|
|
457
|
+
schedule: "every day at noon",
|
|
458
|
+
timezone: "America/New_York",
|
|
459
|
+
handler: async () => {
|
|
460
|
+
await sendDigestEmails();
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Register with raw cron expression
|
|
465
|
+
cron.register({
|
|
466
|
+
name: "sync-inventory",
|
|
467
|
+
schedule: "*/15 * * * *",
|
|
468
|
+
handler: async () => { await syncInventory(); },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Run immediately on registration
|
|
472
|
+
cron.register({
|
|
473
|
+
name: "warm-cache",
|
|
474
|
+
schedule: "every hour",
|
|
475
|
+
runOnInit: true,
|
|
476
|
+
handler: async () => { await warmCache(); },
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Manual trigger (e.g. from an admin endpoint)
|
|
480
|
+
await cron.run("send-digest");
|
|
481
|
+
|
|
482
|
+
// Stop / start individual jobs
|
|
483
|
+
cron.stop("sync-inventory");
|
|
484
|
+
cron.start("sync-inventory");
|
|
485
|
+
|
|
486
|
+
// Replace a job's schedule at runtime
|
|
487
|
+
cron.replace({ name: "send-digest", schedule: "every 30 minutes", handler });
|
|
488
|
+
|
|
489
|
+
// Introspect
|
|
490
|
+
cron.status("send-digest"); // { name, schedule, running, lastRun, executionCount, errorCount }
|
|
491
|
+
cron.statusAll(); // all jobs
|
|
492
|
+
|
|
493
|
+
// Graceful shutdown
|
|
494
|
+
process.on("SIGTERM", () => cron.stopAll());
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Supported shorthands:
|
|
498
|
+
|
|
499
|
+
| Shorthand | Expression |
|
|
500
|
+
|---|---|
|
|
501
|
+
| `"every minute"` | `* * * * *` |
|
|
502
|
+
| `"every 5 minutes"` | `*/5 * * * *` |
|
|
503
|
+
| `"every 15 minutes"` | `*/15 * * * *` |
|
|
504
|
+
| `"every 30 minutes"` | `*/30 * * * *` |
|
|
505
|
+
| `"every hour"` | `0 * * * *` |
|
|
506
|
+
| `"every 6 hours"` | `0 */6 * * *` |
|
|
507
|
+
| `"every 12 hours"` | `0 */12 * * *` |
|
|
508
|
+
| `"every day"` | `0 0 * * *` |
|
|
509
|
+
| `"every day at noon"` | `0 12 * * *` |
|
|
510
|
+
| `"every week"` | `0 0 * * 0` |
|
|
511
|
+
| `"every month"` | `0 0 1 * *` |
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
### Mailer
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
// SMTP (Nodemailer)
|
|
519
|
+
const mailer = new Mailer(
|
|
520
|
+
new SmtpProvider({
|
|
521
|
+
host: "smtp.gmail.com",
|
|
522
|
+
auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASS! },
|
|
523
|
+
defaultFrom: "noreply@myapp.com",
|
|
524
|
+
}),
|
|
525
|
+
{},
|
|
526
|
+
logger, // optional
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Resend
|
|
530
|
+
const mailer = new Mailer(
|
|
531
|
+
new ResendProvider({
|
|
532
|
+
apiKey: process.env.RESEND_KEY!,
|
|
533
|
+
defaultFrom: "noreply@myapp.com",
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// SendGrid
|
|
538
|
+
const mailer = new Mailer(
|
|
539
|
+
new SendGridProvider({ apiKey: process.env.SENDGRID_KEY! }),
|
|
540
|
+
{ defaultFrom: "noreply@myapp.com" },
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Send
|
|
544
|
+
await mailer.send({
|
|
545
|
+
to: "user@example.com",
|
|
546
|
+
subject: "Welcome!",
|
|
547
|
+
html: "<h1>Hello Alice</h1>",
|
|
548
|
+
text: "Hello Alice", // plain text fallback
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Multiple recipients + attachments
|
|
552
|
+
await mailer.send({
|
|
553
|
+
to: ["alice@example.com", "bob@example.com"],
|
|
554
|
+
cc: "manager@example.com",
|
|
555
|
+
subject: "Your invoice",
|
|
556
|
+
html: "<p>Please find your invoice attached.</p>",
|
|
557
|
+
attachments: [{ filename: "invoice.pdf", content: pdfBuffer, contentType: "application/pdf" }],
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Runtime provider swap (fallback)
|
|
561
|
+
try {
|
|
562
|
+
await mailer.send(mail);
|
|
563
|
+
} catch {
|
|
564
|
+
mailer.setProvider(new SendGridProvider({ apiKey: process.env.SENDGRID_KEY! }));
|
|
565
|
+
await mailer.send(mail);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Preview mode — logs instead of sending (auto-enabled in development)
|
|
569
|
+
const mailer = new Mailer(provider, { previewMode: true });
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Logger
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
const logger = new WinstonLogger({
|
|
578
|
+
service: "auth-service",
|
|
579
|
+
level: "debug",
|
|
580
|
+
file: {
|
|
581
|
+
path: "logs/combined.log",
|
|
582
|
+
errorPath: "logs/error.log",
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
logger.info("Server started", { port: 3000 });
|
|
587
|
+
logger.error("DB connection failed", err); // stack trace preserved
|
|
588
|
+
|
|
589
|
+
// Child logger — attach request context to all logs in scope
|
|
590
|
+
app.use((req, res, next) => {
|
|
591
|
+
req.log = logger.child({ requestId: req.id, path: req.path });
|
|
592
|
+
next();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
req.log.info("Request received");
|
|
596
|
+
// → { requestId: "abc-123", path: "/users", message: "Request received" }
|
|
597
|
+
|
|
598
|
+
// Runtime level control
|
|
599
|
+
logger.setLevel("debug");
|
|
600
|
+
logger.isLevelEnabled("debug"); // true
|
|
114
601
|
```
|
|
115
602
|
|
|
116
603
|
---
|
|
@@ -120,7 +607,7 @@ const decoded = await jwtService.decode(token, "mySecret");
|
|
|
120
607
|
| Layer | Description |
|
|
121
608
|
|---|---|
|
|
122
609
|
| **Core** | Pure utilities, errors, and types. Independent from adapters or transports. |
|
|
123
|
-
| **Adapters** | External services (SQS, Redis,
|
|
610
|
+
| **Adapters** | External services (SQS, Redis, S3, Cron, Mailer). Depend only on core. |
|
|
124
611
|
| **Transport** | HTTP layers. May depend on core. |
|
|
125
612
|
| **Security** | JWT, hashing. Depends only on core. |
|
|
126
613
|
| **Logger** | Optional, adapter-friendly, injected as a dependency. |
|
|
@@ -136,3 +623,4 @@ const decoded = await jwtService.decode(token, "mySecret");
|
|
|
136
623
|
- **Scalable** — Built for microservices and multi-service architectures.
|
|
137
624
|
- **Plug-n-Play** — Default logging works with `console`, but advanced logging can be injected.
|
|
138
625
|
- **Framework-Agnostic** — Works with Express, Fastify, NestJS, or bare Node.js.
|
|
626
|
+
- **Type-Safe** — Generics throughout so you get full TypeScript inference without casting.
|