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 CHANGED
@@ -1,39 +1,51 @@
1
- # nodeCore-kit
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 Helpers
12
- - Redis wrapper
13
- - SQS wrapper
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
- - `getContent`, `postContent` — typed fetch wrappers
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, and validation
22
- - JSON parse/stringify helpers
23
- - `joiValidator` for request validation
24
- - `sleep`, `formatDate`, and more
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/decode services
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
- - Optional logger injection
31
- - Compatible with `console` or structured loggers like Winston
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
- - `ServerError`
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
- joiValidator,
56
- paginate,
67
+ hashService,
68
+ jwtService,
69
+ joiMiddleware,
70
+ joiValidate,
71
+ makeRequest,
72
+ retry,
73
+ debounce,
57
74
  SQS,
58
- SqsConfig,
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
- const id = uuid.get("v4");
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
- ### Joi Validator
107
+ ---
108
+
109
+ ### Hashing
71
110
 
72
111
  ```ts
73
- const schema = {
74
- schema: { name: Joi.string().required() },
75
- data: { name: "Alice" },
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
- joiValidator(schema, false);
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
- ### Pagination
133
+ ---
134
+
135
+ ### JWT Service
82
136
 
83
137
  ```ts
84
- const { pageCount, offset } = paginate(100, 2, 10);
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
- ### SQS Adapter
167
+ ---
168
+
169
+ ### Joi Validation
88
170
 
89
171
  ```ts
90
- const config: SqsConfig = {
91
- region: "us-east-1",
92
- accessKeyId: "your-key",
93
- secretAccessKey: "your-secret",
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
- // Optional custom logger
97
- const logger = new WinstonLogger();
199
+ ### HTTP Requests
98
200
 
99
- // Initialize adapter
100
- const sqs = new SQS(config, logger);
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
- // Enqueue a message
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
- message: { hello: "world" },
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
- ### JWT Service
401
+ ---
402
+
403
+ ### S3
110
404
 
111
405
  ```ts
112
- const token = await jwtService.encode({ data: { userId: 123 } }, "mySecret");
113
- const decoded = await jwtService.decode(token, "mySecret");
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, DB). Depend only on core. |
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.