adorn-api 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 (50) hide show
  1. package/README.md +249 -0
  2. package/dist/cli/generate-routes.js +101 -0
  3. package/dist/cli/generate-swagger.js +197 -0
  4. package/dist/controllers/advanced.controller.js +131 -0
  5. package/dist/controllers/user.controller.js +121 -0
  6. package/dist/entities/user.entity.js +1 -0
  7. package/dist/index.js +2 -0
  8. package/dist/lib/common.js +62 -0
  9. package/dist/lib/decorators.js +116 -0
  10. package/dist/middleware/auth.middleware.js +13 -0
  11. package/dist/routes.js +80 -0
  12. package/dist/server.js +18 -0
  13. package/dist/src/cli/generate-routes.js +105 -0
  14. package/dist/src/cli/generate-swagger.js +197 -0
  15. package/dist/src/index.js +4 -0
  16. package/dist/src/lib/common.js +62 -0
  17. package/dist/src/lib/decorators.js +116 -0
  18. package/dist/src/routes.js +80 -0
  19. package/dist/src/server.js +18 -0
  20. package/dist/tests/example-app/controllers/advanced.controller.js +130 -0
  21. package/dist/tests/example-app/controllers/controllers/advanced.controller.js +131 -0
  22. package/dist/tests/example-app/controllers/controllers/user.controller.js +121 -0
  23. package/dist/tests/example-app/controllers/user.controller.js +121 -0
  24. package/dist/tests/example-app/entities/entities/user.entity.js +1 -0
  25. package/dist/tests/example-app/entities/user.entity.js +1 -0
  26. package/dist/tests/example-app/middleware/auth.middleware.js +13 -0
  27. package/dist/tests/example-app/middleware/middleware/auth.middleware.js +13 -0
  28. package/dist/tests/example-app/routes.js +80 -0
  29. package/dist/tests/example-app/server.js +23 -0
  30. package/package.json +34 -0
  31. package/scripts/run-example.js +32 -0
  32. package/src/cli/generate-routes.ts +123 -0
  33. package/src/cli/generate-swagger.ts +216 -0
  34. package/src/index.js +20 -0
  35. package/src/index.ts +4 -0
  36. package/src/lib/common.js +68 -0
  37. package/src/lib/common.ts +35 -0
  38. package/src/lib/decorators.js +128 -0
  39. package/src/lib/decorators.ts +136 -0
  40. package/swagger.json +238 -0
  41. package/tests/e2e.test.ts +72 -0
  42. package/tests/example-app/controllers/advanced.controller.ts +52 -0
  43. package/tests/example-app/controllers/user.controller.ts +35 -0
  44. package/tests/example-app/entities/user.entity.ts +8 -0
  45. package/tests/example-app/middleware/auth.middleware.ts +16 -0
  46. package/tests/example-app/routes.ts +102 -0
  47. package/tests/example-app/server.ts +30 -0
  48. package/tests/generators.test.ts +48 -0
  49. package/tests/utils.ts +46 -0
  50. package/tsconfig.json +20 -0
