@kalutskii/foundation 0.7.7 → 0.7.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilya Kalutskii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @kalutskii/foundation
2
2
 
3
3
  Shared TypeScript foundation for contracts, schemas, framework adapters, and reusable utilities.
4
- The package is designed for Bun, Node.js, Hono applications, and Cloudflare Workers environments.
4
+ The package is designed for Bun, Node.js, and Hono applications.
5
5
 
6
6
  This repository is not intended to document every exported function through standalone usage snippets.
7
7
  Public JSDoc, generated declarations, colocated specifications, and editor inference are the API reference.
@@ -24,6 +24,7 @@ The package contains several deliberately isolated areas:
24
24
  | ---------------- | ----------------------------------------------------------------------------------------- |
25
25
  | `utilities` | Framework-independent datetime, enum, execution, generation, logging, and type utilities. |
26
26
  | `http` | Shared HTTP result contracts, factories, status constants, and result resolvers. |
27
+ | `upload` | Shared file-format metadata, upload validation, and reusable Zod file schemas. |
27
28
  | `zod-validation` | Generic Zod parsing, validation, refinement, and related type utilities. |
28
29
  | `zod-search` | Reusable search and pagination contracts composed from lower-level Zod primitives. |
29
30
  | `zod-bulk` | Include/exclude selection contracts shared by frontend and backend bulk operations. |
@@ -46,7 +47,7 @@ Framework adapters
46
47
 
47
48
 
48
49
  Contract composition
49
- zod-search / zod-bulk
50
+ zod-search / zod-bulk / upload
50
51
 
51
52
 
52
53
  Contract primitives
@@ -88,6 +89,7 @@ src/
88
89
  ├── drizzle/
89
90
  ├── hono/
90
91
  ├── http/
92
+ ├── upload/
91
93
  ├── utilities/
92
94
  ├── zod-bulk/
93
95
  ├── zod-jwt/
@@ -111,20 +113,23 @@ single public API boundary and should export only symbols intentionally supporte
111
113
 
112
114
  ## File responsibilities
113
115
 
114
- | Suffix | Expected content |
115
- | ---------------- | ----------------------------------------------------------------------------- |
116
- | `*.constants.ts` | Immutable values and literal collections without behavior. |
117
- | `*.schemas.ts` | Runtime Zod schemas and factories whose result is a schema. |
118
- | `*.types.ts` | Type aliases, interfaces, generic contracts, and schema-derived output types. |
119
- | `*.factory.ts` | Functions whose primary responsibility is constructing non-schema values. |
120
- | `*.resolvers.ts` | Functions that unwrap, normalize, or translate an existing result. |
121
- | `*.utilities.ts` | Stateless reusable behavior that has no narrower architectural owner. |
122
- | `*.parsing.ts` | Input parsing and preprocessing before domain validation. |
123
- | `*.refiners.ts` | Refinement logic that narrows or safely composes an existing value. |
124
- | `*.execution.ts` | Framework lifecycle execution and error-boundary behavior. |
125
- | `*.logging.ts` | Logging formatters, sinks, or middleware behavior. |
126
- | `*.respond.ts` | Framework response construction and response-specific contracts. |
127
- | `*.spec.ts` | The single colocated runtime and compile-time specification for a module. |
116
+ | Suffix | Expected content |
117
+ | ----------------- | ------------------------------------------------------------------------- |
118
+ | `*.constants.ts` | Immutable configuration values and metadata without behavior. |
119
+ | `*.enums.ts` | Literal collections, derived unions, ergonomic records, and aliases. |
120
+ | `*.schemas.ts` | Runtime Zod schemas and factories whose result is a schema. |
121
+ | `*.types.ts` | Type aliases, generic contracts, and schema-derived output types. |
122
+ | `*.services.ts` | Stateful service classes that coordinate one external capability. |
123
+ | `*.validation.ts` | Ordered validation behavior returning stable domain error keys. |
124
+ | `*.factory.ts` | Functions whose primary responsibility is constructing non-schema values. |
125
+ | `*.resolvers.ts` | Functions that unwrap, normalize, or translate an existing result. |
126
+ | `*.utilities.ts` | Stateless reusable behavior that has no narrower architectural owner. |
127
+ | `*.parsing.ts` | Input parsing and preprocessing before domain validation. |
128
+ | `*.refiners.ts` | Refinement logic that narrows or safely composes an existing value. |
129
+ | `*.execution.ts` | Framework lifecycle execution and error-boundary behavior. |
130
+ | `*.logging.ts` | Logging formatters, sinks, or middleware behavior. |
131
+ | `*.respond.ts` | Framework response construction and response-specific contracts. |
132
+ | `*.spec.ts` | The single colocated runtime and compile-time specification for a module. |
128
133
 
129
134
  Do not place TypeScript-only contracts in a schema file when they can be separated without creating a circular
130
135
  responsibility. Do not split tiny files mechanically either: separation must communicate ownership, not line count.
package/dist/index.d.ts CHANGED
@@ -1,30 +1,27 @@
1
1
  import { SQL } from 'drizzle-orm';
2
2
  import { ErrorHandler, MiddlewareHandler, Context, TypedResponse } from 'hono';
3
- import { Locale } from 'date-fns';
4
3
  import z$1, { z, ZodObject, ZodRawShape } from 'zod';
4
+ import { Locale } from 'date-fns';
5
5
  import { SymmetricAlgorithm } from 'hono/utils/jwt/jwa';
6
6
 
7
7
  /**
8
- * Builds a Drizzle SQL `where` condition from a plain object.
8
+ * Builds a Drizzle `WHERE` clause by combining defined object entries with `and`.
9
+ * Expects the supplied keys to be validated against the table beforehand.
9
10
  *
10
- * Maps each `where` entry to `eq(table[key], value)` and combines them with `and`.
11
- * Assumes that `where` was already validated and contains only existing table fields.
12
- *
13
- * ```typescript
11
+ * @example
14
12
  * await db.update(usersTable).set(values).where(sqlWhere(usersTable, { id: 1 })).returning();
15
- * ```
16
13
  */
17
14
  declare function sqlWhere(table: unknown, where: Record<string, unknown>): SQL;
18
15
 
19
16
  /**
20
- * Global error handler for Hono framework. Catches all exceptions thrown in route handlers and middlewares.
21
- * Distinguishes between expected HTTPExceptions (mapped to their status codes) and unexpected errors (500).
17
+ * Converts expected `HTTPException` values into shared API error envelopes.
18
+ * Unexpected failures are logged and represented by a traceable generic response.
22
19
  */
23
20
  declare const onHandlerError: ErrorHandler;
24
21
 
25
22
  /**
26
- * Request logging middleware for Hono, providing deeply detailed logs for each incoming request.
27
- * Example log output: [12:12:12 (+4 UTC)] hono | POST 200 123ms /api/v1/users (search=term)
23
+ * Logs request method, response status, duration, URL details, and a body preview.
24
+ * Multipart bodies remain unread to avoid buffering uploaded file content into logs.
28
25
  */
