@mattrika/server-utils 0.0.0-stage → 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/README.md +386 -2
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +582 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -0
- package/index.esm.js +552 -0
- package/package.json +33 -3
- package/src/index.d.ts +14 -0
- package/src/lib/constants/common.values.d.ts +2 -0
- package/src/lib/pino-logger/logger.helpers.d.ts +4 -0
- package/src/lib/pino-logger/pino-logger.d.ts +2 -0
- package/src/lib/pino-logger/pino-logger.middleware.d.ts +2 -0
- package/src/lib/predicates/is-prod.d.ts +5 -0
- package/src/lib/schemas/common.schema.d.ts +29 -0
- package/src/lib/services/crypto.service.d.ts +5 -0
- package/src/lib/utils/api-response.util.d.ts +76 -0
- package/src/lib/utils/error.util.d.ts +2 -0
- package/src/lib/utils/on-error.d.ts +2 -0
- package/src/lib/utils/on-not-found.d.ts +2 -0
- package/src/lib/utils/pagination.util.d.ts +2 -0
- package/src/lib/utils/token.util.d.ts +11 -0
- package/src/lib/utils/validation-hook.d.ts +2 -0
package/index.cjs.js
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var pino = require('pino');
|
|
6
|
+
var httpException = require('hono/http-exception');
|
|
7
|
+
var zodOpenapi = require('@hono/zod-openapi');
|
|
8
|
+
var crypto = require('crypto');
|
|
9
|
+
var argon2 = require('@node-rs/argon2');
|
|
10
|
+
var httpStatusCodes = require('stoker/http-status-codes');
|
|
11
|
+
var jwt = require('hono/jwt');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
14
|
+
const MAX_PAGE_SIZE = 100;
|
|
15
|
+
|
|
16
|
+
const MAX_SIZE = 1024 * 1024; // 1MB
|
|
17
|
+
async function safeResponseBody(res) {
|
|
18
|
+
try {
|
|
19
|
+
const contentType = res.headers.get('content-type') || '';
|
|
20
|
+
const disposition = res.headers.get('content-disposition') || '';
|
|
21
|
+
// ❌ Skip downloads
|
|
22
|
+
if (disposition.includes('attachment')) {
|
|
23
|
+
return '[FILE DOWNLOAD SKIPPED]';
|
|
24
|
+
}
|
|
25
|
+
// ❌ Skip non-JSON responses
|
|
26
|
+
if (!contentType.includes('application/json')) {
|
|
27
|
+
return '[NON-JSON RESPONSE]';
|
|
28
|
+
}
|
|
29
|
+
const text = await res.clone().text();
|
|
30
|
+
if (!text) return undefined;
|
|
31
|
+
const size = Buffer.byteLength(text, 'utf8');
|
|
32
|
+
if (size > MAX_SIZE) return `[RESPONSE TOO LARGE: ${Math.round(size / 1024)} KB]`;
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
} catch {
|
|
35
|
+
return '[UNREADABLE RESPONSE]';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function safeRequestBody(req) {
|
|
39
|
+
try {
|
|
40
|
+
const contentType = req.header('content-type') || '';
|
|
41
|
+
// ❌ Skip file uploads
|
|
42
|
+
if (contentType.includes('multipart/form-data')) {
|
|
43
|
+
return '[FILE UPLOAD SKIPPED]';
|
|
44
|
+
}
|
|
45
|
+
if (!contentType.includes('application/json')) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const text = await req.raw.clone().text();
|
|
49
|
+
if (!text) return undefined;
|
|
50
|
+
const size = Buffer.byteLength(text, 'utf8');
|
|
51
|
+
if (size > MAX_SIZE) return `[BODY TOO LARGE: ${Math.round(size / 1024)} KB]`;
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
return '[UNREADABLE REQUEST BODY]';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function ensureDirExists(filePath) {
|
|
58
|
+
const dir = path.dirname(filePath);
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, {
|
|
61
|
+
recursive: true
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks whether APP_ENV environment variable is set to 'production' or NODE_ENV environment variable is set to 'production', in that order.
|
|
68
|
+
* @returns boolean
|
|
69
|
+
*/
|
|
70
|
+
function isProductionAppEnvironment() {
|
|
71
|
+
const appEnv = process?.env?.['APP_ENV'];
|
|
72
|
+
if (appEnv === 'production') return true;
|
|
73
|
+
const nodeEnv = process?.env?.['NODE_ENV'];
|
|
74
|
+
if (nodeEnv === 'production') return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const logFilePath = './logs/app.log';
|
|
79
|
+
let transport;
|
|
80
|
+
function pinoLogger() {
|
|
81
|
+
const isProd = isProductionAppEnvironment();
|
|
82
|
+
try {
|
|
83
|
+
// ensure logs folder exists
|
|
84
|
+
ensureDirExists(logFilePath);
|
|
85
|
+
transport = isProd ? pino.transport({
|
|
86
|
+
target: 'pino/file',
|
|
87
|
+
options: {
|
|
88
|
+
destination: logFilePath
|
|
89
|
+
}
|
|
90
|
+
}) : pino.transport({
|
|
91
|
+
targets: [{
|
|
92
|
+
target: 'pino-pretty',
|
|
93
|
+
options: {
|
|
94
|
+
colorize: true,
|
|
95
|
+
levelFirst: true,
|
|
96
|
+
translateTime: 'SYS:standard'
|
|
97
|
+
}
|
|
98
|
+
}]
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('⚠️ Logger file transport failed, falling back to stdout', err);
|
|
102
|
+
transport = undefined;
|
|
103
|
+
}
|
|
104
|
+
return pino({
|
|
105
|
+
level: process.env?.['PINO_LOG_LEVEL'] || 'info',
|
|
106
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
107
|
+
serializers: {
|
|
108
|
+
err: pino.stdSerializers.errWithCause
|
|
109
|
+
},
|
|
110
|
+
formatters: {
|
|
111
|
+
bindings: bindings => ({
|
|
112
|
+
pid: bindings?.['pid'],
|
|
113
|
+
host: bindings?.['hostname'],
|
|
114
|
+
node_version: process.version
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
redact: {
|
|
118
|
+
paths: ['req.headers.authorization', 'req.headers.cookie', 'req.body.password', 'req.body.confirmPassword', 'req.body.oldPassword', 'req.body.newPassword', 'req.body.token', 'req.body.refreshToken', 'req.body.accessToken', 'req.body.*.password', 'req.body.*.token', 'req.body.*.refreshToken', 'req.body.*.accessToken', 'res.body.token', 'res.body.refreshToken', 'res.body.accessToken', 'res.body.data.token', 'res.body.data.refreshToken', 'res.body.data.accessToken', 'res.body.*.token', 'res.body.*.refreshToken', 'res.body.*.accessToken'],
|
|
119
|
+
censor: '[REDACTED]'
|
|
120
|
+
}
|
|
121
|
+
}, transport);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pinoLoggerMiddleware = () => {
|
|
125
|
+
return async (c, next) => {
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
const logger = pinoLogger();
|
|
128
|
+
const childLogger = logger.child({
|
|
129
|
+
requestId: c.var['requestId'],
|
|
130
|
+
req: {
|
|
131
|
+
method: c.req.method,
|
|
132
|
+
path: c.req.path,
|
|
133
|
+
query: c.req.query(),
|
|
134
|
+
headers: c.req.header(),
|
|
135
|
+
body: c.req.method !== 'GET' ? await safeRequestBody(c.req) : undefined
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
c.set('logger', childLogger);
|
|
139
|
+
childLogger.info('Incoming request');
|
|
140
|
+
try {
|
|
141
|
+
await next();
|
|
142
|
+
const responseBody = await safeResponseBody(c.res);
|
|
143
|
+
childLogger.info({
|
|
144
|
+
res: {
|
|
145
|
+
status: c.res.status,
|
|
146
|
+
durationMs: Date.now() - start,
|
|
147
|
+
body: responseBody
|
|
148
|
+
}
|
|
149
|
+
}, 'Request completed');
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err instanceof httpException.HTTPException) {
|
|
152
|
+
childLogger.warn({
|
|
153
|
+
res: {
|
|
154
|
+
status: err.status
|
|
155
|
+
},
|
|
156
|
+
error: err.message
|
|
157
|
+
}, 'HTTP error');
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
childLogger.error({
|
|
161
|
+
err
|
|
162
|
+
}, 'Unhandled server error');
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const zPagination = zodOpenapi.z.object({
|
|
169
|
+
page: zodOpenapi.z.coerce.number().optional(),
|
|
170
|
+
size: zodOpenapi.z.coerce.number().optional()
|
|
171
|
+
});
|
|
172
|
+
const zCommonQuery = zodOpenapi.z.object({
|
|
173
|
+
search: zodOpenapi.z.string().optional(),
|
|
174
|
+
orderBy: zodOpenapi.z.string().optional(),
|
|
175
|
+
sortOrder: zodOpenapi.z.enum(['asc', 'desc']).optional()
|
|
176
|
+
}).extend(zPagination.shape);
|
|
177
|
+
const zId = zodOpenapi.z.object({
|
|
178
|
+
id: zodOpenapi.z.string()
|
|
179
|
+
});
|
|
180
|
+
const zIds = zodOpenapi.z.object({
|
|
181
|
+
ids: zodOpenapi.z.array(zodOpenapi.z.string()).min(1)
|
|
182
|
+
});
|
|
183
|
+
const zPassword = zodOpenapi.z.string().min(8).max(128).regex(/[A-Z]/, 'Must contain at least one uppercase letter').regex(/[0-9]/, 'Must contain at least one number').regex(/[^A-Za-z0-9\s]/, 'Must contain at least one special character');
|
|
184
|
+
const zEmpty = zodOpenapi.z.object({});
|
|
185
|
+
const zFile = zodOpenapi.z.object({
|
|
186
|
+
file: zodOpenapi.z.instanceof(File).openapi({
|
|
187
|
+
type: 'string',
|
|
188
|
+
format: 'binary'
|
|
189
|
+
}).optional(),
|
|
190
|
+
files: zodOpenapi.z.array(zodOpenapi.z.instanceof(File).openapi({
|
|
191
|
+
type: 'string',
|
|
192
|
+
format: 'binary'
|
|
193
|
+
})).optional()
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// `argon2.Algorithm` is an ambient `const enum`, so its members cannot be
|
|
197
|
+
// accessed when `isolatedModules` is enabled. Argon2id === 2; typing the
|
|
198
|
+
// literal against the library's own option type keeps it type-safe.
|
|
199
|
+
const ARGON2ID = 2;
|
|
200
|
+
class CryptoService {
|
|
201
|
+
static async hash(plain) {
|
|
202
|
+
return argon2.hash(plain, {
|
|
203
|
+
algorithm: ARGON2ID,
|
|
204
|
+
memoryCost: 1 << 16,
|
|
205
|
+
// 64 MB
|
|
206
|
+
timeCost: 3,
|
|
207
|
+
parallelism: 1
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
static async verify(plain, hash) {
|
|
211
|
+
return argon2.verify(hash, plain);
|
|
212
|
+
}
|
|
213
|
+
static random64chars() {
|
|
214
|
+
// 32 bytes hex -> 64 hex chars
|
|
215
|
+
return crypto.randomBytes(32).toString('hex');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const zResponsePagination = zodOpenapi.z.object({
|
|
220
|
+
page: zodOpenapi.z.number(),
|
|
221
|
+
size: zodOpenapi.z.number(),
|
|
222
|
+
totalItems: zodOpenapi.z.number(),
|
|
223
|
+
totalPages: zodOpenapi.z.number()
|
|
224
|
+
});
|
|
225
|
+
function ApiResponse(dataSchema, description) {
|
|
226
|
+
return {
|
|
227
|
+
content: {
|
|
228
|
+
'application/json': {
|
|
229
|
+
schema: zodOpenapi.z.object({
|
|
230
|
+
data: dataSchema,
|
|
231
|
+
message: zodOpenapi.z.string(),
|
|
232
|
+
success: zodOpenapi.z.boolean(),
|
|
233
|
+
error: zodOpenapi.z.any().optional(),
|
|
234
|
+
meta: zodOpenapi.z.any().optional()
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
description
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function ApiListResponse(dataSchema, description) {
|
|
242
|
+
return {
|
|
243
|
+
content: {
|
|
244
|
+
'application/json': {
|
|
245
|
+
schema: zodOpenapi.z.object({
|
|
246
|
+
data: dataSchema,
|
|
247
|
+
message: zodOpenapi.z.string(),
|
|
248
|
+
success: zodOpenapi.z.boolean(),
|
|
249
|
+
error: zodOpenapi.z.any().optional(),
|
|
250
|
+
meta: zodOpenapi.z.any().optional(),
|
|
251
|
+
pagination: zResponsePagination
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
description
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// 1. Base Stream Schema
|
|
259
|
+
const zStreamResponse = zodOpenapi.z.object({
|
|
260
|
+
body: zodOpenapi.z.custom(val => val instanceof ReadableStream, {
|
|
261
|
+
message: 'Body must be a ReadableStream'
|
|
262
|
+
}),
|
|
263
|
+
headers: zodOpenapi.z.object({
|
|
264
|
+
'Content-Type': zodOpenapi.z.string().default('application/octet-stream'),
|
|
265
|
+
'Content-Disposition': zodOpenapi.z.string().refine(val => val.startsWith('inline; filename="'), {
|
|
266
|
+
message: 'Must follow format: inline; filename="..."'
|
|
267
|
+
})
|
|
268
|
+
}).optional()
|
|
269
|
+
});
|
|
270
|
+
// 2. Generic API Response for ReadableStream
|
|
271
|
+
function StreamResponse(description) {
|
|
272
|
+
return {
|
|
273
|
+
content: {
|
|
274
|
+
'application/octet-stream': {
|
|
275
|
+
schema: zStreamResponse
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
description
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function ApiPdfResponse(description) {
|
|
282
|
+
return {
|
|
283
|
+
content: {
|
|
284
|
+
'application/pdf': {
|
|
285
|
+
schema: zodOpenapi.z.object({
|
|
286
|
+
format: zodOpenapi.z.literal('binary'),
|
|
287
|
+
type: zodOpenapi.z.literal('string')
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
description
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function throwHttpError(error, defaultMessage = 'Something went wrong') {
|
|
296
|
+
const code = error instanceof Error ? Number.isNaN(error.cause) ? httpStatusCodes.INTERNAL_SERVER_ERROR : error.cause : httpStatusCodes.INTERNAL_SERVER_ERROR;
|
|
297
|
+
throw new httpException.HTTPException(code, {
|
|
298
|
+
message: error.message ?? defaultMessage,
|
|
299
|
+
cause: error
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function isUniqueViolationPg(error) {
|
|
303
|
+
const e = error;
|
|
304
|
+
return e?.code === '23505'; // unique_violation
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function toErrorCode(value) {
|
|
308
|
+
if (value === undefined || value === null || Number.isNaN(value)) {
|
|
309
|
+
return 500;
|
|
310
|
+
}
|
|
311
|
+
return typeof value === 'number' ? Math.max(value, 0) : 500;
|
|
312
|
+
}
|
|
313
|
+
function onErrorFn(debugLog = true) {
|
|
314
|
+
return async (err, c) => {
|
|
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
|
+
}
|
|
322
|
+
const logger = c.get('logger');
|
|
323
|
+
logger.error({
|
|
324
|
+
err,
|
|
325
|
+
path: c.req.path,
|
|
326
|
+
method: c.req.method
|
|
327
|
+
}, 'Request failed');
|
|
328
|
+
if (debugLog) {
|
|
329
|
+
console.error(err);
|
|
330
|
+
}
|
|
331
|
+
return c.json({
|
|
332
|
+
data: null,
|
|
333
|
+
error: !debugLog ? undefined : err,
|
|
334
|
+
message: originalError.message || err.message,
|
|
335
|
+
success: false
|
|
336
|
+
}, statusCode);
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const notFoundFn = c => {
|
|
341
|
+
return c.json({
|
|
342
|
+
success: false,
|
|
343
|
+
error: 'Not Found',
|
|
344
|
+
data: {},
|
|
345
|
+
message: `Not Found - ${c.req.path}`
|
|
346
|
+
}, httpStatusCodes.NOT_FOUND);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
function buildPaginationResponse(page, size, total) {
|
|
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);
|
|
357
|
+
return {
|
|
358
|
+
page: positivePage,
|
|
359
|
+
size: positiveSize,
|
|
360
|
+
totalItems: positiveTotal,
|
|
361
|
+
totalPages
|
|
362
|
+
};
|
|
363
|
+
}
|
|
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
|
+
|
|
507
|
+
class TokenUtil {
|
|
508
|
+
alg;
|
|
509
|
+
secret;
|
|
510
|
+
constructor(secret, alg = 'HS256') {
|
|
511
|
+
this.alg = alg;
|
|
512
|
+
this.secret = secret;
|
|
513
|
+
}
|
|
514
|
+
async create(payload, lifeSeconds = 15 * 60) {
|
|
515
|
+
if (payload.exp === undefined) {
|
|
516
|
+
// JWT NumericDate claims (`exp`) are seconds since the epoch.
|
|
517
|
+
payload.exp = Math.floor(Date.now() / 1000) + lifeSeconds;
|
|
518
|
+
}
|
|
519
|
+
return await jwt.sign(payload, this.secret, this.alg);
|
|
520
|
+
}
|
|
521
|
+
async decode(token) {
|
|
522
|
+
return await jwt.verify(token, this.secret, this.alg);
|
|
523
|
+
}
|
|
524
|
+
async decodeUnexpired(token) {
|
|
525
|
+
// Disable Hono's built-in `exp` check so an expired token resolves to
|
|
526
|
+
// `null` instead of throwing, then validate it here.
|
|
527
|
+
const {
|
|
528
|
+
exp,
|
|
529
|
+
...rest
|
|
530
|
+
} = await jwt.verify(token, this.secret, {
|
|
531
|
+
alg: this.alg,
|
|
532
|
+
exp: false
|
|
533
|
+
});
|
|
534
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
535
|
+
if (exp !== undefined && exp <= nowSeconds) return null;
|
|
536
|
+
return {
|
|
537
|
+
exp: exp,
|
|
538
|
+
...rest
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
expiresWithin(exp, seconds) {
|
|
542
|
+
return isAfter(new Date(), new Date(exp - seconds * 1000));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function validationHook(result, _c) {
|
|
547
|
+
if (!result.success) {
|
|
548
|
+
throwHttpError(new Error('Validation error', {
|
|
549
|
+
cause: httpStatusCodes.UNPROCESSABLE_ENTITY
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
exports.ApiListResponse = ApiListResponse;
|
|
555
|
+
exports.ApiPdfResponse = ApiPdfResponse;
|
|
556
|
+
exports.ApiResponse = ApiResponse;
|
|
557
|
+
exports.CryptoService = CryptoService;
|
|
558
|
+
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
559
|
+
exports.MAX_PAGE_SIZE = MAX_PAGE_SIZE;
|
|
560
|
+
exports.StreamResponse = StreamResponse;
|
|
561
|
+
exports.TokenUtil = TokenUtil;
|
|
562
|
+
exports.buildPaginationResponse = buildPaginationResponse;
|
|
563
|
+
exports.ensureDirExists = ensureDirExists;
|
|
564
|
+
exports.isProductionAppEnvironment = isProductionAppEnvironment;
|
|
565
|
+
exports.isUniqueViolationPg = isUniqueViolationPg;
|
|
566
|
+
exports.notFoundFn = notFoundFn;
|
|
567
|
+
exports.onErrorFn = onErrorFn;
|
|
568
|
+
exports.pinoLogger = pinoLogger;
|
|
569
|
+
exports.pinoLoggerMiddleware = pinoLoggerMiddleware;
|
|
570
|
+
exports.safeRequestBody = safeRequestBody;
|
|
571
|
+
exports.safeResponseBody = safeResponseBody;
|
|
572
|
+
exports.throwHttpError = throwHttpError;
|
|
573
|
+
exports.validationHook = validationHook;
|
|
574
|
+
exports.zCommonQuery = zCommonQuery;
|
|
575
|
+
exports.zEmpty = zEmpty;
|
|
576
|
+
exports.zFile = zFile;
|
|
577
|
+
exports.zId = zId;
|
|
578
|
+
exports.zIds = zIds;
|
|
579
|
+
exports.zPagination = zPagination;
|
|
580
|
+
exports.zPassword = zPassword;
|
|
581
|
+
exports.zResponsePagination = zResponsePagination;
|
|
582
|
+
exports.zStreamResponse = zStreamResponse;
|
package/index.cjs.mjs
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|