nodecore-kit 0.1.0 → 0.3.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/dist/index.js CHANGED
@@ -1,4 +1,178 @@
1
- // src/utils/index.ts
1
+ // src/transport/http.ts
2
+ import Axios from "axios";
3
+ var makeRequest = async ({
4
+ url,
5
+ method = "GET",
6
+ headers = {},
7
+ token = void 0,
8
+ data = void 0
9
+ }) => {
10
+ try {
11
+ headers["X-Requested-With"] = "XMLHttpRequest";
12
+ token && (headers["Authorization"] = token);
13
+ const payload = {
14
+ method,
15
+ url,
16
+ headers
17
+ };
18
+ if (data)
19
+ payload.data = data;
20
+ const result = await Axios(payload);
21
+ return result.data;
22
+ } catch (err) {
23
+ throw err.response ? { ...err.response.data, httpStatusCode: err.response.status } : err;
24
+ }
25
+ };
26
+
27
+ // src/core/error.ts
28
+ var HTTP_STATUS = {
29
+ OK: { code: 200, message: "OK" },
30
+ CREATED: { code: 201, message: "Created" },
31
+ NO_CONTENT: { code: 204, message: "No Content" },
32
+ BAD_REQUEST: { code: 400, message: "Bad Request" },
33
+ UNAUTHORIZED: { code: 401, message: "Unauthorized" },
34
+ FORBIDDEN: { code: 403, message: "Forbidden" },
35
+ NOT_FOUND: { code: 404, message: "Not Found" },
36
+ CONFLICT: { code: 409, message: "Conflict" },
37
+ UNPROCESSABLE_ENTITY: { code: 422, message: "Unprocessable Entity" },
38
+ TOKEN_EXPIRED: { code: 498, message: "Token Expired" },
39
+ TOKEN_INVALID: { code: 499, message: "Token Invalid" },
40
+ SERVER_ERROR: { code: 500, message: "Internal Server Error" }
41
+ };
42
+ var HTTP_STATUS_CODE_ERROR = {
43
+ 400: "VALIDATION_ERROR",
44
+ 401: "AUTHENTICATION_ERROR",
45
+ 402: "PAYMENT_REQUIRED_ERROR",
46
+ 403: "AUTHORIZATION_ERROR",
47
+ 404: "NOT_FOUND",
48
+ 409: "ENTRY_EXISTS",
49
+ 422: "VALIDATION_ERROR",
50
+ 498: "TOKEN_EXPIRED",
51
+ 499: "TOKEN_INVALID",
52
+ 500: "FATAL_ERROR"
53
+ };
54
+ var AppError = class extends Error {
55
+ constructor(status, message, errorCode, meta) {
56
+ super(message || status.message);
57
+ this.name = new.target.name;
58
+ this.statusCode = status.code;
59
+ this.statusMessage = status.message;
60
+ this.errorCode = errorCode;
61
+ this.meta = meta;
62
+ Error.captureStackTrace(this, new.target);
63
+ }
64
+ };
65
+ var ValidationError = class extends AppError {
66
+ constructor(message, meta) {
67
+ super(
68
+ HTTP_STATUS.UNPROCESSABLE_ENTITY,
69
+ message,
70
+ "VALIDATION_ERROR",
71
+ meta
72
+ );
73
+ }
74
+ };
75
+ var AuthenticationError = class extends AppError {
76
+ constructor(message, meta) {
77
+ super(
78
+ HTTP_STATUS.UNAUTHORIZED,
79
+ message,
80
+ "AUTHENTICATION_ERROR",
81
+ meta
82
+ );
83
+ }
84
+ };
85
+ var AuthorizationError = class extends AppError {
86
+ constructor(message, meta) {
87
+ super(HTTP_STATUS.FORBIDDEN, message, "AUTHORIZATION_ERROR", meta);
88
+ }
89
+ };
90
+ var NotFoundError = class extends AppError {
91
+ constructor(message, meta) {
92
+ super(HTTP_STATUS.NOT_FOUND, message, "NOT_FOUND", meta);
93
+ }
94
+ };
95
+ var TokenExpiredError = class extends AppError {
96
+ constructor(message, meta) {
97
+ super(HTTP_STATUS.TOKEN_EXPIRED, message, "TOKEN_EXPIRED", meta);
98
+ }
99
+ };
100
+ var TokenInvalidError = class extends AppError {
101
+ constructor(message, meta) {
102
+ super(HTTP_STATUS.TOKEN_INVALID, message, "TOKEN_INVALID", meta);
103
+ }
104
+ };
105
+ var BadRequestError = class extends AppError {
106
+ constructor(message, meta) {
107
+ super(HTTP_STATUS.BAD_REQUEST, message, "BAD_REQUEST", meta);
108
+ }
109
+ };
110
+ var ServerError = class extends AppError {
111
+ constructor(message, meta) {
112
+ super(HTTP_STATUS.SERVER_ERROR, message, "SERVER_ERROR", meta);
113
+ }
114
+ };
115
+ var ExistingError = class extends AppError {
116
+ constructor(message, meta) {
117
+ super(HTTP_STATUS.CONFLICT, message, "ENTRY_EXISTS", meta);
118
+ }
119
+ };
120
+ var NoContent = class extends AppError {
121
+ constructor(message, meta) {
122
+ super(HTTP_STATUS.NO_CONTENT, message, "NO_CONTENT", meta);
123
+ }
124
+ };
125
+ var errorHandler = (err, ERROR_TYPE = "FATAL_ERROR", service = "Unknown Service") => {
126
+ const response = {
127
+ success: false,
128
+ message: "Something went wrong",
129
+ error: ERROR_TYPE,
130
+ httpStatusCode: 500,
131
+ service
132
+ };
133
+ try {
134
+ if (!err)
135
+ return response;
136
+ if (err instanceof AppError) {
137
+ return {
138
+ ...response,
139
+ message: err.message,
140
+ error: err.errorCode || err.name,
141
+ httpStatusCode: err.statusCode
142
+ };
143
+ }
144
+ if (err.isAxiosError) {
145
+ return {
146
+ ...response,
147
+ message: err?.response?.data?.message || err.message || response.message,
148
+ error: err?.response?.data?.error || HTTP_STATUS_CODE_ERROR[err?.response?.status] || ERROR_TYPE,
149
+ httpStatusCode: err?.response?.status || 500
150
+ };
151
+ }
152
+ if (err instanceof Error) {
153
+ return {
154
+ ...response,
155
+ message: err.message,
156
+ error: err.name
157
+ };
158
+ }
159
+ if (typeof err === "string") {
160
+ return {
161
+ ...response,
162
+ message: err
163
+ };
164
+ }
165
+ return response;
166
+ } catch {
167
+ return response;
168
+ }
169
+ };
170
+ var expressErrorMiddleware = () => (err, req, res, next) => {
171
+ const error = errorHandler(err);
172
+ res.status(error.httpStatusCode).json(error);
173
+ };
174
+
175
+ // src/core/utils.ts
2
176
  var paginate = (totalCount, currentPage, perPage) => {
3
177
  const previousPage = currentPage - 1;
4
178
  return {
@@ -6,49 +180,486 @@ var paginate = (totalCount, currentPage, perPage) => {
6
180
  offset: currentPage > 1 ? previousPage * perPage : 0
7
181
  };
8
182
  };
183
+ var formatDate = (date) => {
184
+ const day = date.getDate().toString().padStart(2, "0");
185
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
186
+ const year = date.getFullYear();
187
+ return `${day}/${month}/${year}`;
188
+ };
189
+ var parseJSON = (value) => {
190
+ try {
191
+ return JSON.parse(value);
192
+ } catch (err) {
193
+ return value;
194
+ }
195
+ };
196
+ var stringifyJSON = (value) => {
197
+ try {
198
+ return JSON.stringify(value);
199
+ } catch (err) {
200
+ return value;
201
+ }
202
+ };
203
+ var isObject = (val) => val && typeof val === "object" && !Array.isArray(val);
204
+ var sleep = (ms) => new Promise((res) => setTimeout(res, ms));
205
+ var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
206
+ var isEmpty = (val) => val === null || val === void 0 || typeof val === "object" && Object.keys(val).length === 0 || typeof val === "string" && val.trim() === "";
207
+
208
+ // src/core/uuid.ts
209
+ import { v1 as uuidV1, v4 as uuidV4, validate as UUIDValidaton } from "uuid";
210
+ var uuid = {
211
+ toBinary: (uuid2) => {
212
+ if (!uuid2)
213
+ uuid2 = uuidV1();
214
+ else if (typeof uuid2 !== "string" && Buffer.isBuffer(uuid2))
215
+ return uuid2;
216
+ const buf = Buffer.from(uuid2.replace(/-/g, ""), "hex");
217
+ return Buffer.concat([
218
+ buf.subarray(6, 8),
219
+ buf.subarray(4, 6),
220
+ buf.subarray(0, 4),
221
+ buf.subarray(8, 16)
222
+ ]);
223
+ },
224
+ toString: (binary) => {
225
+ if (!binary)
226
+ throw new Error("Kindly supply binary UUID value");
227
+ if (typeof binary === "string")
228
+ return binary;
229
+ return [
230
+ binary.toString("hex", 4, 8),
231
+ binary.toString("hex", 2, 4),
232
+ binary.toString("hex", 0, 2),
233
+ binary.toString("hex", 8, 10),
234
+ binary.toString("hex", 10, 16)
235
+ ].join("-");
236
+ },
237
+ get: (version) => {
238
+ const uuid2 = {
239
+ v1: uuidV1(),
240
+ v4: uuidV4()
241
+ };
242
+ return uuid2[version || "v1"];
243
+ },
244
+ isValid: (uuid2) => UUIDValidaton(uuid2),
245
+ manyToString: (data, keys = []) => {
246
+ if (!data)
247
+ return;
248
+ keys.forEach((key) => {
249
+ if (data[key])
250
+ data[key] = uuid.toString(data[key]);
251
+ });
252
+ return data;
253
+ },
254
+ manyToBinary: (data, keys = []) => {
255
+ if (!data)
256
+ return;
257
+ keys.forEach((key) => {
258
+ if (data[key])
259
+ data[key] = uuid.toBinary(data[key]);
260
+ });
261
+ return data;
262
+ }
263
+ };
9
264
 
10
- // src/http/index.ts
11
- import axios from "axios";
12
- var makeRequest = async (options) => {
13
- const { url, method = "GET", token, headers = {}, ...rest } = options;
14
- const requestHeaders = {
15
- "X-Requested-With": "XMLHttpRequest",
16
- ...headers
265
+ // src/transport/express/joiValidator.ts
266
+ var validate = (schema, object, option = { abortEarly: true, allowUnknown: false }) => {
267
+ const check = schema.validate(object, option);
268
+ if (check.error) {
269
+ throw new ValidationError(check.error.details[0].message);
270
+ }
271
+ return check.value;
272
+ };
273
+ function joiValidator(constraint, isMiddleware = true) {
274
+ if (!constraint)
275
+ throw new ValidationError(
276
+ "Kindly supply validation schema to joiValidator"
277
+ );
278
+ if (!isMiddleware) {
279
+ return validate(constraint.schema, constraint.data, constraint.option);
280
+ }
281
+ return async (req, res, next) => {
282
+ try {
283
+ if (constraint.body) {
284
+ req.body = validate(
285
+ constraint.body.schema,
286
+ req.body,
287
+ constraint.body.options
288
+ );
289
+ }
290
+ if (constraint.params)
291
+ req.params = validate(
292
+ constraint.params.schema,
293
+ req.params,
294
+ constraint.params.options
295
+ );
296
+ if (constraint.query)
297
+ req.query = validate(
298
+ constraint.query.schema,
299
+ req.query,
300
+ constraint.query.options
301
+ );
302
+ if (constraint.headers)
303
+ req.headers = validate(
304
+ constraint.headers.schema,
305
+ req.headers,
306
+ constraint.headers.options
307
+ );
308
+ return next();
309
+ } catch (err) {
310
+ next(err);
311
+ }
17
312
  };
18
- if (token) {
19
- requestHeaders["Authorization"] = token;
313
+ }
314
+
315
+ // src/adapters/redis.ts
316
+ import RedisClient from "ioredis";
317
+ var Redis = class {
318
+ constructor(url, options = {}) {
319
+ if (!url)
320
+ throw new ValidationError("Redis connection URL is required");
321
+ this.client = new RedisClient(url, {
322
+ maxRetriesPerRequest: 3,
323
+ enableReadyCheck: true,
324
+ lazyConnect: true,
325
+ ...options
326
+ });
327
+ this.registerListeners();
20
328
  }
21
- try {
22
- const response = await axios({
23
- url,
24
- method,
25
- headers: requestHeaders,
26
- ...rest
27
- });
28
- return response.data;
29
- } catch (error) {
30
- if (error.response) {
31
- const enhancedError = {
32
- ...error.response.data,
33
- httpStatusCode: error.response.status,
34
- headers: error.response.headers
35
- };
36
- throw enhancedError;
329
+ registerListeners() {
330
+ this.client.on("connect", () => {
331
+ console.info("\u{1F534} Redis connected");
332
+ });
333
+ this.client.on("ready", () => {
334
+ console.info("\u{1F7E2} Redis ready");
335
+ });
336
+ this.client.on("error", (err) => {
337
+ console.error("\u{1F534} Redis error:", err);
338
+ });
339
+ this.client.on("close", () => {
340
+ console.warn("\u{1F7E0} Redis connection closed");
341
+ });
342
+ this.client.on("reconnecting", () => {
343
+ console.warn("\u{1F7E1} Redis reconnecting...");
344
+ });
345
+ }
346
+ async start() {
347
+ try {
348
+ if (this.client.status === "ready")
349
+ return;
350
+ await this.client.connect();
351
+ } catch (err) {
352
+ throw new ServerError("Failed to connect to Redis", { cause: err });
353
+ }
354
+ }
355
+ async disconnect() {
356
+ try {
357
+ if (this.client.status !== "end") {
358
+ await this.client.quit();
359
+ }
360
+ } catch {
361
+ await this.client.disconnect();
362
+ }
363
+ }
364
+ async keys(pattern) {
365
+ if (!pattern || typeof pattern !== "string") {
366
+ throw new ValidationError("Redis key pattern must be a string");
367
+ }
368
+ return this.client.keys(pattern);
369
+ }
370
+ serialize(data) {
371
+ if (typeof data === "string")
372
+ return data;
373
+ if (typeof data === "number")
374
+ return String(data);
375
+ return JSON.stringify(data);
376
+ }
377
+ deserialize(data, parse = true) {
378
+ if (!parse || !data)
379
+ return data;
380
+ return parseJSON(data);
381
+ }
382
+ async set(key, data) {
383
+ if (!key || typeof key !== "string") {
384
+ throw new ValidationError("Redis key must be a string");
385
+ }
386
+ return this.client.set(key, this.serialize(data));
387
+ }
388
+ async setEx(key, data, duration) {
389
+ if (!key || typeof key !== "string") {
390
+ throw new ValidationError("Redis key must be a string");
391
+ }
392
+ const ttl = this.parseDuration(duration);
393
+ return this.client.setex(key, ttl, this.serialize(data));
394
+ }
395
+ async get(key, parse = true) {
396
+ if (!key || typeof key !== "string") {
397
+ throw new ValidationError("Redis key must be a string");
398
+ }
399
+ const data = await this.client.get(key);
400
+ return this.deserialize(data, parse);
401
+ }
402
+ async delete(key) {
403
+ if (!key || typeof key !== "string") {
404
+ throw new ValidationError("Redis key must be a string");
405
+ }
406
+ return Boolean(await this.client.del(key));
407
+ }
408
+ async deleteAll(prefix) {
409
+ const keys = await this.keys(prefix);
410
+ if (!keys.length)
411
+ return 0;
412
+ return this.client.del(...keys);
413
+ }
414
+ async exists(key) {
415
+ return Boolean(await this.client.exists(key));
416
+ }
417
+ async ttl(key) {
418
+ return this.client.ttl(key);
419
+ }
420
+ async expire(key, duration) {
421
+ const ttl = this.parseDuration(duration);
422
+ return Boolean(await this.client.expire(key, ttl));
423
+ }
424
+ async flush() {
425
+ await this.client.flushdb();
426
+ }
427
+ // ───────────────────────────────
428
+ // Auth Cache Helpers
429
+ // ───────────────────────────────
430
+ async getCachedUser(id, throwError = true) {
431
+ const userToken = `${id}-token`;
432
+ const user = await this.get(userToken);
433
+ if (!user && throwError) {
434
+ throw new AuthenticationError("Kindly login, user not found");
435
+ }
436
+ return user;
437
+ }
438
+ async cacheUser(user, ttl = "1 day") {
439
+ if (!user?.id || !user?.tokenRef) {
440
+ throw new ValidationError("Invalid user object for caching");
37
441
  }
38
- throw error;
442
+ await Promise.all([
443
+ this.setEx(user.tokenRef, user, ttl),
444
+ this.setEx(`${user.id}-token`, user, ttl)
445
+ ]);
446
+ }
447
+ async updateAuthData(userId, key, value, action = "ADD") {
448
+ const user = await this.getCachedUser(userId, false);
449
+ if (!user)
450
+ return null;
451
+ if (!Array.isArray(user[key]))
452
+ return user;
453
+ if (action === "ADD" && !user[key].includes(value)) {
454
+ user[key].push(value);
455
+ }
456
+ if (action === "REMOVE") {
457
+ user[key] = user[key].filter((v) => v !== value);
458
+ }
459
+ await this.cacheUser(user);
460
+ return user;
461
+ }
462
+ // ───────────────────────────────
463
+ // Helpers
464
+ // ───────────────────────────────
465
+ parseDuration(duration) {
466
+ if (typeof duration === "number")
467
+ return duration;
468
+ const [valueStr, unit] = duration.split(" ");
469
+ const value = Number(valueStr);
470
+ if (Number.isNaN(value)) {
471
+ throw new ValidationError(`Invalid duration format: ${duration}`);
472
+ }
473
+ switch (unit) {
474
+ case "days":
475
+ case "day":
476
+ return value * 86400;
477
+ case "hours":
478
+ case "hour":
479
+ return value * 3600;
480
+ case "minutes":
481
+ case "minute":
482
+ return value * 60;
483
+ case "seconds":
484
+ case "second":
485
+ return value;
486
+ default:
487
+ throw new ValidationError(`Invalid duration unit: ${unit}`);
488
+ }
489
+ }
490
+ };
491
+
492
+ // src/adapters/sqs.ts
493
+ import AWS from "aws-sdk";
494
+ var SQS = class {
495
+ constructor(config, logger) {
496
+ this.logger = logger || {
497
+ info: (msg, meta) => console.info(msg, meta),
498
+ error: (msg, meta) => console.error(msg, meta),
499
+ warn: (msg, meta) => console.warn(msg, meta),
500
+ debug: (msg, meta) => console.debug(msg, meta)
501
+ };
502
+ this.client = new AWS.SQS({
503
+ region: config.region,
504
+ accessKeyId: config.accessKeyId,
505
+ secretAccessKey: config.secretAccessKey
506
+ });
507
+ this.logger.info("SQS client initialized", { region: config.region });
508
+ }
509
+ async enqueue({ queueUrl, message }) {
510
+ try {
511
+ await this.client.sendMessage({
512
+ QueueUrl: queueUrl,
513
+ MessageBody: typeof message === "string" ? message : JSON.stringify(message)
514
+ }).promise();
515
+ this.logger.info("Message enqueued", { queueUrl });
516
+ return true;
517
+ } catch (err) {
518
+ this.logger.error("SQSEnqueueError", { err, queueUrl });
519
+ throw new ServerError("Failed to enqueue SQS message", { cause: err });
520
+ }
521
+ }
522
+ async dequeue(fields) {
523
+ const {
524
+ queueUrl,
525
+ consumerFunction,
526
+ dlqUrl,
527
+ maxNumberOfMessages = 10,
528
+ waitTimeSeconds = 20
529
+ } = fields;
530
+ while (true) {
531
+ try {
532
+ const { Messages } = await this.client.receiveMessage({
533
+ QueueUrl: queueUrl,
534
+ MaxNumberOfMessages: maxNumberOfMessages,
535
+ WaitTimeSeconds: waitTimeSeconds
536
+ }).promise();
537
+ if (Messages?.length) {
538
+ for (const { Body, ReceiptHandle } of Messages) {
539
+ if (!Body || !ReceiptHandle)
540
+ continue;
541
+ try {
542
+ const message = parseJSON(Body);
543
+ await consumerFunction(message);
544
+ } catch (err) {
545
+ this.logger.error("SQSConsumerError", { err, queueUrl });
546
+ if (dlqUrl) {
547
+ await this.enqueue({ queueUrl: dlqUrl, message: Body });
548
+ }
549
+ } finally {
550
+ await this.client.deleteMessage({
551
+ QueueUrl: queueUrl,
552
+ ReceiptHandle
553
+ }).promise();
554
+ }
555
+ }
556
+ }
557
+ } catch (err) {
558
+ this.logger.error("SQSPollingError", { err, queueUrl });
559
+ }
560
+ }
561
+ }
562
+ };
563
+
564
+ // src/adapters/loggers/winston.ts
565
+ import winston from "winston";
566
+ var WinstonLogger = class {
567
+ constructor() {
568
+ this.logger = winston.createLogger({
569
+ transports: [new winston.transports.Console()]
570
+ });
571
+ }
572
+ info(message, meta) {
573
+ this.logger.info(message, meta);
574
+ }
575
+ error(message, meta) {
576
+ this.logger.error(message, meta);
577
+ }
578
+ warn(message, meta) {
579
+ this.logger.warn(message, meta);
580
+ }
581
+ debug(message, meta) {
582
+ this.logger.debug(message, meta);
583
+ }
584
+ };
585
+
586
+ // src/security/jwt.ts
587
+ import jwt from "jsonwebtoken";
588
+ var jwtService = {
589
+ async encode({
590
+ data,
591
+ secretKey,
592
+ expiresIn = "24h",
593
+ algorithm = "HS256"
594
+ }) {
595
+ if (!secretKey) {
596
+ throw new ValidationError("Secret key is required for JWT encoding");
597
+ }
598
+ const options = {
599
+ expiresIn,
600
+ algorithm
601
+ };
602
+ return new Promise((resolve, reject) => {
603
+ jwt.sign(data, secretKey, options, (err, token) => {
604
+ if (err || !token)
605
+ return reject(err);
606
+ resolve(token);
607
+ });
608
+ });
609
+ },
610
+ async decode({
611
+ token,
612
+ secretKey,
613
+ algorithms
614
+ }) {
615
+ if (!secretKey) {
616
+ throw new ValidationError("Secret key is required for JWT verification");
617
+ }
618
+ if (!token) {
619
+ throw new ValidationError("JWT token is required");
620
+ }
621
+ const options = {};
622
+ if (algorithms) {
623
+ options.algorithms = algorithms;
624
+ }
625
+ return new Promise((resolve, reject) => {
626
+ jwt.verify(token, secretKey, options, (err, decoded) => {
627
+ if (err)
628
+ return reject(err);
629
+ resolve(decoded);
630
+ });
631
+ });
39
632
  }
40
633
  };
41
- var get = (url, options) => makeRequest({ ...options, url, method: "GET" });
42
- var post = (url, data, options) => makeRequest({ ...options, url, method: "POST", data });
43
- var put = (url, data, options) => makeRequest({ ...options, url, method: "PUT", data });
44
- var patch = (url, data, options) => makeRequest({ ...options, url, method: "PATCH", data });
45
- var del = (url, options) => makeRequest({ ...options, url, method: "DELETE" });
46
634
  export {
47
- del,
48
- get,
635
+ AppError,
636
+ AuthenticationError,
637
+ AuthorizationError,
638
+ BadRequestError,
639
+ ExistingError,
640
+ HTTP_STATUS,
641
+ HTTP_STATUS_CODE_ERROR,
642
+ NoContent,
643
+ NotFoundError,
644
+ Redis,
645
+ SQS,
646
+ ServerError,
647
+ TokenExpiredError,
648
+ TokenInvalidError,
649
+ ValidationError,
650
+ WinstonLogger,
651
+ capitalize,
652
+ errorHandler,
653
+ expressErrorMiddleware,
654
+ formatDate,
655
+ isEmpty,
656
+ isObject,
657
+ joiValidator,
658
+ jwtService,
49
659
  makeRequest,
50
660
  paginate,
51
- patch,
52
- post,
53
- put
661
+ parseJSON,
662
+ sleep,
663
+ stringifyJSON,
664
+ uuid
54
665
  };