@kalutskii/foundation 0.1.3 → 0.1.5
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 +10 -10
- package/dist/index.d.ts +73 -2
- package/dist/index.js +107 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @kalutskii/foundation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Collection of utilities, types, and helpers for building typed API contracts and responses in TypeScript projects. Designed for use in Bun/Node.js and Cloudflare Workers environments.
|
|
4
4
|
|
|
5
5
|
## What this package provides
|
|
6
6
|
|
|
@@ -13,13 +13,13 @@ Shared HTTP contracts and helpers for consistent API communication across esb-ma
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
bun add @
|
|
16
|
+
bun add @kalutskii/foundation
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
or
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm i @
|
|
22
|
+
npm i @kalutskii/foundation
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Core concepts
|
|
@@ -39,8 +39,8 @@ Status code groups are exported as constants and used by types:
|
|
|
39
39
|
### 1. Build typed contracts
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
import { failure, success } from '@
|
|
43
|
-
import type { APIContractResult } from '@
|
|
42
|
+
import { failure, success } from '@kalutskii/foundation';
|
|
43
|
+
import type { APIContractResult } from '@kalutskii/foundation';
|
|
44
44
|
|
|
45
45
|
type User = { id: string; name: string };
|
|
46
46
|
|
|
@@ -53,8 +53,8 @@ const result: APIContractResult<User> = Math.random() > 0.5 ? ok : bad;
|
|
|
53
53
|
### 2. Resolve fetchers safely
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
import { fetchAndThrow, fetchSafely } from '@
|
|
57
|
-
import type { APIContractResult } from '@
|
|
56
|
+
import { fetchAndThrow, fetchSafely } from '@kalutskii/foundation';
|
|
57
|
+
import type { APIContractResult } from '@kalutskii/foundation';
|
|
58
58
|
|
|
59
59
|
type User = { id: string; name: string };
|
|
60
60
|
|
|
@@ -78,7 +78,7 @@ try {
|
|
|
78
78
|
### 3. Use typed Hono JSON responses
|
|
79
79
|
|
|
80
80
|
```ts
|
|
81
|
-
import { respond } from '@
|
|
81
|
+
import { respond } from '@kalutskii/foundation';
|
|
82
82
|
import { Hono } from 'hono';
|
|
83
83
|
|
|
84
84
|
const app = new Hono();
|
|
@@ -96,7 +96,7 @@ app.get('/health', (c) => {
|
|
|
96
96
|
### 4. Parse query params with helpers
|
|
97
97
|
|
|
98
98
|
```ts
|
|
99
|
-
import { asQueryBoolean, asQueryNumber } from '@
|
|
99
|
+
import { asQueryBoolean, asQueryNumber } from '@kalutskii/foundation';
|
|
100
100
|
import { z } from 'zod';
|
|
101
101
|
|
|
102
102
|
const pageSchema = asQueryNumber(z.number().int().min(1));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Context, TypedResponse } from 'hono';
|
|
1
|
+
import { ErrorHandler, Context, TypedResponse } from 'hono';
|
|
2
2
|
import { z, ZodNumber } from 'zod';
|
|
3
|
+
import { Locale } from 'date-fns';
|
|
3
4
|
|
|
4
5
|
declare const SUCCESS_STATUS_CODES: readonly [200, 201, 202, 307];
|
|
5
6
|
declare const EXCEPTION_STATUS_CODES: readonly [400, 401, 403, 404, 405, 409, 500];
|
|
@@ -44,6 +45,12 @@ declare function fetchSafely<TResult extends APIContractResult<unknown>>(fetcher
|
|
|
44
45
|
/** Resolves an APIContractResult fetcher, throwing an error if the result is an APIError. */
|
|
45
46
|
declare function fetchAndThrow<TResult extends APIContractResult<unknown>>(fetcher: () => Promise<TResult>): Promise<APIContractData<TResult>>;
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Global error handler for Hono framework. Catches all exceptions thrown in route handlers and middlewares.
|
|
50
|
+
* Distinguishes between expected HTTPExceptions (mapped to their status codes) and unexpected errors (500).
|
|
51
|
+
*/
|
|
52
|
+
declare const onHandlerError: ErrorHandler;
|
|
53
|
+
|
|
47
54
|
/**
|
|
48
55
|
* Type utility that recursively transforms all Date fields to string, as well as handling arrays and objects.
|
|
49
56
|
* This is necessary for proper typing when working with RPC, as JSON does not support the Date type directly.
|
|
@@ -73,4 +80,68 @@ declare function respond<T extends object = Record<string, never>, S extends Suc
|
|
|
73
80
|
data?: T;
|
|
74
81
|
}): Response & TypedResponse<APISuccess<SerializeDates<T>> | APIError, S, 'json'>;
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Returns the current time in the specified timezone.
|
|
85
|
+
* Example: getZonedTime({ tz: 'America/New_York' }) = new Date('2026-03-15T12:00:00Z')
|
|
86
|
+
*/
|
|
87
|
+
declare const getZonedTime: ({ tz }?: {
|
|
88
|
+
tz?: string;
|
|
89
|
+
}) => Date;
|
|
90
|
+
/**
|
|
91
|
+
* Returns the timezone offset in the format (+4 UTC) / (-5 UTC).
|
|
92
|
+
* Example: getUTCOffset(new Date('2026-03-15T12:00:00Z'), 'America/New_York') = '(-4 UTC)'
|
|
93
|
+
*/
|
|
94
|
+
declare const getUTCOffset: (date: Date, tz: string) => string;
|
|
95
|
+
/**
|
|
96
|
+
* Returns the current time in the specified timezone, formatted as HH:mm:ss (+X UTC).
|
|
97
|
+
* Example: getFormattedTime({ tz: 'America/New_York' }) = '12:00:00 (-4 UTC)'
|
|
98
|
+
*/
|
|
99
|
+
declare const getFormattedTime: ({ tz }?: {
|
|
100
|
+
tz?: string;
|
|
101
|
+
}) => string;
|
|
102
|
+
/**
|
|
103
|
+
* Returns the current date in the specified timezone, formatted as dd.MM.yyyy.
|
|
104
|
+
* By default, it also includes time (HH:mm:ss) and timezone offset.
|
|
105
|
+
* Example: getFormattedDate({ tz: 'America/New_York' }) = '03.15.2026 12:00:00 (-4 UTC)'
|
|
106
|
+
*/
|
|
107
|
+
declare const getFormattedDate: ({ tz, withTime, }?: {
|
|
108
|
+
tz?: string;
|
|
109
|
+
withTime?: boolean;
|
|
110
|
+
}) => string;
|
|
111
|
+
/**
|
|
112
|
+
* Formats the given time in the specified timezone, using the provided locale for date formatting.
|
|
113
|
+
* Example: formatTime(new Date('2026-03-15T12:00:00Z'), { tz: 'America/New_York' }) = '12:00:00, March 15, 2026 (-4 UTC)'
|
|
114
|
+
*/
|
|
115
|
+
declare const formatTime: (time: Date, { locale, tz }?: {
|
|
116
|
+
locale?: Locale;
|
|
117
|
+
tz?: string;
|
|
118
|
+
}) => string;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Safely executes functions and handles errors without using try/catch in the calling code.
|
|
122
|
+
* Example: safeExecute(() => fetchData(), (error) => console.error(error));
|
|
123
|
+
*/
|
|
124
|
+
declare function safeExecute<T, E = never>(fn: () => Promise<T> | T, onError?: (error?: unknown) => E | Promise<E>): Promise<T | E>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a random string of the specified length using characters from DEFAULT_CHARACTERS.
|
|
128
|
+
* Example: generateRandomString(5) = 'aZ3fG' / generateRandomString() = 'G5kLm2P9sQ' (default 10 characters)
|
|
129
|
+
*/
|
|
130
|
+
declare function generateRandomString(length?: number): string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* HTTP status colors for better visual parsing in logs:
|
|
134
|
+
* 2xx - green, 4xx - yellow, 5xx - red, others - default color.
|
|
135
|
+
*/
|
|
136
|
+
declare function getColoredHTTPStatus(status: number): (text: string) => string;
|
|
137
|
+
/**
|
|
138
|
+
* Unified logging interface for different levels (info, warn, error)
|
|
139
|
+
* with optional service name and stack trace for errors.
|
|
140
|
+
*/
|
|
141
|
+
declare const log: {
|
|
142
|
+
info: (message: string, service?: string) => void;
|
|
143
|
+
warn: (message: string, service?: string) => void;
|
|
144
|
+
error: (message: string, service?: string, stack?: string) => void;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, SUCCESS_STATUS_CODES, type SerializeDates, type SuccessStatusCode, asQueryBoolean, asQueryNumber, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, log, onHandlerError, respond, safeExecute, success };
|
package/dist/index.js
CHANGED
|
@@ -24,12 +24,108 @@ async function fetchAndThrow(fetcher) {
|
|
|
24
24
|
return response.data;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// src/hono/hono.execution.ts
|
|
28
|
+
import { HTTPException } from "hono/http-exception";
|
|
29
|
+
import { red as red2 } from "kleur";
|
|
30
|
+
|
|
31
|
+
// src/utilities/generation.utilities.ts
|
|
32
|
+
var DEFAULT_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
33
|
+
function generateRandomString(length = 10) {
|
|
34
|
+
const randomValues = crypto.getRandomValues(new Uint32Array(length));
|
|
35
|
+
let result = "";
|
|
36
|
+
for (let i = 0; i < length; i++) {
|
|
37
|
+
result += DEFAULT_CHARACTERS[randomValues[i] % DEFAULT_CHARACTERS.length];
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/utilities/logging.utilities.ts
|
|
43
|
+
import { blue, dim, green, red, white, yellow } from "kleur/colors";
|
|
44
|
+
|
|
45
|
+
// src/utilities/datetime.utilities.ts
|
|
46
|
+
import { format } from "date-fns";
|
|
47
|
+
import { getTimezoneOffset, toZonedTime } from "date-fns-tz";
|
|
48
|
+
import { ru } from "date-fns/locale";
|
|
49
|
+
var getZonedTime = ({ tz = "Europe/London" } = {}) => {
|
|
50
|
+
return toZonedTime(/* @__PURE__ */ new Date(), tz);
|
|
51
|
+
};
|
|
52
|
+
var getUTCOffset = (date, tz) => {
|
|
53
|
+
const offset = getTimezoneOffset(tz, date) / (60 * 60 * 1e3);
|
|
54
|
+
return `(${offset >= 0 ? "+" : ""}${offset} UTC)`;
|
|
55
|
+
};
|
|
56
|
+
var getFormattedTime = ({ tz = "Europe/London" } = {}) => {
|
|
57
|
+
const zonedTime = getZonedTime({ tz });
|
|
58
|
+
return `${format(zonedTime, "HH:mm:ss")} ${getUTCOffset(zonedTime, tz)}`;
|
|
59
|
+
};
|
|
60
|
+
var getFormattedDate = ({ tz = "Europe/London", withTime = true } = {}) => {
|
|
61
|
+
const zonedTime = getZonedTime({ tz });
|
|
62
|
+
const pattern = withTime ? "dd.MM.yyyy HH:mm:ss" : "dd.MM.yyyy";
|
|
63
|
+
const formatted = format(zonedTime, pattern);
|
|
64
|
+
return `${formatted}${withTime ? ` ${getUTCOffset(zonedTime, tz)}` : ""}`;
|
|
65
|
+
};
|
|
66
|
+
var formatTime = (time, { locale = ru, tz = "Europe/London" } = {}) => {
|
|
67
|
+
const zonedTime = toZonedTime(time, tz);
|
|
68
|
+
return `${format(zonedTime, "HH:mm:ss, d MMMM yyyy", { locale })} ${getUTCOffset(zonedTime, tz)}`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/utilities/logging.utilities.ts
|
|
72
|
+
function getColoredHTTPStatus(status) {
|
|
73
|
+
const colorMap = [
|
|
74
|
+
{ range: [200, 299], colorFn: green },
|
|
75
|
+
{ range: [400, 499], colorFn: yellow },
|
|
76
|
+
{ range: [500, 599], colorFn: red }
|
|
77
|
+
];
|
|
78
|
+
const statusColor = colorMap.find(({ range: [min, max] }) => status >= (min || 0) && status <= (max || 600));
|
|
79
|
+
return statusColor ? statusColor.colorFn : (text) => text;
|
|
80
|
+
}
|
|
81
|
+
function writeLog(message, level, service = "log", stack) {
|
|
82
|
+
const logColorMap = { info: blue, warn: yellow, error: red };
|
|
83
|
+
const timestamp = dim(getFormattedTime());
|
|
84
|
+
const serviceName = logColorMap[level](service).padEnd(18);
|
|
85
|
+
const formattedMessage = white(message);
|
|
86
|
+
console.log(`[${timestamp}] ${serviceName} | ${formattedMessage}`);
|
|
87
|
+
if (stack)
|
|
88
|
+
console.log(`[${timestamp}] ${red("\u21B3 trace").padEnd(18)} | ${dim(stack)}`);
|
|
89
|
+
}
|
|
90
|
+
var log = {
|
|
91
|
+
info: (message, service) => writeLog(message, "info", service),
|
|
92
|
+
warn: (message, service) => writeLog(message, "warn", service),
|
|
93
|
+
error: (message, service, stack) => writeLog(message, "error", service, stack)
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/hono/hono.execution.ts
|
|
97
|
+
function proceedUnhandledError(error) {
|
|
98
|
+
const errorId = generateRandomString(6);
|
|
99
|
+
const errorMessage = error instanceof Error ? error.message + error.stack : JSON.stringify(error);
|
|
100
|
+
log.error(`Unhandled error: ${red2(errorMessage)}`, errorId);
|
|
101
|
+
return failure({ status: 500, error: `Internal server error | ${errorId}` });
|
|
102
|
+
}
|
|
103
|
+
var onHandlerError = (error, c) => {
|
|
104
|
+
let response;
|
|
105
|
+
if (error instanceof HTTPException && error.status < 500) {
|
|
106
|
+
response = failure({ status: error.status, error: error.message });
|
|
107
|
+
} else
|
|
108
|
+
response = proceedUnhandledError(error);
|
|
109
|
+
return c.json(response, response.status);
|
|
110
|
+
};
|
|
111
|
+
|
|
27
112
|
// src/hono/hono.respond.ts
|
|
28
113
|
function respond(c, options) {
|
|
29
114
|
return c.json(success({ status: options.status, data: options.data ?? {} }), options.status);
|
|
30
115
|
}
|
|
31
116
|
|
|
32
|
-
// src/utilities/
|
|
117
|
+
// src/utilities/execution.utilities.ts
|
|
118
|
+
async function safeExecute(fn, onError) {
|
|
119
|
+
try {
|
|
120
|
+
return await fn();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (onError)
|
|
123
|
+
return await onError(err);
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/utilities/serialization.utilities.ts
|
|
33
129
|
import { z } from "zod";
|
|
34
130
|
var asQueryNumber = (schema) => z.preprocess((v) => typeof v === "string" ? Number(v) : v, schema);
|
|
35
131
|
var asQueryBoolean = (schema) => {
|
|
@@ -56,6 +152,16 @@ export {
|
|
|
56
152
|
failure,
|
|
57
153
|
fetchAndThrow,
|
|
58
154
|
fetchSafely,
|
|
155
|
+
formatTime,
|
|
156
|
+
generateRandomString,
|
|
157
|
+
getColoredHTTPStatus,
|
|
158
|
+
getFormattedDate,
|
|
159
|
+
getFormattedTime,
|
|
160
|
+
getUTCOffset,
|
|
161
|
+
getZonedTime,
|
|
162
|
+
log,
|
|
163
|
+
onHandlerError,
|
|
59
164
|
respond,
|
|
165
|
+
safeExecute,
|
|
60
166
|
success
|
|
61
167
|
};
|