Skip to content

Commit f74473f

Browse files
constantantclaude
andcommitted
docs: update READMEs for v1.9.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74ca289 commit f74473f

2 files changed

Lines changed: 260 additions & 2 deletions

File tree

tools/openapi-resource-devtools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ providers: [
3535

3636
### Chrome Web Store _(recommended)_
3737

38-
> _Not yet published. Use manual loading below._
38+
Search for **"OpenAPI Resource Mocks DevTools"** in the [Chrome Web Store](https://chromewebstore.google.com).
3939

4040
### Load unpacked (development / local build)
4141

tools/openapi-resource-gen/README.md

Lines changed: 259 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Re-run the same command whenever your spec changes — the generator overwrites
8989
| `includeMocks` | no | `false` | Emit a `.mock.ts` per endpoint plus `index.mock.ts` barrels and a `mocks.manifest.json` — requires [`@constantant/openapi-resource-mocks`](https://www.npmjs.com/package/@constantant/openapi-resource-mocks) |
9090
| `includeMswHandlers` | no | `false` | Emit a `.msw.ts` per endpoint plus `index.msw.ts` barrels — requires [`msw`](https://mswjs.io) >= 2.0.0 |
9191
| `specId` | no | derived from `baseUrlToken` | Identifier embedded in every generated `MockResourceMeta` and in `mocks.manifest.json`. Defaults to `baseUrlToken` with `_BASE_URL` stripped and lowercased (e.g. `PETSTORE_BASE_URL``petstore`). Must match the value used when importing the spec into the DevTools panel. |
92+
| `dateType` | no | `string` | `string` (default — no change), `Date`, or `Temporal`. When set to `Date` or `Temporal`, emits a typed `XxxRevived` alias and a `reviveXxxDates()` helper per endpoint whose response contains `format: date-time` or `format: date` fields. |
93+
| `readonlyResponses` | no | `false` | Wrap all `XxxResponse` and `XxxError` type aliases in `Readonly<>` to prevent accidental mutation of response data. |
9294
| `verbose` | no | `false` | Print a `+`/`~`/`-` summary of created, updated, and deleted files after generation. |
9395

9496
---
@@ -104,6 +106,8 @@ Re-run the same command whenever your spec changes — the generator overwrites
104106
index.mock.ts # (--includeMocks) re-exports all tag mock barrels
105107
index.msw.ts # (--includeMswHandlers) re-exports all tag MSW handler arrays
106108
mocks.manifest.json # (--includeMocks) machine-readable endpoint list + specId for the DevTools panel
109+
webhooks/
110+
{webhook-name}.webhook.ts # (OAS 3.1 webhooks) InjectionToken<HttpInterceptorFn> per webhook
107111
{tag}/
108112
index.ts # re-exports all token files in this tag folder
109113
index.mock.ts # (--includeMocks) re-exports all mock files in this tag
@@ -113,7 +117,7 @@ Re-run the same command whenever your spec changes — the generator overwrites
113117
{operation-id}.msw.ts # (--includeMswHandlers) MSW 2.x handler factory + pre-called array
114118
```
115119

116-
Tags map to subfolders; untagged operations go into `default/`.
120+
Tags map to subfolders; untagged operations go into `default/`. OAS 3.1 `webhooks` map to `webhooks/`.
117121

118122
---
119123

@@ -303,6 +307,89 @@ are never bundled into a production build.
303307

304308
---
305309

310+
## Date revival (`--dateType`)
311+
312+
By default the generator uses `dateType: 'string'` — date/time fields stay as
313+
`string` exactly as `openapi-typescript` emits them. Set `--dateType=Date` or
314+
`--dateType=Temporal` to also generate a `XxxRevived` type alias and a
315+
`reviveXxxDates()` helper for any endpoint whose response contains
316+
`format: date-time` or `format: date` fields:
317+
318+
```typescript
319+
// orders/get-order.token.ts (generated with --dateType=Date)
320+
321+
// The raw response type still reflects the schema faithfully:
322+
export type GetOrderResponse =
323+
paths['/orders/{id}']['get']['responses']['200']['content']['application/json'];
324+
325+
// Revived alias: date fields replaced with Date objects
326+
export type GetOrderRevived = Omit<GetOrderResponse, 'createdAt' | 'updatedAt'> & {
327+
createdAt: Date;
328+
updatedAt: Date;
329+
};
330+
331+
// Pure reviver function — does not mutate the raw object
332+
export function reviveGetOrderDates(raw: GetOrderResponse): GetOrderRevived {
333+
const obj = raw as unknown as Record<string, unknown>;
334+
return {
335+
...obj,
336+
createdAt: obj['createdAt'] != null ? new Date(obj['createdAt'] as string) : obj['createdAt'],
337+
updatedAt: obj['updatedAt'] != null ? new Date(obj['updatedAt'] as string) : obj['updatedAt'],
338+
} as GetOrderRevived;
339+
}
340+
```
341+
342+
For array responses (`GetOrderRevived` becomes `(Omit<…> & { … })[]`), the function
343+
maps over each element. Using `--dateType=Temporal` produces `Temporal.Instant` for
344+
`date-time` fields and `Temporal.PlainDate` for `date` fields:
345+
346+
```typescript
347+
export type GetOrderRevived = Omit<GetOrderResponse, 'createdAt'> & {
348+
createdAt: Temporal.Instant;
349+
};
350+
```
351+
352+
Call the reviver after receiving the resource value:
353+
354+
```typescript
355+
readonly order = this.getOrder('42');
356+
readonly orderRevived = computed(() => {
357+
const raw = this.order.value();
358+
return raw ? reviveGetOrderDates(raw) : undefined;
359+
});
360+
```
361+
362+
---
363+
364+
## Readonly responses (`--readonlyResponses`)
365+
366+
Pass `--readonlyResponses` to wrap every emitted `XxxResponse` and `XxxError` type
367+
alias in `Readonly<>`. This prevents accidental mutation of the returned data at the
368+
TypeScript type level:
369+
370+
```typescript
371+
// Without --readonlyResponses (default):
372+
export type GetOrderResponse =
373+
paths['/orders/{id}']['get']['responses']['200']['content']['application/json'];
374+
375+
// With --readonlyResponses:
376+
export type GetOrderResponse =
377+
Readonly<paths['/orders/{id}']['get']['responses']['200']['content']['application/json']>;
378+
```
379+
380+
Multi-status union aliases use `Readonly<>` on each branch:
381+
382+
```typescript
383+
export type UpsertOrderResponse =
384+
| Readonly<paths['/orders']['put']['responses']['200']['content']['application/json']>
385+
| Readonly<paths['/orders']['put']['responses']['201']['content']['application/json']>;
386+
```
387+
388+
`--readonlyResponses` works in combination with `--dateType` — the `XxxRevived`
389+
alias also wraps the `Omit & { … }` shape in `Readonly<>`.
390+
391+
---
392+
306393
## Generated token anatomy
307394

308395
### GET with query params
@@ -530,6 +617,124 @@ export type UpsertResourceResponse =
530617
The `httpResource<UpsertResourceResponse>` call site receives a value that is the
531618
union of all possible success shapes.
532619

620+
### Typed error aliases
621+
622+
For 4xx/5xx responses that carry JSON bodies, the generator emits an `XxxError` type alias
623+
alongside the response type. This lets callers type the `.error()` signal from `httpResource`:
624+
625+
```typescript
626+
// Single error code
627+
export type GetOrderError =
628+
paths['/orders/{id}']['get']['responses']['404']['content']['application/json'];
629+
630+
// Multiple error codes — union
631+
export type GetOrderError =
632+
| paths['/orders/{id}']['get']['responses']['400']['content']['application/json']
633+
| paths['/orders/{id}']['get']['responses']['404']['content']['application/json'];
634+
```
635+
636+
With `--readonlyResponses` each branch is wrapped in `Readonly<>`.
637+
638+
### Enum label/description maps
639+
640+
When query or path params use the vendor extensions `x-enum-varnames` and/or
641+
`x-enum-descriptions`, the generator emits typed const objects alongside the
642+
`Params` type alias. These are useful for building select options and accessible
643+
tooltips without hand-writing display strings:
644+
645+
```typescript
646+
// find-pets-by-status.token.ts
647+
// Spec has: enum: [available, pending, sold], x-enum-varnames: [Available, Pending, Sold]
648+
export const findPetsByStatusStatusLabels = {
649+
'available': 'Available',
650+
'pending': 'Pending',
651+
'sold': 'Sold',
652+
} as const;
653+
654+
// x-enum-descriptions: [Pets ready to adopt, Pending adoption, Already adopted]
655+
export const findPetsByStatusStatusDescriptions = {
656+
'available': 'Pets ready to adopt',
657+
'pending': 'Pending adoption',
658+
'sold': 'Already adopted',
659+
} as const;
660+
```
661+
662+
The object key is the raw enum value (as it appears in the API request); the object
663+
value is the human-readable label or description from the extension.
664+
665+
### Non-default query param serialization
666+
667+
When a query parameter declares a non-default `style` in the spec (`deepObject`,
668+
`pipeDelimited`, or `spaceDelimited`), the generator emits a module-private
669+
`_serializeParams()` helper inside the token file. This helper converts the typed
670+
`XxxParams` object to a flat `Record<string, string | readonly string[]>` before
671+
passing it to `HttpClient`:
672+
673+
```typescript
674+
// deep-search.token.ts (generated — deepObject param 'filter')
675+
function _serializeParams(p: DeepSearchParams | undefined): Record<string, string | readonly string[]> | undefined {
676+
if (p == null) return undefined;
677+
const _out: Record<string, string | readonly string[]> = {};
678+
for (const [_k, _v] of Object.entries(p as Record<string, unknown>)) {
679+
if (_v == null) continue;
680+
switch (_k) {
681+
case 'filter':
682+
for (const [_dk, _dv] of Object.entries(_v as Record<string, unknown>))
683+
if (_dv != null) _out['filter[' + _dk + ']'] = String(_dv);
684+
break;
685+
default:
686+
_out[_k] = Array.isArray(_v) ? (_v as unknown[]).map(String) : String(_v as string | number | boolean);
687+
}
688+
}
689+
return _out;
690+
}
691+
```
692+
693+
| Spec `style` | Serialization |
694+
|---|---|
695+
| `deepObject` | `filter[key]=value` — one query param per nested key |
696+
| `pipeDelimited` | `tags=a\|b\|c` — pipe-joined array |
697+
| `spaceDelimited` | `tags=a b c` — space-joined array |
698+
699+
Standard comma-separated arrays use the default `HttpClient` serialization; no helper
700+
is emitted for those.
701+
702+
### Discriminated unions
703+
704+
When a response schema uses `oneOf`/`anyOf` with a discriminator, the generator emits
705+
narrowing helpers alongside the `XxxResponse` type:
706+
707+
```typescript
708+
// get-animal.token.ts (discriminator: { propertyName: 'type' })
709+
export type GetAnimalDiscriminatorKey = 'dog' | 'cat';
710+
711+
// Mapping-style: each variant intersects its component schema with the discriminant literal
712+
export type GetAnimalDog = components['schemas']['Dog'] & { 'type': 'dog' };
713+
export type GetAnimalCat = components['schemas']['Cat'] & { 'type': 'cat' };
714+
715+
// Convenience union of all narrowed variants
716+
export type GetAnimalDiscriminated = GetAnimalDog | GetAnimalCat;
717+
```
718+
719+
When the discriminator resolves from a plain enum (no `components/schemas` mapping),
720+
the generator uses `Extract` instead:
721+
722+
```typescript
723+
export type GetAnimalDog = Extract<GetAnimalResponse, { 'type': 'dog' }>;
724+
```
725+
726+
For array responses (`GetAnimalResponse` is `Animal[]`), `GetAnimalDiscriminated`
727+
becomes `(GetAnimalDog | GetAnimalCat)[]`.
728+
729+
Use the narrowed types at the call site:
730+
731+
```typescript
732+
const item = this.getAnimal.value();
733+
if (item && 'type' in item && item.type === 'dog') {
734+
const dog: GetAnimalDog = item; // narrowed
735+
}
736+
```
737+
533738
### Security schemes
534739

535740
The generator emits one file per security scheme. Two patterns are used depending on the
@@ -796,6 +1001,59 @@ navigation in and destroyed on navigation out, with no cross-route state leakage
7961001

7971002
---
7981003

1004+
## OAS 3.1 support
1005+
1006+
The generator handles OpenAPI 3.1 specs in addition to 3.0.x. The main differences
1007+
that affect code generation:
1008+
1009+
### Type arrays and implicit nullability
1010+
1011+
OAS 3.1 allows `type: ['string', 'null']` instead of `nullable: true`. Both forms
1012+
produce the same output from `openapi-typescript` (`string | null`), so generated
1013+
token types remain correct in either spec version.
1014+
1015+
### Webhooks
1016+
1017+
OAS 3.1 `webhooks` entries are emitted as `.webhook.ts` files under a `webhooks/`
1018+
subfolder. Each webhook token holds `InjectionToken<HttpInterceptorFn>` — the
1019+
consumer registers an interceptor that handles incoming webhook requests:
1020+
1021+
```typescript
1022+
// webhooks/order-placed.webhook.ts (generated)
1023+
import { InjectionToken } from '@angular/core';
1024+
import { HttpInterceptorFn } from '@angular/common/http';
1025+
import type { webhooks } from '../schema.d';
1026+
1027+
export type OrderPlacedWebhookPayload =
1028+
NonNullable<webhooks['order.placed']['post']['requestBody']>['content']['application/json'];
1029+
1030+
export type OrderPlacedWebhookResponse =
1031+
webhooks['order.placed']['post']['responses']['200']['content']['application/json'];
1032+
1033+
export const ORDER_PLACED_WEBHOOK = new InjectionToken<HttpInterceptorFn>('ORDER_PLACED_WEBHOOK');
1034+
```
1035+
1036+
Wire up in `app.config.ts`:
1037+
1038+
```typescript
1039+
import { ORDER_PLACED_WEBHOOK } from '@myapp/myapi-data-access';
1040+
import type { OrderPlacedWebhookPayload } from '@myapp/myapi-data-access';
1041+
1042+
const myWebhookHandler: HttpInterceptorFn = (req, next) => {
1043+
// req.body is the incoming webhook payload
1044+
const payload = req.body as OrderPlacedWebhookPayload;
1045+
// ... handle it
1046+
return next(req);
1047+
};
1048+
1049+
{ provide: ORDER_PLACED_WEBHOOK, useValue: myWebhookHandler }
1050+
```
1051+
1052+
Webhook files are included in the root `index.ts` barrel and participate in stale
1053+
file cleanup like all other generated files.
1054+
1055+
---
1056+
7991057
## Implementation notes
8001058

8011059
| Step | Tool | Purpose |

0 commit comments

Comments
 (0)