@mattrika/server-utils 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -8,7 +8,6 @@ var zodOpenapi = require('@hono/zod-openapi');
8
8
  var crypto = require('crypto');
9
9
  var argon2 = require('@node-rs/argon2');
10
10
  var httpStatusCodes = require('stoker/http-status-codes');
11
- var esToolkit = require('es-toolkit');
12
11
  var jwt = require('hono/jwt');
13
12
 
14
13
  const DEFAULT_PAGE_SIZE = 20;
@@ -105,6 +104,9 @@ function pinoLogger() {
105
104
  return pino({
106
105
  level: process.env?.['PINO_LOG_LEVEL'] || 'info',
107
106
  timestamp: pino.stdTimeFunctions.isoTime,
107
+ serializers: {
108
+ err: pino.stdSerializers.errWithCause
109
+ },
108
110
  formatters: {
109
111
  bindings: bindings => ({
110
112
  pid: bindings?.['pid'],
@@ -293,7 +295,8 @@ function ApiPdfResponse(description) {
293
295
  function throwHttpError(error, defaultMessage = 'Something went wrong') {
294
296
  const code = error instanceof Error ? Number.isNaN(error.cause) ? httpStatusCodes.INTERNAL_SERVER_ERROR : error.cause : httpStatusCodes.INTERNAL_SERVER_ERROR;
295
297
  throw new httpException.HTTPException(code, {
296
- message: error.message ?? defaultMessage
298
+ message: error.message ?? defaultMessage,
299
+ cause: error
297
300
  });
298
301
  }
299
302
  function isUniqueViolationPg(error) {
@@ -310,6 +313,12 @@ function toErrorCode(value) {
310
313
  function onErrorFn(debugLog = true) {
311
314
  return async (err, c) => {
312
315
  const statusCode = 'status' in err ? toErrorCode(err.status) : 'cause' in err ? toErrorCode(err.cause) : httpStatusCodes.INTERNAL_SERVER_ERROR;
316
+ let originalError = err;
317
+ const seen = new Set([err]);
318
+ while (originalError.cause instanceof Error && !seen.has(originalError.cause)) {
319
+ originalError = originalError.cause;
320
+ seen.add(originalError);
321
+ }
313
322
  const logger = c.get('logger');
314
323
  logger.error({
315
324
  err,
@@ -317,12 +326,12 @@ function onErrorFn(debugLog = true) {
317
326
  method: c.req.method
318
327
  }, 'Request failed');
319
328
  if (debugLog) {
320
- console.error(JSON.stringify(err, undefined, 2));
329
+ console.error(err);
321
330
  }
322
331
  return c.json({
323
332
  data: null,
324
333
  error: !debugLog ? undefined : err,
325
- message: err.cause ?? err.message,
334
+ message: originalError.message || err.message,
326
335
  success: false
327
336
  }, statusCode);
328
337
  };
@@ -338,19 +347,163 @@ const notFoundFn = c => {
338
347
  };
339
348
 
340
349
  function buildPaginationResponse(page, size, total) {
341
- const numPage = esToolkit.clamp(Number(page), 1) || 1;
342
- const positivePage = numPage < 1 ? 1 : numPage;
343
- const numSize = esToolkit.clamp(Number(size), DEFAULT_PAGE_SIZE);
344
- const numTotal = esToolkit.clamp(Number(total), 0);
345
- const totalPages = Math.ceil(numTotal / numSize);
350
+ const numPage = Number(page);
351
+ const positivePage = Number.isFinite(numPage) ? Math.max(numPage, 1) : 1;
352
+ const numSize = Number(size);
353
+ const positiveSize = Number.isFinite(numSize) && numSize > 0 ? numSize : DEFAULT_PAGE_SIZE;
354
+ const numTotal = Number(total);
355
+ const positiveTotal = Number.isFinite(numTotal) ? Math.max(numTotal, 0) : 0;
356
+ const totalPages = Math.ceil(positiveTotal / positiveSize);
346
357
  return {
347
358
  page: positivePage,
348
- size: numSize,
349
- totalItems: numTotal,
359
+ size: positiveSize,
360
+ totalItems: positiveTotal,
350
361
  totalPages
351
362
  };
352
363
  }
353
364
 
365
+ /**
366
+ * @module constants
367
+ * @summary Useful constants
368
+ * @description
369
+ * Collection of useful date constants.
370
+ *
371
+ * The constants could be imported from `date-fns/constants`:
372
+ *
373
+ * ```ts
374
+ * import { maxTime, minTime } from "./constants/date-fns/constants";
375
+ *
376
+ * function isAllowedTime(time) {
377
+ * return time <= maxTime && time >= minTime;
378
+ * }
379
+ * ```
380
+ */
381
+
382
+
383
+ /**
384
+ * @constant
385
+ * @name constructFromSymbol
386
+ * @summary Symbol enabling Date extensions to inherit properties from the reference date.
387
+ *
388
+ * The symbol is used to enable the `constructFrom` function to construct a date
389
+ * using a reference date and a value. It allows to transfer extra properties
390
+ * from the reference date to the new date. It's useful for extensions like
391
+ * [`TZDate`](https://github.com/date-fns/tz) that accept a time zone as
392
+ * a constructor argument.
393
+ */
394
+ const constructFromSymbol = Symbol.for("constructDateFrom");
395
+
396
+ /**
397
+ * @name constructFrom
398
+ * @category Generic Helpers
399
+ * @summary Constructs a date using the reference date and the value
400
+ *
401
+ * @description
402
+ * The function constructs a new date using the constructor from the reference
403
+ * date and the given value. It helps to build generic functions that accept
404
+ * date extensions.
405
+ *
406
+ * It defaults to `Date` if the passed reference date is a number or a string.
407
+ *
408
+ * Starting from v3.7.0, it allows to construct a date using `[Symbol.for("constructDateFrom")]`
409
+ * enabling to transfer extra properties from the reference date to the new date.
410
+ * It's useful for extensions like [`TZDate`](https://github.com/date-fns/tz)
411
+ * that accept a time zone as a constructor argument.
412
+ *
413
+ * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
414
+ *
415
+ * @param date - The reference date to take constructor from
416
+ * @param value - The value to create the date
417
+ *
418
+ * @returns Date initialized using the given date and value
419
+ *
420
+ * @example
421
+ * import { constructFrom } from "./constructFrom/date-fns";
422
+ *
423
+ * // A function that clones a date preserving the original type
424
+ * function cloneDate<DateType extends Date>(date: DateType): DateType {
425
+ * return constructFrom(
426
+ * date, // Use constructor from the given date
427
+ * date.getTime() // Use the date value to create a new date
428
+ * );
429
+ * }
430
+ */
431
+ function constructFrom(date, value) {
432
+ if (typeof date === "function") return date(value);
433
+
434
+ if (date && typeof date === "object" && constructFromSymbol in date)
435
+ return date[constructFromSymbol](value);
436
+
437
+ if (date instanceof Date) return new date.constructor(value);
438
+
439
+ return new Date(value);
440
+ }
441
+
442
+ /**
443
+ * @name toDate
444
+ * @category Common Helpers
445
+ * @summary Convert the given argument to an instance of Date.
446
+ *
447
+ * @description
448
+ * Convert the given argument to an instance of Date.
449
+ *
450
+ * If the argument is an instance of Date, the function returns its clone.
451
+ *
452
+ * If the argument is a number, it is treated as a timestamp.
453
+ *
454
+ * If the argument is none of the above, the function returns Invalid Date.
455
+ *
456
+ * Starting from v3.7.0, it clones a date using `[Symbol.for("constructDateFrom")]`
457
+ * enabling to transfer extra properties from the reference date to the new date.
458
+ * It's useful for extensions like [`TZDate`](https://github.com/date-fns/tz)
459
+ * that accept a time zone as a constructor argument.
460
+ *
461
+ * **Note**: *all* Date arguments passed to any *date-fns* function is processed by `toDate`.
462
+ *
463
+ * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
464
+ * @typeParam ResultDate - The result `Date` type, it is the type returned from the context function if it is passed, or inferred from the arguments.
465
+ *
466
+ * @param argument - The value to convert
467
+ *
468
+ * @returns The parsed date in the local time zone
469
+ *
470
+ * @example
471
+ * // Clone the date:
472
+ * const result = toDate(new Date(2014, 1, 11, 11, 30, 30))
473
+ * //=> Tue Feb 11 2014 11:30:30
474
+ *
475
+ * @example
476
+ * // Convert the timestamp to date:
477
+ * const result = toDate(1392098430000)
478
+ * //=> Tue Feb 11 2014 11:30:30
479
+ */
480
+ function toDate(argument, context) {
481
+ // [TODO] Get rid of `toDate` or `constructFrom`?
482
+ return constructFrom(argument, argument);
483
+ }
484
+
485
+ /**
486
+ * @name isAfter
487
+ * @category Common Helpers
488
+ * @summary Is the first date after the second one?
489
+ *
490
+ * @description
491
+ * Is the first date after the second one?
492
+ *
493
+ * @param date - The date that should be after the other one to return true
494
+ * @param dateToCompare - The date to compare with
495
+ *
496
+ * @returns The first date is after the second date
497
+ *
498
+ * @example
499
+ * // Is 10 July 1989 after 11 February 1987?
500
+ * const result = isAfter(new Date(1989, 6, 10), new Date(1987, 1, 11))
501
+ * //=> true
502
+ */
503
+ function isAfter(date, dateToCompare) {
504
+ return +toDate(date) > +toDate(dateToCompare);
505
+ }
506
+
354
507
  class TokenUtil {
355
508
  alg;
356
509
  secret;
@@ -385,6 +538,9 @@ class TokenUtil {
385
538
  ...rest
386
539
  };
387
540
  }
541
+ expiresWithin(exp, seconds) {
542
+ return isAfter(new Date(), new Date(exp - seconds * 1000));
543
+ }
388
544
  }
389
545
 
390
546
  function validationHook(result, _c) {
package/index.esm.js CHANGED
@@ -6,7 +6,6 @@ import { z } from '@hono/zod-openapi';
6
6
  import crypto from 'crypto';
7
7
  import argon2 from '@node-rs/argon2';
8
8
  import { INTERNAL_SERVER_ERROR, NOT_FOUND, UNPROCESSABLE_ENTITY } from 'stoker/http-status-codes';
9
- import { clamp } from 'es-toolkit';
10
9
  import { sign, verify } from 'hono/jwt';
11
10
 
12
11
  const DEFAULT_PAGE_SIZE = 20;
@@ -103,6 +102,9 @@ function pinoLogger() {
103
102
  return pino({
104
103
  level: process.env?.['PINO_LOG_LEVEL'] || 'info',
105
104
  timestamp: pino.stdTimeFunctions.isoTime,
105
+ serializers: {
106
+ err: pino.stdSerializers.errWithCause
107
+ },
106
108
  formatters: {
107
109
  bindings: bindings => ({
108
110
  pid: bindings?.['pid'],
@@ -291,7 +293,8 @@ function ApiPdfResponse(description) {
291
293
  function throwHttpError(error, defaultMessage = 'Something went wrong') {
292
294
  const code = error instanceof Error ? Number.isNaN(error.cause) ? INTERNAL_SERVER_ERROR : error.cause : INTERNAL_SERVER_ERROR;
293
295
  throw new HTTPException(code, {
294
- message: error.message ?? defaultMessage
296
+ message: error.message ?? defaultMessage,
297
+ cause: error
295
298
  });
296
299
  }
297
300
  function isUniqueViolationPg(error) {
@@ -308,6 +311,12 @@ function toErrorCode(value) {
308
311
  function onErrorFn(debugLog = true) {
309
312
  return async (err, c) => {
310
313
  const statusCode = 'status' in err ? toErrorCode(err.status) : 'cause' in err ? toErrorCode(err.cause) : INTERNAL_SERVER_ERROR;
314
+ let originalError = err;
315
+ const seen = new Set([err]);
316
+ while (originalError.cause instanceof Error && !seen.has(originalError.cause)) {
317
+ originalError = originalError.cause;
318
+ seen.add(originalError);
319
+ }
311
320
  const logger = c.get('logger');
312
321
  logger.error({
313
322
  err,
@@ -315,12 +324,12 @@ function onErrorFn(debugLog = true) {
315
324
  method: c.req.method
316
325
  }, 'Request failed');
317
326
  if (debugLog) {
318
- console.error(JSON.stringify(err, undefined, 2));
327
+ console.error(err);
319
328
  }
320
329
  return c.json({
321
330
  data: null,
322
331
  error: !debugLog ? undefined : err,
323
- message: err.cause ?? err.message,
332
+ message: originalError.message || err.message,
324
333
  success: false
325
334
  }, statusCode);
326
335
  };
@@ -336,19 +345,163 @@ const notFoundFn = c => {
336
345
  };
337
346
 
338
347
  function buildPaginationResponse(page, size, total) {
339
- const numPage = clamp(Number(page), 1) || 1;
340
- const positivePage = numPage < 1 ? 1 : numPage;
341
- const numSize = clamp(Number(size), DEFAULT_PAGE_SIZE);
342
- const numTotal = clamp(Number(total), 0);
343
- const totalPages = Math.ceil(numTotal / numSize);
348
+ const numPage = Number(page);
349
+ const positivePage = Number.isFinite(numPage) ? Math.max(numPage, 1) : 1;
350
+ const numSize = Number(size);
351
+ const positiveSize = Number.isFinite(numSize) && numSize > 0 ? numSize : DEFAULT_PAGE_SIZE;
352
+ const numTotal = Number(total);
353
+ const positiveTotal = Number.isFinite(numTotal) ? Math.max(numTotal, 0) : 0;
354
+ const totalPages = Math.ceil(positiveTotal / positiveSize);
344
355
  return {
345
356
  page: positivePage,
346
- size: numSize,
347
- totalItems: numTotal,
357
+ size: positiveSize,
358
+ totalItems: positiveTotal,
348
359
  totalPages
349
360
  };
350
361
  }
351
362
 
363
+ /**
364
+ * @module constants
365
+ * @summary Useful constants
366
+ * @description
367
+ * Collection of useful date constants.
368
+ *
369
+ * The constants could be imported from `date-fns/constants`:
370
+ *
371
+ * ```ts
372
+ * import { maxTime, minTime } from "./constants/date-fns/constants";
373
+ *
374
+ * function isAllowedTime(time) {
375
+ * return time <= maxTime && time >= minTime;
376
+ * }
377
+ * ```
378
+ */
379
+
380
+
381
+ /**
382
+ * @constant
383
+ * @name constructFromSymbol
384
+ * @summary Symbol enabling Date extensions to inherit properties from the reference date.
385
+ *
386
+ * The symbol is used to enable the `constructFrom` function to construct a date
387
+ * using a reference date and a value. It allows to transfer extra properties
388
+ * from the reference date to the new date. It's useful for extensions like
389
+ * [`TZDate`](https://github.com/date-fns/tz) that accept a time zone as
390
+ * a constructor argument.
391
+ */
392
+ const constructFromSymbol = Symbol.for("constructDateFrom");
393
+
394
+ /**
395
+ * @name constructFrom
396
+ * @category Generic Helpers
397
+ * @summary Constructs a date using the reference date and the value
398
+ *
399
+ * @description
400
+ * The function constructs a new date using the constructor from the reference
401
+ * date and the given value. It helps to build generic functions that accept
402
+ * date extensions.
403
+ *
404
+ * It defaults to `Date` if the passed reference date is a number or a string.
405
+ *
406
+ * Starting from v3.7.0, it allows to construct a date using `[Symbol.for("constructDateFrom")]`
407
+ * enabling to transfer extra properties from the reference date to the new date.
408
+ * It's useful for extensions like [`TZDate`](https://github.com/date-fns/tz)
409
+ * that accept a time zone as a constructor argument.
410
+ *
411
+ * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
412
+ *
413
+ * @param date - The reference date to take constructor from
414
+ * @param value - The value to create the date
415
+ *
416
+ * @returns Date initialized using the given date and value
417
+ *
418
+ * @example
419
+ * import { constructFrom } from "./constructFrom/date-fns";
420
+ *
421
+ * // A function that clones a date preserving the original type
422
+ * function cloneDate<DateType extends Date>(date: DateType): DateType {
423
+ * return constructFrom(
424
+ * date, // Use constructor from the given date
425
+ * date.getTime() // Use the date value to create a new date
426
+ * );
427
+ * }
428
+ */
429
+ function constructFrom(date, value) {
430
+ if (typeof date === "function") return date(value);
431
+
432
+ if (date && typeof date === "object" && constructFromSymbol in date)
433
+ return date[constructFromSymbol](value);
434
+
435
+ if (date instanceof Date) return new date.constructor(value);
436
+
437
+ return new Date(value);
438
+ }
439
+
440
+ /**
441
+ * @name toDate
442
+ * @category Common Helpers
443
+ * @summary Convert the given argument to an instance of Date.
444
+ *
445
+ * @description
446
+ * Convert the given argument to an instance of Date.
447
+ *
448
+ * If the argument is an instance of Date, the function returns its clone.
449
+ *
450
+ * If the argument is a number, it is treated as a timestamp.
451
+ *
452
+ * If the argument is none of the above, the function returns Invalid Date.
453
+ *
454
+ * Starting from v3.7.0, it clones a date using `[Symbol.for("constructDateFrom")]`
455
+ * enabling to transfer extra properties from the reference date to the new date.
456
+ * It's useful for extensions like [`TZDate`](https://github.com/date-fns/tz)
457
+ * that accept a time zone as a constructor argument.
458
+ *
459
+ * **Note**: *all* Date arguments passed to any *date-fns* function is processed by `toDate`.
460
+ *
461
+ * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
462
+ * @typeParam ResultDate - The result `Date` type, it is the type returned from the context function if it is passed, or inferred from the arguments.
463
+ *
464
+ * @param argument - The value to convert
465
+ *
466
+ * @returns The parsed date in the local time zone
467
+ *
468
+ * @example
469
+ * // Clone the date:
470
+ * const result = toDate(new Date(2014, 1, 11, 11, 30, 30))
471
+ * //=> Tue Feb 11 2014 11:30:30
472
+ *
473
+ * @example
474
+ * // Convert the timestamp to date:
475
+ * const result = toDate(1392098430000)
476
+ * //=> Tue Feb 11 2014 11:30:30
477
+ */
478
+ function toDate(argument, context) {
479
+ // [TODO] Get rid of `toDate` or `constructFrom`?
480
+ return constructFrom(argument, argument);
481
+ }
482
+
483
+ /**
484
+ * @name isAfter
485
+ * @category Common Helpers
486
+ * @summary Is the first date after the second one?
487
+ *
488
+ * @description
489
+ * Is the first date after the second one?
490
+ *
491
+ * @param date - The date that should be after the other one to return true
492
+ * @param dateToCompare - The date to compare with
493
+ *
494
+ * @returns The first date is after the second date
495
+ *
496
+ * @example
497
+ * // Is 10 July 1989 after 11 February 1987?
498
+ * const result = isAfter(new Date(1989, 6, 10), new Date(1987, 1, 11))
499
+ * //=> true
500
+ */
501
+ function isAfter(date, dateToCompare) {
502
+ return +toDate(date) > +toDate(dateToCompare);
503
+ }
504
+
352
505
  class TokenUtil {
353
506
  alg;
354
507
  secret;
@@ -383,6 +536,9 @@ class TokenUtil {
383
536
  ...rest
384
537
  };
385
538
  }
539
+ expiresWithin(exp, seconds) {
540
+ return isAfter(new Date(), new Date(exp - seconds * 1000));
541
+ }
386
542
  }
387
543
 
388
544
  function validationHook(result, _c) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattrika/server-utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,4 +7,5 @@ export declare class TokenUtil {
7
7
  create<T extends JWTPayload>(payload: T, lifeSeconds?: number): Promise<string>;
8
8
  decode<T extends JWTPayload>(token: string): Promise<T | null>;
9
9
  decodeUnexpired<T extends JWTPayload>(token: string): Promise<T | null>;
10
+ expiresWithin(exp: number, seconds: number): boolean;
10
11
  }