#5: createCoreFetch default responseShape flipped to 'body'
Orval's `client: 'fetch'` (with the no-`includeHttpResponseReturnType` default) emits mutator signatures expecting the parsed body — so making `'body'` the default means the no-arg `createCoreFetch()` form is the right answer for the typical Orval-driven derived project. Callers that need Response metadata opt in via `responseShape: 'wrapped'`. Aligns README + docstring + the inline default. Tests updated to assert default-`'body'` and explicit-`'wrapped'`. Bump to 0.4.0 (BREAKING — default flipped). Downstream consumers on the default need to either (a) add explicit `responseShape: 'wrapped'` to preserve old behaviour, or (b) migrate callers to the body shape. fewo-webapp's existing `responseShape: 'body'` override is now redundant and will be dropped in a follow-up. Closes #5 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90c5ca2248
commit
fd451fd452
4 changed files with 28 additions and 22 deletions
14
README.md
14
README.md
|
|
@ -43,12 +43,16 @@ webapp-scaffold-postprocess-openapi && \
|
||||||
orval
|
orval
|
||||||
```
|
```
|
||||||
|
|
||||||
## `createCoreFetch` (v0.2)
|
## `createCoreFetch` (v0.4 — default flipped to `'body'`)
|
||||||
|
|
||||||
Orval's `client: 'fetch'` calls a mutator `coreFetch<T>(url, init)` and
|
Orval's `client: 'fetch'` calls a mutator `coreFetch<T>(url, init)`. The
|
||||||
expects `{data, status, headers}` back. `createCoreFetch(opts)` returns
|
return shape is configurable; **the default is `'body'`** (returns the
|
||||||
such a function, with credentials/CSRF headers / 204 / content-type
|
parsed JSON body directly, matching Orval's
|
||||||
handling already wired, and hooks for project-specific concerns:
|
`includeHttpResponseReturnType: false`). Pass `responseShape: 'wrapped'`
|
||||||
|
to opt back into `{data, status, headers}` when callers need the
|
||||||
|
Response metadata. `createCoreFetch(opts)` returns such a function, with
|
||||||
|
credentials/CSRF headers / 204 / content-type handling already wired,
|
||||||
|
and hooks for project-specific concerns:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createCoreFetch } from '@uschuster/webapp-scaffold/core-fetch';
|
import { createCoreFetch } from '@uschuster/webapp-scaffold/core-fetch';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@uschuster/webapp-scaffold",
|
"name": "@uschuster/webapp-scaffold",
|
||||||
"version": "0.3.7",
|
"version": "0.4.0",
|
||||||
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
|
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,22 @@ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('createCoreFetch — response shapes', () => {
|
describe('createCoreFetch — response shapes', () => {
|
||||||
it("'wrapped' default returns { data, status, headers }", async () => {
|
it("'body' default returns the parsed body directly", async () => {
|
||||||
const cf = createCoreFetch({ fetchImpl: () => Promise.resolve(jsonResponse({ ok: 1 })) });
|
const cf = createCoreFetch({ fetchImpl: () => Promise.resolve(jsonResponse({ ok: 1 })) });
|
||||||
|
const r = await cf<{ ok: number }>('/x');
|
||||||
|
expect(r).toEqual({ ok: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'wrapped' opt-in returns { data, status, headers }", async () => {
|
||||||
|
const cf = createCoreFetch({
|
||||||
|
responseShape: 'wrapped',
|
||||||
|
fetchImpl: () => Promise.resolve(jsonResponse({ ok: 1 })),
|
||||||
|
});
|
||||||
const r = await cf<{ data: { ok: number }; status: number }>('/x');
|
const r = await cf<{ data: { ok: number }; status: number }>('/x');
|
||||||
expect(r.data).toEqual({ ok: 1 });
|
expect(r.data).toEqual({ ok: 1 });
|
||||||
expect(r.status).toBe(200);
|
expect(r.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'body' returns the parsed body directly", async () => {
|
|
||||||
const cf = createCoreFetch({
|
|
||||||
responseShape: 'body',
|
|
||||||
fetchImpl: () => Promise.resolve(jsonResponse({ ok: 1 })),
|
|
||||||
});
|
|
||||||
const r = await cf<{ ok: number }>('/x');
|
|
||||||
expect(r).toEqual({ ok: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('non-JSON 2xx returns the body as plain text', async () => {
|
it('non-JSON 2xx returns the body as plain text', async () => {
|
||||||
const cf = createCoreFetch({
|
const cf = createCoreFetch({
|
||||||
responseShape: 'body',
|
responseShape: 'body',
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
// Orval's `client: 'fetch'` emits functions that call
|
// Orval's `client: 'fetch'` emits functions that call
|
||||||
// coreFetch<T>(url, init)
|
// coreFetch<T>(url, init)
|
||||||
// and expect one of:
|
// and expect one of:
|
||||||
// • { data, status, headers } back (responseShape: 'wrapped', default —
|
// • the parsed body directly (responseShape: 'body', DEFAULT — matches
|
||||||
// matches orval's default `includeHttpResponseReturnType: true`)
|
// orval's `includeHttpResponseReturnType: false`, the Orval-driven
|
||||||
// • the parsed body directly (responseShape: 'body' — matches orval's
|
// "no extra ceremony" path)
|
||||||
// `includeHttpResponseReturnType: false`, fewo's convention)
|
// • { data, status, headers } back (responseShape: 'wrapped' — matches
|
||||||
|
// orval's `includeHttpResponseReturnType: true`; opt in when callers
|
||||||
|
// need the Response metadata)
|
||||||
//
|
//
|
||||||
// Baseline wired in:
|
// Baseline wired in:
|
||||||
// • credentials: 'include' — cookies always sent
|
// • credentials: 'include' — cookies always sent
|
||||||
|
|
@ -29,7 +31,7 @@ export interface CoreFetchOptions {
|
||||||
/** Prefix for every request; strip trailing slash. */
|
/** Prefix for every request; strip trailing slash. */
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
|
||||||
/** `'wrapped'` returns { data, status, headers } (default); `'body'` returns the parsed body. */
|
/** `'body'` returns the parsed body (default); `'wrapped'` returns { data, status, headers }. */
|
||||||
responseShape?: ResponseShape;
|
responseShape?: ResponseShape;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,7 +103,7 @@ export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
|
||||||
const callFetch: typeof fetch = opts.fetchImpl
|
const callFetch: typeof fetch = opts.fetchImpl
|
||||||
? opts.fetchImpl
|
? opts.fetchImpl
|
||||||
: ((...args) => fetch(...(args as Parameters<typeof fetch>))) as typeof fetch;
|
: ((...args) => fetch(...(args as Parameters<typeof fetch>))) as typeof fetch;
|
||||||
const shape: ResponseShape = opts.responseShape ?? 'wrapped';
|
const shape: ResponseShape = opts.responseShape ?? 'body';
|
||||||
|
|
||||||
function wrap(data: unknown, status: number, headers: Headers): unknown {
|
function wrap(data: unknown, status: number, headers: Headers): unknown {
|
||||||
return shape === 'body' ? data : { data, status, headers };
|
return shape === 'body' ? data : { data, status, headers };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue