constantia 1.0.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +370 -0
  3. package/dist/adapters/express.d.ts +36 -0
  4. package/dist/adapters/express.d.ts.map +1 -0
  5. package/dist/adapters/express.js +494 -0
  6. package/dist/adapters/express.js.map +1 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.d.ts.map +1 -0
  9. package/dist/adapters/index.js +3 -0
  10. package/dist/adapters/index.js.map +1 -0
  11. package/dist/adapters/validation.d.ts +3 -0
  12. package/dist/adapters/validation.d.ts.map +1 -0
  13. package/dist/adapters/validation.js +171 -0
  14. package/dist/adapters/validation.js.map +1 -0
  15. package/dist/context.d.ts +37 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +24 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/controllers.d.ts +9 -0
  20. package/dist/controllers.d.ts.map +1 -0
  21. package/dist/controllers.js +28 -0
  22. package/dist/controllers.js.map +1 -0
  23. package/dist/decorators.d.ts +38 -0
  24. package/dist/decorators.d.ts.map +1 -0
  25. package/dist/decorators.js +232 -0
  26. package/dist/decorators.js.map +1 -0
  27. package/dist/errors/index.d.ts +26 -0
  28. package/dist/errors/index.d.ts.map +1 -0
  29. package/dist/errors/index.js +52 -0
  30. package/dist/errors/index.js.map +1 -0
  31. package/dist/index.d.ts +18 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/logger.d.ts +11 -0
  36. package/dist/logger.d.ts.map +1 -0
  37. package/dist/logger.js +29 -0
  38. package/dist/logger.js.map +1 -0
  39. package/dist/metadata.d.ts +62 -0
  40. package/dist/metadata.d.ts.map +1 -0
  41. package/dist/metadata.js +304 -0
  42. package/dist/metadata.js.map +1 -0
  43. package/dist/openapi/helpers.d.ts +8 -0
  44. package/dist/openapi/helpers.d.ts.map +1 -0
  45. package/dist/openapi/helpers.js +343 -0
  46. package/dist/openapi/helpers.js.map +1 -0
  47. package/dist/openapi/index.d.ts +15 -0
  48. package/dist/openapi/index.d.ts.map +1 -0
  49. package/dist/openapi/index.js +134 -0
  50. package/dist/openapi/index.js.map +1 -0
  51. package/dist/openapi/types.d.ts +129 -0
  52. package/dist/openapi/types.d.ts.map +1 -0
  53. package/dist/openapi/types.js +34 -0
  54. package/dist/openapi/types.js.map +1 -0
  55. package/dist/types/files.d.ts +38 -0
  56. package/dist/types/files.d.ts.map +1 -0
  57. package/dist/types/files.js +50 -0
  58. package/dist/types/files.js.map +1 -0
  59. package/dist/types/index.d.ts +20 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/index.js +121 -0
  62. package/dist/types/index.js.map +1 -0
  63. package/dist/types/middleware.d.ts +10 -0
  64. package/dist/types/middleware.d.ts.map +1 -0
  65. package/dist/types/middleware.js +16 -0
  66. package/dist/types/middleware.js.map +1 -0
  67. package/dist/types/stream.d.ts +29 -0
  68. package/dist/types/stream.d.ts.map +1 -0
  69. package/dist/types/stream.js +13 -0
  70. package/dist/types/stream.js.map +1 -0
  71. package/package.json +134 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sperid Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ # Constantia
2
+
3
+ A decorator-based, type-safe web framework for building self-documenting REST APIs with automatic OpenAPI generation. Built on top of [Deepkit](https://deepkit.io/) runtime types for zero-overhead validation.
4
+
5
+ ```typescript
6
+ @Controller('/users')
7
+ class UserController {
8
+ @Get()
9
+ async list(): Promise<{ id: string; name: string }[]> {
10
+ return [{ id: '1', name: 'Alice' }];
11
+ }
12
+
13
+ @Get('/:id')
14
+ async getById(
15
+ @Param('id') id: string,
16
+ ): Promise<{ id: string; name: string }> {
17
+ if (id === '0') throw new BadRequestError('Invalid user ID');
18
+ return { id, name: 'Alice' };
19
+ }
20
+
21
+ @Post()
22
+ async create(
23
+ @Body() body: { name: string; email: string },
24
+ ): Promise<{ id: string }> {
25
+ return { id: '42' };
26
+ }
27
+ }
28
+ ```
29
+
30
+ Define your types in TypeScript, get validation + OpenAPI for free. No schemas, no codegen, no boilerplate.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pnpm add constantia
36
+ pnpm add -D @deepkit/type-compiler@1.0.1-alpha.155
37
+ ```
38
+
39
+ Add to your `package.json`:
40
+
41
+ ```json
42
+ {
43
+ "scripts": {
44
+ "postinstall": "node_modules/.bin/deepkit-type-install"
45
+ },
46
+ "pnpm": {
47
+ "onlyBuiltDependencies": ["@deepkit/type-compiler"]
48
+ }
49
+ }
50
+ ```
51
+
52
+ Add to your `tsconfig.json`:
53
+
54
+ ```json
55
+ {
56
+ "compilerOptions": {
57
+ "experimentalDecorators": true,
58
+ "emitDecoratorMetadata": true
59
+ },
60
+ "reflection": true
61
+ }
62
+ ```
63
+
64
+ > The `"reflection": true` key tells the Deepkit type compiler to emit type metadata at compile time.
65
+
66
+ ### Requirements
67
+
68
+ - **Node.js** >= 20
69
+ - **TypeScript** 5.x
70
+ - **pnpm** >= 8 (recommended)
71
+
72
+ ## Features
73
+
74
+ - **Decorator-based routing** — `@Controller`, `@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`
75
+ - **Automatic type validation** — Parameters validated at runtime from TypeScript types (no schemas to write)
76
+ - **OpenAPI generation** — Full OpenAPI 3.0 spec auto-generated from your controllers
77
+ - **File uploads** — `@File`, `@Files` with size/count limits and temp file management
78
+ - **Streaming** — `@FileStream`, `@DataStream` for large files and real-time data
79
+ - **Middleware pipeline** — Koa-style `@Use` middleware with context injection via `@Inject`
80
+ - **Adapter pattern** — Framework-agnostic core; ships with Express adapter
81
+ - **Error handling** — Typed errors (`BadRequestError`, `NotFoundError`, etc.) that map to HTTP status codes
82
+ - **Configurable logger** — Plug in your own logger or use the built-in console logger
83
+
84
+ ## Quick Start
85
+
86
+ ```typescript
87
+ import express from 'express';
88
+ import {
89
+ Controller,
90
+ Get,
91
+ Post,
92
+ Param,
93
+ Body,
94
+ Query,
95
+ ExpressAdapter,
96
+ registerControllersWrapper,
97
+ registerGlobalMiddlewaresWrapper,
98
+ registerOpenAPI,
99
+ BadRequestError,
100
+ } from 'constantia';
101
+
102
+ @Controller('/users')
103
+ class UserController {
104
+ @Get()
105
+ async list(): Promise<{ id: string; name: string }[]> {
106
+ return [{ id: '1', name: 'Alice' }];
107
+ }
108
+
109
+ @Get('/:id')
110
+ async getById(
111
+ @Param('id') id: string,
112
+ ): Promise<{ id: string; name: string }> {
113
+ if (id === '0') throw new BadRequestError('Invalid user ID');
114
+ return { id, name: 'Alice' };
115
+ }
116
+
117
+ @Post()
118
+ async create(
119
+ @Body() body: { name: string; email: string },
120
+ ): Promise<{ id: string }> {
121
+ return { id: '42' };
122
+ }
123
+
124
+ @Get('/search')
125
+ async search(@Query('q') q: string): Promise<{ results: string[] }> {
126
+ return { results: [`Result for: ${q}`] };
127
+ }
128
+ }
129
+
130
+ const app = express();
131
+ const adapter = new ExpressAdapter(app);
132
+
133
+ registerGlobalMiddlewaresWrapper([])(adapter);
134
+ registerControllersWrapper([UserController])(adapter);
135
+ await registerOpenAPI(adapter, {
136
+ config: { title: 'My API', version: '1.0.0' },
137
+ });
138
+
139
+ app.listen(3000, () => console.log('Listening on :3000'));
140
+ ```
141
+
142
+ Visit `http://localhost:3000/openapi.json` to see the generated spec.
143
+
144
+ ## Decorators
145
+
146
+ ### Class
147
+
148
+ | Decorator | Description |
149
+ | ---------------------- | --------------------------------------- |
150
+ | `@Controller('/path')` | Registers a class as a route controller |
151
+
152
+ ### Methods
153
+
154
+ | Decorator | Description |
155
+ | ------------------- | ----------------------------------------- |
156
+ | `@Get('/path')` | `GET` route |
157
+ | `@Post('/path')` | `POST` route |
158
+ | `@Put('/path')` | `PUT` route |
159
+ | `@Delete('/path')` | `DELETE` route |
160
+ | `@Patch('/path')` | `PATCH` route |
161
+ | `@DefaultHandler()` | Catch-all handler for the controller path |
162
+
163
+ ### Parameters
164
+
165
+ | Decorator | Description |
166
+ | ----------------- | ----------------------------------------------- |
167
+ | `@Param('name')` | URL path parameter (`:name`) |
168
+ | `@Query('name')` | Query string parameter |
169
+ | `@Body()` | Request body (validated as object) |
170
+ | `@Header('name')` | Request header |
171
+ | `@Inject('key')` | Inject a value from context (set by middleware) |
172
+ | `@RawBody()` | Raw request body as `Buffer` |
173
+
174
+ ### Files
175
+
176
+ | Decorator | Description |
177
+ | ------------------------------------------------ | ----------------------------- |
178
+ | `@File()` | Single file upload |
179
+ | `@File('fieldName', { maxFileSize, maxFiles })` | Named file with options |
180
+ | `@Files()` | Multiple files (array) |
181
+ | `@Files('fieldName', { maxFileSize, maxFiles })` | Named multi-file with options |
182
+
183
+ ### Streaming
184
+
185
+ | Decorator | Description |
186
+ | ---------------------------------------------------------------- | -------------------------- |
187
+ | `@FileStream({ contentDisposition, contentType, downloadName })` | Stream a file response |
188
+ | `@DataStream({ contentType })` | Stream data (e.g., ndjson) |
189
+
190
+ Return types must match:
191
+
192
+ ```typescript
193
+ // For @FileStream
194
+ async download(): Promise<FileStreamResponse> { ... }
195
+
196
+ // For @DataStream
197
+ async events(): Promise<DataStreamResponse<MyEvent>> { ... }
198
+ ```
199
+
200
+ ### Middleware
201
+
202
+ ```typescript
203
+ import { Use, Middleware, createMiddlewareFactory } from 'constantia';
204
+
205
+ // Simple middleware
206
+ const logRequest: Middleware = async (ctx, next) => {
207
+ console.log(`${ctx.request.method} ${ctx.request.url}`);
208
+ await next();
209
+ };
210
+
211
+ // Middleware factory (for parameterized middleware)
212
+ const requireRole = createMiddlewareFactory((role: string) => {
213
+ return async (ctx, next) => {
214
+ const user = ctx.get<{ role: string }>('user');
215
+ if (user?.role !== role) throw new UnauthorizedError('Forbidden');
216
+ await next();
217
+ };
218
+ });
219
+
220
+ // Apply at class level (all routes)
221
+ @Use(logRequest)
222
+ @Controller('/admin')
223
+ class AdminController {
224
+ // Apply at method level
225
+ @Use(requireRole('admin'))
226
+ @Get('/dashboard')
227
+ async dashboard(): Promise<{ status: string }> {
228
+ return { status: 'ok' };
229
+ }
230
+ }
231
+ ```
232
+
233
+ ### Context Injection
234
+
235
+ Middleware can inject values into the request context for controllers to consume:
236
+
237
+ ```typescript
238
+ const authMiddleware: Middleware = async (ctx, next) => {
239
+ const token = ctx.request.headers['authorization'];
240
+ const user = await verifyToken(token);
241
+ ctx.set('user', user);
242
+ await next();
243
+ };
244
+
245
+ @Use(authMiddleware)
246
+ @Controller('/profile')
247
+ class ProfileController {
248
+ @Get()
249
+ async getProfile(@Inject('user') user: User): Promise<User> {
250
+ return user;
251
+ }
252
+ }
253
+ ```
254
+
255
+ ## Errors
256
+
257
+ Throw typed errors to return the corresponding HTTP status:
258
+
259
+ ```typescript
260
+ import {
261
+ BadRequestError, // 400
262
+ UnauthorizedError, // 401
263
+ ForbiddenError, // 403
264
+ NotFoundError, // 404
265
+ InternalServerError, // 500
266
+ StatusCodeErrorError, // custom status
267
+ } from 'constantia';
268
+
269
+ throw new BadRequestError('Invalid input');
270
+ throw new NotFoundError('User not found');
271
+ throw new StatusCodeErrorError('Rate limited', 429);
272
+ ```
273
+
274
+ ## OpenAPI
275
+
276
+ The spec is auto-generated from your decorators and TypeScript types. Expose it at runtime:
277
+
278
+ ```typescript
279
+ await registerOpenAPI(adapter, {
280
+ config: {
281
+ title: 'My API',
282
+ version: '2.0.0',
283
+ description: 'My awesome API',
284
+ },
285
+ });
286
+ ```
287
+
288
+ Generate a static `openapi.json` at build time:
289
+
290
+ ```bash
291
+ node dist/app.js --only-generate-spec ./openapi.json
292
+ ```
293
+
294
+ Or generate alongside the running server:
295
+
296
+ ```bash
297
+ node dist/app.js --generate-spec ./openapi.json
298
+ ```
299
+
300
+ ## Custom Logger
301
+
302
+ By default Constantia logs to the console. Plug in your own:
303
+
304
+ ```typescript
305
+ import { setLogger } from 'constantia';
306
+
307
+ setLogger({
308
+ info: (msg) => myLogger.info(msg),
309
+ warn: (msg) => myLogger.warn(msg),
310
+ error: (msg, err) => myLogger.error(msg, err),
311
+ debug: (msg) => myLogger.debug(msg),
312
+ });
313
+ ```
314
+
315
+ ## File Handling
316
+
317
+ Uploaded files are stored as temp files. Important lifecycle methods:
318
+
319
+ ```typescript
320
+ @Post('/upload')
321
+ async upload(@File('document', { maxFileSize: 10 * 1024 * 1024 }) file: IFile) {
322
+ // file.name, file.size, file.mimetype, file.tempFilePath
323
+ const stream = file.getstream(); // ReadStream
324
+
325
+ // If you need to process after the response:
326
+ file.keepAlive(); // prevents auto-cleanup
327
+ // ... later ...
328
+ file.cleanup(); // manual cleanup when done
329
+ }
330
+ ```
331
+
332
+ ## Project Structure (recommended)
333
+
334
+ ```
335
+ src/
336
+ ├── controllers/
337
+ │ ├── user.controller.ts
338
+ │ ├── auth.controller.ts
339
+ │ └── index.ts # export all controllers
340
+ ├── middlewares/
341
+ │ └── index.ts # export global middlewares
342
+ ├── app.ts # boot express + constantia
343
+ └── ...
344
+ ```
345
+
346
+ ```typescript
347
+ // src/app.ts
348
+ import express from 'express';
349
+ import {
350
+ ExpressAdapter,
351
+ registerControllersWrapper,
352
+ registerGlobalMiddlewaresWrapper,
353
+ registerOpenAPI,
354
+ } from 'constantia';
355
+ import { controllers } from './controllers';
356
+ import { globalMiddlewares } from './middlewares';
357
+
358
+ const app = express();
359
+ const adapter = new ExpressAdapter(app);
360
+
361
+ registerGlobalMiddlewaresWrapper(globalMiddlewares)(adapter);
362
+ registerControllersWrapper(controllers)(adapter);
363
+ await registerOpenAPI(adapter);
364
+
365
+ app.listen(3000);
366
+ ```
367
+
368
+ ## License
369
+
370
+ MIT
@@ -0,0 +1,36 @@
1
+ import type { Express } from 'express';
2
+ import type { ControllerMetadata } from '../metadata.js';
3
+ import { type IFrameworkAdapter } from './index.js';
4
+ import { File } from '../types/files.js';
5
+ import { Middleware } from '../types/middleware.js';
6
+ declare global {
7
+ namespace Express {
8
+ interface Request {
9
+ rawBody?: Buffer;
10
+ uploadedFiles?: File[];
11
+ }
12
+ }
13
+ }
14
+ declare class ExpressAdapter implements IFrameworkAdapter {
15
+ private readonly app;
16
+ constructor(app: Express);
17
+ registerGlobalMiddlewares(middlewares: Middleware[]): void;
18
+ registerControllers([metadata, controllerClasses]: [
19
+ ControllerMetadata[],
20
+ Function[]
21
+ ]): void;
22
+ private registerController;
23
+ private registerRoute;
24
+ private buildNativeMiddlewares;
25
+ private runPipeline;
26
+ private makeCoreHandler;
27
+ private handleError;
28
+ private handleStreamResponse;
29
+ private extractParameters;
30
+ private parseNestedFormFields;
31
+ private setNestedValue;
32
+ private coerceValue;
33
+ private registerDefaultHandler;
34
+ }
35
+ export { ExpressAdapter };
36
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAqC,MAAM,SAAS,CAAC;AAI1E,OAAO,KAAK,EAGR,kBAAkB,EACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,IAAI,EAAa,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAYjD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC;SAC1B;KACJ;CACJ;AAED,cAAM,cAAe,YAAW,iBAAiB;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEzC,yBAAyB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,IAAI;IAkB1D,mBAAmB,CAAC,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE;QAC/C,kBAAkB,EAAE;QACpB,QAAQ,EAAE;KACb,GAAG,IAAI;IAUR,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,aAAa;IAgDrB,OAAO,CAAC,sBAAsB;YAyDhB,WAAW;IAYzB,OAAO,CAAC,eAAe;IAsBvB,OAAO,CAAC,WAAW;YAkBL,oBAAoB;IA6FlC,OAAO,CAAC,iBAAiB;IA2NzB,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,sBAAsB;CA+DjC;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}