@kalutskii/foundation 0.1.3

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 ADDED
@@ -0,0 +1,125 @@
1
+ # @esb-market-contracts/core
2
+
3
+ Shared HTTP contracts and helpers for consistent API communication across esb-market-space services.
4
+
5
+ ## What this package provides
6
+
7
+ - Typed API response contracts.
8
+ - Response factories for success and failure payloads.
9
+ - Safe async resolvers for contract-based fetch functions.
10
+ - A Hono helper for typed JSON success responses.
11
+ - Utility types/helpers for query parsing and Date serialization.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ bun add @esb-market-contracts/core
17
+ ```
18
+
19
+ or
20
+
21
+ ```bash
22
+ npm i @esb-market-contracts/core
23
+ ```
24
+
25
+ ## Core concepts
26
+
27
+ Contract shape:
28
+
29
+ - Success: `{ kind: 'data', status, data }`
30
+ - Error: `{ kind: 'error', status, error }`
31
+
32
+ Status code groups are exported as constants and used by types:
33
+
34
+ - `SUCCESS_STATUS_CODES`
35
+ - `EXCEPTION_STATUS_CODES`
36
+
37
+ ## Usage
38
+
39
+ ### 1. Build typed contracts
40
+
41
+ ```ts
42
+ import { failure, success } from '@esb-market-contracts/core';
43
+ import type { APIContractResult } from '@esb-market-contracts/core';
44
+
45
+ type User = { id: string; name: string };
46
+
47
+ const ok = success<User>({ status: 200, data: { id: '1', name: 'Kate' } });
48
+ const bad = failure({ status: 404, error: 'User not found' });
49
+
50
+ const result: APIContractResult<User> = Math.random() > 0.5 ? ok : bad;
51
+ ```
52
+
53
+ ### 2. Resolve fetchers safely
54
+
55
+ ```ts
56
+ import { fetchAndThrow, fetchSafely } from '@esb-market-contracts/core';
57
+ import type { APIContractResult } from '@esb-market-contracts/core';
58
+
59
+ type User = { id: string; name: string };
60
+
61
+ declare function getUser(): Promise<APIContractResult<User>>;
62
+
63
+ const safe = await fetchSafely(getUser);
64
+ if (safe.error) {
65
+ console.error(safe.error.message);
66
+ } else {
67
+ console.log(safe.data.name);
68
+ }
69
+
70
+ try {
71
+ const user = await fetchAndThrow(getUser);
72
+ console.log(user.name);
73
+ } catch (error) {
74
+ console.error(error);
75
+ }
76
+ ```
77
+
78
+ ### 3. Use typed Hono JSON responses
79
+
80
+ ```ts
81
+ import { respond } from '@esb-market-contracts/core';
82
+ import { Hono } from 'hono';
83
+
84
+ const app = new Hono();
85
+
86
+ app.get('/health', (c) => {
87
+ return respond(c, {
88
+ status: 200,
89
+ data: { ok: true, now: new Date() },
90
+ });
91
+ });
92
+ ```
93
+
94
+ `respond` returns a typed JSON response contract and includes API error type in route output unions.
95
+
96
+ ### 4. Parse query params with helpers
97
+
98
+ ```ts
99
+ import { asQueryBoolean, asQueryNumber } from '@esb-market-contracts/core';
100
+ import { z } from 'zod';
101
+
102
+ const pageSchema = asQueryNumber(z.number().int().min(1));
103
+ const enabledSchema = asQueryBoolean(z.boolean());
104
+
105
+ pageSchema.parse('2'); // 2
106
+ enabledSchema.parse('yes'); // true
107
+ enabledSchema.parse('off'); // false
108
+ ```
109
+
110
+ ## Exports
111
+
112
+ The package exports all public APIs from a single entrypoint:
113
+
114
+ - HTTP constants, schemas, factories, and resolvers.
115
+ - Hono `respond` helper.
116
+ - Serialization/query utilities, including `SerializeDates`.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ bun install
122
+ bun run lint
123
+ bun run typecheck
124
+ bun run build
125
+ ```
@@ -0,0 +1,76 @@
1
+ import { Context, TypedResponse } from 'hono';
2
+ import { z, ZodNumber } from 'zod';
3
+
4
+ declare const SUCCESS_STATUS_CODES: readonly [200, 201, 202, 307];
5
+ declare const EXCEPTION_STATUS_CODES: readonly [400, 401, 403, 404, 405, 409, 500];
6
+
7
+ type SuccessStatusCode = (typeof SUCCESS_STATUS_CODES)[number];
8
+ type ExceptionStatusCode = (typeof EXCEPTION_STATUS_CODES)[number];
9
+ type APISuccess<T = void> = {
10
+ kind: 'data';
11
+ status: SuccessStatusCode;
12
+ data: T;
13
+ };
14
+ type APIError = {
15
+ kind: 'error';
16
+ status: ExceptionStatusCode;
17
+ error: string;
18
+ };
19
+ type APIContractResult<TData = void> = APISuccess<TData> | APIError;
20
+ type APIContractData<TResult extends APIContractResult<unknown>> = TResult extends APISuccess<infer TData> ? TData : never;
21
+ type APIContractError<TResult extends APIContractResult<unknown>> = Extract<TResult, APIError>;
22
+ /** Discriminated union result of a fetch operation. If `error` is set, `data` is null and vice versa. */
23
+ type FetchResult<T> = {
24
+ error: null;
25
+ data: T;
26
+ } | {
27
+ error: Error;
28
+ data: null;
29
+ };
30
+
31
+ /** Factory for creating successful API responses with serializable data. */
32
+ declare function success<T = unknown>({ status, data }: {
33
+ status: SuccessStatusCode;
34
+ data: T;
35
+ }): APISuccess<T>;
36
+ /** Factory for creating API error responses, including the error message. */
37
+ declare function failure({ status, error }: {
38
+ status: ExceptionStatusCode;
39
+ error: string;
40
+ }): APIError;
41
+
42
+ /** Resolves an APIContractResult fetcher into a FetchResult discriminated union. */
43
+ declare function fetchSafely<TResult extends APIContractResult<unknown>>(fetcher: () => Promise<TResult>): Promise<FetchResult<APIContractData<TResult>>>;
44
+ /** Resolves an APIContractResult fetcher, throwing an error if the result is an APIError. */
45
+ declare function fetchAndThrow<TResult extends APIContractResult<unknown>>(fetcher: () => Promise<TResult>): Promise<APIContractData<TResult>>;
46
+
47
+ /**
48
+ * Type utility that recursively transforms all Date fields to string, as well as handling arrays and objects.
49
+ * This is necessary for proper typing when working with RPC, as JSON does not support the Date type directly.
50
+ * Example: SerializeDates<{ createdAt: Date; nested: { updatedAt: Date }; tags: Date[] }> \
51
+ * = { createdAt: string; nested: { updatedAt: string }; tags: string[] }
52
+ */
53
+ type SerializeDates<T> = T extends Date ? string : T extends (infer U)[] ? SerializeDates<U>[] : T extends readonly (infer U)[] ? readonly SerializeDates<U>[] : T extends object ? {
54
+ [K in keyof T]: SerializeDates<T[K]>;
55
+ } : T;
56
+ /**
57
+ * Transforms a string to a number (when passing query parameters).
58
+ * Example: asQueryNumber(z.number())('123') = 123
59
+ */
60
+ declare const asQueryNumber: <T extends ZodNumber>(schema: T) => z.ZodPreprocess<T>;
61
+ /**
62
+ * Transforms various string representations of boolean values into actual booleans.
63
+ * See POSITIVE_VALUES and NEGATIVE_VALUES arrays below for supported inputs.
64
+ */
65
+ declare const asQueryBoolean: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>;
66
+
67
+ /**
68
+ * Wraps c.json with a typed success payload & possible APIError.
69
+ * When no data is provided, responds with an empty object {}.
70
+ */
71
+ declare function respond<T extends object = Record<string, never>, S extends SuccessStatusCode = SuccessStatusCode>(c: Context, options: {
72
+ status: S;
73
+ data?: T;
74
+ }): Response & TypedResponse<APISuccess<SerializeDates<T>> | APIError, S, 'json'>;
75
+
76
+ 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, respond, success };
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ // src/http/http.constants.ts
2
+ var SUCCESS_STATUS_CODES = [200, 201, 202, 307];
3
+ var EXCEPTION_STATUS_CODES = [400, 401, 403, 404, 405, 409, 500];
4
+
5
+ // src/http/http.factory.ts
6
+ function success({ status, data }) {
7
+ return { kind: "data", status, data };
8
+ }
9
+ function failure({ status, error }) {
10
+ return { kind: "error", status, error };
11
+ }
12
+
13
+ // src/http/http.resolvers.ts
14
+ async function fetchSafely(fetcher) {
15
+ const response = await fetcher();
16
+ if (response.kind === "error")
17
+ return { error: new Error(response.error), data: null };
18
+ return { error: null, data: response.data };
19
+ }
20
+ async function fetchAndThrow(fetcher) {
21
+ const response = await fetcher();
22
+ if (response.kind === "error")
23
+ throw new Error(response.error);
24
+ return response.data;
25
+ }
26
+
27
+ // src/hono/hono.respond.ts
28
+ function respond(c, options) {
29
+ return c.json(success({ status: options.status, data: options.data ?? {} }), options.status);
30
+ }
31
+
32
+ // src/utilities/serialize.utilities.ts
33
+ import { z } from "zod";
34
+ var asQueryNumber = (schema) => z.preprocess((v) => typeof v === "string" ? Number(v) : v, schema);
35
+ var asQueryBoolean = (schema) => {
36
+ const POSITIVE_VALUES = ["1", "true", "yes", "y", "on"];
37
+ const NEGATIVE_VALUES = ["0", "false", "no", "n", "off"];
38
+ return z.preprocess((value) => {
39
+ if (typeof value === "boolean")
40
+ return value;
41
+ if (typeof value === "string") {
42
+ const normalized = value.trim().toLowerCase();
43
+ if (POSITIVE_VALUES.includes(normalized))
44
+ return true;
45
+ if (NEGATIVE_VALUES.includes(normalized))
46
+ return false;
47
+ }
48
+ return value;
49
+ }, schema);
50
+ };
51
+ export {
52
+ EXCEPTION_STATUS_CODES,
53
+ SUCCESS_STATUS_CODES,
54
+ asQueryBoolean,
55
+ asQueryNumber,
56
+ failure,
57
+ fetchAndThrow,
58
+ fetchSafely,
59
+ respond,
60
+ success
61
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@kalutskii/foundation",
3
+ "version": "0.1.3",
4
+ "description": "Typescript collection of most common utilities, schemas and functions among private projects.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kalutskii/foundation.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "lint": "eslint .",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "peerDependencies": {
28
+ "date-fns": "^4.1.0",
29
+ "date-fns-tz": "^3.2.0",
30
+ "hono": "^4.12.18",
31
+ "kleur": "^4.1.5",
32
+ "typescript": "^5",
33
+ "zod": "^4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^10.0.1",
37
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
38
+ "@types/bun": "latest",
39
+ "esbuild-plugin-tsconfig-paths": "^1.0.2",
40
+ "eslint": "^10.2.1",
41
+ "eslint-config-prettier": "^10.1.8",
42
+ "prettier": "^3.8.1",
43
+ "tsup": "^8.5.1",
44
+ "typescript-eslint": "^8.58.2"
45
+ }
46
+ }