transit-kit 0.3.0 → 0.4.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 +341 -0
- package/dist/cli/generateOpenApi.js +47 -0
- package/dist/cli/generateOpenApi.spec.js +1 -0
- package/dist/server/handlers/api/EndpointDefinition.d.ts +3 -1
- package/dist/server/handlers/api/EndpointHandler.d.ts +6 -1
- package/dist/server/handlers/api/HandlerFromDefinition.d.ts +3 -2
- package/dist/server/handlers/api/createApiHandler.d.ts +3 -1
- package/dist/server/handlers/api/createApiHandler.js +8 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/middleware/auth.d.ts +2 -0
- package/dist/server/middleware/auth.js +13 -0
- package/dist/server/security/SecuritySchema.d.ts +5 -0
- package/dist/server/security/SecuritySchema.js +14 -0
- package/dist/server/security/basicAuth.d.ts +9 -0
- package/dist/server/security/basicAuth.js +21 -0
- package/dist/server/security/bearerAuth.d.ts +9 -0
- package/dist/server/security/bearerAuth.js +18 -0
- package/dist/server/server.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,3 +2,344 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/D4rkr34lm/transit-kit/actions/workflows/ci.yml)
|
|
4
4
|
[](https://coveralls.io/github/D4rkr34lm/transit-kit?branch=main)
|
|
5
|
+
|
|
6
|
+
A declarative TypeScript framework for building type-safe Express.js APIs with automatic OpenAPI generation.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🔒 **Type-Safe**: End-to-end type safety with TypeScript and Zod validation
|
|
11
|
+
- 📝 **Declarative API Definition**: Define your endpoints with clear, declarative syntax
|
|
12
|
+
- 🔄 **Automatic Validation**: Request body and query parameter validation using Zod schemas
|
|
13
|
+
- 📚 **OpenAPI Generation**: Automatically generate OpenAPI documentation from your endpoint definitions
|
|
14
|
+
- ⚡ **Express.js Powered**: Built on top of the battle-tested Express.js framework
|
|
15
|
+
- 🪵 **Built-in Logging**: Request and response logging out of the box
|
|
16
|
+
- 🎯 **Path Parameters**: Full support for path parameters with type safety
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install transit-kit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Create a Server
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createServer } from "transit-kit/server";
|
|
30
|
+
|
|
31
|
+
const server = createServer({
|
|
32
|
+
port: 3000,
|
|
33
|
+
inDevMode: true,
|
|
34
|
+
logger: true, // or pass a custom logger (console-like interface)
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Define an API Endpoint
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { createApiEndpointHandler } from "transit-kit/server";
|
|
42
|
+
import z from "zod";
|
|
43
|
+
|
|
44
|
+
// Define a simple GET endpoint
|
|
45
|
+
const getUserEndpoint = createApiEndpointHandler(
|
|
46
|
+
{
|
|
47
|
+
meta: {
|
|
48
|
+
name: "Get User",
|
|
49
|
+
description: "Retrieves a user by ID",
|
|
50
|
+
group: "Users",
|
|
51
|
+
},
|
|
52
|
+
method: "get",
|
|
53
|
+
path: "/users/:userId",
|
|
54
|
+
responseSchemas: {
|
|
55
|
+
200: {
|
|
56
|
+
dataType: "application/json",
|
|
57
|
+
dataSchema: z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
name: z.string(),
|
|
60
|
+
email: z.string().email(),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
404: {}, // Empty response
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
async (request) => {
|
|
67
|
+
const { userId } = request.params;
|
|
68
|
+
|
|
69
|
+
// Simulate fetching user
|
|
70
|
+
const user = await fetchUser(userId);
|
|
71
|
+
|
|
72
|
+
if (!user) {
|
|
73
|
+
return {
|
|
74
|
+
code: 404,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
code: 200,
|
|
80
|
+
dataType: "application/json",
|
|
81
|
+
json: user,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Define an Endpoint with Request Body and Query Parameters
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const createUserEndpoint = createApiEndpointHandler(
|
|
91
|
+
{
|
|
92
|
+
meta: {
|
|
93
|
+
name: "Create User",
|
|
94
|
+
description: "Creates a new user",
|
|
95
|
+
group: "Users",
|
|
96
|
+
},
|
|
97
|
+
method: "post",
|
|
98
|
+
path: "/users",
|
|
99
|
+
requestBodySchema: z.object({
|
|
100
|
+
name: z.string().min(1),
|
|
101
|
+
email: z.string().email(),
|
|
102
|
+
age: z.number().min(18),
|
|
103
|
+
}),
|
|
104
|
+
querySchema: z.object({
|
|
105
|
+
notify: z.boolean().optional(),
|
|
106
|
+
}),
|
|
107
|
+
responseSchemas: {
|
|
108
|
+
201: {
|
|
109
|
+
dataType: "application/json",
|
|
110
|
+
dataSchema: z.object({
|
|
111
|
+
id: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
email: z.string().email(),
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
400: {
|
|
117
|
+
dataType: "application/json",
|
|
118
|
+
dataSchema: z.object({
|
|
119
|
+
error: z.string(),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async (request) => {
|
|
125
|
+
// Request body is automatically validated and typed
|
|
126
|
+
const { name, email, age } = request.body;
|
|
127
|
+
const { notify } = request.query;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const newUser = await createUser({ name, email, age });
|
|
131
|
+
|
|
132
|
+
if (notify) {
|
|
133
|
+
await sendNotification(email);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
code: 201,
|
|
138
|
+
dataType: "application/json",
|
|
139
|
+
json: newUser,
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
code: 400,
|
|
144
|
+
dataType: "application/json",
|
|
145
|
+
json: { error: error.message },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 4. Register Endpoints and Start Server
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
server.registerApiEndpoint(getUserEndpoint);
|
|
156
|
+
server.registerApiEndpoint(createUserEndpoint);
|
|
157
|
+
|
|
158
|
+
server.start();
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Response Types
|
|
162
|
+
|
|
163
|
+
### JSON Response
|
|
164
|
+
|
|
165
|
+
For endpoints that return JSON data:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
return {
|
|
169
|
+
code: 200,
|
|
170
|
+
dataType: "application/json",
|
|
171
|
+
json: { message: "Success", data: myData },
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Empty Response
|
|
176
|
+
|
|
177
|
+
For endpoints that return no content (e.g., 204 No Content):
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
return {
|
|
181
|
+
code: 204,
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## OpenAPI Generation
|
|
186
|
+
|
|
187
|
+
Transit-kit can automatically generate OpenAPI documentation from your endpoint definitions.
|
|
188
|
+
|
|
189
|
+
### Using the CLI
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx transit-kit generate-openapi --output openapi.json --target ./src
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Options:
|
|
196
|
+
|
|
197
|
+
- `-o, --output <path>`: Output path for the generated OpenAPI document (default: `openapi.json`)
|
|
198
|
+
- `-t, --target <path>`: Target path to search for endpoint definitions (default: `.`)
|
|
199
|
+
|
|
200
|
+
### Programmatic Usage
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { generateOpenApiDoc } from "transit-kit/cli";
|
|
204
|
+
|
|
205
|
+
const openApiDoc = await generateOpenApiDoc("./src");
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The generated OpenAPI document will include:
|
|
209
|
+
|
|
210
|
+
- All registered endpoints
|
|
211
|
+
- Request/response schemas
|
|
212
|
+
- Path parameters
|
|
213
|
+
- Query parameters
|
|
214
|
+
- Request body schemas
|
|
215
|
+
- Response schemas for all status codes
|
|
216
|
+
|
|
217
|
+
## Configuration
|
|
218
|
+
|
|
219
|
+
### Server Configuration
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface ServerConfig {
|
|
223
|
+
inDevMode: boolean; // Enable development mode features
|
|
224
|
+
port: number; // Port to listen on
|
|
225
|
+
logger: Logger | boolean; // Logger instance or boolean to enable/disable
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Custom Logger
|
|
230
|
+
|
|
231
|
+
You can provide a custom logger with a console-like interface:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const customLogger = {
|
|
235
|
+
log: (message: string) => {
|
|
236
|
+
/* custom logging */
|
|
237
|
+
},
|
|
238
|
+
error: (message: string) => {
|
|
239
|
+
/* custom error logging */
|
|
240
|
+
},
|
|
241
|
+
// ... other console methods
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const server = createServer({
|
|
245
|
+
port: 3000,
|
|
246
|
+
inDevMode: false,
|
|
247
|
+
logger: customLogger,
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## API Reference
|
|
252
|
+
|
|
253
|
+
### `createServer(config: ServerConfig): Server`
|
|
254
|
+
|
|
255
|
+
Creates a new server instance with the specified configuration.
|
|
256
|
+
|
|
257
|
+
### `createApiEndpointHandler(definition, handler)`
|
|
258
|
+
|
|
259
|
+
Creates an API endpoint handler with type-safe request/response handling.
|
|
260
|
+
|
|
261
|
+
**Definition properties:**
|
|
262
|
+
|
|
263
|
+
- `meta`: Metadata about the endpoint (name, description, group)
|
|
264
|
+
- `method`: HTTP method (`get`, `post`, `put`, `patch`, `delete`)
|
|
265
|
+
- `path`: Express-style path with optional parameters (e.g., `/users/:userId`)
|
|
266
|
+
- `requestBodySchema`: (Optional) Zod schema for request body validation
|
|
267
|
+
- `querySchema`: (Optional) Zod schema for query parameter validation
|
|
268
|
+
- `responseSchemas`: Map of status codes to response schemas
|
|
269
|
+
|
|
270
|
+
### `server.registerApiEndpoint(endpoint)`
|
|
271
|
+
|
|
272
|
+
Registers an endpoint with the server.
|
|
273
|
+
|
|
274
|
+
### `server.start()`
|
|
275
|
+
|
|
276
|
+
Starts the Express server on the configured port.
|
|
277
|
+
|
|
278
|
+
## Examples
|
|
279
|
+
|
|
280
|
+
### Complete Example
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { createServer, createApiEndpointHandler } from "transit-kit/server";
|
|
284
|
+
import z from "zod";
|
|
285
|
+
|
|
286
|
+
// Create server
|
|
287
|
+
const server = createServer({
|
|
288
|
+
port: 3000,
|
|
289
|
+
inDevMode: true,
|
|
290
|
+
logger: true,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Define endpoints
|
|
294
|
+
const listUsersEndpoint = createApiEndpointHandler(
|
|
295
|
+
{
|
|
296
|
+
meta: {
|
|
297
|
+
name: "List Users",
|
|
298
|
+
description: "Get a list of all users",
|
|
299
|
+
group: "Users",
|
|
300
|
+
},
|
|
301
|
+
method: "get",
|
|
302
|
+
path: "/users",
|
|
303
|
+
querySchema: z.object({
|
|
304
|
+
page: z.number().optional(),
|
|
305
|
+
limit: z.number().optional(),
|
|
306
|
+
}),
|
|
307
|
+
responseSchemas: {
|
|
308
|
+
200: {
|
|
309
|
+
dataType: "application/json",
|
|
310
|
+
dataSchema: z.array(
|
|
311
|
+
z.object({
|
|
312
|
+
id: z.string(),
|
|
313
|
+
name: z.string(),
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
async (request) => {
|
|
320
|
+
const { page = 1, limit = 10 } = request.query;
|
|
321
|
+
const users = await fetchUsers(page, limit);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
code: 200,
|
|
325
|
+
dataType: "application/json",
|
|
326
|
+
json: users,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
server.registerApiEndpoint(listUsersEndpoint);
|
|
332
|
+
server.start();
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT
|
|
338
|
+
|
|
339
|
+
## Contributing
|
|
340
|
+
|
|
341
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
342
|
+
|
|
343
|
+
## Author
|
|
344
|
+
|
|
345
|
+
D4rkr34lm
|
|
@@ -31,6 +31,24 @@ function extractQueryParameters(querySchema) {
|
|
|
31
31
|
return [];
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
function extractRequestSecuritySchemes(definition) {
|
|
35
|
+
const securitySchemes = definition.securitySchemes;
|
|
36
|
+
if (hasValue(securitySchemes)) {
|
|
37
|
+
return securitySchemes.map((schema) => {
|
|
38
|
+
switch (schema.type) {
|
|
39
|
+
case "http":
|
|
40
|
+
return {
|
|
41
|
+
[schema.name]: [],
|
|
42
|
+
};
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported security scheme type: ${schema.type}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
34
52
|
function translateToOpenAPIPathItem(definition) {
|
|
35
53
|
const { meta, path, method, requestBodySchema, querySchema, responseSchemas, } = definition;
|
|
36
54
|
// 1. Path and Parameter extraction
|
|
@@ -80,6 +98,8 @@ function translateToOpenAPIPathItem(definition) {
|
|
|
80
98
|
.reduce((acc, resp) => {
|
|
81
99
|
return { ...acc, ...resp };
|
|
82
100
|
}, {});
|
|
101
|
+
// 5. Security Requirements
|
|
102
|
+
const securityRequirements = extractRequestSecuritySchemes(definition);
|
|
83
103
|
const operation = {
|
|
84
104
|
operationId: meta.name,
|
|
85
105
|
summary: meta.description,
|
|
@@ -88,12 +108,35 @@ function translateToOpenAPIPathItem(definition) {
|
|
|
88
108
|
parameters: operationParameters,
|
|
89
109
|
...requestBody,
|
|
90
110
|
responses,
|
|
111
|
+
security: securityRequirements,
|
|
91
112
|
};
|
|
92
113
|
const pathItem = {
|
|
93
114
|
[method.toLowerCase()]: operation,
|
|
94
115
|
};
|
|
95
116
|
return [openApiPath, pathItem];
|
|
96
117
|
}
|
|
118
|
+
function extractSecuritySchemes(endpointDefinitions) {
|
|
119
|
+
const securitySchemes = Array.from(new Set(endpointDefinitions
|
|
120
|
+
.map((def) => def.securitySchemes)
|
|
121
|
+
.filter(hasValue)
|
|
122
|
+
.flat()));
|
|
123
|
+
const openApiSecuritySchemes = securitySchemes.map((scheme) => {
|
|
124
|
+
switch (scheme.type) {
|
|
125
|
+
case "http":
|
|
126
|
+
return {
|
|
127
|
+
[scheme.name]: {
|
|
128
|
+
type: "http",
|
|
129
|
+
scheme: scheme.scheme,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unsupported security scheme type: ${scheme.type}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return openApiSecuritySchemes.reduce((acc, scheme) => {
|
|
137
|
+
return { ...acc, ...scheme };
|
|
138
|
+
}, {});
|
|
139
|
+
}
|
|
97
140
|
export async function generateOpenApiDoc(targetPath) {
|
|
98
141
|
const serverModule = await import(path.resolve(process.cwd(), targetPath));
|
|
99
142
|
const server = serverModule.default;
|
|
@@ -114,6 +157,7 @@ export async function generateOpenApiDoc(targetPath) {
|
|
|
114
157
|
}
|
|
115
158
|
return acc;
|
|
116
159
|
}, {});
|
|
160
|
+
const securitySchemes = extractSecuritySchemes(endpointDefinitions);
|
|
117
161
|
const openApiDocument = {
|
|
118
162
|
openapi: "3.0.0",
|
|
119
163
|
info: {
|
|
@@ -121,6 +165,9 @@ export async function generateOpenApiDoc(targetPath) {
|
|
|
121
165
|
version: "1.0.0",
|
|
122
166
|
},
|
|
123
167
|
paths: paths,
|
|
168
|
+
components: {
|
|
169
|
+
securitySchemes,
|
|
170
|
+
},
|
|
124
171
|
};
|
|
125
172
|
return openApiDocument;
|
|
126
173
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { HttpMethod } from "../../constants/HttpMethods";
|
|
3
|
+
import { SecurityScheme } from "../../security/SecuritySchema";
|
|
3
4
|
import { GenericResponseSchemaMap } from "./responses/index";
|
|
4
5
|
export interface ApiEndpointMeta {
|
|
5
6
|
name: string;
|
|
6
7
|
group: string;
|
|
7
8
|
description: string;
|
|
8
9
|
}
|
|
9
|
-
export type ApiEndpointDefinition<Path extends string = string, Method extends HttpMethod = HttpMethod, RequestBody extends z.ZodType | undefined = z.ZodType | undefined, Query extends z.ZodType | undefined = z.ZodType | undefined, ResponseMap extends GenericResponseSchemaMap = GenericResponseSchemaMap> = {
|
|
10
|
+
export type ApiEndpointDefinition<Path extends string = string, Method extends HttpMethod = HttpMethod, RequestBody extends z.ZodType | undefined = z.ZodType | undefined, Query extends z.ZodType | undefined = z.ZodType | undefined, ResponseMap extends GenericResponseSchemaMap = GenericResponseSchemaMap, SecuritySchemes extends SecurityScheme<unknown>[] = SecurityScheme<unknown>[]> = {
|
|
10
11
|
meta: ApiEndpointMeta;
|
|
11
12
|
path: Path;
|
|
12
13
|
method: Method;
|
|
13
14
|
requestBodySchema?: RequestBody;
|
|
14
15
|
querySchema?: Query;
|
|
15
16
|
responseSchemas: ResponseMap;
|
|
17
|
+
securitySchemes?: SecuritySchemes;
|
|
16
18
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Request } from "express";
|
|
2
2
|
import { HttpStatusCodes } from "../../constants/HttpStatusCodes";
|
|
3
3
|
import { GenericResponse } from "./responses";
|
|
4
|
-
export type ApiEndpointHandler<PathParams extends Record<string, string> = {}, RequestBody = unknown, Query = unknown, Responses extends GenericResponse = never> = (request: Request<PathParams, unknown, RequestBody, Query, Record<string, unknown
|
|
4
|
+
export type ApiEndpointHandler<PathParams extends Record<string, string> = {}, RequestBody = unknown, Query = unknown, Responses extends GenericResponse = never, Caller = unknown> = (request: Request<PathParams, unknown, RequestBody, Query, Record<string, unknown>>, extractedRequestData: {
|
|
5
|
+
parameters: PathParams;
|
|
6
|
+
query: Query;
|
|
7
|
+
body: RequestBody;
|
|
8
|
+
caller: Caller;
|
|
9
|
+
}) => Promise<Responses | {
|
|
5
10
|
code: (typeof HttpStatusCodes)["InternalServerError_500"];
|
|
6
11
|
}>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { HttpStatusCode } from "../../constants/HttpStatusCodes";
|
|
3
|
+
import { SecurityScheme } from "../../security/SecuritySchema";
|
|
3
4
|
import { Prettify } from "../../utils/types";
|
|
4
5
|
import { ApiEndpointHandler } from "./EndpointHandler";
|
|
5
6
|
import { ExtractPathParams } from "./PathParameters";
|
|
6
7
|
import { EmptyResponse, EmptyResponseSchema } from "./responses/emptyResponse";
|
|
7
8
|
import { GenericResponse, GenericResponseSchemaMap } from "./responses/index";
|
|
8
9
|
import { JsonResponseSchema, JsonResponseSchemaToResponseType } from "./responses/jsonResponse";
|
|
9
|
-
export type HandlerForDefinition<Path extends string, RequestBody extends z.ZodType | undefined, Query extends z.ZodType | undefined, ResponsesMap extends GenericResponseSchemaMap> = ApiEndpointHandler<ExtractPathParams<Path>, RequestBody extends undefined ? undefined : z.infer<RequestBody>, Query extends undefined ? undefined : z.infer<Query>, Exclude<Prettify<{
|
|
10
|
+
export type HandlerForDefinition<Path extends string, RequestBody extends z.ZodType | undefined, Query extends z.ZodType | undefined, ResponsesMap extends GenericResponseSchemaMap, SecuritySchemas extends SecurityScheme<unknown>[] = []> = ApiEndpointHandler<ExtractPathParams<Path>, RequestBody extends undefined ? undefined : z.infer<RequestBody>, Query extends undefined ? undefined : z.infer<Query>, Exclude<Prettify<{
|
|
10
11
|
[K in keyof ResponsesMap]: K extends HttpStatusCode ? ResponsesMap[K] extends JsonResponseSchema ? JsonResponseSchemaToResponseType<K, ResponsesMap[K]> : ResponsesMap[K] extends EmptyResponseSchema ? EmptyResponse<K> : ResponsesMap[K] extends undefined ? never : GenericResponse : never;
|
|
11
|
-
}[keyof ResponsesMap]>, undefined
|
|
12
|
+
}[keyof ResponsesMap]>, undefined>, SecuritySchemas extends SecurityScheme<infer Caller>[] ? Caller : unknown>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { HttpMethod } from "../../constants/HttpMethods";
|
|
3
|
+
import { SecurityScheme } from "../../security/SecuritySchema";
|
|
3
4
|
import { Prettify } from "../../utils/types";
|
|
4
5
|
import { ApiEndpointDefinition } from "./EndpointDefinition";
|
|
5
6
|
import { ApiEndpointHandler } from "./EndpointHandler";
|
|
6
7
|
import { HandlerForDefinition } from "./HandlerFromDefinition";
|
|
7
8
|
import { GenericResponse, GenericResponseSchemaMap } from "./responses";
|
|
8
|
-
export declare function createApiEndpointHandler<const ResponsesMap extends GenericResponseSchemaMap, const Path extends string, const Method extends HttpMethod, const RequestBody extends z.ZodType | undefined = undefined, const Query extends z.ZodType | undefined = undefined>(definition: Prettify<ApiEndpointDefinition<Path, Method, RequestBody, Query, ResponsesMap>>, handler: HandlerForDefinition<Path, RequestBody, Query, ResponsesMap>): {
|
|
9
|
+
export declare function createApiEndpointHandler<const ResponsesMap extends GenericResponseSchemaMap, const Path extends string, const Method extends HttpMethod, const RequestBody extends z.ZodType | undefined = undefined, const Query extends z.ZodType | undefined = undefined, const SecuritySchemas extends SecurityScheme<unknown>[] = []>(definition: Prettify<ApiEndpointDefinition<Path, Method, RequestBody, Query, ResponsesMap, SecuritySchemas>>, handler: HandlerForDefinition<Path, RequestBody, Query, ResponsesMap>): {
|
|
9
10
|
definition: {
|
|
10
11
|
meta: import("./EndpointDefinition").ApiEndpointMeta;
|
|
11
12
|
path: Path;
|
|
@@ -13,6 +14,7 @@ export declare function createApiEndpointHandler<const ResponsesMap extends Gene
|
|
|
13
14
|
requestBodySchema?: RequestBody | undefined;
|
|
14
15
|
querySchema?: Query | undefined;
|
|
15
16
|
responseSchemas: ResponsesMap;
|
|
17
|
+
securitySchemes?: SecuritySchemas | undefined;
|
|
16
18
|
};
|
|
17
19
|
handler: HandlerForDefinition<Path, RequestBody, Query, ResponsesMap>;
|
|
18
20
|
};
|
|
@@ -8,7 +8,14 @@ export function createApiEndpointHandler(definition, handler) {
|
|
|
8
8
|
}
|
|
9
9
|
export function buildApiEndpointHandler(handler) {
|
|
10
10
|
return expressAsyncHandler(async (request, response) => {
|
|
11
|
-
const
|
|
11
|
+
const caller = response.locals.caller;
|
|
12
|
+
const extractedRequestData = {
|
|
13
|
+
parameters: request.params,
|
|
14
|
+
query: request.query,
|
|
15
|
+
body: request.body,
|
|
16
|
+
caller,
|
|
17
|
+
};
|
|
18
|
+
const result = await handler(request, extractedRequestData);
|
|
12
19
|
if (isJsonResponse(result)) {
|
|
13
20
|
response.status(result.code).json(result.json);
|
|
14
21
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createApiEndpointHandler } from "./handlers/api/createApiHandler";
|
|
2
|
+
export { buildAuthenticationMiddleware } from "./middleware/auth";
|
|
2
3
|
export { createServer, type Server, type ServerConfig } from "./server";
|
|
3
4
|
declare const _default: {};
|
|
4
5
|
export default _default;
|
package/dist/server/index.js
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { SecurityScheme } from "../security/SecuritySchema";
|
|
2
|
+
export declare function buildAuthenticationMiddleware<Caller>(schemes: SecurityScheme<Caller>[]): import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import expressAsyncHandler from "express-async-handler";
|
|
2
|
+
import { authenticate } from "../security/SecuritySchema";
|
|
3
|
+
export function buildAuthenticationMiddleware(schemes) {
|
|
4
|
+
return expressAsyncHandler(async (request, response, next) => {
|
|
5
|
+
const caller = await authenticate(schemes, request);
|
|
6
|
+
if (caller == null) {
|
|
7
|
+
response.status(401).json({ message: "Unauthorized" });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
response.locals.caller = caller;
|
|
11
|
+
next();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
import { BasicAuthScheme } from "./basicAuth";
|
|
3
|
+
import { BearerAuthScheme } from "./bearerAuth";
|
|
4
|
+
export type SecurityScheme<Caller> = BasicAuthScheme<Caller> | BearerAuthScheme<Caller>;
|
|
5
|
+
export declare function authenticate<Caller>(schemes: SecurityScheme<Caller>[], request: Request): Promise<Caller | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildBasicAuthenticator } from "./basicAuth";
|
|
2
|
+
import { buildBearerAuthenticator } from "./bearerAuth";
|
|
3
|
+
export async function authenticate(schemes, request) {
|
|
4
|
+
const authenticationResults = await Promise.all(schemes.map((scheme) => {
|
|
5
|
+
switch (scheme.scheme) {
|
|
6
|
+
case "basic":
|
|
7
|
+
return buildBasicAuthenticator(scheme)(request);
|
|
8
|
+
case "bearer":
|
|
9
|
+
return buildBearerAuthenticator(scheme)(request);
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
const successfulAuthentication = authenticationResults.find((result) => result !== null);
|
|
13
|
+
return successfulAuthentication ?? null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
export interface BasicAuthScheme<Caller = unknown> {
|
|
3
|
+
name: string;
|
|
4
|
+
type: "http";
|
|
5
|
+
scheme: "basic";
|
|
6
|
+
validateCaller: (username: string, password: string) => Promise<Caller | null>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createBasicAuthSchema<Caller>(name: string, validateCaller: (username: string, password: string) => Promise<Caller | null>): BasicAuthScheme<Caller>;
|
|
9
|
+
export declare function buildBasicAuthenticator<Caller>(scheme: BasicAuthScheme<Caller>): (request: Request) => Promise<Caller | null>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { hasNoValue } from "../utils/typeGuards";
|
|
2
|
+
export function createBasicAuthSchema(name, validateCaller) {
|
|
3
|
+
return {
|
|
4
|
+
name,
|
|
5
|
+
type: "http",
|
|
6
|
+
scheme: "basic",
|
|
7
|
+
validateCaller,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function buildBasicAuthenticator(scheme) {
|
|
11
|
+
return async (request) => {
|
|
12
|
+
const authHeader = request.headers.authorization;
|
|
13
|
+
if (hasNoValue(authHeader) || !authHeader.startsWith("Basic ")) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const base64Credentials = authHeader.slice("Basic ".length);
|
|
17
|
+
const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
|
|
18
|
+
const [username, password] = credentials.split(":", 2);
|
|
19
|
+
return scheme.validateCaller(username, password);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
export interface BearerAuthScheme<Caller = unknown> {
|
|
3
|
+
name: string;
|
|
4
|
+
type: "http";
|
|
5
|
+
scheme: "bearer";
|
|
6
|
+
validateCaller: (token: string) => Promise<Caller | null>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createBearerAuthSchema<Caller>(name: string, validateCaller: (token: string) => Promise<Caller | null>): BearerAuthScheme<Caller>;
|
|
9
|
+
export declare function buildBearerAuthenticator<Caller>(scheme: BearerAuthScheme<Caller>): (request: Request) => Promise<Caller | null>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createBearerAuthSchema(name, validateCaller) {
|
|
2
|
+
return {
|
|
3
|
+
name,
|
|
4
|
+
type: "http",
|
|
5
|
+
scheme: "bearer",
|
|
6
|
+
validateCaller,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function buildBearerAuthenticator(scheme) {
|
|
10
|
+
return async (request) => {
|
|
11
|
+
const authHeader = request.headers.authorization;
|
|
12
|
+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const token = authHeader.slice("Bearer ".length);
|
|
16
|
+
return scheme.validateCaller(token);
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/server/server.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import cookieParser from "cookie-parser";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { buildApiEndpointHandler } from "./handlers/api/createApiHandler";
|
|
4
|
+
import { buildAuthenticationMiddleware } from "./middleware/auth";
|
|
4
5
|
import { buildRequestLogger, buildResponseLogger } from "./middleware/logging";
|
|
5
6
|
import { buildBodyValidatorMiddleware, buildQueryValidatorMiddleware, } from "./middleware/validation";
|
|
7
|
+
import { isEmpty } from "./utils/funcs";
|
|
6
8
|
import { NoOpLogger } from "./utils/logging";
|
|
7
9
|
import { hasNoValue, hasValue } from "./utils/typeGuards";
|
|
8
10
|
function registerApiEndpoint(expressApp, endpoint) {
|
|
9
11
|
const { definition, handler } = endpoint;
|
|
10
12
|
const handlerStack = [
|
|
13
|
+
hasValue(definition.securitySchemes) && !isEmpty(definition.securitySchemes)
|
|
14
|
+
? buildAuthenticationMiddleware(definition.securitySchemes)
|
|
15
|
+
: null,
|
|
11
16
|
hasValue(definition.querySchema)
|
|
12
17
|
? buildQueryValidatorMiddleware(definition.querySchema)
|
|
13
18
|
: null,
|