@tstdl/base 0.93.139 → 0.93.141

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.
Files changed (218) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.d.ts +1 -1
  28. package/application/application.js +3 -3
  29. package/application/providers.d.ts +20 -2
  30. package/application/providers.js +34 -7
  31. package/audit/README.md +267 -0
  32. package/audit/module.d.ts +5 -0
  33. package/audit/module.js +9 -1
  34. package/authentication/README.md +288 -0
  35. package/authentication/client/authentication.service.d.ts +12 -11
  36. package/authentication/client/authentication.service.js +21 -21
  37. package/authentication/client/http-client.middleware.js +2 -2
  38. package/authentication/server/module.d.ts +5 -0
  39. package/authentication/server/module.js +9 -1
  40. package/authentication/tests/authentication.api-controller.test.js +1 -1
  41. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  42. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  43. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  44. package/authentication/tests/authentication.client-service.test.js +1 -1
  45. package/browser/README.md +401 -0
  46. package/cancellation/README.md +156 -0
  47. package/cancellation/tests/coverage.test.d.ts +1 -0
  48. package/cancellation/tests/coverage.test.js +49 -0
  49. package/cancellation/tests/leak.test.js +24 -29
  50. package/cancellation/tests/token.test.d.ts +1 -0
  51. package/cancellation/tests/token.test.js +136 -0
  52. package/cancellation/token.d.ts +53 -177
  53. package/cancellation/token.js +132 -208
  54. package/circuit-breaker/postgres/module.d.ts +1 -0
  55. package/circuit-breaker/postgres/module.js +5 -1
  56. package/context/README.md +174 -0
  57. package/cookie/README.md +161 -0
  58. package/css/README.md +157 -0
  59. package/data-structures/README.md +320 -0
  60. package/decorators/README.md +140 -0
  61. package/distributed-loop/README.md +231 -0
  62. package/distributed-loop/distributed-loop.js +1 -1
  63. package/document-management/README.md +403 -0
  64. package/document-management/server/configure.js +5 -1
  65. package/document-management/server/module.d.ts +1 -1
  66. package/document-management/server/module.js +1 -1
  67. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  68. package/document-management/server/services/document-management.service.js +9 -7
  69. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  70. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  71. package/document-management/tests/document-management-core.test.js +2 -7
  72. package/document-management/tests/document-management.api.test.js +6 -7
  73. package/document-management/tests/document-statistics.service.test.js +11 -12
  74. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  75. package/document-management/tests/document.service.test.js +3 -3
  76. package/document-management/tests/enum-helpers.test.js +2 -3
  77. package/dom/README.md +213 -0
  78. package/enumerable/README.md +259 -0
  79. package/enumeration/README.md +121 -0
  80. package/errors/README.md +267 -0
  81. package/examples/document-management/main.d.ts +1 -0
  82. package/examples/document-management/main.js +14 -11
  83. package/file/README.md +191 -0
  84. package/formats/README.md +210 -0
  85. package/function/README.md +144 -0
  86. package/http/README.md +318 -0
  87. package/http/client/adapters/undici.adapter.js +1 -1
  88. package/http/client/http-client-request.d.ts +6 -5
  89. package/http/client/http-client-request.js +8 -9
  90. package/http/server/node/node-http-server.js +1 -2
  91. package/image-service/README.md +137 -0
  92. package/injector/README.md +491 -0
  93. package/intl/README.md +113 -0
  94. package/json-path/README.md +182 -0
  95. package/jsx/README.md +154 -0
  96. package/key-value-store/README.md +191 -0
  97. package/key-value-store/postgres/module.d.ts +1 -0
  98. package/key-value-store/postgres/module.js +5 -1
  99. package/lock/README.md +249 -0
  100. package/lock/postgres/module.d.ts +1 -0
  101. package/lock/postgres/module.js +5 -1
  102. package/lock/web/web-lock.js +119 -47
  103. package/logger/README.md +287 -0
  104. package/mail/README.md +256 -0
  105. package/mail/module.d.ts +5 -1
  106. package/mail/module.js +11 -6
  107. package/memory/README.md +144 -0
  108. package/message-bus/README.md +244 -0
  109. package/message-bus/message-bus-base.js +1 -1
  110. package/module/README.md +182 -0
  111. package/module/module.d.ts +1 -1
  112. package/module/module.js +77 -17
  113. package/module/modules/web-server.module.js +3 -4
  114. package/notification/server/module.d.ts +1 -0
  115. package/notification/server/module.js +5 -1
  116. package/notification/tests/notification-flow.test.js +2 -2
  117. package/notification/tests/notification-type.service.test.js +24 -15
  118. package/object-storage/README.md +300 -0
  119. package/openid-connect/README.md +274 -0
  120. package/orm/README.md +423 -0
  121. package/orm/decorators.d.ts +5 -1
  122. package/orm/decorators.js +1 -1
  123. package/orm/server/drizzle/schema-converter.js +17 -30
  124. package/orm/server/encryption.d.ts +0 -1
  125. package/orm/server/encryption.js +1 -4
  126. package/orm/server/index.d.ts +1 -6
  127. package/orm/server/index.js +1 -6
  128. package/orm/server/migration.d.ts +19 -0
  129. package/orm/server/migration.js +72 -0
  130. package/orm/server/repository.d.ts +1 -1
  131. package/orm/server/transaction.d.ts +5 -10
  132. package/orm/server/transaction.js +22 -26
  133. package/orm/server/transactional.js +3 -3
  134. package/orm/tests/database-migration.test.d.ts +1 -0
  135. package/orm/tests/database-migration.test.js +82 -0
  136. package/orm/tests/encryption.test.js +3 -4
  137. package/orm/utils.d.ts +17 -2
  138. package/orm/utils.js +49 -1
  139. package/package.json +9 -6
  140. package/password/README.md +164 -0
  141. package/pdf/README.md +246 -0
  142. package/polyfills.js +1 -0
  143. package/pool/README.md +198 -0
  144. package/process/README.md +237 -0
  145. package/promise/README.md +252 -0
  146. package/promise/cancelable-promise.js +1 -1
  147. package/random/README.md +193 -0
  148. package/rate-limit/postgres/module.d.ts +1 -0
  149. package/rate-limit/postgres/module.js +5 -1
  150. package/reflection/README.md +305 -0
  151. package/reflection/decorator-data.js +11 -12
  152. package/rpc/README.md +386 -0
  153. package/rxjs-utils/README.md +262 -0
  154. package/schema/README.md +342 -0
  155. package/serializer/README.md +342 -0
  156. package/signals/implementation/README.md +134 -0
  157. package/sse/README.md +278 -0
  158. package/task-queue/README.md +293 -0
  159. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  160. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  161. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  162. package/task-queue/postgres/module.d.ts +1 -0
  163. package/task-queue/postgres/module.js +5 -1
  164. package/task-queue/postgres/schemas.d.ts +9 -6
  165. package/task-queue/postgres/schemas.js +4 -3
  166. package/task-queue/postgres/task-queue.d.ts +4 -13
  167. package/task-queue/postgres/task-queue.js +462 -355
  168. package/task-queue/postgres/task.model.d.ts +12 -5
  169. package/task-queue/postgres/task.model.js +51 -25
  170. package/task-queue/task-context.d.ts +2 -2
  171. package/task-queue/task-context.js +8 -8
  172. package/task-queue/task-queue.d.ts +53 -19
  173. package/task-queue/task-queue.js +121 -55
  174. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  175. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  176. package/task-queue/tests/complex.test.js +45 -229
  177. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  178. package/task-queue/tests/coverage-branch.test.js +407 -0
  179. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  180. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  181. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  182. package/task-queue/tests/dag-dependencies.test.js +41 -0
  183. package/task-queue/tests/dependencies.test.js +28 -26
  184. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  185. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  186. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  187. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  188. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  189. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  190. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  191. package/task-queue/tests/queue.test.js +128 -8
  192. package/task-queue/tests/worker.test.js +39 -16
  193. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  194. package/task-queue/tests/zombie-parent.test.js +45 -0
  195. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  196. package/task-queue/tests/zombie-recovery.test.js +51 -0
  197. package/templates/README.md +287 -0
  198. package/test5.js +5 -5
  199. package/testing/README.md +157 -0
  200. package/testing/integration-setup.d.ts +4 -4
  201. package/testing/integration-setup.js +54 -29
  202. package/text/README.md +346 -0
  203. package/text/localization.service.js +2 -2
  204. package/threading/README.md +238 -0
  205. package/types/README.md +311 -0
  206. package/utils/README.md +322 -0
  207. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  208. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  209. package/utils/async-iterable-helpers/take-until.js +4 -4
  210. package/utils/backoff.js +89 -30
  211. package/utils/file-reader.js +1 -2
  212. package/utils/retry-with-backoff.js +1 -1
  213. package/utils/timer.d.ts +1 -1
  214. package/utils/timer.js +5 -7
  215. package/utils/timing.d.ts +1 -1
  216. package/utils/timing.js +2 -4
  217. package/utils/z-base32.d.ts +1 -0
  218. 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. |
