@kalutskii/foundation 0.7.8 → 0.7.10

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
@@ -40,50 +40,64 @@ import { blue, dim, green, red, white, yellow } from "kleur/colors";
40
40
  import { format } from "date-fns";
41
41
  import { getTimezoneOffset, toZonedTime } from "date-fns-tz";
42
42
  import { ru } from "date-fns/locale";
43
- var getZonedTime = ({ tz = "Europe/London" } = {}) => {
43
+ function getZonedTime({ tz = "Europe/London" } = {}) {
44
44
  return toZonedTime(/* @__PURE__ */ new Date(), tz);
45
- };
46
- var getUTCOffset = (date, tz) => {
45
+ }
46
+ function getUTCOffset(date, tz) {
47
47
  const offset = getTimezoneOffset(tz, date) / (60 * 60 * 1e3);
48
48
  return `(${offset >= 0 ? "+" : ""}${offset} UTC)`;
49
- };
50
- var getFormattedTime = ({ tz = "Europe/London" } = {}) => {
49
+ }
50
+ function getFormattedTime({ tz = "Europe/London" } = {}) {
51
51
  const zonedTime = getZonedTime({ tz });
52
52
  return `${format(zonedTime, "HH:mm:ss")} ${getUTCOffset(zonedTime, tz)}`;
53
- };
54
- var getFormattedDate = ({ tz = "Europe/London", withTime = true } = {}) => {
53
+ }
54
+ function getFormattedDate({ tz = "Europe/London", withTime = true } = {}) {
55
55
  const zonedTime = getZonedTime({ tz });
56
56
  const pattern = withTime ? "dd.MM.yyyy HH:mm:ss" : "dd.MM.yyyy";
57
57
  const formatted = format(zonedTime, pattern);
58
58
  return `${formatted}${withTime ? ` ${getUTCOffset(zonedTime, tz)}` : ""}`;
59
- };
60
- var formatTime = (time, { locale = ru, tz = "Europe/London" } = {}) => {
59
+ }
60
+ function formatTime(time, { locale = ru, tz = "Europe/London" } = {}) {
61
61
  const zonedTime = toZonedTime(time, tz);
62
62
  return `${format(zonedTime, "HH:mm:ss, d MMMM yyyy", { locale })} ${getUTCOffset(zonedTime, tz)}`;
63
- };
63
+ }
64
64
 
65
65
  // src/utilities/logging.utilities.ts
66
+ var HTTP_STATUS_COLORS = [
67
+ { range: [200, 299], color: green },
68
+ { range: [400, 499], color: yellow },
69
+ { range: [500, 599], color: red }
70
+ ];
71
+ var LOG_LEVEL_COLORS = { info: blue, warn: yellow, error: red };
66
72
  function getColoredHTTPStatus(status) {
67
- const colorMap = [
68
- { range: [200, 299], colorFn: green },
69
- { range: [400, 499], colorFn: yellow },
70
- { range: [500, 599], colorFn: red }
71
- ];
72
- const statusColor = colorMap.find(({ range: [min, max] }) => status >= min && status <= max);
73
- return statusColor ? statusColor.colorFn : (text) => text;
73
+ const statusColor = HTTP_STATUS_COLORS.find(({ range: [minimum, maximum] }) => {
74
+ return status >= minimum && status <= maximum;
75
+ });
76
+ return statusColor ? statusColor.color : (text) => text;
74
77
  }
75
78
  function writeLog(message, level, service = "log", stack) {
76
- const logColorMap = { info: blue, warn: yellow, error: red };
77
79
  const timestamp = dim(getFormattedTime());
78
- const serviceName = logColorMap[level](service.padEnd(12));
80
+ const serviceName = LOG_LEVEL_COLORS[level](service.padEnd(12));
79
81
  const formattedMessage = white(message);
80
82
  console.log(`[${timestamp}] ${serviceName} | ${formattedMessage}`);
81
83
  if (stack)
82
84
  console.log(`[${timestamp}] ${red("\u21B3 trace").padEnd(18)} | ${dim(stack)}`);
83
85
  }
