@r0hitsharma/http-client-core 0.12.0-rohit-fork-ci.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/bin/generate-openapi.ts +58 -0
- package/dist/bin/generate-openapi.d.ts +2 -0
- package/dist/bin/generate-openapi.js +44 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +37 -0
- package/oxfmt.config.ts +5 -0
- package/oxlint.config.ts +7 -0
- package/package.json +47 -0
- package/src/index.ts +81 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @r0hitsharma/http-client-core
|
|
2
|
+
|
|
3
|
+
Typed HTTP client utilities built on OpenAPI and Zod: an `openapi-fetch` client
|
|
4
|
+
factory, the OpenAPI helper types the rest of the toolkit shares, and helpers
|
|
5
|
+
that turn a document's component schemas into zod validators.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @r0hitsharma/http-client-core
|
|
11
|
+
npm install --save-dev openapi-typescript
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Generate types from an OpenAPI document
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx uikit-openapi-generate --schema openapi.json --output src/api.types.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Create a typed client
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createApiClient } from '@r0hitsharma/http-client-core';
|
|
26
|
+
|
|
27
|
+
import type { paths } from './api.types';
|
|
28
|
+
|
|
29
|
+
const client = createApiClient<paths>('https://api.example.com');
|
|
30
|
+
|
|
31
|
+
// Fully typed request and response
|
|
32
|
+
const { data, error } = await client.GET('/users/{id}', {
|
|
33
|
+
params: { path: { id: '123' } },
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The second argument is the rest of the `openapi-fetch` client config (everything
|
|
38
|
+
except `baseUrl`), which is how a test or a mock layer injects its own `fetch`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const client = createApiClient<paths>('https://api.example.com', {
|
|
42
|
+
fetch: myFetch,
|
|
43
|
+
headers: { 'X-Tenant': tenantId },
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Build a zod validator from a component schema
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { getComponentSchemaFromOpenApi } from '@r0hitsharma/http-client-core';
|
|
51
|
+
|
|
52
|
+
import openApiDocument from '../openapi.json';
|
|
53
|
+
|
|
54
|
+
const userSchema = getComponentSchemaFromOpenApi(openApiDocument, 'User');
|
|
55
|
+
const result = userSchema.safeParse(await response.json());
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`normalizeOpenApiRefs` is exported for documents that need their
|
|
59
|
+
`#/components/schemas/...` refs rewritten to zod's `#/$defs/...` form ahead of
|
|
60
|
+
time; `getComponentSchemaFromOpenApi` applies it internally.
|
|
61
|
+
|
|
62
|
+
For response validation wired into TanStack Query, use
|
|
63
|
+
`createZodResponseMiddleware` from
|
|
64
|
+
[http-client-react](../http-client-react) rather than calling this directly.
|
|
65
|
+
|
|
66
|
+
### Shared OpenAPI types
|
|
67
|
+
|
|
68
|
+
The `openapi-fetch` and `openapi-typescript-helpers` types that anything built on
|
|
69
|
+
this client needs are re-exported here — `ApiClient`, `ApiClientOptions`,
|
|
70
|
+
`FetchResponse`, `MaybeOptionalInit`, `HttpMethod`, `MediaType`,
|
|
71
|
+
`PathsWithMethod`, `RequiredKeysOf` — so downstream packages type against one
|
|
72
|
+
pinned copy of the OpenAPI toolchain instead of each declaring their own
|
|
73
|
+
dependency on it.
|
|
74
|
+
|
|
75
|
+
## Peer dependencies
|
|
76
|
+
|
|
77
|
+
- `openapi-typescript`: for generating types from OpenAPI documents
|
|
78
|
+
|
|
79
|
+
## See also
|
|
80
|
+
|
|
81
|
+
- [http-client-react](../http-client-react) for the TanStack Query layer
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
function parseArg(name: string, fallback?: string): string | undefined {
|
|
9
|
+
const prefix = `--${name}=`;
|
|
10
|
+
const inline = process.argv.find((arg) => arg.startsWith(prefix));
|
|
11
|
+
if (inline) {
|
|
12
|
+
return inline.slice(prefix.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const idx = process.argv.indexOf(`--${name}`);
|
|
16
|
+
if (idx >= 0 && process.argv[idx + 1]) {
|
|
17
|
+
return process.argv[idx + 1];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const schema = parseArg('schema', process.env.OPENAPI_FILE);
|
|
24
|
+
const output = parseArg('output', 'generated/openapi-types.ts') ?? 'generated/openapi-types.ts';
|
|
25
|
+
|
|
26
|
+
if (!schema) {
|
|
27
|
+
console.error('Missing schema path. Use --schema <path-to-openapi-json>.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const absoluteSchema = path.resolve(process.cwd(), schema);
|
|
32
|
+
const absoluteOutput = path.resolve(process.cwd(), output);
|
|
33
|
+
|
|
34
|
+
if (!existsSync(absoluteSchema)) {
|
|
35
|
+
console.error(`OpenAPI schema file does not exist: ${absoluteSchema}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(path.dirname(absoluteOutput), { recursive: true });
|
|
40
|
+
|
|
41
|
+
// openapi-typescript builds its output with the classic TypeScript compiler API, which
|
|
42
|
+
// TypeScript 7.0 does not ship (openapi-ts/openapi-typescript#2841). Running from an empty
|
|
43
|
+
// directory makes npx resolve it in an isolated tree alongside a TypeScript that still has that
|
|
44
|
+
// API, instead of picking up the host project's compiler. Exact pins: the
|
|
45
|
+
// isolated tree resolves outside any project .npmrc, so range specifiers
|
|
46
|
+
// would float past consumer supply-chain guards (min-release-age).
|
|
47
|
+
const isolatedCwd = mkdtempSync(path.join(os.tmpdir(), 'openapi-typescript-'));
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
execSync(
|
|
51
|
+
`npx --yes --package=openapi-typescript@7.13.0 --package=typescript@5.9.3 openapi-typescript "${absoluteSchema}" --output "${absoluteOutput}"`,
|
|
52
|
+
{ cwd: isolatedCwd, stdio: 'inherit' },
|
|
53
|
+
);
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(isolatedCwd, { force: true, recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`Generated: ${absoluteOutput}`);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
function parseArg(name, fallback) {
|
|
7
|
+
const prefix = `--${name}=`;
|
|
8
|
+
const inline = process.argv.find((arg) => arg.startsWith(prefix));
|
|
9
|
+
if (inline) {
|
|
10
|
+
return inline.slice(prefix.length);
|
|
11
|
+
}
|
|
12
|
+
const idx = process.argv.indexOf(`--${name}`);
|
|
13
|
+
if (idx >= 0 && process.argv[idx + 1]) {
|
|
14
|
+
return process.argv[idx + 1];
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
const schema = parseArg('schema', process.env.OPENAPI_FILE);
|
|
19
|
+
const output = parseArg('output', 'generated/openapi-types.ts') ?? 'generated/openapi-types.ts';
|
|
20
|
+
if (!schema) {
|
|
21
|
+
console.error('Missing schema path. Use --schema <path-to-openapi-json>.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const absoluteSchema = path.resolve(process.cwd(), schema);
|
|
25
|
+
const absoluteOutput = path.resolve(process.cwd(), output);
|
|
26
|
+
if (!existsSync(absoluteSchema)) {
|
|
27
|
+
console.error(`OpenAPI schema file does not exist: ${absoluteSchema}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
mkdirSync(path.dirname(absoluteOutput), { recursive: true });
|
|
31
|
+
// openapi-typescript builds its output with the classic TypeScript compiler API, which
|
|
32
|
+
// TypeScript 7.0 does not ship (openapi-ts/openapi-typescript#2841). Running from an empty
|
|
33
|
+
// directory makes npx resolve it in an isolated tree alongside a TypeScript that still has that
|
|
34
|
+
// API, instead of picking up the host project's compiler. Exact pins: the
|
|
35
|
+
// isolated tree resolves outside any project .npmrc, so range specifiers
|
|
36
|
+
// would float past consumer supply-chain guards (min-release-age).
|
|
37
|
+
const isolatedCwd = mkdtempSync(path.join(os.tmpdir(), 'openapi-typescript-'));
|
|
38
|
+
try {
|
|
39
|
+
execSync(`npx --yes --package=openapi-typescript@7.13.0 --package=typescript@5.9.3 openapi-typescript "${absoluteSchema}" --output "${absoluteOutput}"`, { cwd: isolatedCwd, stdio: 'inherit' });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rmSync(isolatedCwd, { force: true, recursive: true });
|
|
43
|
+
}
|
|
44
|
+
console.log(`Generated: ${absoluteOutput}`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClientOptions } from 'openapi-fetch';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* The `openapi-fetch` / `openapi-typescript` helper types that anything built
|
|
5
|
+
* on this client needs. They are re-exported here so downstream packages
|
|
6
|
+
* (`http-client-react` and `http-client-msw`) type against the same helper
|
|
7
|
+
* versions this package is pinned to, rather than each declaring its own
|
|
8
|
+
* dependency on the OpenAPI toolchain.
|
|
9
|
+
*/
|
|
10
|
+
export type { Client as ApiClient, ClientOptions as ApiClientOptions, FetchResponse, MaybeOptionalInit, } from 'openapi-fetch';
|
|
11
|
+
export type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf, } from 'openapi-typescript-helpers';
|
|
12
|
+
export type JsonSchema = z.core.JSONSchema.JSONSchema;
|
|
13
|
+
/**
|
|
14
|
+
* `options` is the full `openapi-fetch` client config minus `baseUrl`, which
|
|
15
|
+
* stays the first positional argument for backwards compatibility. It is what
|
|
16
|
+
* lets a test or a mock layer inject its own `fetch`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createApiClient: <TPaths extends {}>(baseUrl?: string, options?: Omit<ClientOptions, 'baseUrl'>) => import("openapi-fetch").Client<TPaths, `${string}/${string}`>;
|
|
19
|
+
export declare function normalizeOpenApiRefs(value: unknown): unknown;
|
|
20
|
+
export declare function getComponentSchemaFromOpenApi(openApi: unknown, name: string): z.ZodType;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import createClient from 'openapi-fetch';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* `options` is the full `openapi-fetch` client config minus `baseUrl`, which
|
|
5
|
+
* stays the first positional argument for backwards compatibility. It is what
|
|
6
|
+
* lets a test or a mock layer inject its own `fetch`.
|
|
7
|
+
*/
|
|
8
|
+
export const createApiClient = (baseUrl = '/', options) => {
|
|
9
|
+
return createClient({ ...options, baseUrl });
|
|
10
|
+
};
|
|
11
|
+
export function normalizeOpenApiRefs(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.map(normalizeOpenApiRefs);
|
|
14
|
+
}
|
|
15
|
+
if (value && typeof value === 'object') {
|
|
16
|
+
const record = value;
|
|
17
|
+
const normalized = {};
|
|
18
|
+
for (const [key, entryValue] of Object.entries(record)) {
|
|
19
|
+
if (key === '$ref' && typeof entryValue === 'string') {
|
|
20
|
+
normalized[key] = entryValue.replace('#/components/schemas/', '#/$defs/');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
normalized[key] = normalizeOpenApiRefs(entryValue);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function getComponentSchemaFromOpenApi(openApi, name) {
|
|
31
|
+
const schema = openApi;
|
|
32
|
+
const defs = normalizeOpenApiRefs(schema.components?.schemas ?? {});
|
|
33
|
+
return z.fromJSONSchema({
|
|
34
|
+
$ref: `#/$defs/${name}`,
|
|
35
|
+
$defs: defs,
|
|
36
|
+
});
|
|
37
|
+
}
|
package/oxfmt.config.ts
ADDED
package/oxlint.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@r0hitsharma/http-client-core",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/src/index.js",
|
|
8
|
+
"types": "dist/src/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"uikit-openapi-generate": "./dist/bin/generate-openapi.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"default": "./dist/src/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"prepare": "npm run build",
|
|
22
|
+
"type:check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"lint": "oxlint -c oxlint.config.ts --max-warnings=0 src",
|
|
24
|
+
"lint:fix": "oxlint -c oxlint.config.ts --fix src",
|
|
25
|
+
"format": "oxfmt -c oxfmt.config.ts --write src",
|
|
26
|
+
"format:check": "oxfmt -c oxfmt.config.ts --check src"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"openapi-fetch": "0.17.0",
|
|
30
|
+
"openapi-typescript-helpers": "0.1.0",
|
|
31
|
+
"zod": "4.5.4"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"openapi-typescript": "^7.13.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@r0hitsharma/oxfmt-config": "*",
|
|
38
|
+
"@r0hitsharma/tsconfig": "*",
|
|
39
|
+
"@types/node": "24.13.3",
|
|
40
|
+
"oxfmt": "0.67.0",
|
|
41
|
+
"oxlint": "1.82.0"
|
|
42
|
+
},
|
|
43
|
+
"version": "0.12.0-rohit-fork-ci.1",
|
|
44
|
+
"repository": {
|
|
45
|
+
"url": "https://github.com/r0hitsharma/uikit"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import createClient from 'openapi-fetch';
|
|
2
|
+
import type { ClientOptions } from 'openapi-fetch';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `openapi-fetch` / `openapi-typescript` helper types that anything built
|
|
7
|
+
* on this client needs. They are re-exported here so downstream packages
|
|
8
|
+
* (`http-client-react` and `http-client-msw`) type against the same helper
|
|
9
|
+
* versions this package is pinned to, rather than each declaring its own
|
|
10
|
+
* dependency on the OpenAPI toolchain.
|
|
11
|
+
*/
|
|
12
|
+
export type {
|
|
13
|
+
Client as ApiClient,
|
|
14
|
+
ClientOptions as ApiClientOptions,
|
|
15
|
+
FetchResponse,
|
|
16
|
+
MaybeOptionalInit,
|
|
17
|
+
} from 'openapi-fetch';
|
|
18
|
+
export type {
|
|
19
|
+
HttpMethod,
|
|
20
|
+
MediaType,
|
|
21
|
+
PathsWithMethod,
|
|
22
|
+
RequiredKeysOf,
|
|
23
|
+
} from 'openapi-typescript-helpers';
|
|
24
|
+
|
|
25
|
+
export type JsonSchema = z.core.JSONSchema.JSONSchema;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `options` is the full `openapi-fetch` client config minus `baseUrl`, which
|
|
29
|
+
* stays the first positional argument for backwards compatibility. It is what
|
|
30
|
+
* lets a test or a mock layer inject its own `fetch`.
|
|
31
|
+
*/
|
|
32
|
+
export const createApiClient = <TPaths extends {}>(
|
|
33
|
+
baseUrl: string = '/',
|
|
34
|
+
options?: Omit<ClientOptions, 'baseUrl'>,
|
|
35
|
+
) => {
|
|
36
|
+
return createClient<TPaths>({ ...options, baseUrl });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function normalizeOpenApiRefs(value: unknown): unknown {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map(normalizeOpenApiRefs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value && typeof value === 'object') {
|
|
45
|
+
const record = value as Record<string, unknown>;
|
|
46
|
+
const normalized: Record<string, unknown> = {};
|
|
47
|
+
|
|
48
|
+
for (const [key, entryValue] of Object.entries(record)) {
|
|
49
|
+
if (key === '$ref' && typeof entryValue === 'string') {
|
|
50
|
+
normalized[key] = entryValue.replace(
|
|
51
|
+
'#/components/schemas/',
|
|
52
|
+
'#/$defs/',
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
normalized[key] = normalizeOpenApiRefs(entryValue);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getComponentSchemaFromOpenApi(
|
|
66
|
+
openApi: unknown,
|
|
67
|
+
name: string,
|
|
68
|
+
): z.ZodType {
|
|
69
|
+
const schema = openApi as {
|
|
70
|
+
components?: { schemas?: Record<string, unknown> };
|
|
71
|
+
};
|
|
72
|
+
const defs = normalizeOpenApiRefs(schema.components?.schemas ?? {}) as Record<
|
|
73
|
+
string,
|
|
74
|
+
JsonSchema
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
return z.fromJSONSchema({
|
|
78
|
+
$ref: `#/$defs/${name}`,
|
|
79
|
+
$defs: defs,
|
|
80
|
+
} satisfies JsonSchema);
|
|
81
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@r0hitsharma/tsconfig/node",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowImportingTsExtensions": false,
|
|
5
|
+
"types": ["node"],
|
|
6
|
+
"noEmit": false,
|
|
7
|
+
"emitDeclarationOnly": false,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": false,
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"outDir": "dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "bin/**/*.ts"]
|
|
14
|
+
}
|