@celerity-sdk/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @celerity-sdk/core
2
2
 
3
- Core SDK for building Celerity applications - decorators, dependency injection, layers, guards, handler adapters, and the application factory.
3
+ Core SDK for building Celerity applications decorators, dependency injection, layers, guards, handler pipelines, and the application factory.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,15 +8,16 @@ Core SDK for building Celerity applications - decorators, dependency injection,
8
8
  pnpm add @celerity-sdk/core
9
9
  ```
10
10
 
11
- ## Handler Styles
11
+ Requires `reflect-metadata` and `@celerity-sdk/types` as peer dependencies.
12
12
 
13
- ### Class-based (decorator-first)
13
+ ## Handler Types
14
14
 
15
- Decorators drive routing. The class is registered as a controller and methods are decorated with HTTP method decorators.
15
+ The core package supports five handler types, each with class-based (decorator-first) and function-based (blueprint-first) styles.
16
16
 
17
- ```typescript
18
- import { Controller, Get, Post, Body } from "@celerity-sdk/core";
17
+ ### HTTP
19
18
 
19
+ ```typescript
20
+ // Class-based
20
21
  @Controller("/orders")
21
22
  class OrdersHandler {
22
23
  @Get("/{orderId}")
@@ -29,41 +30,157 @@ class OrdersHandler {
29
30
  return { created: true };
30
31
  }
31
32
  }
33
+
34
+ // Function-based
35
+ const getHealth = httpGet("/health", (req, ctx) => ({ status: "ok" }));
36
+ const handler = createHttpHandler({}, (req, ctx) => ({ ok: true }));
37
+ ```
38
+
39
+ ### WebSocket
40
+
41
+ ```typescript
42
+ // Class-based
43
+ @WebSocketController()
44
+ class ChatHandler {
45
+ @OnConnect()
46
+ connect(@ConnectionId() connId: string) {}
47
+
48
+ @OnMessage()
49
+ message(@ConnectionId() connId: string, @MessageBody() body: unknown) {}
50
+
51
+ @OnDisconnect()
52
+ disconnect(@ConnectionId() connId: string) {}
53
+ }
54
+
55
+ // Function-based
56
+ const wsHandler = createWebSocketHandler({ route: "$default" }, (msg, ctx) => {});
32
57
  ```
33
58
 
34
- ### Function-based (blueprint-first or handler-first)
59
+ ### Consumer
35
60
 
36
- Lightweight handlers created with `createHttpHandler` or shorthand helpers. Routing can be defined inline or left to the blueprint.
61
+ ```typescript
62
+ // Class-based
63
+ @Consumer("ordersConsumer")
64
+ class OrderConsumer {
65
+ @MessageHandler()
66
+ async process(
67
+ @Messages(OrderSchema) messages: ValidatedConsumerMessage<Order>[],
68
+ ): Promise<EventResult> {
69
+ return { success: true };
70
+ }
71
+ }
72
+
73
+ // Function-based
74
+ const consumer = createConsumerHandler(
75
+ { messageSchema: OrderSchema },
76
+ async (messages, ctx) => ({ success: true }),
77
+ );
78
+ ```
79
+
80
+ ### Schedule
37
81
 
38
82
  ```typescript
39
- import { httpGet, createHttpHandler } from "@celerity-sdk/core";
83
+ // Class-based — cross-cutting, works on any controller type
84
+ @Controller()
85
+ class MaintenanceTasks {
86
+ @ScheduleHandler("dailyCleanup")
87
+ async cleanup(@ScheduleInput() input: unknown): Promise<EventResult> {
88
+ return { success: true };
89
+ }
40
90
 
41
- // Handler-first: path and method defined in code
42
- const getHealth = httpGet("/health", (req, ctx) => ({ status: "ok" }));
91
+ @ScheduleHandler("rate(1 hour)")
92
+ async sync(): Promise<EventResult> {
93
+ return { success: true };
94
+ }
95
+ }
43
96
 
44
- // Blueprint-first: path and method defined in the Celerity blueprint
45
- const handler = createHttpHandler({}, (req, ctx) => ({ ok: true }));
97
+ // Function-based
98
+ const scheduled = createScheduleHandler(
99
+ { source: "dailyCleanup", schedule: "rate(1 day)" },
100
+ async (event, ctx) => ({ success: true }),
101
+ );
102
+ ```
103
+
104
+ ### Custom / Invocable
105
+
106
+ ```typescript
107
+ // Class-based
108
+ @Controller()
109
+ class Reports {
110
+ @Invoke("generateReport")
111
+ async generate(@Payload() data: unknown) {
112
+ return { reportId: "r-1" };
113
+ }
114
+ }
115
+
116
+ // Function-based
117
+ const custom = createCustomHandler(
118
+ { name: "generateReport" },
119
+ async (payload, ctx) => ({ reportId: "r-1" }),
120
+ );
46
121
  ```
