@appweaver/create-weaver-app 1.3.1 → 1.4.1
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.
- package/package.json +1 -1
- package/skill/GUIDELINES.md +18 -4
- package/skill/SKILL.md +753 -707
- package/skill/references/cli.md +4 -0
- package/skill/references/client.md +609 -599
- package/skill/references/configuration.md +7 -6
- package/skill/references/resources.md +237 -43
- package/skill/references/security.md +29 -5
- package/skill/references/storage.md +38 -4
- package/templates/default/appweaver.test.json.tpl +2 -1
- package/templates/default/src/resources/user/service.ts.tpl +2 -1
|
@@ -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,
|
|
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
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
// Delete
|
|
395
|
-
await client.post.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
| `
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
|
477
|
-
|
|
478
|
-
| `
|
|
479
|
-
| `
|
|
480
|
-
| `
|
|
481
|
-
| `
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
```ts
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
510
|
-
file = await client.
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
```
|