84
86
  var log = {
87
+ /**
88
+ * Writes an informational message with an optional service label.
89
+ * Missing service names use the shared `log` fallback label.
90
+ */
85
91
  info: (message, service) => writeLog(message, "info", service),
92
+ /**
93
+ * Writes a warning message with an optional service label.
94
+ * Missing service names use the shared `log` fallback label.
95
+ */
86
96
  warn: (message, service) => writeLog(message, "warn", service),
97
+ /**
98
+ * Writes an error message with optional service and stack trace context.
99
+ * Stack traces are rendered beneath the primary error message when supplied.
100
+ */
87
101
  error: (message, service, stack) => writeLog(message, "error", service, stack)
88
102
  };
89
103
 
@@ -123,7 +137,7 @@ var honoLoggingHandler = async (c, next) => {
123
137
  const coloredMethod = bold(blue2(c.req.method.padEnd(4)));
124
138
  const coloredStatus = getColoredHTTPStatus(c.res.status)(String(c.res.status).padEnd(4));
125
139
  const coloredTime = dim2(`${duration}ms`.padStart(6));
126
- const coloredPath = white2(c.req.path.padEnd(44));
140
+ const coloredPath = white2(c.req.path.padEnd(32));
127
141
  const coloredSearchParams = searchParams ? dim2(` (${searchParams})`) : "";
128
142
  const coloredBody = bodyPreview ? dim2(` ${bodyPreview}`) : "";
129
143
  log.info(`${coloredMethod} ${coloredStatus} ${coloredTime} ${coloredPath}${coloredSearchParams}${coloredBody}`, "hono");
@@ -162,14 +176,131 @@ function createStringEnumRecord(values) {
162
176
  return Object.freeze(Object.fromEntries(values.map((value) => [value.replace(/\./g, "_").toUpperCase(), value])));
163
177
  }
164
178
 
179
+ // src/upload/upload.enums.ts
180
+ var fileFormatsArray = [
181
+ ...["png", "jpg", "webp", "avif", "heic"],
182
+ // Image formats.
183
+ ...["pdf", "rtf", "txt"]
184
+ // Text and document formats.
185
+ ];
186
+ var fileFormatsRecord = createStringEnumRecord(fileFormatsArray);
187
+ var fileFormat = fileFormatsRecord;
188
+ var uploadValidationErrorsArray = [
189
+ "empty_file",
190
+ "unsupported_file_format",
191
+ "file_size_exceeded",
192
+ "files_count_exceeded"
193
+ ];
194
+ var uploadValidationErrorsRecord = createStringEnumRecord(uploadValidationErrorsArray);
195
+ var uploadValidationError = uploadValidationErrorsRecord;
196
+
197
+ // src/upload/upload.constants.ts
198
+ var fileFormatsConfig = {
199
+ // Image formats.
200
+ [fileFormatsRecord.PNG]: { name: "PNG", mimeTypes: ["image/png"], extensions: [".png"] },
201
+ [fileFormatsRecord.JPG]: { name: "JPG", mimeTypes: ["image/jpeg"], extensions: [".jpg", ".jpeg"] },
202
+ [fileFormatsRecord.WEBP]: { name: "WEBP", mimeTypes: ["image/webp"], extensions: [".webp"] },
203
+ [fileFormatsRecord.AVIF]: { name: "AVIF", mimeTypes: ["image/avif"], extensions: [".avif"] },
204
+ [fileFormatsRecord.HEIC]: { name: "HEIC", mimeTypes: ["image/heic", "image/heif"], extensions: [".heic", ".heif"] },
205
+ // Text and document formats.
206
+ [fileFormatsRecord.PDF]: { name: "PDF", mimeTypes: ["application/pdf"], extensions: [".pdf"] },
207
+ [fileFormatsRecord.RTF]: { name: "RTF", mimeTypes: ["application/rtf"], extensions: [".rtf"] },
208
+ [fileFormatsRecord.TXT]: { name: "TXT", mimeTypes: ["text/plain"], extensions: [".txt"] }
209
+ };
210
+
211
+ // src/upload/upload.factory.ts
212
+ function defineUploadPreset(preset) {
213
+ return preset;
214
+ }
215
+
216
+ // src/upload/upload.presets.ts
217
+ var DEFAULT_UPLOAD_MAX_FILE_SIZE = 20 * 1024 * 1024;
218
+ var imageUploadPreset = defineUploadPreset({
219
+ formats: [fileFormat.PNG, fileFormat.JPG, fileFormat.WEBP, fileFormat.AVIF, fileFormat.HEIC],
220
+ maxFileSize: DEFAULT_UPLOAD_MAX_FILE_SIZE
221
+ });
222
+ var documentUploadPreset = defineUploadPreset({
223
+ formats: [fileFormat.PDF, fileFormat.RTF, fileFormat.TXT],
224
+ maxFileSize: DEFAULT_UPLOAD_MAX_FILE_SIZE
225
+ });
226
+
227
+ // src/upload/upload.schemas.ts
228
+ import { z } from "zod";
229
+
230
+ // src/upload/upload.utilities.ts
231
+ function getFileExtension(fileName) {
232
+ const extensionIndex = fileName.lastIndexOf(".");
233
+ return extensionIndex > 0 && extensionIndex < fileName.length - 1 ? fileName.slice(extensionIndex).toLowerCase() : "";
234
+ }
235
+ function normalizeFileExtension(extension) {
236
+ const normalizedExtension = extension.toLowerCase();
237
+ return normalizedExtension.startsWith(".") ? normalizedExtension : `.${normalizedExtension}`;
238
+ }
239
+ function matchesMimeType(fileMimeType, configuredMimeType) {
240
+ if (configuredMimeType.endsWith("/*"))
241
+ return fileMimeType.startsWith(configuredMimeType.slice(0, -1));
242
+ return fileMimeType === configuredMimeType;
243
+ }
244
+ function isFileMimeTypeSupported(file, formats) {
245
+ return formats.some((format2) => fileFormatsConfig[format2].mimeTypes.some((mimeType) => matchesMimeType(file.type, mimeType)));
246
+ }
247
+ function isFileExtensionSupported(file, formats) {
248
+ const fileExtension = getFileExtension(file.name);
249
+ return formats.some((format2) => fileFormatsConfig[format2].extensions.some((extension) => normalizeFileExtension(extension) === fileExtension));
250
+ }
251
+ function isFileFormatSupported(file, formats) {
252
+ return isFileMimeTypeSupported(file, formats) || isFileExtensionSupported(file, formats);
253
+ }
254
+ function createUploadAccept(formats) {
255
+ const acceptedValues = formats.flatMap((format2) => [
256
+ ...fileFormatsConfig[format2].mimeTypes,
257
+ ...fileFormatsConfig[format2].extensions
258
+ ]);
259
+ return [...new Set(acceptedValues)].join(",");
260
+ }
261
+
262
+ // src/upload/upload.schemas.ts
263
+ function zodUploadFileSchema(options) {
264
+ const { formats, maxFileSize, extensionFallback = false } = options;
265
+ return z.file().min(1, uploadValidationError.EMPTY_FILE).max(maxFileSize, uploadValidationError.FILE_SIZE_EXCEEDED).refine((file) => extensionFallback ? isFileFormatSupported(file, formats) : isFileMimeTypeSupported(file, formats), uploadValidationError.UNSUPPORTED_FILE_FORMAT);
266
+ }
267
+
268
+ // src/upload/upload.validation.ts
269
+ function validateUploadFile(file, formats, maxFileSize) {
270
+ if (file.size === 0)
271
+ return uploadValidationError.EMPTY_FILE;
272
+ if (isFileFormatSupported(file, formats) === false)
273
+ return uploadValidationError.UNSUPPORTED_FILE_FORMAT;
274
+ if (file.size > maxFileSize)
275
+ return uploadValidationError.FILE_SIZE_EXCEEDED;
276
+ return void 0;
277
+ }
278
+ function validateUploadFiles(incomingFiles, options) {
279
+ const { currentFilesCount, formats, maxFileSize, maxFilesCount } = options;
280
+ const availableFilesCount = maxFilesCount === void 0 ? Infinity : maxFilesCount - currentFilesCount;
281
+ const acceptedFiles = [];
282
+ let validationError;
283
+ for (const file of incomingFiles) {
284
+ const fileValidationError = validateUploadFile(file, formats, maxFileSize) ?? // Collection limits apply only after intrinsic file validation succeeds.
285
+ // This preserves remaining slots for supported files from the same batch.
286
+ (acceptedFiles.length >= availableFilesCount ? uploadValidationError.FILES_COUNT_EXCEEDED : void 0);
287
+ if (fileValidationError) {
288
+ validationError = fileValidationError;
289
+ continue;
290
+ }
291
+ acceptedFiles.push(file);
292
+ }
293
+ return { acceptedFiles, validationError };
294
+ }
295
+
165
296
  // src/utilities/execution.utilities.ts
166
297
  async function safeExecute(fn, onError) {
167
298
  try {
168
299
  return await fn();
169
- } catch (err) {
300
+ } catch (error) {
170
301
  if (onError)
171
- return await onError(err);
172
- throw err;
302
+ return await onError(error);
303
+ throw error;
173
304
  }
174
305
  }
175
306
  async function measureExecutionTime(execution) {
@@ -180,19 +311,19 @@ async function measureExecutionTime(execution) {
180
311
  }
181
312
 
182
313
  // src/zod-bulk/zod-bulk.schemas.ts
183
- import { z } from "zod";
314
+ import { z as z2 } from "zod";
184
315
  var DEFAULT_ERROR_MESSAGE = "Selection identifiers must be provided.";
185
- var zodBulkSelectionSchema = ({ identifierSchema }) => {
186
- const identifiersSchema = z.array(identifierSchema).refine((identifiers) => new Set(identifiers).size === identifiers.length, {
316
+ function zodBulkSelectionSchema({ identifierSchema }) {
317
+ const identifiersSchema = z2.array(identifierSchema).refine((identifiers) => new Set(identifiers).size === identifiers.length, {
187
318
  message: DEFAULT_ERROR_MESSAGE
188
319
  });
189
- return z.discriminatedUnion("mode", [
190
- z.object({ mode: z.literal("include"), identifiers: identifiersSchema }).strict(),
191
- z.object({ mode: z.literal("exclude"), excludedIdentifiers: identifiersSchema }).strict()
320
+ return z2.discriminatedUnion("mode", [
321
+ z2.object({ mode: z2.literal("include"), identifiers: identifiersSchema }).strict(),
322
+ z2.object({ mode: z2.literal("exclude"), excludedIdentifiers: identifiersSchema }).strict()
192
323
  ]);
193
- };
324
+ }
194
325
 
195
- // src/zod-jwt/zod-jwt.schemas.ts
326
+ // src/zod-jwt/zod-jwt.services.ts
196
327
  import { decode as decodeJWT, sign as signJWT, verify as verifyJWT } from "hono/jwt";
197
328
  var ZodJWTService = class {
198
329
  payloadSchema;
@@ -204,37 +335,41 @@ var ZodJWTService = class {
204
335
  this.defaultExpirationSeconds = options?.defaultExpirationSeconds ?? 60 * 15;
205
336
  }
206
337
  /**
207
- * Signs a JWT token with the provided payload and secret, using the configured algorithm and expiration settings.
208
- * Example usage: `const accessToken = await JWTServiceInstance.sign({ userId: 123 }, 'my-secret', { expiresInSeconds: 300 });`
338
+ * Signs a payload using the configured algorithm and expiration settings.
339
+ * Per-call expiration overrides the default configured by the service.
340
+ *
341
+ * @example
342
+ * const token = await jwtService.sign({ userId: '123' }, secret, { expiresInSeconds: 300 });
209
343
  */
210
344
  async sign(payload, secret, options) {
211
345
  const exp = Math.floor(Date.now() / 1e3) + (options?.expiresInSeconds ?? this.defaultExpirationSeconds);
212
346
  return signJWT({ ...payload, exp }, secret, this.algorithm);
213
347
  }
214
348
  /**
215
- * Decodes a JWT token, and if a Zod schema is provided, validates the payload against it.
216
- * Returns the decoded payload if valid, or null if invalid or if the token cannot be decoded.
349
+ * Decodes without authenticating it and parses its payload when a schema exists.
350
+ * Schema validation failures return `null`, while malformed tokens still reject.
217
351
  *
218
- * Example usage: `const payload = await JWTServiceInstance.decode(token);`
352
+ * @example
353
+ * const payload = await jwtService.decode(token);
219
354
  */
220
355
  async decode(token) {
221
356
  const { payload } = decodeJWT(token);
222
- if (!this.payloadSchema) {
357
+ if (this.payloadSchema === void 0) {
223
358
  return payload;
224
359
  }
225
360
  const { success: success2, data } = await this.payloadSchema.safeParseAsync(payload);
226
361
  return success2 ? data : null;
227
362
  }
228
363
  /**
229
- * Verifies and decodes a JWT token using the provided secret and configured algorithm,
230
- * then validates the payload against the Zod schema if one is provided.
364
+ * Verifies a token using the configured algorithm and parses its payload.
365
+ * Signature, expiration, and schema validation failures reject the operation.
231
366
  *
232
- * Throws an error if verification fails or if the payload is invalid according to the schema.
233
- * Example usage: `const payload = await JWTServiceInstance.verifyOrThrow(token, 'my-secret');`
367
+ * @example
368
+ * const payload = await jwtService.verifyOrThrow(token, secret);
234
369
  */
235
370
  async verifyOrThrow(token, secret) {
236
371
  const payload = await verifyJWT(token, secret, this.algorithm);
237
- if (!this.payloadSchema) {
372
+ if (this.payloadSchema === void 0) {
238
373
  return payload;
239
374
  }
240
375
  return this.payloadSchema.parseAsync(payload);
@@ -242,23 +377,23 @@ var ZodJWTService = class {
242
377
  };
243
378
 
244
379
  // src/zod-search/zod-search.pagination.schemas.ts
245
- import { z as z2 } from "zod";
246
- var zodPaginationSchema = z2.object({
380
+ import { z as z3 } from "zod";
381
+ var zodPaginationSchema = z3.object({
247
382
  /**
248
383
  * Zero-based number of records skipped before collecting a result page.
249
384
  * String values are coerced to support validation of URL query input.
250
385
  */
251
- offset: z2.coerce.number().int().nonnegative().default(0),
386
+ offset: z3.coerce.number().int().nonnegative().default(0),
252
387
  /**
253
388
  * Positive maximum number of records returned in a single result page.
254
389
  * String values are coerced to support validation of URL query input.
255
390
  */
256
- limit: z2.coerce.number().int().positive().default(10)
391
+ limit: z3.coerce.number().int().positive().default(10)
257
392
  });
258
393
  var zodPaginationShape = zodPaginationSchema.shape;
259
394
 
260
395
  // src/zod-search/zod-search.schemas.ts
261
- import { z as z3 } from "zod";
396
+ import { z as z4 } from "zod";
262
397
 
263
398
  // src/zod-validation/zod-validation.refiners.ts
264
399
  var DEFAULT_ERROR_MESSAGE2 = "Invalid input. At least one field must be provided.";
@@ -269,7 +404,7 @@ var zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
269
404
  }).transform((val) => val);
270
405
 
271
406
  // src/zod-search/zod-search.schemas.ts
272
- var zodSearchQuerySchema = z3.string().trim().min(1).optional();
407
+ var zodSearchQuerySchema = z4.string().trim().min(1).optional();
273
408
  var createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial());
274
409
  var zodSearchSchema = (options) => {
275
410
  const whereSchema = options.filters !== void 0 ? createZodSearchWhereSchema(options.filters) : options.whereSchema;
@@ -278,11 +413,11 @@ var zodSearchSchema = (options) => {
278
413
  ...options.queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
279
414
  ...options.paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
280
415
  };
281
- return z3.object(shape).strict();
416
+ return z4.object(shape).strict();
282
417
  };
283
418
 
284
419
  // src/zod-validation/zod-validation.parsing.ts
285
- import { z as z4 } from "zod";
420
+ import { z as z5 } from "zod";
286
421
 
287
422
  // src/zod-validation/zod-validation.utilities.ts
288
423
  var isPlainObject = (value) => {
@@ -309,37 +444,60 @@ var parseQueryValue = (value) => {
309
444
  return Number(normalized);
310
445
  return value;
311
446
  };
312
- var asQuery = (schema) => z4.preprocess(parseQueryValue, schema);
447
+ var asQuery = (schema) => z5.preprocess(parseQueryValue, schema);
313
448
  export {
449
+ DEFAULT_UPLOAD_MAX_FILE_SIZE,
314
450
  EXCEPTION_STATUS_CODES,
315
451
  SUCCESS_STATUS_CODES,
316
452
  ZodJWTService,
317
453
  asQuery,
318
454
  createStringEnumRecord,
455
+ createUploadAccept,
456
+ createZodSearchWhereSchema,
457
+ defineUploadPreset,
458
+ documentUploadPreset,
319
459
  failure,
320
460
  fetchAndThrow,
321
461
  fetchSafely,
462
+ fileFormat,
463
+ fileFormatsArray,
464
+ fileFormatsConfig,
465
+ fileFormatsRecord,
322
466
  fileRespond,
323
467
  formatTime,
324
468
  generateRandomString,
325
469
  getColoredHTTPStatus,
470
+ getFileExtension,
326
471
  getFormattedDate,
327
472
  getFormattedTime,
328
473
  getUTCOffset,
329
474
  getZonedTime,
330
475
  honoLoggingHandler,
476
+ imageUploadPreset,
477
+ isFileExtensionSupported,
478
+ isFileFormatSupported,
479
+ isFileMimeTypeSupported,
331
480
  isPlainObject,
332
481
  log,
482
+ matchesMimeType,
333
483
  measureExecutionTime,
484
+ normalizeFileExtension,
334
485
  onHandlerError,
335
486
  parseQueryValue,
336
487
  respond,
337
488
  safeExecute,
338
489
  sqlWhere,
339
490
  success,
491
+ uploadValidationError,
492
+ uploadValidationErrorsArray,
493
+ uploadValidationErrorsRecord,
494
+ validateUploadFile,
495
+ validateUploadFiles,
340
496
  zodAtLeastOne,
341
497
  zodBulkSelectionSchema,
342
498
  zodPaginationSchema,
343
499
  zodPaginationShape,
344
- zodSearchSchema
500
+ zodSearchQuerySchema,
501
+ zodSearchSchema,
502
+ zodUploadFileSchema
345
503
  };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@kalutskii/foundation",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "Typescript collection of most common utilities, schemas and functions among private projects.",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "repository": {
7
8
  "type": "git",
@@ -12,10 +13,12 @@
12
13
  },
13
14
  "scripts": {
14
15
  "build": "tsup",
16
+ "test": "bun test",
15
17
  "typecheck": "tsc --noEmit",
16
18
  "lint": "eslint . --ext .ts --fix",
19
+ "lint:check": "eslint . --ext .ts",
17
20
  "format": "prettier --write .",
18
- "test": "bun test"
21
+ "format:check": "prettier --check ."
19
22
  },
20
23
  "exports": {
21
24
  ".": {
@@ -27,7 +30,6 @@
27
30
  "dist"
28
31
  ],
29
32
  "peerDependencies": {
30
- "@hono/zod-validator": "^0.8.0",
31
33
  "date-fns": "^4.1.0",
32
34
  "date-fns-tz": "^3.2.0",
33
35
  "drizzle-orm": "^1.0.0-beta.22",