47
122
 
48
123
  ## Decorators
49
124
 
125
+ ### Class Decorators
126
+
50
127
  | Decorator | Purpose |
51
128
  |---|---|
52
- | `@Controller(prefix)` | Marks a class as an HTTP handler controller |
53
- | `@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`, `@Head`, `@Options` | HTTP method routing |
54
- | `@Body`, `@Query`, `@Param`, `@Headers`, `@Auth`, `@Req`, `@Cookies`, `@RequestId` | Parameter extraction |
55
- | `@Injectable()` | Marks a class for DI |
56
- | `@Inject(token)` | Overrides constructor parameter injection token |
57
- | `@Module(metadata)` | Defines a module with controllers, providers, imports, and exports |
129
+ | `@Controller(prefix?)` | HTTP handler controller with optional path prefix |
130
+ | `@WebSocketController()` | WebSocket handler controller |
131
+ | `@Consumer(source?)` | Consumer controller with optional blueprint resource name |
132
+ | `@Injectable()` | Marks a class for DI registration |
58
133
  | `@Guard(name)` | Declares a class as a named custom guard |
134
+ | `@Module(metadata)` | Defines a module with controllers, providers, imports, and exports |
135
+
136
+ ### HTTP Method Decorators
137
+
138
+ | Decorator | Purpose |
139
+ |---|---|
140
+ | `@Get(path?)`, `@Post(path?)`, `@Put(path?)`, `@Patch(path?)`, `@Delete(path?)`, `@Head(path?)`, `@Options(path?)` | HTTP method routing on controller methods |
141
+
142
+ ### WebSocket Event Decorators
143
+
144
+ | Decorator | Purpose |
145
+ |---|---|
146
+ | `@OnConnect()` | Handles `$connect` events |
147
+ | `@OnMessage(route?)` | Handles messages (custom route or `$default`) |
148
+ | `@OnDisconnect()` | Handles `$disconnect` events |
149
+
150
+ ### Other Handler Decorators
151
+
152
+ | Decorator | Purpose |
153
+ |---|---|
154
+ | `@MessageHandler(route?)` | Consumer message handler with optional routing key |
155
+ | `@ScheduleHandler(arg?)` | Schedule handler — string or `{ source?, schedule? }` |
156
+ | `@Invoke(name)` | Custom invocable handler |
157
+
158
+ ### Cross-Cutting Decorators
159
+
160
+ | Decorator | Purpose |
161
+ |---|---|
59
162
  | `@ProtectedBy(...guards)` | Declares guard requirements (class or method level) |
60
163
  | `@Public()` | Opts a method out of guard protection |
61
- | `@UseLayer(layer)` / `@UseLayers(...layers)` | Attaches layers to a handler method |
164
+ | `@UseLayer(...layers)` / `@UseLayers(layers)` | Attaches layers to handler or controller |
165
+ | `@UseResource(...names)` / `@UseResources(names)` | Declares blueprint resource dependencies |
62
166
  | `@SetMetadata(key, value)` / `@Action(name)` | Attaches custom metadata |
