-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.config.ts
More file actions
164 lines (149 loc) · 6.76 KB
/
Copy pathapi.config.ts
File metadata and controls
164 lines (149 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* The single API client for the whole app. Import `api` anywhere.
*
* Backed by the free, CORS-enabled https://dummyjson.com fake API. The codegen
* turned `openapi.json` into two artifacts this file wires together:
* - `OperationsMap` (a TYPE) from ./types/generated/api.types.ts
* - `generatedModules` (a VALUE) from ./types/generated/api.modules.ts
*
* `createTypedClient<OperationsMap>()(config, generatedModules)` turns them into
* a fully-typed, autocompleting client:
*
* api.products.getProductById({ id: 1 }) // -> Promise<Product>
* api.products.listProducts({ limit: 10, skip: 0 }) // -> Promise<ProductList>
* api.auth.login({ body: { username, password } }) // -> Promise<AuthUser>
*
* Regenerate after editing openapi.json (the Vite plugin also does this on
* dev/build — see vite.config.ts):
* npx @developerehsan/api-client generate \
* --input ./src/lib/api/openapi.json \
* --output ./src/lib/api/types/generated
*/
import { createTypedClient } from '@developerehsan/api-client';
import { generatedModules } from './types/generated/api.modules';
import type { OperationsMap } from './types/generated/api.types';
/**
* A tiny in-memory token store. `api.auth.login(...)` writes it; the bearer
* auth strategy below reads it so subsequent calls (e.g. `auth.getCurrentUser`)
* are authenticated. In a real app persist this to memory/secure storage.
*/
export const tokenStore: { access: string | null } = { access: null };
export const api = createTypedClient<OperationsMap>()(
{
// DummyJSON is served from the root; the spec's paths (/products, /auth, …)
// are appended to this base.
baseURL: 'https://dummyjson.com',
// Named base URLs you can switch between at runtime with
// `api.setEnvironment(name)` (which also clears the cache). DummyJSON only
// has one public host, so both entries point at it — but the runtime switch
// and cache-clear are real (see the Feature Lab "Environments" button).
environments: {
primary: 'https://dummyjson.com',
mirror: 'https://dummyjson.com',
},
activeEnvironment: 'primary',
// Dev logging prints each request/response to the console; response
// validation checks bodies against the loaded schema (loose = warn only).
dev: { logging: true, validateResponses: true },
openapi: {
mode: 'runtime',
validation: { enabled: true, mode: 'loose' },
},
// Bearer auth: sends `Authorization: Bearer <token>` when a token exists.
// `onMissingToken: 'skip'` sends anonymous requests silently when logged out
// (most DummyJSON endpoints are public; only /auth/me needs the token).
auth: {
strategy: 'bearer',
getToken: () => tokenStore.access,
onMissingToken: 'skip',
},
// Pipeline defaults for the whole app.
http: {
timeout: 12_000,
retry: { attempts: 3, backoff: 'exponential', baseDelay: 400 },
queue: { concurrency: 6 },
},
// GET responses are cached; a stale entry is served instantly, then
// revalidated in the background.
cache: { strategy: 'stale-while-revalidate', ttl: 30_000 },
// A newer call for the same endpoint within 300ms auto-cancels the previous
// in-flight one (great for search-as-you-type).
cancellation: { dedupeWindow: 300 },
// The plain-object `modules` form is the final source of truth: the FIRST
// param of every method is always the typed `ctx` (stripped from the exposed
// signature), and your custom methods + return types WIN over the generated
// spec. (For method-NAME autocomplete on a known module, opt into
// `createModuleDefiner` — see the README.)
modules: {
// Build auto-modules for every tag in the spec (products/users/posts/…).
auto: true,
auth: {
methods: {
// Custom login that ALSO persists the token, then returns the user.
// `ctx.request` autocompletes the known path and derives the body type.
login: async (ctx, body: OperationsMap['login']['body']) => {
const user = (await ctx.request({ method: 'POST', path: '/auth/login', body }))
.data as OperationsMap['login']['response'];
tokenStore.access = user.accessToken ?? null;
return user;
},
},
},
products: {
methods: {
// A custom, non-spec method composed from two requests: fetch a product
// and its category siblings. Appears as `api.products.getWithSiblings(id)`.
getWithSiblings: async (ctx, id: number) => {
const product = (
await ctx.request({ method: 'GET', path: '/products/{id}', pathParams: { id } })
).data as OperationsMap['getProductById']['response'];
const siblings = (
await ctx.request({
method: 'GET',
path: '/products/search',
query: { q: product.category ?? '', limit: 5 },
})
).data as OperationsMap['searchProducts']['response'];
return { product, siblings: siblings.products };
},
// Generated methods you don't override (listProducts, getProductById,
// addProduct, …) remain available with their spec types.
},
},
// A brand-new module (not in the spec) used to demonstrate retries: it
// hits DummyJSON's `/http/500` endpoint, which always returns 500. Because
// 5xx is retryable, the client retries per `http.retry` (3 attempts,
// exponential backoff) and then surfaces a typed `ApiError`. Appears as
// `api.debug.failing()` and powers the Feature Lab "Retry & backoff" button.
debug: {
methods: {
failing: async (ctx) => ctx.request({ method: 'GET', path: '/http/500' }),
},
},
// A brand-new module (not in the spec): non-HTTP logic via ctx.run, which
// gets the same queue/dedup/retry/timeout as HTTP calls. Appears as
// `api.analytics.*`.
analytics: {
methods: {
// ctx.run wraps arbitrary async work with opt-in retry + dedup.
summarize: async (ctx) => {
return ctx.run(
'summarize',
async () => {
const top = (
await ctx.request({ method: 'GET', path: '/products', query: { limit: 100 } })
).data as OperationsMap['listProducts']['response'];
const avgPrice =
top.products.reduce((s, p) => s + p.price, 0) / (top.products.length || 1);
return { count: top.total, avgPrice: Math.round(avgPrice * 100) / 100 };
},
{ dedupe: true, retry: { attempts: 2 } },
);
},
},
},
},
},
generatedModules,
);
export type Api = typeof api;