@@ -1,4 +1,5 @@
1
- import { CancellationToken } from '../../cancellation/index.js';
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
- abortSignal: getCancellationSignal(requestOptions?.abortSignal),
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?.abortSignal)) {
176
- options.abortSignal.addEventListener('abort', () => sse.close(), { once: true });
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({ abortSignal: abortController.signal });
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.abortSignal.isSet).toBe(false);
56
+ expect(lastRequest.cancellationSignal.isSet).toBe(false);
57
57
  abortController.abort();
58
- expect(lastRequest.abortSignal.isSet).toBe(true);
58
+ expect(lastRequest.cancellationSignal.isSet).toBe(true);
59
59
  // 2. Only params
60
- await client.onlyParams({ id: '123' }, { abortSignal: cancellationToken.abortSignal });
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.abortSignal.isSet).toBe(false);
66
+ expect(lastRequest.cancellationSignal.isSet).toBe(false);
67
67
  cancellationToken.set();
68
- expect(lastRequest.abortSignal.isSet).toBe(true);
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' }, { abortSignal: abortController3.signal });
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.abortSignal.isSet).toBe(false);
78
+ expect(lastRequest.cancellationSignal.isSet).toBe(false);
79
79
  abortController3.abort();
80
- expect(lastRequest.abortSignal.isSet).toBe(true);
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.abortSignal.isSet).toBe(false);
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?: any): ErrorResponse;
29
- export declare function createErrorResponse(name: string, message: string, details?: any): ErrorResponse;
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(errorOrName, message = '', details) {
29
- let response;
30
- if (errorOrName instanceof Error) {
31
- const handler = errorHandlers.get(errorOrName.name);
32
- if (isDefined(handler)) {
33
- const data = handler.serializer(errorOrName);
34
- response = {
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
- response = {
55
- error: {
56
- name: errorOrName,
57
- message,
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 response;
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 = any>(definition: ApiDefinition | ApiDefinitionProvider, injectableOptions?: InjectableOptionsWithoutLifecycle<T, A>): ClassDecorator;
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(constructor) {
25
- registeredApiControllers.set(constructor, definition);
26
- Singleton(injectableOptions)(constructor);
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
  import type { Token } from '../../authentication/index.js';
2
3
  import type { ApiRequestData } from '../types.js';
3
4
  export declare abstract class ApiRequestTokenProvider {
@@ -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
- return next();
8
+ await next();
9
+ return;
9
10
  }
10
11
  const allowMethods = [...api.endpoints.keys()].join(', ');
11
12
  response.statusCode = 204;
@@ -25,6 +25,7 @@ function matchBodyType(bodyType) {
25
25
  return 'application/octet-stream';
26
26
  case 'events':
27
27
  return 'text/event-stream';
28
+ case 'none':
29
+ return undefined;
28
30
  }
29
- throw new Error(`Unsupported body type: ${bodyType}`);
30
31
  }