@appweaver/cli 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,599 +1,609 @@
1
- # Client
2
-
3
- `@appweaver/client` is a type-safe HTTP client generator and runtime library for consuming Appweaver APIs. It has two
4
- distinct parts:
5
-
6
- - **`weaver-client` CLI** — reads an OpenAPI v3 schema and emits TypeScript types and a typed client class.
7
- - **Runtime library** — provides `FetchClient` and a set of module clients used directly in application code.
8
-
9
- ---
10
-
11
- ## Module formats (ESM & CommonJS)
12
-
13
- The package ships **both** an ESM and a CommonJS build, selected automatically via the `exports` map — no configuration
14
- needed. Both import styles work for the main entry and the `/angular` subpath:
15
-
16
- ```ts
17
- // ESM (tree-shakable — preferred for bundlers like Angular/Vite/webpack prod builds)
18
- import { FetchClient, ClientError } from '@appweaver/client';
19
- import { AngularClient } from '@appweaver/client/angular';
20
-
21
- // CommonJS (e.g. plain Node scripts without a build step)
22
- const { FetchClient, ClientError } = require('@appweaver/client');
23
- const { AngularClient } = require('@appweaver/client/angular');
24
- ```
25
-
26
- ---
27
-
28
- ## `weaver-client` CLI
29
-
30
- ```
31
- weaver-client <command>
32
- ```
33
-
34
- | Command | Alias | Description |
35
- |-------------|-------|-------------------------------------------------|
36
- | `generate` | `g` | Generate TypeScript types and/or a client class |
37
- | `--version` | `-v` | Output package version |
38
- | `--help` | `-h` | Output usage information |
39
-
40
- ---
41
-
42
- ### `weaver-client generate`
43
-
44
- ```
45
- weaver-client generate|g <schemaPath> [options]
46
- ```
47
-
48
- Reads an OpenAPI v3 schema and generates TypeScript types and a typed client class.
49
-
50
- **Arguments:**
51
-
52
- | Argument | Description |
53
- |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
54
- | `<schemaPath>` | Path to the OpenAPI schema. Accepts a relative or absolute file path (including a Windows drive path such as `C:\api\openapi.json`) or a URL (`http://`, `https://`, `file://`). JSON and YAML formats are both supported. |
55
-
56
- **Options:**
57
-
58
- | Option | Description | Default |
59
- |-----------------------|--------------------------------------------------------------------------------------------|---------------------------|
60
- | `--outputPath [path]` | Output path for both types and client (used when `--typesPath`/`--clientPath` are not set) | `./generated/client.ts` |
61
- | `--typesPath [path]` | Output path for generated TypeScript types only | same as `outputPath` |
62
- | `--clientPath [path]` | Output path for generated client class only | same as `outputPath` |
63
- | `--clientName [name]` | Custom name for the generated client class | derived from schema title |
64
- | `--framework [name]` | Framework for the generated client class (`fetch` or `angular`) | `fetch` |
65
- | `--typesOnly` | Generate TypeScript types only, skip client class generation | `false` |
66
- | `--clientOnly` | Generate client class only, skip TypeScript types generation | `false` |
67
- | `--noTypes` | Generate client class without TypeScript type support | `false` |
68
-
69
- **Generation process:**
70
-
71
- 1. Reads and parses the schema (JSON or YAML, local or remote).
72
- 2. Generates TypeScript interfaces via `openapi-typescript`, enriching them with JSDoc validation tags (`@minLength`,
73
- `@maxLength`, `@minimum`, `@maximum`, `@pattern`, `@format`).
74
- 3. Deduplicates union types and extracts inline schemas to named exported types, including the ones carrying a
75
- description (i.e. `PostQuerySort`).
76
- 4. Hoists the schemas the document repeats inline into a shared definition each, so the generated types declare them
77
- once and reference them everywhere else. This covers the enums (every sortable field of every resource shares one
78
- `SortDirection` rather than declaring an `asc | desc` enum of its own) and the value a filterable field accepts
79
- (`QueryFilterValue`, built from the plain `QueryFilterScalar`), while every property keeps its own description.
80
- 5. Emits every enum as a constant object plus a type alias of its values, so both the member (`SortDirection.asc`) and
81
- the plain literal (`'asc'`) are accepted wherever the enum is used. When the types are written to a declaration file
82
- (`.d.ts`), the constant is declared rather than initialized, since such a file carries no runtime values.
83
- 6. Classifies all API paths into route groups: resources, auth, account, health, files, and custom.
84
- 7. Emits a typed client class extending `FetchClient<Paths>` with a getter for each route group. Resources with
85
- unsupported operations are excluded at compile time using `Omit`.
86
- 8. Formats all output with Prettier.
87
- 9. Writes files with an autogenerated header comment.
88
-
89
- **Examples:**
90
-
91
- ```bash
92
- # Generate types + client from a local OpenAPI file (single output file)
93
- weaver-client generate ./openapi.json --outputPath ./src/generated/client.ts
94
-
95
- # Generate types + client from a running server
96
- weaver-client generate http://localhost:3000/openapi.json --outputPath ./src/generated/client.ts
97
-
98
- # Generate types only
99
- weaver-client generate ./openapi.json --typesOnly --outputPath ./src/types/api.ts
100
-
101
- # Generate client class only
102
- weaver-client generate ./openapi.json --clientOnly --outputPath ./src/client.ts
103
-
104
- # Generate client class without TypeScript types
105
- weaver-client generate ./openapi.json --noTypes --outputPath ./src/client.ts
106
-
107
- # Separate output files with a custom class name
108
- weaver-client generate ./openapi.json \
109
- --typesPath ./src/types/api.ts \
110
- --clientPath ./src/client.ts \
111
- --clientName CmsApiClient
112
- ```
113
-
114
- ---
115
-
116
- ## Typical end-to-end workflow
117
-
118
- ```bash
119
- # 1. Export the OpenAPI spec from the running Appweaver API
120
- weaver openapi --outputPath ./openapi.json
121
-
122
- # 2. Generate the typed client
123
- weaver-client generate ./openapi.json --outputPath ./generated/client.ts
124
-
125
- # 3. Use the generated client in application code
126
- import { createClient } from './generated/client';
127
-
128
- const client = createClient({ baseUrl: 'http://localhost:3000', auth: { jwt: 'token' } });
129
- const users = await client.user.query({ filter: { enabled: true } });
130
- ```
131
-
132
- ---
133
-
134
- ## Generated output
135
-
136
- Running `weaver-client generate` produces one or two files depending on the options used.
137
-
138
- ### Types file
139
-
140
- Contains all TypeScript interfaces and type aliases derived from the OpenAPI schema. The key export is the `paths`
141
- namespace used to parameterise `FetchClient`. Module-level types (`AuthModuleType`, `AccountModuleType`,
142
- `HealthModuleType`, per-resource `*ResourceModuleType`) are also exported and consumed by the client class.
143
-
144
- Every schema definition becomes an exported type named after it, so the request and response shapes can be referenced
145
- directly. The per-resource module type holds the same types under the keys the `ResourceClient` methods use:
146
-
147
- ```ts
148
- import { PostQuerySort, SortDirection } from './generated/schema';
149
-
150
- const sort: PostQuerySort = { createdAt: SortDirection.desc, title: SortDirection.asc };
151
- const posts = await client.post.query({ sort, page: 1, size: 20 });
152
- ```
153
-
154
- Enums are generated as a constant object together with a type alias of its values, so the members and the raw literals
155
- are interchangeable and no import is needed for the literal form:
156
-
157
- ```ts
158
- export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
159
- export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
160
-
161
- // Both are valid and equally type safe
162
- const byMember: PostQuerySort = { createdAt: SortDirection.desc };
163
- const byLiteral: PostQuerySort = { createdAt: 'desc' };
164
- ```
165
-
166
- The value a filterable field accepts is declared once as well, rather than being spelled out again for every field of
167
- every resource. A scalar field references `QueryFilterValue`, and a relation field adds the filter of the related
168
- resource to the plain values it accepts:
169
-
170
- ```ts
171
- export type QueryFilterScalar = string | number | boolean | null;
172
- export type QueryFilterValue = QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
173
-
174
- export type PostQueryFilter = {
175
- /** @description Filter by the title field */
176
- title?: QueryFilterValue;
177
- /** @description Filter by the author relation, matching an id, a list of ids, or a nested User filter */
178
- author?: QueryFilterScalar | QueryFilterScalar[] | UserQueryFilter | UserQueryFilter[];
179
- // ...
180
- };
181
- ```
182
-
183
- ### Client file
184
-
185
- ```ts
186
- // Generated by Appweaver. Please do not edit this file manually.
187
-
188
- import { ClientConfig, FetchClient } from '@appweaver/client';
189
- import * as Type from './schema';
190
-
191
- export class CMSAPIClient extends FetchClient<Type.paths> {
192
- public auth = this.authClient<Type.AuthModuleType>('/auth');
193
-
194
- public account = this.accountClient<Type.AccountModuleType>('/auth/account');
195
-
196
- public health = this.healthClient<Type.HealthModuleType>('/health');
197
-
198
- public files = this.filesClient('/files');
199
-
200
- // Resource with some operations excluded (compile-time Omit)
201
- public apiKey = this.resourceClient<
202
- Type.ApiKeyResourceModuleType,
203
- ['aggregate', 'export', 'uploadFiles', 'deleteFiles']
204
- >('/api/api-keys');
205
-
206
- public post = this.resourceClient<Type.PostResourceModuleType, ['delete']>(
207
- '/api/posts'
208
- );
209
-
210
- public user = this.resourceClient<Type.UserResourceModuleType>('/api/users');
211
-
212
- // Custom routes that don't match a standard module
213
- public info = this.customRequest('get', '/api/');
214
- public postApiPublishPosts = this.customRequest('post', '/api/publish-posts');
215
- }
216
-
217
- export function createClient(config: ClientConfig): CMSAPIClient {
218
- return new CMSAPIClient(config);
219
- }
220
- ```
221
-
222
- The generated second type argument to `resourceClient` (e.g., `['aggregate', 'export', 'uploadFiles', 'deleteFiles']`)
223
- removes those methods from the returned `ResourceClient` at the TypeScript level, preventing accidental calls to
224
- operations not exposed by the API.
225
-
226
- ### Angular client (`--framework angular`)
227
-
228
- Passing `--framework angular` generates a client class extending `AngularClient` instead of `FetchClient`. The generated
229
- class is constructed with Angular's `HttpClient` and all its methods return RxJS `Observable`s instead of
230
- `Promise`s:
231
-
232
- ```ts
233
- import { ClientConfig, ClientError } from '@appweaver/client';
234
- import { AngularClient } from '@appweaver/client/angular';
235
- import { HttpClient } from '@angular/common/http';
236
-
237
- // In an Angular service or provider:
238
- const client = new CMSAPIClient(httpClient, { baseUrl: 'http://localhost:3000' });
239
- client.user.query({ filter: { enabled: true } }).subscribe((users) => {
240
- });
241
- ```
242
-
243
- **Important:** `AngularClient` is only available from the `@appweaver/client/angular` subpath — it is not exported from
244
- the main `@appweaver/client` entry point. This keeps `rxjs` completely out of the module graph (runtime and types) for
245
- `FetchClient` users. `rxjs` is an **optional peer dependency**: Angular projects already have it installed, while
246
- fetch-only projects do not need it at all.
247
-
248
- ---
249
-
250
- ## Runtime library
251
-
252
- ### `ClientConfig`
253
-
254
- ```ts
255
- type ClientConfig = {
256
- baseUrl: string; // Base URL prepended to every request path
257
- timeout?: number; // Request timeout in milliseconds
258
- middlewares?: Middleware[]; // Additional openapi-fetch middlewares
259
- auth?: AuthConfig; // Authentication strategy
260
- };
261
- ```
262
-
263
- ### Authentication
264
-
265
- Exactly one authentication strategy may be provided via `auth`. Each strategy accepts a static value, a typed config
266
- object, or an async function that receives the outgoing `Request` and returns the auth value dynamically.
267
-
268
- ```ts
269
- // JWT Bearer — plain token string
270
- let client = createClient({
271
- baseUrl: 'https://api.example.com',
272
- auth: { jwt: 'my-access-token' }
273
- });
274
-
275
- // JWT Bearer — object with optional refresh token
276
- client = createClient({
277
- baseUrl: 'https://api.example.com',
278
- auth: { jwt: { accessToken: 'access', refreshToken: 'refresh' } }
279
- });
280
-
281
- // JWT Bearer — dynamic async function
282
- client = createClient({
283
- baseUrl: 'https://api.example.com',
284
- auth: { jwt: async (req) => await tokenStore.getToken() }
285
- });
286
-
287
- // API Key — plain string (uses X-Api-Key header by default)
288
- client = createClient({
289
- baseUrl: 'https://api.example.com',
290
- auth: { apiKey: 'my-api-key' }
291
- });
292
-
293
- // API Key — custom header name
294
- client = createClient({
295
- baseUrl: 'https://api.example.com',
296
- auth: { apiKey: { key: 'my-api-key', header: 'X-Custom-Key' } }
297
- });
298
-
299
- // HTTP Basic — plain Base64 string
300
- client = createClient({
301
- baseUrl: 'https://api.example.com',
302
- auth: { basic: btoa('user:pass') }
303
- });
304
-
305
- // HTTP Basic — username/password object (auto-encodes)
306
- client = createClient({
307
- baseUrl: 'https://api.example.com',
308
- auth: { basic: { username: 'user', password: 'pass' } }
309
- });
310
- ```
311
-
312
- ### Request timeout
313
-
314
- ```ts
315
- const client = createClient({
316
- baseUrl: 'https://api.example.com',
317
- timeout: 5000 // abort requests after 5 seconds
318
- });
319
- ```
320
-
321
- ### Custom middlewares
322
-
323
- Additional `openapi-fetch` middlewares are applied after the built-in timeout and auth middlewares:
324
-
325
- ```ts
326
- const client = createClient({
327
- baseUrl: 'https://api.example.com',
328
- middlewares: [
329
- {
330
- async onRequest({ request }) {
331
- request.headers.set('X-Request-Id', crypto.randomUUID());
332
- return request;
333
- }
334
- }
335
- ]
336
- });
337
- ```
338
-
339
- ---
340
-
341
- ## Module clients
342
-
343
- ### `ResourceClient`
344
-
345
- Exposes CRUD and file operations for a single resource endpoint.
346
-
347
- | Method | HTTP | Description |
348
- |--------------------------------|--------------------------------------|----------------------------------------------------------|
349
- | `find(id, options?)` | `GET /{resource}/{id}` | Fetch a single record by ID |
350
- | `query(request, options?)` | `POST /{resource}/query` | Filter, sort, and paginate the collection |
351
- | `aggregate(request, options?)` | `POST /{resource}/aggregate` | Aggregation query (count, sum, avg, etc.) |
352
- | `create(resource, options?)` | `POST /{resource}` | Create a new record |
353
- | `update(resource, options?)` | `PUT /{resource}/{id}` | Update an existing record (`resource` must include `id`) |
354
- | `delete(id, options?)` | `DELETE /{resource}/{id}` | Delete a record by ID |
355
- | `export(request, options?)` | `POST /{resource}/export` | Export collection as a file; returns `FileDataResponse` |
356
- | `uploadFiles(files, options?)` | `POST /{resource}/{id}/files` | Upload files to a record |
357
- | `deleteFiles(files, options?)` | `POST /{resource}/{id}/delete-files` | Remove files from a record |
358
-
359
- ```ts
360
- // Find a single record
361
- const post = await client.post.find(1);
362
-
363
- // Query with filters, sorting and pagination. The sort accepts a comma-separated
364
- // field list ('-createdAt,id') or an object of field directions
365
- const result = await client.post.query({
366
- filter: { published: true },
367
- sort: { author: { lastName: 'asc' }, createdAt: 'desc' },
368
- page: 1,
369
- size: 20
370
- });
371
-
372
- // Aggregate. The select holds the operators to apply per numeric or date field
373
- const stats = await client.post.aggregate({
374
- select: { counter: { count: true, sum: true }, createdAt: { min: true } },
375
- dateField: 'createdAt'
376
- });
377
-
378
- // Create
379
- const newPost = await client.post.create({ title: 'Hello', body: '...' });
380
-
381
- // Update (id must be part of the payload)
382
- const updated = await client.post.update({ id: 1, title: 'Updated' });
383
-
384
- // Delete
385
- const deleted = await client.post.delete(1);
386
-
387
- // Export as CSV
388
- const file = await client.post.export({ filter: { published: true } });
389
- // file.body — ReadableStream, file.filename — string, file.contentType — string
390
-
391
- // Upload files
392
- const files = await client.post.uploadFiles({ cover: new File([], 'cover.jpg') });
393
-
394
- // Delete files
395
- await client.post.deleteFiles({ fileIds: [42, 43] });
396
- ```
397
-
398
- ### `AuthClient`
399
-
400
- | Method | HTTP | Description |
401
- |-------------------------------------|------------------------------|---------------------------------------------|
402
- | `login(request, options?)` | `POST /auth/login` | Authenticate with credentials |
403
- | `logout(options?)` | `POST /auth/logout` | Invalidate the current session |
404
- | `refresh(options?)` | `POST /auth/refresh` | Renew access token |
405
- | `changePassword(request, options?)` | `POST /auth/change-password` | Change the authenticated user's password |
406
- | `exchangeToken(request, options?)` | `POST /auth/exchange-token` | Exchange a third-party token for app tokens |
407
- | `me(options?)` | `GET /auth/me` | Get the current user's identity |
408
-
409
- ```ts
410
- const session = await client.auth.login({ email: 'user@example.com', password: 'secret' });
411
- const me = await client.auth.me();
412
- await client.auth.logout();
413
- ```
414
-
415
- ### `AccountClient`
416
-
417
- | Method | HTTP | Description |
418
- |----------------------------------------|-------------------------------------------|------------------------------------|
419
- | `sendVerifyEmail(request, options?)` | `POST /auth/account/send-verify-email` | Send email verification link |
420
- | `verifyEmail(request, options?)` | `POST /auth/account/verify-email` | Verify email with token |
421
- | `verifyEmailRedirect(token, options?)` | `GET /auth/account/verify-email-redirect` | Handle redirect-based verification |
422
- | `sendResetPassword(request, options?)` | `POST /auth/account/send-reset-password` | Send password reset email |
423
- | `resetPassword(request, options?)` | `POST /auth/account/reset-password` | Reset password with token |
424
- | `send2FACode(request, options?)` | `POST /auth/account/send-2fa-code` | Send a 2FA code to the user |
425
- | `verify2FACode(request, options?)` | `POST /auth/account/verify-2fa-code` | Verify a submitted 2FA code |
426
-
427
- ```ts
428
- await client.account.sendVerifyEmail({ email: 'user@example.com' });
429
- await client.account.verifyEmail({ token: 'abc123' });
430
- await client.account.sendResetPassword({ email: 'user@example.com' });
431
- await client.account.resetPassword({ token: 'abc123', password: 'newpass' });
432
- ```
433
-
434
- ### `HealthClient`
435
-
436
- | Method | HTTP | Description |
437
- |-------------------|---------------------|---------------------|
438
- | `check(options?)` | `GET /health` | Health check status |
439
- | `ready(options?)` | `GET /health/ready` | Readiness probe |
440
-
441
- ```ts
442
- const status = await client.health.check();
443
- ```
444
-
445
- ### `FilesClient`
446
-
447
- | Method | HTTP | Description |
448
- |-----------------------------|-----------------------|---------------------------|
449
- | `public(path, options?)` | `GET /files/{path}` | Download a public file |
450
- | `protected(path, options?)` | `GET /files/p/{path}` | Download a protected file |
451
-
452
- ```ts
453
- const file = await client.files.public('images/photo.jpg');
454
- const fileData = await file.base64(); // or use file.stream to pipe output to file etc.
455
- ```
456
-
457
- ### `FileDataResponse`
458
-
459
- Both `FilesClient` methods and `ResourceClient.export` return a `FileDataResponse` instance instead of a raw response
460
- body. It exposes file metadata as properties and provides helper methods to consume the content.
461
-
462
- **Properties:**
463
-
464
- | Property | Type | Description |
465
- |-------------|---------------------|-----------------------------------------------------------------|
466
- | `stream` | `ReadableStream` | Readable stream of the file content |
467
- | `fileName` | `string` | File name extracted from the `Content-Disposition` header |
468
- | `type` | `string` | MIME type from the `Content-Type` header |
469
- | `length` | `number` | File size in bytes from the `Content-Length` header |
470
- | `range` | `FileContentRange?` | Byte range info present on partial-content (HTTP 206) responses |
471
- | `maxAge` | `number?` | Cache duration in seconds from `Cache-Control: max-age` |
472
- | `expiresAt` | `string?` | Expiration timestamp from the `Expires` header |
473
-
474
- **Methods:**
475
-
476
- | Method | Returns | Description |
477
- |--------------------------|------------------------|-----------------------------------------------------------------------|
478
- | `buffer()` | `Promise<ArrayBuffer>` | Raw binary content as an `ArrayBuffer` |
479
- | `blob()` | `Promise<Blob>` | Content wrapped in a `Blob` with the correct MIME type |
480
- | `text(encoding: string)` | `Promise<string>` | Content decoded to string using specified encoding (default is UTF-8) |
481
- | `base64()` | `Promise<string>` | Content as a Base64 data URL (`data:<type>;base64,...`) |
482
-
483
- The stream is read lazily on the first call to any consumption method, and the result is cached — later calls return the
484
- same `ArrayBuffer` without re-reading the stream.
485
-
486
- The `FileContentRange` type describes a partial-content range:
487
-
488
- ```ts
489
- type FileContentRange = {
490
- start: number; // Starting byte position (inclusive)
491
- end: number; // Ending byte position (inclusive)
492
- total: number; // Total size of the complete file in bytes
493
- };
494
- ```
495
-
496
- **Examples:**
497
-
498
- ```ts
499
- // Download a public file and convert to a data URL for display
500
- let file = await client.files.public('images/photo.jpg');
501
- const dataUrl = await file.base64();
502
- img.src = dataUrl;
503
-
504
- // Download a protected file and save as a Blob
505
- file = await client.files.protected('reports/data.pdf');
506
- const blob = await file.blob();
507
- const url = URL.createObjectURL(blob);
508
-
509
- // Export a resource collection as CSV and read as text
510
- file = await client.post.export({ filter: { published: true } });
511
- const csv = await file.text();
512
-
513
- // Inspect metadata before consuming
514
- console.log(file.fileName); // e.g. "export.csv"
515
- console.log(file.type); // e.g. "text/csv"
516
- console.log(file.length); // e.g. 4096
517
- ```
518
-
519
- ---
520
-
521
- ## Direct requests
522
-
523
- ### `sendRequest`
524
-
525
- Sends a typed request and returns the parsed response body. Throws `ClientError` on any non-2xx response.
526
-
527
- ```ts
528
- const data = await client.sendRequest('get', '/api/custom-endpoint');
529
- ```
530
-
531
- ### `sendRequestRaw`
532
-
533
- Returns the raw `{ data, error, response }` tuple from `openapi-fetch` without throwing. Useful when the caller needs to
534
- inspect error details or branch on status codes.
535
-
536
- ```ts
537
- const { data, error, response } = await client.sendRequestRaw('post', '/api/custom-endpoint', {
538
- body: { key: 'value' }
539
- });
540
-
541
- if (error) {
542
- console.error(`Failed with ${response.status}`);
543
- } else {
544
- console.log(data);
545
- }
546
- ```
547
-
548
- ### `getClient`
549
-
550
- Returns the underlying `openapi-fetch` `Client` instance for advanced use cases not covered by the helper methods.
551
-
552
- ```ts
553
- const rawClient = client.getClient();
554
- const { data } = await rawClient.GET('/api/custom-endpoint');
555
- ```
556
-
557
- ### Custom request (generated)
558
-
559
- Routes that do not match any standard module pattern are exposed via `customRequest` in the generated class:
560
-
561
- ```ts
562
- // In the generated client:
563
- const postApiPublishPosts = this.customRequest('post', '/api/publish-posts');
564
-
565
- // Usage:
566
- const result = await client.postApiPublishPosts({ body: { ids: [1, 2, 3] } });
567
- ```
568
-
569
- ---
570
-
571
- ## Error handling
572
-
573
- All module client methods and `sendRequest` throw `ClientError` on non-2xx responses:
574
-
575
- ```ts
576
- import { ClientError } from '@appweaver/client';
577
-
578
- try {
579
- const post = await client.post.find(999);
580
- } catch (err) {
581
- if (err instanceof ClientError) {
582
- console.error(err.message); // Error message from the API response body
583
- console.error(err.errorCode); // HTTP status code (or API errorCode field)
584
- console.error(err.response); // Native Response object
585
- }
586
- }
587
- ```
588
-
589
- Use `sendRequestRaw` to avoid exceptions and handle errors inline:
590
-
591
- ```ts
592
- const { data, error, response } = await client.sendRequestRaw('get', '/api/posts/{id}', {
593
- params: { path: { id: 999 } }
594
- });
595
-
596
- if (response.status === 404) {
597
- // handle not found
598
- }
599
- ```
1
+ # Client
2
+
3
+ `@appweaver/client` is a type-safe HTTP client generator and runtime library for consuming Appweaver APIs. It has two
4
+ distinct parts:
5
+
6
+ - **`weaver-client` CLI** — reads an OpenAPI v3 schema and emits TypeScript types and a typed client class.
7
+ - **Runtime library** — provides `FetchClient` and a set of module clients used directly in application code.
8
+
9
+ ---
10
+
11
+ ## Module formats (ESM & CommonJS)
12
+
13
+ The package ships **both** an ESM and a CommonJS build, selected automatically via the `exports` map — no configuration
14
+ needed. Both import styles work for the main entry and the `/angular` subpath:
15
+
16
+ ```ts
17
+ // ESM (tree-shakable — preferred for bundlers like Angular/Vite/webpack prod builds)
18
+ import { FetchClient, ClientError } from '@appweaver/client';
19
+ import { AngularClient } from '@appweaver/client/angular';
20
+
21
+ // CommonJS (e.g. plain Node scripts without a build step)
22
+ const { FetchClient, ClientError } = require('@appweaver/client');
23
+ const { AngularClient } = require('@appweaver/client/angular');
24
+ ```
25
+
26
+ ---
27
+
28
+ ## `weaver-client` CLI
29
+
30
+ ```
31
+ weaver-client <command>
32
+ ```
33
+
34
+ | Command | Alias | Description |
35
+ |-------------|-------|-------------------------------------------------|
36
+ | `generate` | `g` | Generate TypeScript types and/or a client class |
37
+ | `--version` | `-v` | Output package version |
38
+ | `--help` | `-h` | Output usage information |
39
+
40
+ ---
41
+
42
+ ### `weaver-client generate`
43
+
44
+ ```
45
+ weaver-client generate|g <schemaPath> [options]
46
+ ```
47
+
48
+ Reads an OpenAPI v3 schema and generates TypeScript types and a typed client class.
49
+
50
+ **Arguments:**
51
+
52
+ | Argument | Description |
53
+ |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
54
+ | `<schemaPath>` | Path to the OpenAPI schema. Accepts a relative or absolute file path (including a Windows drive path such as `C:\api\openapi.json`) or a URL (`http://`, `https://`, `file://`). JSON and YAML formats are both supported. |
55
+
56
+ **Options:**
57
+
58
+ | Option | Description | Default |
59
+ |-----------------------|--------------------------------------------------------------------------------------------|---------------------------|
60
+ | `--outputPath [path]` | Output path for both types and client (used when `--typesPath`/`--clientPath` are not set) | `./generated/client.ts` |
61
+ | `--typesPath [path]` | Output path for generated TypeScript types only | same as `outputPath` |
62
+ | `--clientPath [path]` | Output path for generated client class only | same as `outputPath` |
63
+ | `--clientName [name]` | Custom name for the generated client class | derived from schema title |
64
+ | `--framework [name]` | Framework for the generated client class (`fetch` or `angular`) | `fetch` |
65
+ | `--typesOnly` | Generate TypeScript types only, skip client class generation | `false` |
66
+ | `--clientOnly` | Generate client class only, skip TypeScript types generation | `false` |
67
+ | `--noTypes` | Generate client class without TypeScript type support | `false` |
68
+
69
+ **Generation process:**
70
+
71
+ 1. Reads and parses the schema (JSON or YAML, local or remote).
72
+ 2. Generates TypeScript interfaces via `openapi-typescript`, enriching them with JSDoc validation tags (`@minLength`,
73
+ `@maxLength`, `@minimum`, `@maximum`, `@pattern`, `@format`).
74
+ 3. Deduplicates union types and extracts inline schemas to named exported types, including the ones carrying a
75
+ description (i.e. `PostQuerySort`).
76
+ 4. Hoists the schemas the document repeats inline into a shared definition each, so the generated types declare them
77
+ once and reference them everywhere else. This covers the enums (every sortable field of every resource shares one
78
+ `SortDirection` rather than declaring an `asc | desc` enum of its own) and the value a filterable field accepts
79
+ (`QueryFilterValue`, built from the plain `QueryFilterScalar`), while every property keeps its own description.
80
+ 5. Emits every enum as a constant object plus a type alias of its values, so both the member (`SortDirection.asc`) and
81
+ the plain literal (`'asc'`) are accepted wherever the enum is used. When the types are written to a declaration file
82
+ (`.d.ts`), the constant is declared rather than initialized, since such a file carries no runtime values.
83
+ 6. Classifies all API paths into route groups: resources, auth, account, health, files, and custom.
84
+ 7. Emits a typed client class extending `FetchClient<Paths>` with a getter for each route group. Resources with
85
+ unsupported operations are excluded at compile time using `Omit`.
86
+ 8. Formats all output with Prettier.
87
+ 9. Writes files with an autogenerated header comment.
88
+
89
+ **Examples:**
90
+
91
+ ```bash
92
+ # Generate types + client from a local OpenAPI file (single output file)
93
+ weaver-client generate ./openapi.json --outputPath ./src/generated/client.ts
94
+
95
+ # Generate types + client from a running server
96
+ weaver-client generate http://localhost:3000/openapi.json --outputPath ./src/generated/client.ts
97
+
98
+ # Generate types only
99
+ weaver-client generate ./openapi.json --typesOnly --outputPath ./src/types/api.ts
100
+
101
+ # Generate client class only
102
+ weaver-client generate ./openapi.json --clientOnly --outputPath ./src/client.ts
103
+
104
+ # Generate client class without TypeScript types
105
+ weaver-client generate ./openapi.json --noTypes --outputPath ./src/client.ts
106
+
107
+ # Separate output files with a custom class name
108
+ weaver-client generate ./openapi.json \
109
+ --typesPath ./src/types/api.ts \
110
+ --clientPath ./src/client.ts \
111
+ --clientName CmsApiClient
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Typical end-to-end workflow
117
+
118
+ ```bash
119
+ # 1. Export the OpenAPI spec from the running Appweaver API
120
+ weaver openapi --outputPath ./openapi.json
121
+
122
+ # 2. Generate the typed client
123
+ weaver-client generate ./openapi.json --outputPath ./generated/client.ts
124
+
125
+ # 3. Use the generated client in application code
126
+ import { createClient } from './generated/client';
127
+
128
+ const client = createClient({ baseUrl: 'http://localhost:3000', auth: { jwt: 'token' } });
129
+ const users = await client.user.query({ filter: { enabled: true } });
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Generated output
135
+
136
+ Running `weaver-client generate` produces one or two files depending on the options used.
137
+
138
+ ### Types file
139
+
140
+ Contains all TypeScript interfaces and type aliases derived from the OpenAPI schema. The key export is the `paths`
141
+ namespace used to parameterise `FetchClient`. Module-level types (`AuthModuleType`, `AccountModuleType`,
142
+ `HealthModuleType`, per-resource `*ResourceModuleType`) are also exported and consumed by the client class.
143
+
144
+ Every schema definition becomes an exported type named after it, so the request and response shapes can be referenced
145
+ directly. The per-resource module type holds the same types under the keys the `ResourceClient` methods use:
146
+
147
+ ```ts
148
+ import { PostQuerySort, SortDirection } from './generated/schema';
149
+
150
+ const sort: PostQuerySort = { createdAt: SortDirection.desc, title: SortDirection.asc };
151
+ const posts = await client.post.query({ sort, page: 1, size: 20 });
152
+ ```
153
+
154
+ Enums are generated as a constant object together with a type alias of its values, so the members and the raw literals
155
+ are interchangeable and no import is needed for the literal form:
156
+
157
+ ```ts
158
+ export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
159
+ export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
160
+
161
+ // Both are valid and equally type safe
162
+ const byMember: PostQuerySort = { createdAt: SortDirection.desc };
163
+ const byLiteral: PostQuerySort = { createdAt: 'desc' };
164
+ ```
165
+
166
+ The value a filterable field accepts is declared once as well, rather than being spelled out again for every field of
167
+ every resource. A scalar field references `QueryFilterValue`, and a relation field adds the filter of the related
168
+ resource to the plain values it accepts:
169
+
170
+ ```ts
171
+ export type QueryFilterScalar = string | number | boolean | null;
172
+ export type QueryFilterValue = QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
173
+
174
+ export type PostQueryFilter = {
175
+ /** @description Filter by the title field */
176
+ title?: QueryFilterValue;
177
+ /** @description Filter by the author relation, matching an id, a list of ids, or a nested User filter */
178
+ author?: QueryFilterScalar | QueryFilterScalar[] | UserQueryFilter | UserQueryFilter[];
179
+ // ...
180
+ };
181
+ ```
182
+
183
+ ### Client file
184
+
185
+ ```ts
186
+ // Generated by Appweaver. Please do not edit this file manually.
187
+
188
+ import { ClientConfig, FetchClient } from '@appweaver/client';
189
+ import * as Type from './schema';
190
+
191
+ export class CMSAPIClient extends FetchClient<Type.paths> {
192
+ public auth = this.authClient<Type.AuthModuleType>('/auth');
193
+
194
+ public account = this.accountClient<Type.AccountModuleType>('/auth/account');
195
+
196
+ public health = this.healthClient<Type.HealthModuleType>('/health');
197
+
198
+ public files = this.filesClient('/files');
199
+
200
+ // Resource with some operations excluded (compile-time Omit)
201
+ public apiKey = this.resourceClient<
202
+ Type.ApiKeyResourceModuleType,
203
+ ['aggregate', 'export', 'uploadFiles', 'deleteFiles']
204
+ >('/api/api-keys');
205
+
206
+ public post = this.resourceClient<Type.PostResourceModuleType, ['delete']>(
207
+ '/api/posts'
208
+ );
209
+
210
+ public user = this.resourceClient<Type.UserResourceModuleType>('/api/users');
211
+
212
+ // Custom routes that don't match a standard module
213
+ public info = this.customRequest('get', '/api/');
214
+ public postApiPublishPosts = this.customRequest('post', '/api/publish-posts');
215
+ }
216
+
217
+ export function createClient(config: ClientConfig): CMSAPIClient {
218
+ return new CMSAPIClient(config);
219
+ }
220
+ ```
221
+
222
+ The generated second type argument to `resourceClient` (e.g., `['aggregate', 'export', 'uploadFiles', 'deleteFiles']`)
223
+ removes those methods from the returned `ResourceClient` at the TypeScript level, preventing accidental calls to
224
+ operations not exposed by the API.
225
+
226
+ ### Angular client (`--framework angular`)
227
+
228
+ Passing `--framework angular` generates a client class extending `AngularClient` instead of `FetchClient`. The generated
229
+ class is constructed with Angular's `HttpClient` and all its methods return RxJS `Observable`s instead of
230
+ `Promise`s:
231
+
232
+ ```ts
233
+ import { ClientConfig, ClientError } from '@appweaver/client';
234
+ import { AngularClient } from '@appweaver/client/angular';
235
+ import { HttpClient } from '@angular/common/http';
236
+
237
+ // In an Angular service or provider:
238
+ const client = new CMSAPIClient(httpClient, { baseUrl: 'http://localhost:3000' });
239
+ client.user.query({ filter: { enabled: true } }).subscribe((users) => {
240
+ });
241
+ ```
242
+
243
+ **Important:** `AngularClient` is only available from the `@appweaver/client/angular` subpath — it is not exported from
244
+ the main `@appweaver/client` entry point. This keeps `rxjs` completely out of the module graph (runtime and types) for
245
+ `FetchClient` users. `rxjs` is an **optional peer dependency**: Angular projects already have it installed, while
246
+ fetch-only projects do not need it at all.
247
+
248
+ ---
249
+
250
+ ## Runtime library
251
+
252
+ ### `ClientConfig`
253
+
254
+ ```ts
255
+ type ClientConfig = {
256
+ baseUrl: string; // Base URL prepended to every request path
257
+ timeout?: number; // Request timeout in milliseconds
258
+ middlewares?: Middleware[]; // Additional openapi-fetch middlewares
259
+ auth?: AuthConfig; // Authentication strategy
260
+ };
261
+ ```
262
+
263
+ ### Authentication
264
+
265
+ Exactly one authentication strategy may be provided via `auth`. Each strategy accepts a static value, a typed config
266
+ object, or an async function that receives the outgoing `Request` and returns the auth value dynamically.
267
+
268
+ ```ts
269
+ // JWT Bearer — plain token string
270
+ let client = createClient({
271
+ baseUrl: 'https://api.example.com',
272
+ auth: { jwt: 'my-access-token' }
273
+ });
274
+
275
+ // JWT Bearer — object with optional refresh token
276
+ client = createClient({
277
+ baseUrl: 'https://api.example.com',
278
+ auth: { jwt: { accessToken: 'access', refreshToken: 'refresh' } }
279
+ });
280
+
281
+ // JWT Bearer — dynamic async function
282
+ client = createClient({
283
+ baseUrl: 'https://api.example.com',
284
+ auth: { jwt: async (req) => await tokenStore.getToken() }
285
+ });
286
+
287
+ // API Key — plain string (uses X-Api-Key header by default)
288
+ client = createClient({
289
+ baseUrl: 'https://api.example.com',
290
+ auth: { apiKey: 'my-api-key' }
291
+ });
292
+
293
+ // API Key — custom header name
294
+ client = createClient({
295
+ baseUrl: 'https://api.example.com',
296
+ auth: { apiKey: { key: 'my-api-key', header: 'X-Custom-Key' } }
297
+ });
298
+
299
+ // HTTP Basic — plain Base64 string
300
+ client = createClient({
301
+ baseUrl: 'https://api.example.com',
302
+ auth: { basic: btoa('user:pass') }
303
+ });
304
+
305
+ // HTTP Basic — username/password object (auto-encodes)
306
+ client = createClient({
307
+ baseUrl: 'https://api.example.com',
308
+ auth: { basic: { username: 'user', password: 'pass' } }
309
+ });
310
+ ```
311
+
312
+ ### Request timeout
313
+
314
+ ```ts
315
+ const client = createClient({
316
+ baseUrl: 'https://api.example.com',
317
+ timeout: 5000 // abort requests after 5 seconds
318
+ });
319
+ ```
320
+
321
+ ### Custom middlewares
322
+
323
+ Additional `openapi-fetch` middlewares are applied after the built-in timeout and auth middlewares:
324
+
325
+ ```ts
326
+ const client = createClient({
327
+ baseUrl: 'https://api.example.com',
328
+ middlewares: [
329
+ {
330
+ async onRequest({ request }) {
331
+ request.headers.set('X-Request-Id', crypto.randomUUID());
332
+ return request;
333
+ }
334
+ }
335
+ ]
336
+ });
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Module clients
342
+
343
+ ### `ResourceClient`
344
+
345
+ Exposes CRUD and file operations for a single resource endpoint.
346
+
347
+ | Method | HTTP | Description |
348
+ |--------------------------------|--------------------------------------|----------------------------------------------------------|
349
+ | `find(id, options?)` | `GET /{resource}/{id}` | Fetch a single record by ID |
350
+ | `query(request, options?)` | `POST /{resource}/query` | Filter, sort, and paginate the collection |
351
+ | `aggregate(request, options?)` | `POST /{resource}/aggregate` | Aggregation query (count, sum, avg, etc.) |
352
+ | `create(resource, options?)` | `POST /{resource}` | Create a new record |
353
+ | `update(resource, options?)` | `PUT /{resource}/{id}` | Update an existing record (`resource` must include `id`) |
354
+ | `delete(id, options?)` | `DELETE /{resource}/{id}` | Delete a record by ID |
355
+ | `export(request, options?)` | `POST /{resource}/export` | Export collection as a file; returns `FileDataResponse` |
356
+ | `uploadFiles(files, options?)` | `POST /{resource}/{id}/files` | Upload files to a record |
357
+ | `deleteFiles(files, options?)` | `POST /{resource}/{id}/delete-files` | Remove files from a record |
358
+
359
+ ```ts
360
+ // Find a single record
361
+ const post = await client.post.find(1);
362
+
363
+ // Query with filters, sorting and pagination. The sort accepts a comma-separated
364
+ // field list ('-createdAt,title') or an object of field directions
365
+ const result = await client.post.query({
366
+ filter: { published: true },
367
+ sort: { author: { lastName: 'asc' }, createdAt: 'desc' },
368
+ page: 1,
369
+ size: 20
370
+ });
371
+
372
+ // Page by cursor instead of by offset, sending back the cursor of the response
373
+ // and skipping the count the first page already returned
374
+ const next = await client.post.query({
375
+ filter: { published: true },
376
+ sort: { author: { lastName: 'asc' }, createdAt: 'desc' },
377
+ size: 20,
378
+ cursor: result.nextCursor,
379
+ totalCount: false
380
+ });
381
+
382
+ // Aggregate. The select holds the operators to apply per numeric or date field
383
+ const stats = await client.post.aggregate({
384
+ select: { counter: { count: true, sum: true }, createdAt: { min: true } },
385
+ dateField: 'createdAt'
386
+ });
387
+
388
+ // Create
389
+ const newPost = await client.post.create({ title: 'Hello', body: '...' });
390
+
391
+ // Update (id must be part of the payload)
392
+ const updated = await client.post.update({ id: 1, title: 'Updated' });
393
+
394
+ // Delete
395
+ const deleted = await client.post.delete(1);
396
+
397
+ // Export as CSV
398
+ const file = await client.post.export({ filter: { published: true } });
399
+ // file.body — ReadableStream, file.filename — string, file.contentType — string
400
+
401
+ // Upload files
402
+ const files = await client.post.uploadFiles({ cover: new File([], 'cover.jpg') });
403
+
404
+ // Delete files
405
+ await client.post.deleteFiles({ fileIds: [42, 43] });
406
+ ```
407
+
408
+ ### `AuthClient`
409
+
410
+ | Method | HTTP | Description |
411
+ |-------------------------------------|------------------------------|---------------------------------------------|
412
+ | `login(request, options?)` | `POST /auth/login` | Authenticate with credentials |
413
+ | `logout(options?)` | `POST /auth/logout` | Invalidate the current session |
414
+ | `refresh(options?)` | `POST /auth/refresh` | Renew access token |
415
+ | `changePassword(request, options?)` | `POST /auth/change-password` | Change the authenticated user's password |
416
+ | `exchangeToken(request, options?)` | `POST /auth/exchange-token` | Exchange a third-party token for app tokens |
417
+ | `me(options?)` | `GET /auth/me` | Get the current user's identity |
418
+
419
+ ```ts
420
+ const session = await client.auth.login({ email: 'user@example.com', password: 'secret' });
421
+ const me = await client.auth.me();
422
+ await client.auth.logout();
423
+ ```
424
+
425
+ ### `AccountClient`
426
+
427
+ | Method | HTTP | Description |
428
+ |----------------------------------------|-------------------------------------------|------------------------------------|
429
+ | `sendVerifyEmail(request, options?)` | `POST /auth/account/send-verify-email` | Send email verification link |
430
+ | `verifyEmail(request, options?)` | `POST /auth/account/verify-email` | Verify email with token |
431
+ | `verifyEmailRedirect(token, options?)` | `GET /auth/account/verify-email-redirect` | Handle redirect-based verification |
432
+ | `sendResetPassword(request, options?)` | `POST /auth/account/send-reset-password` | Send password reset email |
433
+ | `resetPassword(request, options?)` | `POST /auth/account/reset-password` | Reset password with token |
434
+ | `send2FACode(request, options?)` | `POST /auth/account/send-2fa-code` | Send a 2FA code to the user |
435
+ | `verify2FACode(request, options?)` | `POST /auth/account/verify-2fa-code` | Verify a submitted 2FA code |
436
+
437
+ ```ts
438
+ await client.account.sendVerifyEmail({ email: 'user@example.com' });
439
+ await client.account.verifyEmail({ token: 'abc123' });
440
+ await client.account.sendResetPassword({ email: 'user@example.com' });
441
+ await client.account.resetPassword({ token: 'abc123', password: 'newpass' });
442
+ ```
443
+
444
+ ### `HealthClient`
445
+
446
+ | Method | HTTP | Description |
447
+ |-------------------|---------------------|---------------------|
448
+ | `check(options?)` | `GET /health` | Health check status |
449
+ | `ready(options?)` | `GET /health/ready` | Readiness probe |
450
+
451
+ ```ts
452
+ const status = await client.health.check();
453
+ ```
454
+
455
+ ### `FilesClient`
456
+
457
+ | Method | HTTP | Description |
458
+ |-----------------------------|-----------------------|---------------------------|
459
+ | `public(path, options?)` | `GET /files/{path}` | Download a public file |
460
+ | `protected(path, options?)` | `GET /files/p/{path}` | Download a protected file |
461
+
462
+ ```ts
463
+ const file = await client.files.public('images/photo.jpg');
464
+ const fileData = await file.base64(); // or use file.stream to pipe output to file etc.
465
+ ```
466
+
467
+ ### `FileDataResponse`
468
+
469
+ Both `FilesClient` methods and `ResourceClient.export` return a `FileDataResponse` instance instead of a raw response
470
+ body. It exposes file metadata as properties and provides helper methods to consume the content.
471
+
472
+ **Properties:**
473
+
474
+ | Property | Type | Description |
475
+ |-------------|---------------------|-----------------------------------------------------------------|
476
+ | `stream` | `ReadableStream` | Readable stream of the file content |
477
+ | `fileName` | `string` | File name extracted from the `Content-Disposition` header |
478
+ | `type` | `string` | MIME type from the `Content-Type` header |
479
+ | `length` | `number` | File size in bytes from the `Content-Length` header |
480
+ | `range` | `FileContentRange?` | Byte range info present on partial-content (HTTP 206) responses |
481
+ | `maxAge` | `number?` | Cache duration in seconds from `Cache-Control: max-age` |
482
+ | `expiresAt` | `string?` | Expiration timestamp from the `Expires` header |
483
+
484
+ **Methods:**
485
+
486
+ | Method | Returns | Description |
487
+ |--------------------------|------------------------|-----------------------------------------------------------------------|
488
+ | `buffer()` | `Promise<ArrayBuffer>` | Raw binary content as an `ArrayBuffer` |
489
+ | `blob()` | `Promise<Blob>` | Content wrapped in a `Blob` with the correct MIME type |
490
+ | `text(encoding: string)` | `Promise<string>` | Content decoded to string using specified encoding (default is UTF-8) |
491
+ | `base64()` | `Promise<string>` | Content as a Base64 data URL (`data:<type>;base64,...`) |
492
+
493
+ The stream is read lazily on the first call to any consumption method, and the result is cached — later calls return the
494
+ same `ArrayBuffer` without re-reading the stream.
495
+
496
+ The `FileContentRange` type describes a partial-content range:
497
+
498
+ ```ts
499
+ type FileContentRange = {
500
+ start: number; // Starting byte position (inclusive)
501
+ end: number; // Ending byte position (inclusive)
502
+ total: number; // Total size of the complete file in bytes
503
+ };
504
+ ```
505
+
506
+ **Examples:**
507
+
508
+ ```ts
509
+ // Download a public file and convert to a data URL for display
510
+ let file = await client.files.public('images/photo.jpg');
511
+ const dataUrl = await file.base64();
512
+ img.src = dataUrl;
513
+
514
+ // Download a protected file and save as a Blob
515
+ file = await client.files.protected('reports/data.pdf');
516
+ const blob = await file.blob();
517
+ const url = URL.createObjectURL(blob);
518
+
519
+ // Export a resource collection as CSV and read as text
520
+ file = await client.post.export({ filter: { published: true } });
521
+ const csv = await file.text();
522
+
523
+ // Inspect metadata before consuming
524
+ console.log(file.fileName); // e.g. "export.csv"
525
+ console.log(file.type); // e.g. "text/csv"
526
+ console.log(file.length); // e.g. 4096
527
+ ```
528
+
529
+ ---
530
+
531
+ ## Direct requests
532
+
533
+ ### `sendRequest`
534
+
535
+ Sends a typed request and returns the parsed response body. Throws `ClientError` on any non-2xx response.
536
+
537
+ ```ts
538
+ const data = await client.sendRequest('get', '/api/custom-endpoint');
539
+ ```
540
+
541
+ ### `sendRequestRaw`
542
+
543
+ Returns the raw `{ data, error, response }` tuple from `openapi-fetch` without throwing. Useful when the caller needs to
544
+ inspect error details or branch on status codes.
545
+
546
+ ```ts
547
+ const { data, error, response } = await client.sendRequestRaw('post', '/api/custom-endpoint', {
548
+ body: { key: 'value' }
549
+ });
550
+
551
+ if (error) {
552
+ console.error(`Failed with ${response.status}`);
553
+ } else {
554
+ console.log(data);
555
+ }
556
+ ```
557
+
558
+ ### `getClient`
559
+
560
+ Returns the underlying `openapi-fetch` `Client` instance for advanced use cases not covered by the helper methods.
561
+
562
+ ```ts
563
+ const rawClient = client.getClient();
564
+ const { data } = await rawClient.GET('/api/custom-endpoint');
565
+ ```
566
+
567
+ ### Custom request (generated)
568
+
569
+ Routes that do not match any standard module pattern are exposed via `customRequest` in the generated class:
570
+
571
+ ```ts
572
+ // In the generated client:
573
+ const postApiPublishPosts = this.customRequest('post', '/api/publish-posts');
574
+
575
+ // Usage:
576
+ const result = await client.postApiPublishPosts({ body: { ids: [1, 2, 3] } });
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Error handling
582
+
583
+ All module client methods and `sendRequest` throw `ClientError` on non-2xx responses:
584
+
585
+ ```ts
586
+ import { ClientError } from '@appweaver/client';
587
+
588
+ try {
589
+ const post = await client.post.find(999);
590
+ } catch (err) {
591
+ if (err instanceof ClientError) {
592
+ console.error(err.message); // Error message from the API response body
593
+ console.error(err.errorCode); // HTTP status code (or API errorCode field)
594
+ console.error(err.response); // Native Response object
595
+ }
596
+ }
597
+ ```
598
+
599
+ Use `sendRequestRaw` to avoid exceptions and handle errors inline:
600
+
601
+ ```ts
602
+ const { data, error, response } = await client.sendRequestRaw('get', '/api/posts/{id}', {
603
+ params: { path: { id: 999 } }
604
+ });
605
+
606
+ if (response.status === 404) {
607
+ // handle not found
608
+ }
609
+ ```