@tstdl/base 0.93.139 → 0.93.140
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/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.js +2 -2
- package/audit/README.md +267 -0
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/lock/README.md +249 -0
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +1 -1
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/package.json +8 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/reflection/README.md +305 -0
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +300 -0
- package/task-queue/postgres/task-queue.d.ts +2 -1
- package/task-queue/postgres/task-queue.js +32 -2
- package/task-queue/task-context.js +1 -1
- package/task-queue/task-queue.d.ts +17 -0
- package/task-queue/task-queue.js +103 -45
- package/task-queue/tests/complex.test.js +4 -4
- package/task-queue/tests/dependencies.test.js +4 -2
- package/task-queue/tests/queue.test.js +111 -0
- package/task-queue/tests/worker.test.js +21 -13
- package/templates/README.md +287 -0
- package/testing/README.md +157 -0
- package/text/README.md +346 -0
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
package/api/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# @tstdl/base/api
|
|
2
|
+
|
|
3
|
+
A comprehensive, type-safe framework for defining, implementing, and consuming HTTP APIs in TypeScript. It leverages a shared API definition to ensure strict contract adherence between server controllers and client implementations without the need for code generation.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [✨ Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
10
|
+
- [1. Define the API](#1-define-the-api)
|
|
11
|
+
- [2. Implement the Server](#2-implement-the-server)
|
|
12
|
+
- [3. Configure the Gateway](#3-configure-the-gateway)
|
|
13
|
+
- [4. Use the Client](#4-use-the-client)
|
|
14
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
15
|
+
- [Authentication](#authentication)
|
|
16
|
+
- [Streaming & Server-Sent Events](#streaming--server-sent-events)
|
|
17
|
+
- [Error Handling](#error-handling)
|
|
18
|
+
- [Middleware & CORS](#middleware--cors)
|
|
19
|
+
- [📚 API](#-api)
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- **Single Source of Truth**: Define your API contract once and use it for both server implementation and client generation.
|
|
24
|
+
- **End-to-End Type Safety**: Parameters, request bodies, and response types are statically typed on both ends.
|
|
25
|
+
- **Runtime Validation**: Integrated with `@tstdl/base/schema` to automatically validate inputs and outputs.
|
|
26
|
+
- **Zero-Boilerplate Client**: Generate a fully functional, typed client class at runtime using `compileClient`.
|
|
27
|
+
- **Dependency Injection**: Server controllers are fully integrated with the `@tstdl/base/injector` system.
|
|
28
|
+
- **Streaming Support**: First-class support for binary streams (`ReadableStream`) and Server-Sent Events (`DataStream`).
|
|
29
|
+
- **Standardized Error Handling**: Automatic serialization and deserialization of errors (e.g., `NotFoundError`, `ForbiddenError`) across the wire.
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
The workflow revolves around three main components:
|
|
34
|
+
|
|
35
|
+
1. **API Definition**: A plain TypeScript object created via `defineApi`. It describes resources, endpoints, HTTP methods, and schemas for parameters/results.
|
|
36
|
+
2. **API Controller**: A class decorated with `@apiController` that implements the logic for the defined endpoints. It implements the `ApiController<Definition>` interface to ensure strict adherence to the contract.
|
|
37
|
+
3. **API Client**: A class generated via `compileClient(Definition)` that provides methods matching the endpoints. It handles URL construction, serialization, and HTTP requests.
|
|
38
|
+
|
|
39
|
+
## 🚀 Basic Usage
|
|
40
|
+
|
|
41
|
+
### 1. Define the API
|
|
42
|
+
|
|
43
|
+
Create a shared file (e.g., `user.api.ts`) accessible to both your server and client code.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { defineApi } from '@tstdl/base/api';
|
|
47
|
+
import { array, object, string, optional } from '@tstdl/base/schema';
|
|
48
|
+
|
|
49
|
+
// Define your data models (usually classes with schema decorators)
|
|
50
|
+
class User {
|
|
51
|
+
@string() id: string;
|
|
52
|
+
@string() name: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const userApiDefinition = defineApi({
|
|
56
|
+
resource: 'users', // Base path: /users
|
|
57
|
+
endpoints: {
|
|
58
|
+
// GET /users/:id
|
|
59
|
+
get: {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
resource: ':id',
|
|
62
|
+
parameters: object({
|
|
63
|
+
id: string(),
|
|
64
|
+
}),
|
|
65
|
+
result: User,
|
|
66
|
+
},
|
|
67
|
+
// POST /users
|
|
68
|
+
create: {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
parameters: object({
|
|
71
|
+
name: string(),
|
|
72
|
+
role: optional(string()),
|
|
73
|
+
}),
|
|
74
|
+
result: User,
|
|
75
|
+
},
|
|
76
|
+
// GET /users
|
|
77
|
+
list: {
|
|
78
|
+
method: 'GET',
|
|
79
|
+
result: array(User),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export type UserApiDefinition = typeof userApiDefinition;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Implement the Server
|
|
88
|
+
|
|
89
|
+
Implement the controller class. The `ApiController` interface ensures you implement every endpoint defined in the contract.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { apiController, type ApiController, type ApiRequestContext, type ApiServerResult } from '@tstdl/base/api/server';
|
|
93
|
+
import { NotFoundError } from '@tstdl/base/errors';
|
|
94
|
+
import { inject } from '@tstdl/base/injector';
|
|
95
|
+
import { userApiDefinition, type UserApiDefinition } from './user.api.js';
|
|
96
|
+
import { UserService } from './user.service.js'; // Assuming a service exists
|
|
97
|
+
|
|
98
|
+
@apiController(userApiDefinition)
|
|
99
|
+
export class UserApiController implements ApiController<UserApiDefinition> {
|
|
100
|
+
readonly #userService = inject(UserService);
|
|
101
|
+
|
|
102
|
+
async get({ parameters }: ApiRequestContext<UserApiDefinition, 'get'>): Promise<ApiServerResult<UserApiDefinition, 'get'>> {
|
|
103
|
+
const user = await this.#userService.findById(parameters.id);
|
|
104
|
+
|
|
105
|
+
if (!user) {
|
|
106
|
+
throw new NotFoundError(`User ${parameters.id} not found`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return user;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async create({ parameters }: ApiRequestContext<UserApiDefinition, 'create'>): Promise<ApiServerResult<UserApiDefinition, 'create'>> {
|
|
113
|
+
return this.#userService.create(parameters);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async list(): Promise<ApiServerResult<UserApiDefinition, 'list'>> {
|
|
117
|
+
return this.#userService.findAll();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3. Configure the Gateway
|
|
123
|
+
|
|
124
|
+
Register the controller and configure the server gateway in your application bootstrap.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { configureApiServer } from '@tstdl/base/api/server';
|
|
128
|
+
import { configureNodeHttpServer } from '@tstdl/base/http/server/node';
|
|
129
|
+
import { UserApiController } from './user.controller.js';
|
|
130
|
+
|
|
131
|
+
// ... inside your bootstrap function
|
|
132
|
+
configureNodeHttpServer(); // Sets up the underlying HTTP server
|
|
133
|
+
|
|
134
|
+
configureApiServer({
|
|
135
|
+
controllers: [UserApiController],
|
|
136
|
+
gatewayOptions: {
|
|
137
|
+
prefix: 'api', // Global prefix, e.g., /api/users
|
|
138
|
+
cors: {
|
|
139
|
+
accessControlAllowOrigin: '*', // Configure CORS as needed
|
|
140
|
+
},
|
|
141
|
+
csrf: {
|
|
142
|
+
trustedHosts: ['myapp.com'], // Enable CSRF protection
|
|
143
|
+
},
|
|
144
|
+
supressedErrors: [UnauthorizedError], // Don't log 401s as full errors
|
|
145
|
+
defaultMaxBytes: 10 * 1024 * 1024, // 10MB default limit
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 4. Use the Client
|
|
151
|
+
|
|
152
|
+
On the client side, compile the client and use it.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { compileClient } from '@tstdl/base/api/client';
|
|
156
|
+
import { configureUndiciHttpClientAdapter } from '@tstdl/base/http/client/adapters/undici.adapter'; // or fetch adapter for browser
|
|
157
|
+
import { configureHttpClient, HttpClient } from '@tstdl/base/http/client';
|
|
158
|
+
import { inject } from '@tstdl/base/injector';
|
|
159
|
+
import { userApiDefinition } from './shared/user.api.js';
|
|
160
|
+
|
|
161
|
+
// 1. Configure HTTP Client (once per app)
|
|
162
|
+
configureUndiciHttpClientAdapter();
|
|
163
|
+
configureHttpClient({ baseUrl: 'http://localhost:8080/api' });
|
|
164
|
+
|
|
165
|
+
// 2. Compile the specific API client
|
|
166
|
+
const UserApiClient = compileClient(userApiDefinition);
|
|
167
|
+
|
|
168
|
+
// 3. Use it
|
|
169
|
+
async function main() {
|
|
170
|
+
// You can inject it if using the DI system, or instantiate with an HttpClient
|
|
171
|
+
const client = inject(UserApiClient);
|
|
172
|
+
|
|
173
|
+
const newUser = await client.create({ name: 'Alice' });
|
|
174
|
+
console.log('Created:', newUser);
|
|
175
|
+
|
|
176
|
+
const user = await client.get({ id: newUser.id });
|
|
177
|
+
console.log('Fetched:', user);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🔧 Advanced Topics
|
|
182
|
+
|
|
183
|
+
### Authentication
|
|
184
|
+
|
|
185
|
+
To handle authentication, implement an `ApiRequestTokenProvider`. This provider extracts token information (like a JWT) from the request.
|
|
186
|
+
|
|
187
|
+
1. **Define the Provider:**
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { ApiRequestTokenProvider, type ApiRequestData } from '@tstdl/base/api/server';
|
|
191
|
+
import { Singleton } from '@tstdl/base/injector';
|
|
192
|
+
import { UnauthorizedError } from '@tstdl/base/errors';
|
|
193
|
+
|
|
194
|
+
export type MyToken = { userId: string; role: string };
|
|
195
|
+
|
|
196
|
+
@Singleton()
|
|
197
|
+
export class MyTokenProvider extends ApiRequestTokenProvider {
|
|
198
|
+
override async tryGetToken<T>(data: ApiRequestData): Promise<T | null> {
|
|
199
|
+
const authHeader = data.request.headers.authorization;
|
|
200
|
+
if (!authHeader) return null;
|
|
201
|
+
|
|
202
|
+
// ... validate and decode token ...
|
|
203
|
+
return { userId: '123', role: 'admin' } as T;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
2. **Register the Provider:**
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
configureApiServer({
|
|
212
|
+
// ...
|
|
213
|
+
requestTokenProvider: MyTokenProvider,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
3. **Use in Controller:**
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// In API definition
|
|
221
|
+
endpoints: {
|
|
222
|
+
secureAction: { /* ... */, credentials: true }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// In Controller
|
|
226
|
+
async secureAction({ getToken, getAuditor }: ApiRequestContext<...>) {
|
|
227
|
+
const token = await getToken<MyToken>(); // Throws if no token
|
|
228
|
+
console.log(token.userId);
|
|
229
|
+
|
|
230
|
+
const auditor = await getAuditor();
|
|
231
|
+
auditor.log('Action performed', { userId: token.userId });
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Streaming & Server-Sent Events
|
|
236
|
+
|
|
237
|
+
The framework supports streaming binary data and Server-Sent Events (SSE).
|
|
238
|
+
|
|
239
|
+
**Binary Stream:**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Definition
|
|
243
|
+
download: {
|
|
244
|
+
method: 'GET',
|
|
245
|
+
result: ReadableStream, // or Uint8Array, or Blob
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Controller
|
|
249
|
+
async download(): Promise<ApiServerResult<...>> {
|
|
250
|
+
const fileStream = createReadStream('file.txt');
|
|
251
|
+
// Convert node stream to web ReadableStream if necessary, or return Uint8Array
|
|
252
|
+
return readableStreamFromNode(fileStream);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Server-Sent Events (DataStream):**
|
|
257
|
+
|
|
258
|
+
Use `DataStream` to stream typed objects to the client.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { DataStream } from '@tstdl/base/sse';
|
|
262
|
+
|
|
263
|
+
// Definition
|
|
264
|
+
events: {
|
|
265
|
+
method: 'GET',
|
|
266
|
+
result: DataStream(MyEventType), // MyEventType is a Schema/Class
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Controller
|
|
270
|
+
async *events(): ApiServerResult<...> {
|
|
271
|
+
yield { type: 'ping', timestamp: Date.now() };
|
|
272
|
+
await timeout(1000);
|
|
273
|
+
yield { type: 'pong', timestamp: Date.now() };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Client
|
|
277
|
+
const stream$ = await client.events(); // Returns Observable<MyEventType>
|
|
278
|
+
stream$.subscribe(event => console.log(event));
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Manual Response Control
|
|
282
|
+
|
|
283
|
+
Sometimes you need full control over the HTTP response (e.g., setting custom headers, status codes, or cookies). You can return an `HttpServerResponse` directly from your controller.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { HttpServerResponse } from '@tstdl/base/api/server';
|
|
287
|
+
|
|
288
|
+
async customResponse(): Promise<ApiServerResult<...>> {
|
|
289
|
+
return new HttpServerResponse()
|
|
290
|
+
.setStatusCode(201)
|
|
291
|
+
.setHeader('X-Custom-Header', 'Value')
|
|
292
|
+
.setJsonBody({ message: 'Created' });
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Advanced Endpoint Configuration
|
|
297
|
+
|
|
298
|
+
Endpoints can be further customized in the `ApiDefinition`.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
export const myApi = defineApi({
|
|
302
|
+
resource: 'files',
|
|
303
|
+
endpoints: {
|
|
304
|
+
upload: {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
body: ReadableStream, // Expect a binary stream
|
|
307
|
+
maxBytes: 100 * 1024 * 1024, // 100MB limit for this endpoint
|
|
308
|
+
credentials: true, // Send/accept cookies
|
|
309
|
+
cors: {
|
|
310
|
+
accessControlAllowOrigin: 'https://trusted.com',
|
|
311
|
+
},
|
|
312
|
+
data: { auditCategory: 'file-upload' }, // Custom metadata
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Error Handling
|
|
319
|
+
|
|
320
|
+
Standard errors thrown in controllers are automatically mapped to HTTP status codes.
|
|
321
|
+
|
|
322
|
+
| Error Class | Status Code |
|
|
323
|
+
| :---------------------- | :---------- |
|
|
324
|
+
| `BadRequestError` | 400 |
|
|
325
|
+
| `UnauthorizedError` | 401 |
|
|
326
|
+
| `ForbiddenError` | 403 |
|
|
327
|
+
| `NotFoundError` | 404 |
|
|
328
|
+
| `MethodNotAllowedError` | 405 |
|
|
329
|
+
| `NotImplementedError` | 501 |
|
|
330
|
+
|
|
331
|
+
You can register custom error handlers using `registerErrorHandler`.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { registerErrorHandler } from '@tstdl/base/api';
|
|
335
|
+
import { CustomError } from '@tstdl/base/errors';
|
|
336
|
+
|
|
337
|
+
class MyCustomError extends CustomError {
|
|
338
|
+
static errorName = 'MyCustomError';
|
|
339
|
+
constructor(public code: number) {
|
|
340
|
+
super('Custom error');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Register mapping
|
|
345
|
+
registerErrorHandler(
|
|
346
|
+
MyCustomError,
|
|
347
|
+
418, // Status code
|
|
348
|
+
(error) => ({ code: error.code }), // Serializer
|
|
349
|
+
(data, responseError) => new MyCustomError(data.code), // Deserializer
|
|
350
|
+
);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Middleware & CORS
|
|
354
|
+
|
|
355
|
+
The `ApiGateway` uses a middleware pipeline. You can configure default middlewares like CORS via `gatewayOptions`.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
configureApiServer({
|
|
359
|
+
gatewayOptions: {
|
|
360
|
+
cors: {
|
|
361
|
+
accessControlAllowOrigin: 'https://myapp.com',
|
|
362
|
+
accessControlAllowCredentials: true,
|
|
363
|
+
},
|
|
364
|
+
// Add custom middlewares
|
|
365
|
+
middlewares: [
|
|
366
|
+
async (context, next) => {
|
|
367
|
+
console.log(`Request: ${context.request.url}`);
|
|
368
|
+
await next();
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## 📚 API
|
|
376
|
+
|
|
377
|
+
### Definition & Client
|
|
378
|
+
|
|
379
|
+
| Symbol | Description |
|
|
380
|
+
| :------------------------------------ | :------------------------------------------------ |
|
|
381
|
+
| `defineApi(definition)` | Helper to create a typed API definition object. |
|
|
382
|
+
| `compileClient(definition, options?)` | Generates a class constructor for the API client. |
|
|
383
|
+
| `ApiClient<T>` | Type representing the generated client class. |
|
|
384
|
+
|
|
385
|
+
### Server
|
|
386
|
+
|
|
387
|
+
| Symbol | Description |
|
|
388
|
+
| :---------------------------- | :----------------------------------------------------------------------------------------- |
|
|
389
|
+
| `@apiController(definition)` | Decorator to register a class as an implementation for an API definition. |
|
|
390
|
+
| `configureApiServer(options)` | Bootstraps the API server module, registering controllers and gateway options. |
|
|
391
|
+
| `ApiGateway` | The main entry point that handles HTTP requests and routes them to controllers. |
|
|
392
|
+
| `ApiRequestTokenProvider` | Abstract class for implementing token extraction logic. |
|
|
393
|
+
| `ApiRequestContext` | Passed to controller methods; contains `parameters`, `body`, `request`, and `getToken`. |
|
|
394
|
+
| `HttpServerResponse` | Class representing the HTTP response; can be returned from controllers for manual control. |
|
|
395
|
+
|
|
396
|
+
### Types
|
|
397
|
+
|
|
398
|
+
| Symbol | Description |
|
|
399
|
+
| :---------------------- | :---------------------------------------------------------------------------------------------- |
|
|
400
|
+
| `ApiDefinition` | The structure of the API contract (resource, endpoints, optional prefix). |
|
|
401
|
+
| `ApiEndpointDefinition` | Configuration for a single endpoint (method, resource, parameters, body, result, maxBytes, etc). |
|
|
402
|
+
| `ApiServerResult<T, K>` | The expected return type for a server controller method (Typed result or `HttpServerResponse`). |
|
|
403
|
+
| `ApiClientResult<T, K>` | The expected return type for a client method. |
|
package/api/client/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { from, takeUntil } from 'rxjs';
|
|
2
|
+
import { CancellationSignal } from '../../cancellation/token.js';
|
|
2
3
|
import { HttpClient, HttpClientRequest } from '../../http/client/index.js';
|
|
3
4
|
import { bustCache, normalizeSingleHttpValue } from '../../http/index.js';
|
|
4
5
|
import { inject } from '../../injector/inject.js';
|
|
@@ -109,7 +110,7 @@ export function compileClient(definition, options = defaultOptions) {
|
|
|
109
110
|
body: getRequestBody(requestBody),
|
|
110
111
|
credentials: (endpoint.credentials == true) ? 'include' : undefined,
|
|
111
112
|
context: { ...context, ...requestOptions?.context },
|
|
112
|
-
|
|
113
|
+
cancellationSignal: requestOptions?.cancellationSignal,
|
|
113
114
|
timeout: requestOptions?.timeout,
|
|
114
115
|
headers: requestOptions?.headers,
|
|
115
116
|
authorization: requestOptions?.authorization,
|
|
@@ -172,20 +173,13 @@ function getServerSentEvents(baseUrl, resource, endpoint, parameters, options) {
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
const sse = new ServerSentEvents(url.toString(), { withCredentials: endpoint.credentials });
|
|
175
|
-
if (isDefined(options?.
|
|
176
|
-
|
|
176
|
+
if (isDefined(options?.cancellationSignal)) {
|
|
177
|
+
from(CancellationSignal.from(options.cancellationSignal))
|
|
178
|
+
.pipe(takeUntil(sse.close$))
|
|
179
|
+
.subscribe(() => sse.close());
|
|
177
180
|
}
|
|
178
181
|
return sse;
|
|
179
182
|
}
|
|
180
|
-
function getCancellationSignal(signal) {
|
|
181
|
-
if (isUndefined(signal)) {
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
if (signal instanceof AbortSignal) {
|
|
185
|
-
return CancellationToken.from(signal).signal;
|
|
186
|
-
}
|
|
187
|
-
return signal;
|
|
188
|
-
}
|
|
189
183
|
export function getHttpClientOfApiClient(apiClient) {
|
|
190
184
|
return apiClient[httpClientSymbol];
|
|
191
185
|
}
|
|
@@ -48,36 +48,36 @@ describe('ApiClient', () => {
|
|
|
48
48
|
const abortController = new AbortController();
|
|
49
49
|
const cancellationToken = new CancellationToken();
|
|
50
50
|
// 1. No params
|
|
51
|
-
await client.noParams({
|
|
51
|
+
await client.noParams({ cancellationSignal: abortController.signal });
|
|
52
52
|
expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
53
53
|
url: expect.stringContaining('no-params'),
|
|
54
54
|
}));
|
|
55
55
|
let lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
|
|
56
|
-
expect(lastRequest.
|
|
56
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(false);
|
|
57
57
|
abortController.abort();
|
|
58
|
-
expect(lastRequest.
|
|
58
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(true);
|
|
59
59
|
// 2. Only params
|
|
60
|
-
await client.onlyParams({ id: '123' }, {
|
|
60
|
+
await client.onlyParams({ id: '123' }, { cancellationSignal: cancellationToken.abortSignal });
|
|
61
61
|
expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
62
62
|
url: expect.stringContaining('only-params'),
|
|
63
63
|
parameters: { id: '123' },
|
|
64
64
|
}));
|
|
65
65
|
lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
|
|
66
|
-
expect(lastRequest.
|
|
66
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(false);
|
|
67
67
|
cancellationToken.set();
|
|
68
|
-
expect(lastRequest.
|
|
68
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(true);
|
|
69
69
|
// 3. Params and body
|
|
70
70
|
const abortController3 = new AbortController();
|
|
71
|
-
await client.paramsAndBody({ id: '456' }, { data: 'val' }, {
|
|
71
|
+
await client.paramsAndBody({ id: '456' }, { data: 'val' }, { cancellationSignal: abortController3.signal });
|
|
72
72
|
expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
73
73
|
url: expect.stringContaining('params-and-body'),
|
|
74
74
|
parameters: { id: '456' },
|
|
75
75
|
body: { json: { data: 'val' } },
|
|
76
76
|
}));
|
|
77
77
|
lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
|
|
78
|
-
expect(lastRequest.
|
|
78
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(false);
|
|
79
79
|
abortController3.abort();
|
|
80
|
-
expect(lastRequest.
|
|
80
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(true);
|
|
81
81
|
// 4. Omitted requestOptions
|
|
82
82
|
await client.onlyParams({ id: '789' });
|
|
83
83
|
expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
@@ -85,7 +85,7 @@ describe('ApiClient', () => {
|
|
|
85
85
|
parameters: { id: '789' },
|
|
86
86
|
}));
|
|
87
87
|
lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
|
|
88
|
-
expect(lastRequest.
|
|
88
|
+
expect(lastRequest.cancellationSignal.isSet).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
it('should handle different HTTP methods', async () => {
|
|
91
91
|
const apiDefinition = defineApi({
|
|
@@ -10,7 +10,7 @@ export function serializeSchemaError(error) {
|
|
|
10
10
|
export function serializeInnerSchemaError(error) {
|
|
11
11
|
return {
|
|
12
12
|
message: error.message,
|
|
13
|
-
...serializeSchemaError(error)
|
|
13
|
+
...serializeSchemaError(error),
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
export function deserializeSchemaError(message, data) {
|
package/api/response.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export type ResponseError = {
|
|
|
25
25
|
export declare function registerErrorHandler<T extends CustomError, TData extends ErrorHandlerData>(constructor: CustomErrorStatic<T>, statusCode: number, serializer: ErrorSerializer<T, TData>, deserializer: ErrorDeserializer<T, TData>): void;
|
|
26
26
|
export declare function hasErrorHandler(typeOrResponseOrName: CustomErrorStatic | ErrorResponse | string): boolean;
|
|
27
27
|
export declare function getErrorStatusCode(error: CustomError, defaultStatusCode?: number): number;
|
|
28
|
-
export declare function createErrorResponse(error: Error, details?:
|
|
29
|
-
export declare function createErrorResponse(name: string, message: string, details?:
|
|
28
|
+
export declare function createErrorResponse(error: Error, details?: unknown): ErrorResponse;
|
|
29
|
+
export declare function createErrorResponse(name: string, message: string, details?: unknown): ErrorResponse;
|
|
30
30
|
export declare function logAndGetErrorResponse(logger: Logger, supressedErrors: Set<Type<Error>>, error: unknown): {
|
|
31
31
|
statusCode: number;
|
|
32
32
|
errorResponse: ErrorResponse;
|
package/api/response.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SecretRequirementsError } from '../authentication/errors/secret-requirements.error.js';
|
|
2
|
-
import { SchemaError } from '../schema/schema.error.js';
|
|
3
2
|
import { formatError } from '../errors/index.js';
|
|
3
|
+
import { SchemaError } from '../schema/schema.error.js';
|
|
4
4
|
import { ApiError, BadRequestError, ForbiddenError, InvalidCredentialsError, InvalidTokenError, MaxBytesExceededError, MethodNotAllowedError, NotFoundError, NotImplementedError, NotSupportedError, UnauthorizedError, UnsupportedMediaTypeError } from '../errors/index.js';
|
|
5
5
|
import { assertString, isDefined, isFunction, isObject, isString } from '../utils/type-guards.js';
|
|
6
6
|
import { deserializeSchemaError, serializeSchemaError } from './default-error-handlers.js';
|
|
@@ -25,41 +25,28 @@ export function hasErrorHandler(typeOrResponseOrName) {
|
|
|
25
25
|
export function getErrorStatusCode(error, defaultStatusCode = 500) {
|
|
26
26
|
return errorHandlers.get(error.name)?.statusCode ?? defaultStatusCode;
|
|
27
27
|
}
|
|
28
|
-
export function createErrorResponse(
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
error: {
|
|
36
|
-
name: errorOrName.name,
|
|
37
|
-
message: errorOrName.message,
|
|
38
|
-
details,
|
|
39
|
-
data,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
response = {
|
|
45
|
-
error: {
|
|
46
|
-
name: errorOrName.name,
|
|
47
|
-
message: errorOrName.message,
|
|
48
|
-
details,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
28
|
+
export function createErrorResponse(...args) {
|
|
29
|
+
let name;
|
|
30
|
+
let message;
|
|
31
|
+
let details;
|
|
32
|
+
let data;
|
|
33
|
+
if (isString(args[0])) {
|
|
34
|
+
[name, message, details] = args;
|
|
52
35
|
}
|
|
53
36
|
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
details,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
37
|
+
let error;
|
|
38
|
+
[error, details] = args;
|
|
39
|
+
({ name, message } = error);
|
|
40
|
+
data = errorHandlers.get(name)?.serializer(error);
|
|
61
41
|
}
|
|
62
|
-
return
|
|
42
|
+
return {
|
|
43
|
+
error: {
|
|
44
|
+
name,
|
|
45
|
+
message,
|
|
46
|
+
details,
|
|
47
|
+
...(isDefined(data) ? { data } : undefined),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
63
50
|
}
|
|
64
51
|
export function logAndGetErrorResponse(logger, supressedErrors, error) {
|
|
65
52
|
if (error instanceof Error) {
|
|
@@ -100,6 +87,7 @@ export function parseErrorResponse(response, fallbackToGenericApiError = true) {
|
|
|
100
87
|
export function isErrorResponse(response) {
|
|
101
88
|
return isObject(response) && isDefined(response.error);
|
|
102
89
|
}
|
|
90
|
+
// biome-ignore-start lint/style/noMagicNumbers: http status codes
|
|
103
91
|
registerErrorHandler(ApiError, 400, ({ response }) => response, (response) => new ApiError(response));
|
|
104
92
|
registerErrorHandler(BadRequestError, 400, () => undefined, (_, error) => new BadRequestError(error.message));
|
|
105
93
|
registerErrorHandler(ForbiddenError, 403, () => undefined, (_, error) => new ForbiddenError(error.message));
|
|
@@ -114,3 +102,4 @@ registerErrorHandler(SchemaError, 400, serializeSchemaError, (data, error) => de
|
|
|
114
102
|
registerErrorHandler(SecretRequirementsError, 403, () => undefined, (_, error) => new SecretRequirementsError(error.message));
|
|
115
103
|
registerErrorHandler(UnauthorizedError, 401, () => undefined, (_, error) => new UnauthorizedError(error.message));
|
|
116
104
|
registerErrorHandler(UnsupportedMediaTypeError, 415, () => undefined, (_, error) => new UnsupportedMediaTypeError(error.message));
|
|
105
|
+
// biome-ignore-end lint/style/noMagicNumbers: http status codes
|
|
@@ -6,6 +6,6 @@ export declare const apiControllerDefinition: unique symbol;
|
|
|
6
6
|
export declare function getApiControllerDefinition(controller: Type | ApiController): ApiDefinition;
|
|
7
7
|
export declare function isApiController(controller: Type): boolean;
|
|
8
8
|
export declare function ensureApiController(controller: Type): void;
|
|
9
|
-
export declare function apiController<T = Type<ApiController>, A =
|
|
9
|
+
export declare function apiController<T = Type<ApiController>, A = unknown>(definition: ApiDefinition | ApiDefinitionProvider, injectableOptions?: InjectableOptionsWithoutLifecycle<T, A>): ClassDecorator;
|
|
10
10
|
export declare function implementApi<T extends ApiDefinition>(definition: T, implementation: ApiController<T>): Constructor<ApiController<T>>;
|
|
11
11
|
export {};
|
|
@@ -21,9 +21,9 @@ export function ensureApiController(controller) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
export function apiController(definition, injectableOptions = {}) {
|
|
24
|
-
function apiControllerDecorator(
|
|
25
|
-
registeredApiControllers.set(
|
|
26
|
-
Singleton(injectableOptions)(
|
|
24
|
+
function apiControllerDecorator(type) {
|
|
25
|
+
registeredApiControllers.set(type, definition);
|
|
26
|
+
Singleton(injectableOptions)(type);
|
|
27
27
|
}
|
|
28
28
|
return apiControllerDecorator;
|
|
29
29
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: ok */
|
|
1
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -5,7 +5,8 @@ export async function allowedMethodsMiddleware({ endpoint, api, request, respons
|
|
|
5
5
|
if (isUndefined(endpoint)) {
|
|
6
6
|
throw new MethodNotAllowedError(`Method ${request.method} for resource ${api.resource} not available.`);
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
await next();
|
|
9
|
+
return;
|
|
9
10
|
}
|
|
10
11
|
const allowMethods = [...api.endpoints.keys()].join(', ');
|
|
11
12
|
response.statusCode = 204;
|