167
+ | `@Inject(token)` | Overrides DI token for a constructor parameter |
168
+
169
+ ### Parameter Decorators
170
+
171
+ | Handler Type | Decorators |
172
+ |---|---|
173
+ | HTTP | `@Body(schema?)`, `@Query(key?, schema?)`, `@Param(key?, schema?)`, `@Headers(key?, schema?)`, `@Auth()`, `@Token()`, `@Req()`, `@Cookies(key?)`, `@RequestId()` |
174
+ | WebSocket | `@ConnectionId()`, `@MessageBody(schema?)`, `@MessageId()`, `@RequestContext()`, `@EventType()` |
175
+ | Consumer | `@Messages(schema?)`, `@EventInput()`, `@Vendor()`, `@ConsumerTraceContext()` |
176
+ | Schedule | `@ScheduleInput(schema?)`, `@ScheduleId()`, `@ScheduleExpression()`, `@ScheduleEventInputParam()` |
177
+ | Custom | `@Payload(schema?)`, `@InvokeContext()` |
178
+
179
+ Parameter decorators that accept a `schema` use any Zod-compatible `Schema<T>` (`{ parse(data): T }`) for automatic validation.
63
180
 
64
181
  ## Dependency Injection
65
182
 
66
- The DI container supports class, factory, and value providers with automatic constructor injection.
183
+ The DI container supports class, factory, and value providers with automatic constructor injection, lazy resolution, and circular dependency detection.
67
184
 
68
185
  ```typescript
69
186
  @Injectable()
@@ -72,20 +189,41 @@ class OrderService {
72
189
  }
73
190
 
74
191
  @Module({
75
- providers: [OrderService, DatabaseClient],
192
+ providers: [
193
+ OrderService,
194
+ { provide: DB_TOKEN, useFactory: () => new DatabaseClient(), onClose: (db) => db.close() },
195
+ ],
76
196
  controllers: [OrdersHandler],
77
197
  })
78
198
  class AppModule {}
79
199
  ```
80
200
 
201
+ Provider types:
202
+
203
+ | Type | Registration |
204
+ |---|---|
205
+ | Class | `@Injectable()` class or `{ provide: token, useClass: MyClass }` |
206
+ | Factory | `{ provide: token, useFactory: (...deps) => value, inject?: [tokens] }` |
207
+ | Value | `{ provide: token, useValue: value }` |
208
+
209
+ All providers support an optional `onClose` hook for shutdown cleanup. The container also auto-detects `close`, `end`, `quit`, `disconnect`, and `destroy` methods.
210
+
81
211
  ## Layers
82
212
 
83
- Layers are the cross-cutting mechanism (like middleware in Express). They wrap handler execution and can modify the request, response, or context.
213
+ Layers are the cross-cutting mechanism (similar to middleware). They wrap handler execution in a composable pipeline.
84
214
 
85
215
  ```typescript
86
- import { validate } from "@celerity-sdk/core";
216
+ class LoggingLayer implements CelerityLayer {
217
+ async handle(ctx: BaseHandlerContext, next: NextFunction) {
218
+ console.log("before");
219
+ const result = await next();
220
+ console.log("after");
221
+ return result;
222
+ }
223
+ }
87
224
 
88
225
  @Controller("/orders")
226
+ @UseLayer(LoggingLayer)
89
227
  class OrdersHandler {
90
228
  @Post("/")
91
229
  @UseLayer(validate({ body: orderSchema }))
@@ -95,54 +233,150 @@ class OrdersHandler {
95
233
  }
96
234
  ```
97
235
 
