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. Named after the class, camelCase like every generated file: cartApiClient.ts |
commandClient.ts | Resolved aggregate command paths, body aliases (AddCartItemCommand; none for a body already named …Command), 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 the .ts files of this run 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. A model of an object also lists the properties its schema requires (- required: a, b, or - required: (none)), since every generated property is declared without ?. A nullable value written as anyOf: [{type: null}, X] is documented from X, so a nullable integer keeps its format. --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 - the path parameters in the order the path holds them, then query and header parameters in document order, 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. - A type name keeps each part of the schema name that starts with an upper-case letter and holds no separator as written, acronyms included:
MCPListToolsstaysMCPListTools,OpenAIFilestaysOpenAIFile. A part with a separator or a lower-case start is pascal-cased (order_item→OrderItem). Method names, enum members and endpoint constants keep their own rules. - 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>. - A command's body type is declared as
<Body>Command(AddCartItemCommand = CommandBody<AddCartItem>), so a name never repeatsCommand: a body already namedMountedCommandgets no alias, and its methods takeCommandBody<MountedCommand>. A command type that would take the name of a model of the aggregate's package or of another command's body (commandsFooandFooCommand) fails with exit code 4, naming both schemas; rename one of them. - Command methods take the path variables Wow's interceptor does not fill as positional parameters in the order the path holds them:
/cart/{id}/{customerId}gives(id, customerId, …).
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, and takes wow-client's COMMAND_STREAM_ENDPOINT (@api('', COMMAND_STREAM_ENDPOINT)): it asks for text/event-stream and errors the stream with a WowError when the server sends an error event, as CommandClient.sendAndWaitStream does. To reach a service directly, without a gateway that routes by context alias, pass basePath: ''; a query client factory takes contextAlias: '' instead:
import { Fetcher } from '@ahoo-wang/fetcher';
import { CartCommandClient, cartQueryClientFactory } from './generated/index.js';
const fetcher = new Fetcher({ baseURL: 'http://localhost:8080' });
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. The generated index.ts files export only the files this run generates: a hand-written file in the output directory, or a modified stale one, is never re-exported, whatever the tsconfig include covers; import it from its own module.
A manifest the run cannot read fails it with exit code 5 before anything is written, and the message says what to do. One that is not JSON, typically a merge conflict in a committed manifest: resolve the conflict, or delete the file, knowing that without it this run cannot remove the stale files of the last one. One written by a newer wow-generator (a manifest version above 1): upgrade wow-generator. Anything else: restore it from version control, or delete it. A generated path escaping the output root also 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. A run interrupted with Ctrl-C while it writes finishes the files it started, then stops without deleting stale files or writing the manifest, so the manifest never records a run that did not finish. 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/output/outputStore.ts
typescript/wow-generator/src/analysis/apiClients.ts
typescript/wow-generator/src/emitters/apiClients.ts
typescript/wow-generator/src/emitters/queryClients.ts