@@ -0,0 +1,136 @@
1
+ // src/lib/decorators.ts
2
+
3
+ // 1. HTTP Methods
4
+ export type HttpMethod = 'get' | 'post' | 'put' | 'delete';
5
+
6
+ // Context metadata symbol for standard decorators
7
+ const META_KEY = Symbol('adorn:route');
8
+ export const SCHEMA_META = Symbol('adorn:schema');
9
+ export const AUTH_META = Symbol('adorn:auth');
10
+
11
+ export interface RouteDefinition {
12
+ method: HttpMethod;
13
+ path: string;
14
+ methodName: string;
15
+ }
16
+
17
+ // -- Method Decorator --
18
+ // Using Standard TC39 Signature
19
+ export function Get(path: string) {
20
+ return function (originalMethod: any, context: ClassMethodDecoratorContext) {
21
+ // In standard decorators, we can attach metadata to the class prototype via context
22
+ context.addInitializer(function () {
23
+ const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
24
+ routes.push({
25
+ method: 'get',
26
+ path,
27
+ methodName: String(context.name),
28
+ });
29
+ (this as any)[META_KEY] = routes;
30
+ });
31
+ return originalMethod;
32
+ };
33
+ }
34
+
35
+ export function Post(path: string) {
36
+ return function (originalMethod: any, context: ClassMethodDecoratorContext) {
37
+ context.addInitializer(function () {
38
+ const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
39
+ routes.push({
40
+ method: 'post',
41
+ path,
42
+ methodName: String(context.name),
43
+ });
44
+ (this as any)[META_KEY] = routes;
45
+ });
46
+ return originalMethod;
47
+ };
48
+ }
49
+
50
+ export function Put(path: string) {
51
+ return function (originalMethod: any, context: ClassMethodDecoratorContext) {
52
+ context.addInitializer(function () {
53
+ const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
54
+ routes.push({
55
+ method: 'put',
56
+ path,
57
+ methodName: String(context.name),
58
+ });
59
+ (this as any)[META_KEY] = routes;
60
+ });
61
+ return originalMethod;
62
+ };
63
+ }
64
+
65
+ export function Delete(path: string) {
66
+ return function (originalMethod: any, context: ClassMethodDecoratorContext) {
67
+ context.addInitializer(function () {
68
+ const routes: RouteDefinition[] = (this as any)[META_KEY] || [];
69
+ routes.push({
70
+ method: 'delete',
71
+ path,
72
+ methodName: String(context.name),
73
+ });
74
+ (this as any)[META_KEY] = routes;
75
+ });
76
+ return originalMethod;
77
+ };
78
+ }
79
+
80
+ // -- Class Decorator --
81
+ export function Controller(basePath: string) {
82
+ return function (target: any, context: ClassDecoratorContext) {
83
+ // We attach the base path to the class constructor
84
+ context.addInitializer(function () {
85
+ (this as any)._basePath = basePath;
86
+ });
87
+ return target;
88
+ };
89
+ }
90
+
91
+ // -- DTO Field Decorators --
92
+ // Since we can't decorate parameters, we decorate fields in a class
93
+ // e.g. class GetUserParams { @FromPath id: string }
94
+
95
+ export function FromQuery(name?: string) {
96
+ return function (target: undefined, context: ClassFieldDecoratorContext) {
97
+ context.addInitializer(function () {
98
+ const meta = (this as any)[SCHEMA_META] || {};
99
+ meta[context.name] = { type: 'query' };
100
+ (this as any)[SCHEMA_META] = meta;
101
+ });
102
+ return function (initialValue: any) { return initialValue; };
103
+ };
104
+ }
105
+
106
+ export function FromPath(name?: string) {
107
+ return function (target: undefined, context: ClassFieldDecoratorContext) {
108
+ context.addInitializer(function () {
109
+ const meta = (this as any)[SCHEMA_META] || {};
110
+ meta[context.name] = { type: 'path' };
111
+ (this as any)[SCHEMA_META] = meta;
112
+ });
113
+ return function (initialValue: any) { return initialValue; };
114
+ };
115
+ }
116
+
117
+ export function FromBody() {
118
+ return function (target: undefined, context: ClassFieldDecoratorContext) {
119
+ context.addInitializer(function () {
120
+ const meta = (this as any)[SCHEMA_META] || {};
121
+ meta[context.name] = { type: 'body' };
122
+ (this as any)[SCHEMA_META] = meta;
123
+ });
124
+ return function (initialValue: any) { return initialValue; };
125
+ };
126
+ }
127
+
128
+ // -- Authentication Decorator --
129
+ export function Authorized(role?: string) {
130
+ return function (target: any, context: ClassMethodDecoratorContext | ClassDecoratorContext) {
131
+ context.addInitializer(function () {
132
+ (this as any)[AUTH_META] = role || 'default';
133
+ });
134
+ return target;
135
+ };
136
+ }
package/swagger.json ADDED
@@ -0,0 +1,238 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "title": "Adorn API",
5
+ "version": "2.0.0"
6
+ },
7
+ "paths": {
8
+ "/advanced/{tenantId}/users": {
9
+ "get": {
10
+ "operationId": "listUsers",
11
+ "parameters": [
12
+ {
13
+ "name": "search",
14
+ "in": "query",
15
+ "required": false,
16
+ "schema": {
17
+ "type": "string"
18
+ }
19
+ },
20
+ {
21
+ "name": "tenantId",
22
+ "in": "path",
23
+ "required": true,
24
+ "schema": {
25
+ "type": "string"
26
+ }
27
+ },
28
+ {
29
+ "name": "page",
30
+ "in": "query",
31
+ "required": true,
32
+ "schema": {
33
+ "type": "integer"
34
+ }
35
+ },
36
+ {
37
+ "name": "limit",
38
+ "in": "query",
39
+ "required": true,
40
+ "schema": {
41
+ "type": "integer"
42
+ }
43
+ }
44
+ ],
45
+ "responses": {
46
+ "200": {
47
+ "description": "Success",
48
+ "content": {
49
+ "application/json": {
50
+ "schema": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "object",
54
+ "properties": {
55
+ "id": {
56
+ "type": "string"
57
+ },
58
+ "name": {
59
+ "type": "string"
60
+ },
61
+ "email": {
62
+ "type": "string"
63
+ },
64
+ "isActive": {
65
+ "type": "boolean"
66
+ },
67
+ "createdAt": {
68
+ "type": "string"
69
+ }
70
+ },
71
+ "required": [
72
+ "id",
73
+ "name",
74
+ "email",
75
+ "isActive",
76
+ "createdAt"
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "/advanced/": {
87
+ "post": {
88
+ "operationId": "create",
89
+ "parameters": [],
90
+ "requestBody": {
91
+ "content": {
92
+ "application/json": {
93
+ "schema": {
94
+ "type": "object",
95
+ "properties": {
96
+ "name": {
97
+ "type": "string"
98
+ },
99
+ "email": {
100
+ "type": "string"
101
+ }
102
+ },
103
+ "required": [
104
+ "name",
105
+ "email"
106
+ ]
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "responses": {
112
+ "200": {
113
+ "description": "Success",
114
+ "content": {
115
+ "application/json": {
116
+ "schema": {
117
+ "type": "object",
118
+ "properties": {
119
+ "id": {
120
+ "type": "string"
121
+ },
122
+ "name": {
123
+ "type": "string"
124
+ },
125
+ "email": {
126
+ "type": "string"
127
+ },
128
+ "isActive": {
129
+ "type": "boolean"
130
+ },
131
+ "createdAt": {
132
+ "type": "string"
133
+ }
134
+ },
135
+ "required": [
136
+ "id",
137
+ "name",
138
+ "email",
139
+ "isActive",
140
+ "createdAt"
141
+ ]
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ },
149
+ "/users/{userId}": {
150
+ "get": {
151
+ "operationId": "getUser",
152
+ "parameters": [
153
+ {
154
+ "name": "userId",
155
+ "in": "path",
156
+ "required": true,
157
+ "schema": {
158
+ "type": "string"
159
+ }
160
+ },
161
+ {
162
+ "name": "details",
163
+ "in": "query",
164
+ "required": false,
165
+ "schema": {
166
+ "type": "integer",
167
+ "enum": [
168
+ null,
169
+ null
170
+ ]
171
+ }
172
+ }
173
+ ],
174
+ "responses": {
175
+ "200": {
176
+ "description": "Success",
177
+ "content": {
178
+ "application/json": {
179
+ "schema": {
180
+ "type": "string"
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ },
188
+ "/users/": {
189
+ "post": {
190
+ "operationId": "createUser",
191
+ "parameters": [],
192
+ "requestBody": {
193
+ "content": {
194
+ "application/json": {
195
+ "schema": {
196
+ "type": "object",
197
+ "properties": {
198
+ "name": {
199
+ "type": "string"
200
+ },
201
+ "email": {
202
+ "type": "string"
203
+ }
204
+ },
205
+ "required": [
206
+ "name",
207
+ "email"
208
+ ]
209
+ }
210
+ }
211
+ }
212
+ },
213
+ "responses": {
214
+ "200": {
215
+ "description": "Success",
216
+ "content": {
217
+ "application/json": {
218
+ "schema": {
219
+ "type": "string"
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ },
228
+ "components": {
229
+ "schemas": {},
230
+ "securitySchemes": {
231
+ "bearerAuth": {
232
+ "type": "http",
233
+ "scheme": "bearer",
234
+ "bearerFormat": "JWT"
235
+ }
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,72 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { spawn, ChildProcessWithoutNullStreams } from "node:child_process";
3
+ import path from "node:path";
4
+ import { projectRoot, runTsNodeScript } from "./utils.js";
5
+
6
+ const BASE_URL = "http://localhost:3000";
7
+
8
+ let serverProcess: ChildProcessWithoutNullStreams | undefined;
9
+
10
+ beforeAll(
11
+ async () => {
12
+ await runTsNodeScript("src/cli/generate-swagger.ts");
13
+ await runTsNodeScript("src/cli/generate-routes.ts");
14
+ serverProcess = spawn(process.execPath, [path.join(projectRoot, "scripts", "run-example.js")], {
15
+ cwd: projectRoot,
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ });
18
+
19
+ await waitForServerReady(serverProcess);
20
+ },
21
+ 30000
22
+ );
23
+
24
+ afterAll(() => {
25
+ if (serverProcess && !serverProcess.killed) {
26
+ serverProcess.kill();
27
+ }
28
+ });
29
+
30
+ describe("adorn-api example server", () => {
31
+ it("lists users from the advanced endpoint", async () => {
32
+ const response = await fetch(`${BASE_URL}/advanced/tenant-12/users?page=1&limit=5`);
33
+ expect(response.status).toBe(200);
34
+ const body = await response.json();
35
+ expect(Array.isArray(body)).toBe(true);
36
+ expect(body[0]).toMatchObject({
37
+ id: "1",
38
+ name: "Alice",
39
+ });
40
+ });
41
+
42
+ it("returns user details from the user controller", async () => {
43
+ const response = await fetch(`${BASE_URL}/users/abc?details=true`);
44
+ expect(response.status).toBe(200);
45
+ const text = await response.text();
46
+ expect(text).toContain("Getting user abc");
47
+ expect(text).toContain("details: true");
48
+ });
49
+ });
50
+
51
+ async function waitForServerReady(proc: ChildProcessWithoutNullStreams, timeoutMs = 20000) {
52
+ const deadline = Date.now() + timeoutMs;
53
+ while (Date.now() < deadline) {
54
+ if (proc.exitCode !== null) {
55
+ throw new Error("Example server exited before becoming ready");
56
+ }
57
+
58
+ try {
59
+ const response = await fetch(`${BASE_URL}/docs`);
60
+ if (response.ok) {
61
+ await response.text();
62
+ return;
63
+ }
64
+ } catch {
65
+ // ignore errors while the server is starting
66
+ }
67
+
68
+ await new Promise((resolve) => setTimeout(resolve, 250));
69
+ }
70
+
71
+ throw new Error("Timed out waiting for the example server to become ready");
72
+ }
@@ -0,0 +1,52 @@
1
+ // src/controllers/advanced.controller.ts
2
+ import { Controller, Get, Post, FromBody, FromPath, PaginationQuery, EntityResponse, CreateInput } from "../../../src/index.js";
3
+ import { User } from "../entities/user.entity.js";
4
+
5
+ // --- 1. Advanced Request DTOs ---
6
+
7
+ // INHERITANCE: UserListRequest automatically gets 'page' and 'limit' from PaginationQuery
8
+ // AND the generator will find them because we scan the Type properties.
9
+ export class UserListRequest extends PaginationQuery {
10
+ // Implicitly @FromQuery because it's a GET request and extends a class
11
+ search?: string;
12
+
13
+ @FromPath()
14
+ tenantId!: string;
15
+ }
16
+
17
+ // COMPOSITION: Using the Type Helper for safety, but class for Decorators
18
+ // We implement the Type Helper to ensure our Class matches the Entity rule
19
+ export class CreateUserDto implements CreateInput<User, 'name' | 'email'> {
20
+ @FromBody()
21
+ name!: string;
22
+
23
+ @FromBody()
24
+ email!: string;
25
+
26
+ // If I miss a field here that is required in CreateInput, TS throws an error at edit time.
27
+ }
28
+
29
+ // --- 2. The Controller ---
30
+
31
+ @Controller("advanced")
32
+ export class AdvancedController {
33
+
34
+ @Get("/{tenantId}/users")
35
+ // Generic Return Type: The generator must resolve EntityResponse<User[]> -> User schema
36
+ public async listUsers(req: UserListRequest): Promise<EntityResponse<User[]>> {
37
+ return [
38
+ { id: "1", name: "Alice", email: "a@a.com", isActive: true, createdAt: "now" }
39
+ ];
40
+ }
41
+
42
+ @Post("/")
43
+ public async create(req: CreateUserDto): Promise<EntityResponse<User>> {
44
+ return {
45
+ id: "123",
46
+ name: req.name,
47
+ email: req.email,
48
+ isActive: true,
49
+ createdAt: "now"
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,35 @@
1
+ // src/controllers/user.controller.ts
2
+ import { Controller, Get, Post, FromQuery, FromPath, FromBody } from "../../../src/index.js";
3
+
4
+ // --- DTO Definitions (The substitute for Parameter Decorators) ---
5
+ export class GetUserRequest {
6
+ @FromPath()
7
+ userId!: string;
8
+
9
+ @FromQuery()
10
+ details?: boolean;
11
+ }
12
+
13
+ export class CreateUserRequest {
14
+ @FromBody()
15
+ name!: string;
16
+
17
+ @FromBody()
18
+ email!: string;
19
+ }
20
+
21
+ // --- The Controller ---
22
+ @Controller("users")
23
+ export class UserController {
24
+
25
+ // Strong typing: 'req' is checked at edit time.
26
+ @Get("/{userId}")
27
+ public async getUser(req: GetUserRequest): Promise<string> {
28
+ return `Getting user ${req.userId} with details: ${req.details}`;
29
+ }
30
+
31
+ @Post("/")
32
+ public async createUser(req: CreateUserRequest): Promise<void> {
33
+ console.log(`Creating user ${req.name}`);
34
+ }
35
+ }
@@ -0,0 +1,8 @@
1
+ // src/entities/user.entity.ts
2
+ export interface User {
3
+ id: string;
4
+ name: string;
5
+ email: string;
6
+ isActive: boolean;
7
+ createdAt: string;
8
+ }
@@ -0,0 +1,16 @@
1
+ // src/middleware/auth.middleware.ts
2
+ import { Request, Response, NextFunction } from "express";
3
+
4
+ export function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
5
+ const token = req.headers.authorization;
6
+ if (!token) {
7
+ res.status(401).json({ message: "No token provided" });
8
+ return;
9
+ }
10
+
11
+ if (token === "Bearer secret") {
12
+ next();
13
+ } else {
14
+ res.status(403).json({ message: "Invalid token" });
15
+ }
16
+ }
@@ -0,0 +1,102 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ // WARNING: This file was auto-generated by adorn-api. Do not edit.
4
+ import { Express, Request, Response } from 'express';
5
+ import { UserListRequest } from './controllers/advanced.controller.js';
6
+ import { CreateUserDto } from './controllers/advanced.controller.js';
7
+ import { AdvancedController } from './controllers/advanced.controller.js';
8
+ import { GetUserRequest } from './controllers/user.controller.js';
9
+ import { CreateUserRequest } from './controllers/user.controller.js';
10
+ import { UserController } from './controllers/user.controller.js';
11
+
12
+ export function RegisterRoutes(app: Express) {
13
+
14
+ app.get('/advanced/:tenantId/users', async (req: Request, res: Response) => {
15
+ const controller = new AdvancedController();
16
+ try {
17
+
18
+ const input: any = {};
19
+ // Map Query
20
+ Object.assign(input, req.query);
21
+ // Map Params
22
+ Object.assign(input, req.params);
23
+ // Map Body
24
+ Object.assign(input, req.body);
25
+
26
+ // In a real app, you would run 'zod' or 'class-validator' here on 'input'
27
+
28
+ const response = await controller.listUsers(input);
29
+ res.status(200).json(response);
30
+ } catch (err: any) {
31
+ console.error(err);
32
+ res.status(500).send(err.message);
33
+ }
34
+ });
35
+
36
+ app.post('/advanced/', async (req: Request, res: Response) => {
37
+ const controller = new AdvancedController();
38
+ try {
39
+
40
+ const input: any = {};
41
+ // Map Query
42
+ Object.assign(input, req.query);
43
+ // Map Params
44
+ Object.assign(input, req.params);
45
+ // Map Body
46
+ Object.assign(input, req.body);
47
+
48
+ // In a real app, you would run 'zod' or 'class-validator' here on 'input'
49
+
50
+ const response = await controller.create(input);
51
+ res.status(200).json(response);
52
+ } catch (err: any) {
53
+ console.error(err);
54
+ res.status(500).send(err.message);
55
+ }
56
+ });
57
+
58
+ app.get('/users/:userId', async (req: Request, res: Response) => {
59
+ const controller = new UserController();
60
+ try {
61
+
62
+ const input: any = {};
63
+ // Map Query
64
+ Object.assign(input, req.query);
65
+ // Map Params
66
+ Object.assign(input, req.params);
67
+ // Map Body
68
+ Object.assign(input, req.body);
69
+
70
+ // In a real app, you would run 'zod' or 'class-validator' here on 'input'
71
+
72
+ const response = await controller.getUser(input);
73
+ res.status(200).json(response);
74
+ } catch (err: any) {
75
+ console.error(err);
76
+ res.status(500).send(err.message);
77
+ }
78
+ });
79
+
80
+ app.post('/users/', async (req: Request, res: Response) => {
81
+ const controller = new UserController();
82
+ try {
83
+
84
+ const input: any = {};
85
+ // Map Query
86
+ Object.assign(input, req.query);
87
+ // Map Params
88
+ Object.assign(input, req.params);
89
+ // Map Body
90
+ Object.assign(input, req.body);
91
+
92
+ // In a real app, you would run 'zod' or 'class-validator' here on 'input'
93
+
94
+ const response = await controller.createUser(input);
95
+ res.status(200).json(response);
96
+ } catch (err: any) {
97
+ console.error(err);
98
+ res.status(500).send(err.message);
99
+ }
100
+ });
101
+
102
+ }
@@ -0,0 +1,30 @@
1
+ // tests/example-app/server.ts
2
+ // Example Express server using adorn-api
3
+ import express, { Express } from "express";
4
+ import bodyParser from "body-parser";
5
+ import { readFileSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join } from "path";
8
+ import swaggerUi from "swagger-ui-express";
9
+
10
+ // Import generated routes
11
+ import { RegisterRoutes } from "./routes.js";
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ const app: Express = express();
17
+
18
+ app.use(bodyParser.json());
19
+
20
+ // Register the Generated Routes
21
+ RegisterRoutes(app);
22
+
23
+ // Serve Swagger UI
24
+ const swaggerDoc = JSON.parse(readFileSync(join(__dirname, "../../swagger.json"), "utf-8"));
25
+ app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDoc));
26
+
27
+ app.listen(3000, () => {
28
+ console.log("🚀 Example server running on http://localhost:3000");
29
+ console.log("📄 Swagger running on http://localhost:3000/docs");
30
+ });