236
+ Layer execution order: `[system layers] → [app layers] → [class layers] → [method layers] → handler`.
237
+
238
+ The built-in `validate()` layer factory supports schema validation for all handler types:
239
+
240
+ ```typescript
241
+ validate({ body: schema }) // HTTP body
242
+ validate({ query: schema }) // HTTP query
243
+ validate({ consumerMessage: schema }) // Consumer messages
244
+ validate({ scheduleInput: schema }) // Schedule input
245
+ validate({ customPayload: schema }) // Custom payload
246
+ validate({ wsMessageBody: schema }) // WebSocket message body
247
+ ```
248
+
249
+ ## Guards
250
+
251
+ Guards are declarative — they annotate handlers with protection requirements but do not execute in the Node.js process. Guard enforcement happens at the Rust runtime layer (containers) or API Gateway (serverless).
252
+
253
+ ```typescript
254
+ @Guard("jwt")
255
+ class JwtGuard {
256
+ async check(req: GuardHandlerRequest, ctx: GuardHandlerContext) {
257
+ return { sub: "user-1", role: "admin" };
258
+ }
259
+ }
260
+
261
+ @Controller("/api")
262
+ @ProtectedBy("jwt")
263
+ class ApiController {
264
+ @Public()
265
+ @Get("/health")
266
+ health() { return { ok: true }; }
267
+
268
+ @Get("/secret")
269
+ secret(@Auth() claims: unknown) { return claims; }
270
+ }
271
+ ```
272
+
273
+ Function-based guards: `createGuard({ name: "jwt" }, async (req, ctx) => claims)`.
274
+
98
275
  ## Application Factory
99
276
 
100
277
  ```typescript
101
278
  import { CelerityFactory } from "@celerity-sdk/core";
102
279
 
280
+ // Auto-detects platform from CELERITY_RUNTIME_PLATFORM env var
103
281
  const app = await CelerityFactory.create(AppModule);
104
- // Auto-detects platform from CELERITY_PLATFORM env var
105
282
  ```
106
283
 
