Generated output and regeneration
Generation produces TypeScript source, not a standalone HTTP implementation. Compile decorator classes with experimentalDecorators: true; install the packages imported by the actual output.
Output families
| Output | Generation rules |
|---|---|
types.ts | Component schemas grouped by schema-name namespace; model types/enums and imported Wow types |
*ApiClient.ts | Ordinary tagged operations with operationId; operation tags must all be eligible API tags; wow/Actuator/Wow aggregate tags excluded |
commandClient.ts | Resolved aggregate command paths, body aliases, regular and stream command clients |
queryClient.ts | Aggregate QueryClientFactory, state/field types, domain-event union (never when empty) and event title enum |
boundedContext.ts | Context-alias constant for resolved contexts |
index.ts | Recursive exports for .ts files and nonempty subdirectories |
.wow-generator.json | Version 1 ownership manifest with SHA-256 hashes of generated .ts files; an older .fetcher-generator.json is read once and replaced |
Source conventions
Every generated file starts with // Code generated by wow-generator. DO NOT EDIT. and uses single quotes, two-space indentation and semicolons. Relative imports end in .js and each barrel re-exports ./dir/index.js, so the output compiles under "moduleResolution": "NodeNext" as well as "bundler".
Imports are written explicitly while generating, so the output is byte-for-byte the same whether or not the output directory resolves @ahoo-wang/*. Before saving, the generator checks that every file declares or imports each name it uses and declares none twice; if not, nothing is written. That check does not replace the consumer compiler.
A name that two sibling modules both export, such as a model of the same name in two packages, is left out of their common index.ts with a warning; import it from its own module.
Model doc comments carry a summary: title, description, schema key, format, default, example and constraints. --schema-docs full (option schemaDocs: 'full') also embeds the complete JSON schema.
Methods
The method name is, in order: apiClients[tag].methodNames[operationId] from the configuration, the operation's x-fetcher-method extension, or the last dot-separated segment of the operationId, camel-cased:
| operationId | Method |
|---|---|
getUserById | getUserById |
delete_user_by_id | deleteUserById |
getUser_1 | getUser1 |
users.list | list |
example.cart.add_cart_item | addCartItem |
The name depends on the operation alone, so adding an operation never renames an existing method. Two operations of one client that arrive at the same name fail the run with exit code 4, naming both operations; give one of them a name with methodNames or x-fetcher-method.
Ordinary methods take every path, query and header parameter as a typed positional parameter named after it (item-id → itemId), and the request body as its own @body() parameter. Required ones come first in document order - path, query, header, then the body - and optional ones follow, so a caller never passes undefined to reach a required one. Then come httpRequest?: ParameterRequest, for anything else a request may set (extra headers, a timeout, a signal), and attributes?: Record<string, unknown>:
search(@path('item-id') itemId: string, @query('q') q: string,
@header('X-Tenant') xTenant: string, @query('page') page?: number,
@query('status') status?: 'open' | 'closed',
@request() httpRequest?: ParameterRequest,
@attribute() attributes?: Record<string, unknown>): Promise<Item[]>| Parameter or body | Generated type |
|---|---|
| Path parameter | Always required; ignorePathParameters leaves some out |
| Query or header parameter | Required when the document says required: true; a primitive enum is the union of its literals |
| Cookie parameter | Left out with a warning: the browser sends cookies, and fetch cannot set them |
JSON body (application/json, +json) | The schema's type; properties the schema does not require stay optional through PartialBy<Item, 'id'> |
multipart/form-data | FormData |
application/x-www-form-urlencoded | URLSearchParams |
text/* | string |
| Any other media type | BodyInit |
The body is required when requestBody.required is true. A parameter's description becomes a @param tag of the method's doc comment. Path-level parameters are inherited; operation parameters override matching in/name. An operation without an operationId or a tag is skipped with a warning.
The return type comes from the success response: 200, else the lowest other 2xx, else 2XX. Any JSON media type (application/json, +json, with or without a charset) is typed from its schema, then a wildcard schema, then SSE. text/*, and a string schema under a wildcard, return Promise<string>; recognized SSE returns JsonServerSentEventStream, with any fallback when the event model cannot be inferred. Without an inferred body, the fallback is Promise<Response> with native Response extraction. Generated methods rely on decorators replacing their throw autoGeneratedError(...) placeholder at runtime.
Names and schemas
- Names become identifiers: parameter
item-id→itemId, schemaPage«User»→PageUser,1stThing→_1stThing, commandpay-order→PAY_ORDERandpayOrder.*/inside descriptions is escaped. - Two schemas that normalise to the same model fail with exit code 4. Enum values that normalise alike get distinct members. Tags that name the same client generate a numbered class (
User2ApiClient) with a warning. - A
$refthat points at nothing fails with exit code 4, listing the references. { nullable: true, allOf: [{ $ref }] }admitsnull. AoneOfwith a discriminator narrows each branch by the discriminator property. A map of its own type generates an interface with an index signature. Models namedRecordorResponseare imported under an alias so they do not shadow the globals.- An empty command or event body is
Record<string, never>.
Wow query schemas map to @ahoo-wang/wow-client types. A wow.api.query.ListQuery or PagedQuery schema with a filter property (Wow 8.11 and later) maps to FilterListQuery or FilterPagedQuery from the root entry. wow.api.query.Condition, ConditionOptions, and Operator, and a ListQuery or PagedQuery schema without filter (Wow 8.10), map to the deprecated types of @ahoo-wang/wow-client/legacy, which is removed in v10.
Runtime setup
pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-decorator @ahoo-wang/fetcher-eventstream @ahoo-wang/wow-client
pnpm exec wow-generator generate -i ./openapi.json -o ./src/generated -t ./tsconfig.json
pnpm exec tsc --noEmit -p ./tsconfig.jsonCreate the generated client with its ApiMetadata constructor, typically { fetcher }. Configure the Fetcher baseURL for the target server; generation does not call the generated API.
Command clients, and API clients of a document with x-wow-context-alias, merge the apiMetadata passed to the constructor over their defaults, so new CartCommandClient({ fetcher }) keeps the bounded context's base path and sends to /example/.... The stream command client inherits that constructor. To reach a service directly, without a gateway that routes by context alias, pass basePath: ''; a query client factory takes contextAlias: '' instead:
const commands = new CartCommandClient({ fetcher, basePath: '' });
const snapshots = cartQueryClientFactory.createSnapshotQueryClient({
fetcher,
contextAlias: '',
});The query client factory's aggregateName is the aggregate's route segment read from its snapshot routes, for example sales-order for the aggregate order declared with @AggregateRoute(resourceName = "sales-order"). Its fields type argument is `${OrderAggregatedFields}`, the string values of the field enum: an enum member and the matching string both compile, a misspelt field does not. Code that annotates a query as ListQuery or FilterListQuery, whose fields default to string, names the fields type: ListQuery<`${CartAggregatedFields}`>. See declarative endpoints.
Ownership and failures
Files emitted again at the same path are replaced: keep hand-written customizations outside generated files. Stale files are deleted only if they were recorded in the prior manifest and their content hash is unchanged. Modified stale files and unrelated files are preserved; preservation does not make them part of the current generated API. Index rebuilding can still include source files present in the project.
An invalid manifest or a generated path escaping the output root throws. Saves are awaited before stale deletion and the new manifest, but the operation is not an atomic directory transaction: partial writes can remain after failure. Do not delete the manifest to force cleanup; use a dedicated output directory and review its diff after regeneration. The generator's own name check does not prove the output type-checks against your dependencies: run the consumer compiler.
Implementation sources
typescript/wow-generator/src/utils/sourceFiles.ts:33
typescript/wow-generator/src/client/apiClientGenerator.ts:73
typescript/wow-generator/src/client/queryClientGenerator.ts:35