29
26
  declare const honoLoggingHandler: MiddlewareHandler;
30
27
 
@@ -33,10 +30,10 @@ declare const EXCEPTION_STATUS_CODES: readonly [400, 401, 403, 404, 405, 409, 50
33
30
 
34
31
  type SuccessStatusCode = (typeof SUCCESS_STATUS_CODES)[number];
35
32
  type ExceptionStatusCode = (typeof EXCEPTION_STATUS_CODES)[number];
36
- type APISuccess<T = void> = {
33
+ type APISuccess<TData = void> = {
37
34
  kind: 'data';
38
35
  status: SuccessStatusCode;
39
- data: T;
36
+ data: TData;
40
37
  };
41
38
  type APIError = {
42
39
  kind: 'error';
@@ -46,48 +43,55 @@ type APIError = {
46
43
  type APIContractResult<TData = void> = APISuccess<TData> | APIError;
47
44
  type APIContractData<TResult extends APIContractResult<unknown>> = TResult extends APISuccess<infer TData> ? TData : never;
48
45
  type APIContractError<TResult extends APIContractResult<unknown>> = Extract<TResult, APIError>;
49
- /**
50
- * Discriminated union result of a fetch operation.
51
- * If `error` is set, `data` is null and vice versa.
52
- */
53
- type FetchResult<T> = {
46
+ type FetchResult<TData> = {
54
47
  error: null;
55
- data: T;
48
+ data: TData;
56
49
  } | {
57
50
  error: string;
58
51
  data: null;
59
52
  };
60
53
 
61
54
  /**
62
- * Wraps c.json with a typed success payload / (or void data) & possible APIError response.
63
- * When no data is provided, responds with an empty object {} (purely for type consistency).
55
+ * Options for a typed JSON response wrapped in the shared API success envelope.
56
+ * The status generic preserves the literal code inferred by the route contract.
64
57
  */
65
- declare function respond<T extends object = Record<string, never>, S extends SuccessStatusCode = SuccessStatusCode>(c: Context, options: {
66
- status: S;
67
- data?: T;
68
- }): Response & TypedResponse<APISuccess<T> | APIError, S, 'json'>;
58
+ type HonoRespondOptions<TData extends object, TStatus extends SuccessStatusCode> = {
59
+ status: TStatus;
60
+ data?: TData;
61
+ };
69
62
  /**
70
- * Responds with a file download, setting the appropriate headers for content disposition and type.
71
- * Supports specifying a filename and optional content type, defaulting to 'application/octet-stream'.
63
+ * Options for a downloadable binary response with attachment metadata.
64
+ * The content type remains optional and falls back to a generic binary type.
72
65
  */
73
- declare function fileRespond<S extends SuccessStatusCode>(c: Context, options: {
74
- status: S;
66
+ type HonoFileRespondOptions<TStatus extends SuccessStatusCode> = {
67
+ status: TStatus;
75
68
  content: Uint8Array<ArrayBuffer>;
76
69
  filename: string;
77
70
  contentType?: string;
78
- }): Response;
71
+ };
72
+
73
+ /**
74
+ * Wraps `c.json` in the shared success envelope while preserving its literal status.
75
+ * Missing response data is represented by an empty object for contract consistency.
76
+ */
77
+ declare function respond<T extends object = Record<string, never>, S extends SuccessStatusCode = SuccessStatusCode>(c: Context, options: HonoRespondOptions<T, S>): Response & TypedResponse<APISuccess<T> | APIError, S, 'json'>;
78
+ /**
79
+ * Responds with downloadable binary content and its attachment headers.
80
+ * Unknown/undefined content types default to `application/octet-stream`.
81
+ */
82
+ declare function fileRespond<S extends SuccessStatusCode>(c: Context, options: HonoFileRespondOptions<S>): Response;
79
83
 
80
84
  /**
81
- * Factory for creating successful API responses with serializable data.
82
- * Example usage: `success({ status: 200, data: { message: 'Operation successful' } })`
85
+ * Creates a successful API envelope without cloning its data.
86
+ * Generic inference preserves the exact supplied payload type.
83
87
  */
84
88
  declare function success<T = unknown>({ status, data }: {
85
89
  status: SuccessStatusCode;
86
90
  data: T;
87
91
  }): APISuccess<T>;
88
92
  /**
89
- * Factory for creating API error responses, including the error message.
90
- * Example usage: `failure({ status: 404, error: 'User not found' })`
93
+ * Creates a failed API envelope with one supported exception status.
94
+ * The supplied message remains unchanged for downstream presentation.
91
95
  */
92
96
  declare function failure({ status, error }: {
93
97
  status: ExceptionStatusCode;
@@ -95,132 +99,369 @@ declare function failure({ status, error }: {
95
99
  }): APIError;
96
100
 
97
101
  /**
98
- * Resolves an APIContractResult fetcher into a FetchResult discriminated union.
99
- * Does not throw errors, instead returning them as property of the FetchResult.
100
- * Example usage: const result = await fetchSafely(() => api.fetchUser(userId));
102
+ * Converts an API contract envelope into a mutually exclusive safe result.
103
+ * Only `APIError` values are normalized; rejected fetchers still reject.
101
104
  */
102
105
  declare function fetchSafely<TResult extends APIContractResult<unknown>>(fetcher: () => Promise<TResult>): Promise<FetchResult<APIContractData<TResult>>>;
103
106
  /**
104
- * Resolves an APIContractResult, throwing an error if the result is an APIError.
105
- * Example usage: const data = await fetchAndThrow(() => api.fetchUser(userId));
107
+ * Returns successful API data and throws for an `APIError` envelope.
108
+ * Rejected fetchers propagate their original error without replacement.
106
109
  */
107
110
  declare function fetchAndThrow<TResult extends APIContractResult<unknown>>(fetcher: () => Promise<TResult>): Promise<APIContractData<TResult>>;
108
111
 
109
112
  /**
110
- * Returns the current date and time shifted to the specified timezone.
113
+ * Canonical metadata shared by upload controls and runtime validation.
114
+ * Every supported format defines display, MIME, and extension values.
115
+ */
116
+ declare const fileFormatsConfig: {
117
+ readonly png: {
118
+ readonly name: "PNG";
119
+ readonly mimeTypes: readonly ["image/png"];
120
+ readonly extensions: readonly [".png"];
121
+ };
122
+ readonly jpg: {
123
+ readonly name: "JPG";
124
+ readonly mimeTypes: readonly ["image/jpeg"];
125
+ readonly extensions: readonly [".jpg", ".jpeg"];
126
+ };
127
+ readonly webp: {
128
+ readonly name: "WEBP";
129
+ readonly mimeTypes: readonly ["image/webp"];
130
+ readonly extensions: readonly [".webp"];
131
+ };
132
+ readonly avif: {
133
+ readonly name: "AVIF";
134
+ readonly mimeTypes: readonly ["image/avif"];
135
+ readonly extensions: readonly [".avif"];
136
+ };
137
+ readonly heic: {
138
+ readonly name: "HEIC";
139
+ readonly mimeTypes: readonly ["image/heic", "image/heif"];
140
+ readonly extensions: readonly [".heic", ".heif"];
141
+ };
142
+ readonly pdf: {
143
+ readonly name: "PDF";
144
+ readonly mimeTypes: readonly ["application/pdf"];
145
+ readonly extensions: readonly [".pdf"];
146
+ };
147
+ readonly rtf: {
148
+ readonly name: "RTF";
149
+ readonly mimeTypes: readonly ["application/rtf"];
150
+ readonly extensions: readonly [".rtf"];
151
+ };
152
+ readonly txt: {
153
+ readonly name: "TXT";
154
+ readonly mimeTypes: readonly ["text/plain"];
155
+ readonly extensions: readonly [".txt"];
156
+ };
157
+ };
158
+
159
+ declare const fileFormatsArray: readonly ["png", "jpg", "webp", "avif", "heic", "pdf", "rtf", "txt"];
160
+ type FileFormat = (typeof fileFormatsArray)[number];
161
+ declare const fileFormatsRecord: Readonly<{
162
+ AVIF: "avif";
163
+ JPG: "jpg";
164
+ PDF: "pdf";
165
+ PNG: "png";
166
+ RTF: "rtf";
167
+ TXT: "txt";
168
+ WEBP: "webp";
169
+ HEIC: "heic";
170
+ }>;
171
+ declare const fileFormat: Readonly<{
172
+ AVIF: "avif";
173
+ JPG: "jpg";
174
+ PDF: "pdf";
175
+ PNG: "png";
176
+ RTF: "rtf";
177
+ TXT: "txt";
178
+ WEBP: "webp";
179
+ HEIC: "heic";
180
+ }>;
181
+ declare const uploadValidationErrorsArray: readonly ["empty_file", "unsupported_file_format", "file_size_exceeded", "files_count_exceeded"];
182
+ type UploadValidationError = (typeof uploadValidationErrorsArray)[number];
183
+ declare const uploadValidationErrorsRecord: Readonly<{
184
+ EMPTY_FILE: "empty_file";
185
+ UNSUPPORTED_FILE_FORMAT: "unsupported_file_format";
186
+ FILE_SIZE_EXCEEDED: "file_size_exceeded";
187
+ FILES_COUNT_EXCEEDED: "files_count_exceeded";
188
+ }>;
189
+ declare const uploadValidationError: Readonly<{
190
+ EMPTY_FILE: "empty_file";
191
+ UNSUPPORTED_FILE_FORMAT: "unsupported_file_format";
192
+ FILE_SIZE_EXCEEDED: "file_size_exceeded";
193
+ FILES_COUNT_EXCEEDED: "files_count_exceeded";
194
+ }>;
195
+
196
+ type FileFormatConfig = (typeof fileFormatsConfig)[FileFormat];
197
+ /**
198
+ * Constraints used to validate an incoming collection of browser files.
199
+ * Existing and incoming counts are combined when enforcing capacity.
200
+ */
201
+ type UploadFilesValidationOptions = Readonly<{
202
+ /**
203
+ * Number of files already retained before the incoming batch is validated.
204
+ * Existing entries reduce the remaining capacity without being revalidated.
205
+ */
206
+ currentFilesCount: number;
207
+ /**
208
+ * Supported formats used to validate every file in the incoming batch.
209
+ * MIME and extension metadata are resolved through the canonical catalog.
210
+ */
211
+ formats: readonly FileFormat[];
212
+ /**
213
+ * Maximum accepted size of one uploaded file measured in bytes.
214
+ * Files exceeding this boundary receive the stable size error key.
215
+ */
216
+ maxFileSize: number;
217
+ /**
218
+ * Optional maximum number of retained files after accepting the batch.
219
+ * Omitting this value leaves collection capacity unrestricted.
220
+ */
221
+ maxFilesCount?: number;
222
+ }>;
223
+ /**
224
+ * Accepted files and the final rejection encountered in one batch.
225
+ * Valid entries remain available when another entry fails validation.
226
+ */
227
+ type UploadFilesValidationResult = Readonly<{
228
+ /**
229
+ * Valid incoming files that fit the remaining collection capacity.
230
+ * Accepted file objects preserve their original identity and ordering.
231
+ */
232
+ acceptedFiles: File[];
233
+ /**
234
+ * Final stable rejection encountered while processing the incoming batch.
235
+ * The field remains absent when every supplied file is accepted.
236
+ */
237
+ validationError?: UploadValidationError;
238
+ }>;
239
+ /**
240
+ * Options used to construct one reusable Zod file schema.
241
+ * Extension fallback is disabled by default to preserve strict MIME checks.
242
+ */
243
+ type ZodUploadFileSchemaOptions = Readonly<{
244
+ /**
245
+ * Supported formats accepted by the generated file schema.
246
+ * Strict MIME validation uses metadata from the canonical format catalog.
247
+ */
248
+ formats: readonly FileFormat[];
249
+ /**
250
+ * Maximum accepted file size measured in bytes for the generated file schema.
251
+ * Empty files remain invalid independently of this configured boundary.
252
+ */
253
+ maxFileSize: number;
254
+ /**
255
+ * Allows a supported extension to compensate for missing MIME metadata.
256
+ * The fallback remains disabled by default to preserve strict validation.
257
+ */
258
+ extensionFallback?: boolean;
259
+ }>;
260
+
261
+ /**
262
+ * Builds a reusable Zod schema for one uploaded file with format and size.
263
+ * MIME matching is strict unless extension fallback is explicitly enabled.
264
+ */
265
+ declare function zodUploadFileSchema(options: ZodUploadFileSchemaOptions): z.ZodFile;
266
+
267
+ /**
268
+ * Extracts a normalized trailing extension from a complete file name.
269
+ * Returns an empty string when no valid dot-delimited suffix exists.
270
+ */
271
+ declare function getFileExtension(fileName: string): string;
272
+ /**
273
+ * Normalizes a configured extension to a lowercase dot-prefixed value.
274
+ * Existing prefixes remain intact so repeated normalization is stable.
275
+ */
276
+ declare function normalizeFileExtension(extension: string): string;
277
+ /**
278
+ * Compares a concrete MIME type with an exact or wildcard configuration.
279
+ * Wildcards match every subtype belonging to the configured media group.
280
+ */
281
+ declare function matchesMimeType(fileMimeType: string, configuredMimeType: string): boolean;
282
+ /**
283
+ * Checks whether a file MIME type belongs to one selected format.
284
+ * File names and extensions cannot make an unsupported MIME type valid.
285
+ */
286
+ declare function isFileMimeTypeSupported(file: File, formats: readonly FileFormat[]): boolean;
287
+ /**
288
+ * Checks whether a file extension belongs to one selected format.
289
+ * The comparison is case-insensitive and requires a complete suffix.
290
+ */
291
+ declare function isFileExtensionSupported(file: File, formats: readonly FileFormat[]): boolean;
292
+ /**
293
+ * Checks whether a file matches one configured MIME type or extension.
294
+ * This permissive predicate supports clients where MIME metadata is absent.
295
+ */
296
+ declare function isFileFormatSupported(file: File, formats: readonly FileFormat[]): boolean;
297
+ /**
298
+ * Builds a native file-picker hint from configured MIME types and extensions.
299
+ * Duplicate values are removed while their configuration order is retained.
300
+ */
301
+ declare function createUploadAccept(formats: readonly FileFormat[]): string;
302
+
303
+ /**
304
+ * Validates one file against configured format and size constraints.
305
+ * Returns the first stable error key or nothing for a valid file.
306
+ */
307
+ declare function validateUploadFile(file: File, formats: readonly FileFormat[], maxFileSize: number): UploadValidationError | undefined;
308
+ /**
309
+ * Validates an incoming collection while preserving every accepted file.
310
+ * Returns accepted entries and the final rejection key from the batch.
311
+ */
312
+ declare function validateUploadFiles(incomingFiles: readonly File[], options: UploadFilesValidationOptions): UploadFilesValidationResult;
313
+
314
+ /**
315
+ * Projects the current instant onto the wall-clock fields of another timezone.
316
+ * The timezone defaults to `Europe/London` when no option is provided.
111
317
  *
112
- * This is useful when the application needs to display "now" from another
113
- * region's point of view instead of relying on the server's local timezone.
318
+ * @example
319
+ * const londonTime = getZonedTime({ tz: 'Europe/London' });
114
320
  */
115
- declare const getZonedTime: ({ tz }?: {
321
+ declare function getZonedTime({ tz }?: {
116
322
  tz?: string;
117
- }) => Date;
323
+ }): Date;
118
324
  /**
119
- * Returns the UTC offset for the specified timezone at the given date.
325
+ * Formats the UTC offset of a timezone at the supplied date.
326
+ * An explicit date preserves daylight-saving and historical offset rules.
120
327
  *
121
- * The offset is formatted for display next to dates and times, for example
122
- * `(+4 UTC)`, `(0 UTC)`, or `(-5 UTC)`. The date is required because timezone
123
- * offsets may change depending on daylight saving time or historical rules.
328
+ * @example
329
+ * getUTCOffset(date, 'Europe/Moscow'); // `(+3 UTC)`
124
330
  */
125
- declare const getUTCOffset: (date: Date, tz: string) => string;
331
+ declare function getUTCOffset(date: Date, tz: string): string;
126
332
  /**
127
- * Returns the current time formatted in the specified timezone.
128
- *
129
- * The result includes both the local time and its UTC offset, making it suitable
130
- * for UI labels, logs, bot messages, and other places where the user should see
131
- * not only the time itself, but also the timezone context behind that value.
333
+ * Formats the current time and UTC offset in the selected timezone.
334
+ * The timezone defaults to `Europe/London` when no option is provided.
132
335
  *
133
- * Format: `HH:mm:ss (+X UTC)`.
336
+ * @example
337
+ * getFormattedTime({ tz: 'Europe/Moscow' }); // `03:04:05 (+3 UTC)`
134
338
  */
135
- declare const getFormattedTime: ({ tz }?: {
339
+ declare function getFormattedTime({ tz }?: {
136
340
  tz?: string;
137
- }) => string;
341
+ }): string;
138
342
  /**
139
- * Returns the current date formatted in the specified timezone.
140
- *
141
- * By default, the result includes date, time, and UTC offset. This is useful for
142
- * complete timestamps displayed in UI, bot messages, reports, or diagnostics.
343
+ * Formats the current date with optional time and UTC offset components.
344
+ * Time is included by default and uses the selected timezone for display.
143
345
  *
144
- * Format with time: `dd.MM.yyyy HH:mm:ss (+X UTC)`.
145
- * Format without time: `dd.MM.yyyy`.
346
+ * @example
347
+ * getFormattedDate({ tz: 'UTC', withTime: false }); // `02.01.2024`
146
348
  */
147
- declare const getFormattedDate: ({ tz, withTime, }?: {
349
+ declare function getFormattedDate({ tz, withTime, }?: {
148
350
  tz?: string;
149
351
  withTime?: boolean;
150
- }) => string;
352
+ }): string;
151
353
  /**
152
- * Formats the provided date and time in the specified timezone.
153
- *
154
- * Unlike helpers that always use the current moment, this function accepts an
155
- * explicit Date value and formats that exact point in time for another timezone.
354
+ * Formats an explicit instant in another timezone using the selected locale.
355
+ * The result includes localized date text, wall-clock time, and UTC offset.
156
356
  *
157
- * Format: `HH:mm:ss, d MMMM yyyy (+X UTC)`.
357
+ * @example
358
+ * formatTime(date, { locale: ru, tz: 'Europe/Moscow' });
158
359
  */
159
- declare const formatTime: (time: Date, { locale, tz }?: {
360
+ declare function formatTime(time: Date, { locale, tz }?: {
160
361
  locale?: Locale;
161
362
  tz?: string;
162
- }) => string;
363
+ }): string;
163
364
 
164
365
  /**
165
- * Recursively replaces dots in a string with underscores in a type-safe manner.
166
- * Example: `ReplaceDotsWithUnderscores<'foo.bar.baz.ok'>` `'foo_bar_baz_ok'`.
366
+ * Recursively replaces dots in a string literal with underscores.
367
+ * Every other character remains unchanged in the resulting type.
368
+ *
369
+ * @example
370
+ * type Key = ReplaceDotsWithUnderscores<'foo.bar.baz'>; // `foo_bar_baz`
167
371
  */
168
- type ReplaceDotsWithUnderscores<T extends string> = T extends `${infer A}.${infer B}` ? `${A}_${ReplaceDotsWithUnderscores<B>}` : T;
372
+ type ReplaceDotsWithUnderscores<TValue extends string> = TValue extends `${infer Head}.${infer Tail}` ? `${Head}_${ReplaceDotsWithUnderscores<Tail>}` : TValue;
169
373
  /**
170
- * Type-safe record mapping string values to ergonomic enum-like keys.
171
- * Keys are uppercased and dots are replaced with underscores.
374
+ * Maps string literals to immutable uppercase enum-like keys.
375
+ * Dots become underscores while every value preserves its original literal.
376
+ *
377
+ * @example
378
+ * type Statuses = StringEnumRecord<readonly ['review.pending', 'published']>;
172
379
  */
173
- type StringEnumRecord<T extends readonly string[]> = Readonly<{
174
- [Value in T[number] as Uppercase<ReplaceDotsWithUnderscores<Value>>]: Value;
380
+ type StringEnumRecord<TValues extends readonly string[]> = Readonly<{
381
+ [Value in TValues[number] as Uppercase<ReplaceDotsWithUnderscores<Value>>]: Value;
175
382
  }>;
176
383
  /**
177
384
  * Creates an immutable enum-like record from a readonly string array.
178
- * Example: `['foo.s', 'baz'] as const` `{ FOO_S: 'foo.s', BAZ: 'baz' }`.
385
+ * Keys are uppercased and every dot is replaced with an underscore.
386
+ *
387
+ * @example
388
+ * createStringEnumRecord(['foo.s', 'baz'] as const); // `{ FOO_S: 'foo.s', BAZ: 'baz' }`
179
389
  */
180
390
  declare function createStringEnumRecord<const T extends readonly string[]>(values: T): StringEnumRecord<T>;
181
391
 
182
392
  /**
183
- * Safely executes functions and handles errors without using try/catch in the calling code.
184
- * Example usage: `safeExecute(() => fetchData(), (error) => console.error(error))`
393
+ * Resolves synchronous and asynchronous executions through one promise-based contract.
394
+ * Failures use the supplied fallback or propagate unchanged when none is available.
395
+ *
396
+ * @example
397
+ * await safeExecute(() => fetchData(), (error) => console.error(error));
398
+ */
399
+ declare function safeExecute<T, E = never>(fn: () => Promise<T> | T, onError?: (error: unknown) => E | Promise<E>): Promise<T | E>;
400
+ /**
401
+ * Result returned after measuring one successful asynchronous execution.
402
+ * The original value is preserved beside its rounded millisecond duration.
185
403
  */
186
- declare function safeExecute<T, E = never>(fn: () => Promise<T> | T, onError?: (error?: unknown) => E | Promise<E>): Promise<T | E>;
187
404
  type MeasuredExecution<T> = {
188
- /** The result of the executed function. */
405
+ /**
406
+ * Value resolved by the measured execution without cloning or transformation.
407
+ * Its generic type remains identical to the original asynchronous result.
408
+ */
189
409
  result: T;
190
- /** The time taken to execute the function, in milliseconds. */
410
+ /**
411
+ * Rounded wall-clock duration of the measured execution in milliseconds.
412
+ * The value is always collected after the supplied promise resolves.
413
+ */
191
414
  executionTime: number;
192
415
  };
193
416
  /**
194
- * Utility function to measure the execution time of an asynchronous function.
195
- * @returns An object containing the result of the execution and the time taken in milliseconds.
417
+ * Measures an asynchronous execution while preserving its resolved result.
418
+ * Rejected executions propagate unchanged and do not produce a measurement.
196
419
  *
197
- * ```typescript
420
+ * @example
198
421
  * const { result, executionTime } = await measureExecutionTime(async () => {
199
422
  * return await fetchData();
200
423
  * });
201
424
  * console.log(`Execution time: ${executionTime}ms`);
202
- * ```
203
425
  */
204
426
  declare function measureExecutionTime<T>(execution: () => Promise<T>): Promise<MeasuredExecution<T>>;
205
427
 
206
428
  /**
207
- * Creates a random string of the specified length using characters from DEFAULT_CHARACTERS.
208
- * Example usage: `generateRandomString(5) = 'aZ3fG' / generateRandomString() = 'G5kLm2P9sQ'`
429
+ * Creates a cryptographically sourced string from the alphanumeric set.
430
+ * Requested length defaults to `10` characters when omitted.
431
+ *
432
+ * @example
433
+ * generateRandomString(5); // `aZ3fG`
434
+ * generateRandomString(); // `G5kLm2P9sQ`
209
435
  */
210
436
  declare function generateRandomString(length?: number): string;
211
437
 
212
438
  /**
213
- * HTTP status colors for better visual parsing in logs:
214
- * 2xx - green, 4xx - yellow, 5xx - red, others - default color.
439
+ * Selects a terminal color function for one HTTP response status.
440
+ * Successes are green, client errors yellow, and server errors red.
441
+ *
442
+ * @example
443
+ * getColoredHTTPStatus(404)('404');
215
444
  */
216
445
  declare function getColoredHTTPStatus(status: number): (text: string) => string;
217
446
  /**
218
- * Unified logging interface for different levels (info, warn, error)
219
- * with optional service name and stack trace for errors (error level).
447
+ * Shared logging interface for informational, warning, and error messages.
448
+ * Every level supports a service label while errors may include a stack trace.
220
449
  */
221
450
  declare const log: {
451
+ /**
452
+ * Writes an informational message with an optional service label.
453
+ * Missing service names use the shared `log` fallback label.
454
+ */
222
455
  info: (message: string, service?: string) => void;
456
+ /**
457
+ * Writes a warning message with an optional service label.
458
+ * Missing service names use the shared `log` fallback label.
459
+ */
223
460
  warn: (message: string, service?: string) => void;
461
+ /**
462
+ * Writes an error message with optional service and stack trace context.
463
+ * Stack traces are rendered beneath the primary error message when supplied.
464
+ */
224
465
  error: (message: string, service?: string, stack?: string) => void;
225
466
  };
226
467
 
@@ -244,13 +485,13 @@ type Simplify<T> = {
244
485
  * identifierSchema: z.string().min(1),
245
486
  * });
246
487
  */
247
- declare const zodBulkSelectionSchema: <const TIdentifierSchema extends z.ZodType<string | number>>({ identifierSchema, }: {
488
+ declare function zodBulkSelectionSchema<const TIdentifierSchema extends z.ZodType<string | number>>({ identifierSchema, }: {
248
489
  /**
249
490
  * Schema used to validate every included or excluded entity identifier.
250
491
  * Its transforms and exact inferred output are preserved in both branches.
251
492
  */
252
493
  identifierSchema: TIdentifierSchema;
253
- }) => z.ZodDiscriminatedUnion<[z.ZodObject<{
494
+ }): z.ZodDiscriminatedUnion<[z.ZodObject<{
254
495
  mode: z.ZodLiteral<"include">;
255
496
  identifiers: z.ZodArray<TIdentifierSchema>;
256
497
  }, z.core.$strict>, z.ZodObject<{
@@ -278,30 +519,47 @@ type ZodBulkExcludeSelection<TIdentifier extends string | number = string> = Ext
278
519
  mode: 'exclude';
279
520
  }>;
280
521
 
281
- /** Options for configuring the JWT service. */
522
+ /**
523
+ * Configures the algorithm and default expiration used by `ZodJWTService`.
524
+ * Omitted values fall back to `HS256` and fifteen minutes respectively.
525
+ */
282
526
  type JWTServiceOptions = {
283
- /** The algorithm to use for signing and verifying JWTs. Defaults to 'HS256'. */
527
+ /**
528
+ * Symmetric algorithm used to sign and verify every token handled by the service.
529
+ * Defaults to `HS256` when the configuration does not provide another value.
530
+ */
284
531
  algorithm?: SymmetricAlgorithm;
285
- /** The default number of seconds until a signed JWT expires. Defaults to 900 seconds (15 minutes). */
532
+ /**
533
+ * Default lifetime assigned to signed tokens, expressed in seconds.
534
+ * Defaults to `900` seconds, which is equivalent to fifteen minutes.
535
+ */
286
536
  defaultExpirationSeconds?: number;
287
537
  };
288
- /** Options for signing a JWT. */
538
+ /**
539
+ * Configures one JWT signing operation without changing service defaults.
540
+ * A supplied expiration takes precedence over `defaultExpirationSeconds`.
541
+ */
289
542
  type JWTSignOptions = {
290
- /** The number of seconds until the JWT expires. */
543
+ /**
544
+ * Lifetime assigned to the token created by this signing operation.
545
+ * Overrides the service default and remains expressed in seconds.
546
+ */
291
547
  expiresInSeconds?: number;
292
548
  };
293
549
  /**
294
- * Utility types for inferring payload types from Zod schemas.
550
+ * Resolves the parsed payload of a Zod schema or preserves a direct payload type.
551
+ * Schema transforms are reflected in the resulting inferred payload.
295
552
  */
296
553
  type Payload<T> = T extends z$1.ZodType ? z$1.infer<T> : T;
297
554
  /**
298
- * Utility type to extract the payload schema type from a Zod schema.
299
- * */
555
+ * Preserves a Zod payload schema and rejects direct payload types with `never`.
556
+ * The result controls whether runtime payload validation is available.
557
+ */
300
558
  type PayloadSchema<T> = T extends z$1.ZodType ? T : never;
301
559
 
302
560
  /**
303
- * A service class for handling JWT operations with optional Zod schema validation for the payload.
304
- * The class is generic, allowing you to specify the type of the payload or a schema for validation.
561
+ * Signs, decodes, and verifies JWTs with optional Zod payload validation.
562
+ * A supplied schema parses payloads returned by decoding and verification.
305
563
  */
306
564
  declare class ZodJWTService<TPayloadOrSchema> {
307
565
  readonly payloadSchema: PayloadSchema<TPayloadOrSchema> | undefined;
@@ -309,23 +567,27 @@ declare class ZodJWTService<TPayloadOrSchema> {
309
567
  protected readonly defaultExpirationSeconds: number;
310
568
  constructor(payloadSchema?: PayloadSchema<TPayloadOrSchema>, options?: JWTServiceOptions);
311
569
  /**
312
- * Signs a JWT token with the provided payload and secret, using the configured algorithm and expiration settings.
313
- * Example usage: `const accessToken = await JWTServiceInstance.sign({ userId: 123 }, 'my-secret', { expiresInSeconds: 300 });`
570
+ * Signs a payload using the configured algorithm and expiration settings.
571
+ * Per-call expiration overrides the default configured by the service.
572
+ *
573
+ * @example
574
+ * const token = await jwtService.sign({ userId: '123' }, secret, { expiresInSeconds: 300 });
314
575
  */
315
576
  sign(payload: Payload<TPayloadOrSchema>, secret: string, options?: JWTSignOptions): Promise<string>;
316
577
  /**
317
- * Decodes a JWT token, and if a Zod schema is provided, validates the payload against it.
318
- * Returns the decoded payload if valid, or null if invalid or if the token cannot be decoded.
578
+ * Decodes without authenticating it and parses its payload when a schema exists.
579
+ * Schema validation failures return `null`, while malformed tokens still reject.
319
580
  *
320
- * Example usage: `const payload = await JWTServiceInstance.decode(token);`
581
+ * @example
582
+ * const payload = await jwtService.decode(token);
321
583
  */
322
584
  decode(token: string): Promise<Payload<TPayloadOrSchema> | null>;
323
585
  /**
324
- * Verifies and decodes a JWT token using the provided secret and configured algorithm,
325
- * then validates the payload against the Zod schema if one is provided.
586
+ * Verifies a token using the configured algorithm and parses its payload.
587
+ * Signature, expiration, and schema validation failures reject the operation.
326
588
  *
327
- * Throws an error if verification fails or if the payload is invalid according to the schema.
328
- * Example usage: `const payload = await JWTServiceInstance.verifyOrThrow(token, 'my-secret');`
589
+ * @example
590
+ * const payload = await jwtService.verifyOrThrow(token, secret);
329
591
  */
330
592
  verifyOrThrow(token: string, secret: string): Promise<Payload<TPayloadOrSchema>>;
331
593
  }
@@ -462,11 +724,10 @@ type AsQuery<T> = T extends null | undefined ? T : T extends QueryPrimitive ? st
462
724
  * This is useful for scenarios where you want to enforce that at least one
463
725
  * of several optional (nullable) properties must be provided in an object.
464
726
  *
465
- * ```typescript
727
+ * @example
466
728
  * type Example = AtLeastOne<{ a?: string; b?: number; c?: boolean }>;
467
729
  * // Valid: { a: "hello" }, { b: 42 }, { c: true }, { a: "hello", b: 42 }
468
730
  * // Invalid: {}, { a: undefined, b: undefined, c: undefined }
469
- * ```
470
731
  */
471
732
  type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simplify<Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>> : never;
472
733
 
@@ -476,9 +737,8 @@ type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simp
476
737
  * 2. A `.transform()` that narrows the output type to `AtLeastOne<T>`,
477
738
  * making it directly assignable to domain `*Select` and `*Update` types without casting.
478
739
  *
479
- * ```typescript
740
+ * @example
480
741
  * zQuery(zodAtLeastOne(userSelectSchema)) // result: UserSelect ✓
481
- * ```
482
742
  */
483
743
  declare const zodAtLeastOne: <T extends ZodObject<ZodRawShape>>(schema: T) => z.ZodPipe<T, z.ZodTransform<Awaited<AtLeastOne<z.core.output<T>>>, z.core.output<T>>>;
484
744
 
@@ -490,7 +750,7 @@ declare const parseQueryValue: (value: unknown) => unknown;
490
750
  * numbers or booleans. This helper converts only clear primitive values, allowing
491
751
  * regular schemas like `z.number()` and `z.boolean()` to validate query input directly.
492
752
  *
493
- * ```typescript
753
+ * @example
494
754
  * const schema = asQuery(z.object({
495
755
  * page: z.number().int().positive(),
496
756
  * isActive: z.boolean(),
@@ -498,7 +758,6 @@ declare const parseQueryValue: (value: unknown) => unknown;
498
758
  *
499
759
  * schema.parse({ page: '2', isActive: 'true' });
500
760
  * // { page: 2, isActive: true }
501
- * ```
502
761
  */
503
762
  declare const asQuery: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>;
504
763
 
@@ -508,4 +767,4 @@ declare const asQuery: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>
508
767
  */
509
768
  declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
510
769
 
511
- export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, type ZodBulkExcludeSelection, type ZodBulkIncludeSelection, type ZodBulkSelection, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, asQuery, createStringEnumRecord, failure, fetchAndThrow, fetchSafely, fileRespond, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, log, measureExecutionTime, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, zodAtLeastOne, zodBulkSelectionSchema, zodPaginationSchema, zodPaginationShape, zodSearchSchema };
770
+ export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type FileFormat, type FileFormatConfig, type HonoFileRespondOptions, type HonoRespondOptions, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, type UploadFilesValidationOptions, type UploadFilesValidationResult, type UploadValidationError, type ZodBulkExcludeSelection, type ZodBulkIncludeSelection, type ZodBulkSelection, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, type ZodUploadFileSchemaOptions, asQuery, createStringEnumRecord, createUploadAccept, createZodSearchWhereSchema, failure, fetchAndThrow, fetchSafely, fileFormat, fileFormatsArray, fileFormatsConfig, fileFormatsRecord, fileRespond, formatTime, generateRandomString, getColoredHTTPStatus, getFileExtension, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isFileExtensionSupported, isFileFormatSupported, isFileMimeTypeSupported, isPlainObject, log, matchesMimeType, measureExecutionTime, normalizeFileExtension, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, uploadValidationError, uploadValidationErrorsArray, uploadValidationErrorsRecord, validateUploadFile, validateUploadFiles, zodAtLeastOne, zodBulkSelectionSchema, zodPaginationSchema, zodPaginationShape, zodSearchQuerySchema, zodSearchSchema, zodUploadFileSchema };
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
 
@@ -116,14 +130,14 @@ var honoLoggingHandler = async (c, next) => {
116
130
  // Otherwise, we read the request body as text and normalize whitespace.
117
131
  (await c.req.raw.clone().text()).replaceAll(/\s+/g, " ").trim()
118
132
  );
119
- const bodyPreview = body.length > 50 ? `${body.slice(0, 25)}\u2026${body.slice(-25)}` : body;
133
+ const bodyPreview = body.length > 80 ? `${body.slice(0, 40)}\u2026${body.slice(-40)}` : body;
120
134
  const startTime = performance.now();
121
135
  await next();
122
136
  const duration = Math.round(performance.now() - startTime);
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,115 @@ 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.schemas.ts
212
+ import { z } from "zod";
213
+
214
+ // src/upload/upload.utilities.ts
215
+ function getFileExtension(fileName) {
216
+ const extensionIndex = fileName.lastIndexOf(".");
217
+ return extensionIndex > 0 && extensionIndex < fileName.length - 1 ? fileName.slice(extensionIndex).toLowerCase() : "";
218
+ }
219
+ function normalizeFileExtension(extension) {
220
+ const normalizedExtension = extension.toLowerCase();
221
+ return normalizedExtension.startsWith(".") ? normalizedExtension : `.${normalizedExtension}`;
222
+ }
223
+ function matchesMimeType(fileMimeType, configuredMimeType) {
224
+ if (configuredMimeType.endsWith("/*"))
225
+ return fileMimeType.startsWith(configuredMimeType.slice(0, -1));
226
+ return fileMimeType === configuredMimeType;
227
+ }
228
+ function isFileMimeTypeSupported(file, formats) {
229
+ return formats.some((format2) => fileFormatsConfig[format2].mimeTypes.some((mimeType) => matchesMimeType(file.type, mimeType)));
230
+ }
231
+ function isFileExtensionSupported(file, formats) {
232
+ const fileExtension = getFileExtension(file.name);
233
+ return formats.some((format2) => fileFormatsConfig[format2].extensions.some((extension) => normalizeFileExtension(extension) === fileExtension));
234
+ }
235
+ function isFileFormatSupported(file, formats) {
236
+ return isFileMimeTypeSupported(file, formats) || isFileExtensionSupported(file, formats);
237
+ }
238
+ function createUploadAccept(formats) {
239
+ const acceptedValues = formats.flatMap((format2) => [
240
+ ...fileFormatsConfig[format2].mimeTypes,
241
+ ...fileFormatsConfig[format2].extensions
242
+ ]);
243
+ return [...new Set(acceptedValues)].join(",");
244
+ }
245
+
246
+ // src/upload/upload.schemas.ts
247
+ function zodUploadFileSchema(options) {
248
+ const { formats, maxFileSize, extensionFallback = false } = options;
249
+ 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);
250
+ }
251
+
252
+ // src/upload/upload.validation.ts
253
+ function validateUploadFile(file, formats, maxFileSize) {
254
+ if (file.size === 0)
255
+ return uploadValidationError.EMPTY_FILE;
256
+ if (isFileFormatSupported(file, formats) === false)
257
+ return uploadValidationError.UNSUPPORTED_FILE_FORMAT;
258
+ if (file.size > maxFileSize)
259
+ return uploadValidationError.FILE_SIZE_EXCEEDED;
260
+ return void 0;
261
+ }
262
+ function validateUploadFiles(incomingFiles, options) {
263
+ const { currentFilesCount, formats, maxFileSize, maxFilesCount } = options;
264
+ const availableFilesCount = maxFilesCount === void 0 ? Infinity : maxFilesCount - currentFilesCount;
265
+ const acceptedFiles = [];
266
+ let validationError;
267
+ for (const file of incomingFiles) {
268
+ const fileValidationError = validateUploadFile(file, formats, maxFileSize) ?? // Collection limits apply only after intrinsic file validation succeeds.
269
+ // This preserves remaining slots for supported files from the same batch.
270
+ (acceptedFiles.length >= availableFilesCount ? uploadValidationError.FILES_COUNT_EXCEEDED : void 0);
271
+ if (fileValidationError) {
272
+ validationError = fileValidationError;
273
+ continue;
274
+ }
275
+ acceptedFiles.push(file);
276
+ }
277
+ return { acceptedFiles, validationError };
278
+ }
279
+
165
280
  // src/utilities/execution.utilities.ts
166
281
  async function safeExecute(fn, onError) {
167
282
  try {
168
283
  return await fn();
169
- } catch (err) {
284
+ } catch (error) {
170
285
  if (onError)
171
- return await onError(err);
172
- throw err;
286
+ return await onError(error);
287
+ throw error;
173
288
  }
174
289
  }
175
290
  async function measureExecutionTime(execution) {
@@ -180,19 +295,19 @@ async function measureExecutionTime(execution) {
180
295
  }
181
296
 
182
297
  // src/zod-bulk/zod-bulk.schemas.ts
183
- import { z } from "zod";
298
+ import { z as z2 } from "zod";
184
299
  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, {
300
+ function zodBulkSelectionSchema({ identifierSchema }) {
301
+ const identifiersSchema = z2.array(identifierSchema).refine((identifiers) => new Set(identifiers).size === identifiers.length, {
187
302
  message: DEFAULT_ERROR_MESSAGE
188
303
  });
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()
304
+ return z2.discriminatedUnion("mode", [
305
+ z2.object({ mode: z2.literal("include"), identifiers: identifiersSchema }).strict(),
306
+ z2.object({ mode: z2.literal("exclude"), excludedIdentifiers: identifiersSchema }).strict()
192
307
  ]);
193
- };
308
+ }
194
309
 
195
- // src/zod-jwt/zod-jwt.schemas.ts
310
+ // src/zod-jwt/zod-jwt.services.ts
196
311
  import { decode as decodeJWT, sign as signJWT, verify as verifyJWT } from "hono/jwt";
197
312
  var ZodJWTService = class {
198
313
  payloadSchema;
@@ -204,37 +319,41 @@ var ZodJWTService = class {
204
319
  this.defaultExpirationSeconds = options?.defaultExpirationSeconds ?? 60 * 15;
205
320
  }
206
321
  /**
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 });`
322
+ * Signs a payload using the configured algorithm and expiration settings.
323
+ * Per-call expiration overrides the default configured by the service.
324
+ *
325
+ * @example
326
+ * const token = await jwtService.sign({ userId: '123' }, secret, { expiresInSeconds: 300 });
209
327
  */
210
328
  async sign(payload, secret, options) {
211
329
  const exp = Math.floor(Date.now() / 1e3) + (options?.expiresInSeconds ?? this.defaultExpirationSeconds);
212
330
  return signJWT({ ...payload, exp }, secret, this.algorithm);
213
331
  }
214
332
  /**
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.
333
+ * Decodes without authenticating it and parses its payload when a schema exists.
334
+ * Schema validation failures return `null`, while malformed tokens still reject.
217
335
  *
218
- * Example usage: `const payload = await JWTServiceInstance.decode(token);`
336
+ * @example
337
+ * const payload = await jwtService.decode(token);
219
338
  */
220
339
  async decode(token) {
221
340
  const { payload } = decodeJWT(token);
222
- if (!this.payloadSchema) {
341
+ if (this.payloadSchema === void 0) {
223
342
  return payload;
224
343
  }
225
344
  const { success: success2, data } = await this.payloadSchema.safeParseAsync(payload);
226
345
  return success2 ? data : null;
227
346
  }
228
347
  /**
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.
348
+ * Verifies a token using the configured algorithm and parses its payload.
349
+ * Signature, expiration, and schema validation failures reject the operation.
231
350
  *
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');`
351
+ * @example
352
+ * const payload = await jwtService.verifyOrThrow(token, secret);
234
353
  */
235
354
  async verifyOrThrow(token, secret) {
236
355
  const payload = await verifyJWT(token, secret, this.algorithm);
237
- if (!this.payloadSchema) {
356
+ if (this.payloadSchema === void 0) {
238
357
  return payload;
239
358
  }
240
359
  return this.payloadSchema.parseAsync(payload);
@@ -242,23 +361,23 @@ var ZodJWTService = class {
242
361
  };
243
362
 
244
363
  // src/zod-search/zod-search.pagination.schemas.ts
245
- import { z as z2 } from "zod";
246
- var zodPaginationSchema = z2.object({
364
+ import { z as z3 } from "zod";
365
+ var zodPaginationSchema = z3.object({
247
366
  /**
248
367
  * Zero-based number of records skipped before collecting a result page.
249
368
  * String values are coerced to support validation of URL query input.
250
369
  */
251
- offset: z2.coerce.number().int().nonnegative().default(0),
370
+ offset: z3.coerce.number().int().nonnegative().default(0),
252
371
  /**
253
372
  * Positive maximum number of records returned in a single result page.
254
373
  * String values are coerced to support validation of URL query input.
255
374
  */
256
- limit: z2.coerce.number().int().positive().default(10)
375
+ limit: z3.coerce.number().int().positive().default(10)
257
376
  });
258
377
  var zodPaginationShape = zodPaginationSchema.shape;
259
378
 
260
379
  // src/zod-search/zod-search.schemas.ts
261
- import { z as z3 } from "zod";
380
+ import { z as z4 } from "zod";
262
381
 
263
382
  // src/zod-validation/zod-validation.refiners.ts
264
383
  var DEFAULT_ERROR_MESSAGE2 = "Invalid input. At least one field must be provided.";
@@ -269,7 +388,7 @@ var zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
269
388
  }).transform((val) => val);
270
389
 
271
390
  // src/zod-search/zod-search.schemas.ts
272
- var zodSearchQuerySchema = z3.string().trim().min(1).optional();
391
+ var zodSearchQuerySchema = z4.string().trim().min(1).optional();
273
392
  var createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial());
274
393
  var zodSearchSchema = (options) => {
275
394
  const whereSchema = options.filters !== void 0 ? createZodSearchWhereSchema(options.filters) : options.whereSchema;
@@ -278,11 +397,11 @@ var zodSearchSchema = (options) => {
278
397
  ...options.queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
279
398
  ...options.paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
280
399
  };
281
- return z3.object(shape).strict();
400
+ return z4.object(shape).strict();
282
401
  };
283
402
 
284
403
  // src/zod-validation/zod-validation.parsing.ts
285
- import { z as z4 } from "zod";
404
+ import { z as z5 } from "zod";
286
405
 
287
406
  // src/zod-validation/zod-validation.utilities.ts
288
407
  var isPlainObject = (value) => {
@@ -309,37 +428,56 @@ var parseQueryValue = (value) => {
309
428
  return Number(normalized);
310
429
  return value;
311
430
  };
312
- var asQuery = (schema) => z4.preprocess(parseQueryValue, schema);
431
+ var asQuery = (schema) => z5.preprocess(parseQueryValue, schema);
313
432
  export {
314
433
  EXCEPTION_STATUS_CODES,
315
434
  SUCCESS_STATUS_CODES,
316
435
  ZodJWTService,
317
436
  asQuery,
318
437
  createStringEnumRecord,
438
+ createUploadAccept,
439
+ createZodSearchWhereSchema,
319
440
  failure,
320
441
  fetchAndThrow,
321
442
  fetchSafely,
443
+ fileFormat,
444
+ fileFormatsArray,
445
+ fileFormatsConfig,
446
+ fileFormatsRecord,
322
447
  fileRespond,
323
448
  formatTime,
324
449
  generateRandomString,
325
450
  getColoredHTTPStatus,
451
+ getFileExtension,
326
452
  getFormattedDate,
327
453
  getFormattedTime,
328
454
  getUTCOffset,
329
455
  getZonedTime,
330
456
  honoLoggingHandler,
457
+ isFileExtensionSupported,
458
+ isFileFormatSupported,
459
+ isFileMimeTypeSupported,
331
460
  isPlainObject,
332
461
  log,
462
+ matchesMimeType,
333
463
  measureExecutionTime,
464
+ normalizeFileExtension,
334
465
  onHandlerError,
335
466
  parseQueryValue,
336
467
  respond,
337
468
  safeExecute,
338
469
  sqlWhere,
339
470
  success,
471
+ uploadValidationError,
472
+ uploadValidationErrorsArray,
473
+ uploadValidationErrorsRecord,
474
+ validateUploadFile,
475
+ validateUploadFiles,
340
476
  zodAtLeastOne,
341
477
  zodBulkSelectionSchema,
342
478
  zodPaginationSchema,
343
479
  zodPaginationShape,
344
- zodSearchSchema
480
+ zodSearchQuerySchema,
481
+ zodSearchSchema,
482
+ zodUploadFileSchema
345
483
  };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@kalutskii/foundation",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
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",