107
- ## Handler Resolution
108
-
109
- When a handler is invoked by ID (e.g. from a blueprint's `spec.handler` field), the SDK resolves it using a multi-step strategy:
110
-
111
- 1. **Direct registry ID match** - looks up the handler by its explicit `id` (set via decorator or `createHttpHandler`).
112
- 2. **Module resolution fallback** - if the direct lookup fails, the ID is treated as a module reference and dynamically imported:
113
- - `"handlers.hello"` - named export `hello` from module `handlers`
114
- - `"handlers"` - default export from module `handlers`
115
- - `"app.module"` - tries named export split first (`module: "app"`, `export: "module"`), then falls back to default export from module `app.module`
116
- 3. **Path/method routing** - if both ID-based lookups fail, falls back to matching the incoming request's HTTP method and path against the registry.
284
+ Three application types:
117
285
 
118
- Module resolution matches the imported function against the registry by reference (`===`). Once matched, the handler ID is assigned and subsequent invocations use the direct lookup without repeated imports.
286
+ | Class | Purpose |
287
+ |---|---|
288
+ | `CelerityApplication` | Runtime container application (default) |
289
+ | `ServerlessApplication` | Serverless adapter (created when `adapter` option provided) |
290
+ | `TestingApplication` | Lightweight app for unit testing handlers |
119
291
 
120
- This is primarily relevant for blueprint-first function handlers where `spec.handler` references like `"handlers.hello"` map to exported functions that have no routing information in code.
292
+ ## Module System
121
293
 
122
- ## Guards
294
+ Modules organize controllers, providers, and sub-modules into a dependency graph.
123
295
 
124
- Guards are declarative. They annotate handlers with protection requirements but do not execute in the Node.js process. Guard enforcement happens at the Rust runtime layer (containers) or API Gateway (serverless).
296
+ ```typescript
297
+ @Module({
298
+ imports: [DatabaseModule, AuthModule],
299
+ controllers: [UserController, OrderController],
300
+ providers: [UserService],
301
+ functionHandlers: [healthCheck],
302
+ guards: [JwtGuard],
303
+ })
304
+ class AppModule {}
305
+ ```
125
306
 
126
307
  ## Testing
127
308
 
128
309
  ```typescript
129
- import { TestingApplication, mockRequest } from "@celerity-sdk/core";
310
+ import { CelerityFactory, mockRequest, mockConsumerEvent, mockScheduleEvent } from "@celerity-sdk/core";
311
+
312
+ const app = await CelerityFactory.createTestingApp(AppModule);
313
+
314
+ // HTTP
315
+ const response = await app.injectHttp(mockRequest({ method: "GET", path: "/orders/1" }));
130
316
 
131
- const app = new TestingApplication(AppModule);
132
- const response = await app.handle(mockRequest({ method: "GET", path: "/orders/1" }));
317
+ // Consumer
318
+ const result = await app.injectConsumer("tag", mockConsumerEvent("tag"));
319
+
320
+ // Schedule
321
+ const result = await app.injectSchedule("tag", mockScheduleEvent("tag"));
322
+
323
+ // Custom
324
+ const result = await app.injectCustom("generateReport", { type: "monthly" });
325
+
326
+ // WebSocket
327
+ await app.injectWebSocket("$default", mockWebSocketMessage());
133
328
  ```
134
329
 
330
+ ## Handler Resolution
331
+
332
+ When a handler is invoked by ID (e.g. from a blueprint's `spec.handler` field), the SDK resolves it using a multi-step strategy:
333
+
334
+ 1. **Direct registry ID match** — looks up the handler by its explicit `id`.
335
+ 2. **Module resolution fallback** — treats the ID as a module reference and dynamically imports it (e.g. `"handlers.hello"` → named export `hello` from module `handlers`).
336
+ 3. **Path/method routing** — falls back to matching the incoming request's HTTP method and path against the registry.
337
+
338
+ ## HTTP Exceptions
339
+
340
+ All extend `HttpException(statusCode, message, details?)` and are caught by the HTTP pipeline to return structured error responses:
341
+
342
+ | Exception | Status |
343
+ |---|---|
344
+ | `BadRequestException` | 400 |
345
+ | `UnauthorizedException` | 401 |
346
+ | `ForbiddenException` | 403 |
347
+ | `NotFoundException` | 404 |
348
+ | `MethodNotAllowedException` | 405 |
349
+ | `NotAcceptableException` | 406 |
350
+ | `ConflictException` | 409 |
351
+ | `GoneException` | 410 |
352
+ | `UnprocessableEntityException` | 422 |
353
+ | `TooManyRequestsException` | 429 |
354
+ | `InternalServerErrorException` | 500 |
355
+ | `NotImplementedException` | 501 |
356
+ | `BadGatewayException` | 502 |
357
+ | `ServiceUnavailableException` | 503 |
358
+ | `GatewayTimeoutException` | 504 |
359
+
135
360
  ## Advanced Exports
136
361
 
137
- The following exports are available for adapter authors building custom serverless or runtime adapters:
362
+ For adapter authors building custom serverless or runtime integrations:
138
363
 
139
364
  | Export | Purpose |
140
365
  |---|---|
141
- | `resolveHandlerByModuleRef(id, registry, baseDir)` | Resolve a handler ID as a module reference via dynamic import |
142
- | `executeHandlerPipeline(handler, request, options)` | Execute the full layer + handler pipeline |
143
- | `HandlerRegistry` | Handler registry class with route and ID-based lookups |
144
- | `bootstrapForRuntime(modulePath?, systemLayers?)` | Bootstrap for the Celerity runtime host |
366
+ | `HandlerRegistry` | Handler registry with route and ID-based lookups |
367
+ | `resolveHandlerByModuleRef` | Resolve a handler ID as a module reference via dynamic import |
368
+ | `executeHttpPipeline` | Execute the HTTP layer + handler pipeline |
369
+ | `executeWebSocketPipeline` | Execute the WebSocket pipeline |
370
+ | `executeConsumerPipeline` | Execute the consumer pipeline |
371
+ | `executeSchedulePipeline` | Execute the schedule pipeline |
372
+ | `executeCustomPipeline` | Execute the custom handler pipeline |
373
+ | `executeGuardPipeline` | Execute the guard pipeline |
374
+ | `bootstrapForRuntime` | Bootstrap for the Celerity runtime host |
145
375
  | `mapRuntimeRequest` / `mapToRuntimeResponse` | Runtime request/response mappers |
376
+ | `mapWebSocketMessage` | Runtime WebSocket message mapper |
377
+ | `mapConsumerEventInput` | Runtime consumer event mapper |
378
+ | `mapScheduleEventInput` | Runtime schedule event mapper |
379
+ | `mapToNapiEventResult` | Runtime EventResult mapper |
146
380
 
147
381
  ## Part of the Celerity Framework
148
382