transit-kit 0.1.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/.github/workflows/ci.yml +73 -0
- package/.github/workflows/release.yml +37 -0
- package/README.md +1 -0
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.js +20 -0
- package/dist/cli/generateOpenApi.d.ts +2 -0
- package/dist/cli/generateOpenApi.js +152 -0
- package/dist/server/constants/HttpMethods.d.ts +10 -0
- package/dist/server/constants/HttpMethods.js +9 -0
- package/dist/server/constants/HttpStatusCodes.d.ts +48 -0
- package/dist/server/constants/HttpStatusCodes.js +27 -0
- package/dist/server/handlers/api/EndpointDefinition.d.ts +16 -0
- package/dist/server/handlers/api/EndpointDefinition.js +1 -0
- package/dist/server/handlers/api/EndpointHandler.d.ts +6 -0
- package/dist/server/handlers/api/EndpointHandler.js +1 -0
- package/dist/server/handlers/api/HandlerFormDefinition.spec.d.ts +1 -0
- package/dist/server/handlers/api/HandlerFormDefinition.spec.js +19 -0
- package/dist/server/handlers/api/HandlerFromDefinition.d.ts +11 -0
- package/dist/server/handlers/api/HandlerFromDefinition.js +1 -0
- package/dist/server/handlers/api/PathParameters.d.ts +5 -0
- package/dist/server/handlers/api/PathParameters.js +1 -0
- package/dist/server/handlers/api/PathParameters.spec.d.ts +1 -0
- package/dist/server/handlers/api/PathParameters.spec.js +12 -0
- package/dist/server/handlers/api/createApiHandler.d.ts +12 -0
- package/dist/server/handlers/api/createApiHandler.js +20 -0
- package/dist/server/handlers/api/responses/emptyResponse.d.ts +7 -0
- package/dist/server/handlers/api/responses/emptyResponse.js +1 -0
- package/dist/server/handlers/api/responses/index.d.ts +7 -0
- package/dist/server/handlers/api/responses/index.js +1 -0
- package/dist/server/handlers/api/responses/jsonResponse.d.ts +14 -0
- package/dist/server/handlers/api/responses/jsonResponse.js +13 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/server/middleware/logging.d.ts +4 -0
- package/dist/server/middleware/logging.js +25 -0
- package/dist/server/middleware/validation.d.ts +4 -0
- package/dist/server/middleware/validation.js +27 -0
- package/dist/server/server.d.ts +21 -0
- package/dist/server/server.js +43 -0
- package/dist/server/utils/funcs.d.ts +1 -0
- package/dist/server/utils/funcs.js +3 -0
- package/dist/server/utils/logging.d.ts +3 -0
- package/dist/server/utils/logging.js +7 -0
- package/dist/server/utils/typeGuards.d.ts +2 -0
- package/dist/server/utils/typeGuards.js +6 -0
- package/dist/server/utils/types.d.ts +3 -0
- package/dist/server/utils/types.js +1 -0
- package/eslint-configs/eslint.base.config.js +30 -0
- package/eslint-configs/eslint.node.config.js +23 -0
- package/eslint-configs/eslint.test.config.js +15 -0
- package/eslint.config.ts +6 -0
- package/package.json +46 -0
- package/prettier.config.js +14 -0
- package/src/cli/cli.ts +37 -0
- package/src/cli/generateOpenApi.ts +217 -0
- package/src/server/constants/HttpMethods.ts +11 -0
- package/src/server/constants/HttpStatusCodes.ts +46 -0
- package/src/server/handlers/api/EndpointDefinition.ts +24 -0
- package/src/server/handlers/api/EndpointHandler.ts +24 -0
- package/src/server/handlers/api/HandlerFormDefinition.spec.ts +120 -0
- package/src/server/handlers/api/HandlerFromDefinition.ts +33 -0
- package/src/server/handlers/api/PathParameters.spec.ts +28 -0
- package/src/server/handlers/api/PathParameters.ts +10 -0
- package/src/server/handlers/api/createApiHandler.ts +44 -0
- package/src/server/handlers/api/responses/emptyResponse.ts +12 -0
- package/src/server/handlers/api/responses/index.ts +15 -0
- package/src/server/handlers/api/responses/jsonResponse.ts +44 -0
- package/src/server/index.ts +4 -0
- package/src/server/middleware/logging.ts +41 -0
- package/src/server/middleware/validation.ts +35 -0
- package/src/server/server.ts +90 -0
- package/src/server/utils/funcs.ts +3 -0
- package/src/server/utils/logging.ts +10 -0
- package/src/server/utils/typeGuards.ts +9 -0
- package/src/server/utils/types.ts +3 -0
- package/transitKit.code-workspace +36 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: 🔍 Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: "20"
|
|
21
|
+
cache: "npm"
|
|
22
|
+
|
|
23
|
+
- name: 📦 Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: ✨ Run linter
|
|
27
|
+
run: npm run lint
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
name: 🧪 Test
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- name: 📥 Checkout code
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: ⚙️ Setup Node.js
|
|
37
|
+
uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: "22"
|
|
40
|
+
cache: "npm"
|
|
41
|
+
|
|
42
|
+
- name: 📦 Install dependencies
|
|
43
|
+
run: npm ci
|
|
44
|
+
|
|
45
|
+
- name: 🧪 Run tests
|
|
46
|
+
run: npm run test -- --run
|
|
47
|
+
|
|
48
|
+
build:
|
|
49
|
+
name: 🏗️ Build
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
needs: [lint, test]
|
|
52
|
+
steps:
|
|
53
|
+
- name: 📥 Checkout code
|
|
54
|
+
uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: ⚙️ Setup Node.js
|
|
57
|
+
uses: actions/setup-node@v4
|
|
58
|
+
with:
|
|
59
|
+
node-version: "20"
|
|
60
|
+
cache: "npm"
|
|
61
|
+
|
|
62
|
+
- name: 📦 Install dependencies
|
|
63
|
+
run: npm ci
|
|
64
|
+
|
|
65
|
+
- name: 🔨 Build package
|
|
66
|
+
run: npm run build
|
|
67
|
+
|
|
68
|
+
- name: 📤 Upload build artifacts
|
|
69
|
+
uses: actions/upload-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: dist
|
|
72
|
+
path: dist/
|
|
73
|
+
retention-days: 7
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
name: 🚀 Publish to npm
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- name: 📥 Checkout code
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: ⚙️ Setup Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: "20"
|
|
22
|
+
registry-url: "https://registry.npmjs.org"
|
|
23
|
+
cache: "npm"
|
|
24
|
+
|
|
25
|
+
- name: 📦 Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: 🧪 Run tests
|
|
29
|
+
run: npm run test -- --run
|
|
30
|
+
|
|
31
|
+
- name: 🔨 Build package
|
|
32
|
+
run: npm run build
|
|
33
|
+
|
|
34
|
+
- name: 🚀 Publish to npm
|
|
35
|
+
run: npm publish --provenance
|
|
36
|
+
env:
|
|
37
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# declarative-server
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { generateOpenApiDoc } from "./generateOpenApi";
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name("transit-kit")
|
|
7
|
+
.description("CLI of the transitKit backend framework")
|
|
8
|
+
.version("1.0.0");
|
|
9
|
+
program
|
|
10
|
+
.command("generate-openapi")
|
|
11
|
+
.option("-o, --output <path>", "Output path for the generated OpenAPI document", "openapi.json")
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
const { output } = options;
|
|
14
|
+
const generatedDoc = await generateOpenApiDoc();
|
|
15
|
+
fs.writeFileSync(output, JSON.stringify(generatedDoc, null, 2), {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
});
|
|
18
|
+
console.log(`OpenAPI document generated at: ${output}`);
|
|
19
|
+
});
|
|
20
|
+
program.parse();
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { hasNoValue, hasValue } from "../server/utils/typeGuards";
|
|
5
|
+
import { isJsonResponseSchema } from "../server/handlers/api/responses/jsonResponse";
|
|
6
|
+
async function findEndpointDefinitions() {
|
|
7
|
+
const files = await glob("**/*.ts", {
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
ignore: ["**/node_modules/**", "**/*.spec.ts"],
|
|
10
|
+
});
|
|
11
|
+
const definitions = await Promise.all(files.map(async (file) => {
|
|
12
|
+
const absolutePath = path.resolve(file);
|
|
13
|
+
const fileUrl = path.toNamespacedPath(absolutePath).startsWith("/")
|
|
14
|
+
? `file://${absolutePath}`
|
|
15
|
+
: `file:///${absolutePath}`;
|
|
16
|
+
try {
|
|
17
|
+
const module = await import(fileUrl);
|
|
18
|
+
if (module.default) {
|
|
19
|
+
const def = module.default.__API_ENDPOINT_DEFINITION__;
|
|
20
|
+
if (hasNoValue(def)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return def;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`Error importing ${file}:`, error);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
return definitions.filter(hasValue);
|
|
35
|
+
}
|
|
36
|
+
function extractPathAndParameters(path) {
|
|
37
|
+
const parameters = path.match(/:([a-zA-Z0-9_]+)/g)?.map((param) => {
|
|
38
|
+
return {
|
|
39
|
+
name: param.substring(1),
|
|
40
|
+
in: "path",
|
|
41
|
+
required: true,
|
|
42
|
+
schema: { type: "string" },
|
|
43
|
+
description: `Path parameter ${param}`,
|
|
44
|
+
};
|
|
45
|
+
}) ?? [];
|
|
46
|
+
const openApiPath = path.replace(/:([a-zA-Z0-9_]+)/g, (_, paramName) => {
|
|
47
|
+
return `{${paramName}}`;
|
|
48
|
+
});
|
|
49
|
+
return { openApiPath, parameters };
|
|
50
|
+
}
|
|
51
|
+
function extractQueryParameters(querySchema) {
|
|
52
|
+
const querySchemaObject = z.toJSONSchema(querySchema);
|
|
53
|
+
if (querySchemaObject.properties) {
|
|
54
|
+
return Object.entries(querySchemaObject.properties).map(([name, schema]) => ({
|
|
55
|
+
name: name,
|
|
56
|
+
in: "query",
|
|
57
|
+
required: querySchemaObject.required?.includes(name) || false,
|
|
58
|
+
schema: schema,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function translateToOpenAPIPathItem(definition) {
|
|
66
|
+
const { meta, path, method, requestBodySchema, querySchema, responseSchemas, } = definition;
|
|
67
|
+
// 1. Path and Parameter extraction
|
|
68
|
+
const { openApiPath, parameters: pathParameters } = extractPathAndParameters(path);
|
|
69
|
+
const queryParameters = hasValue(querySchema)
|
|
70
|
+
? extractQueryParameters(querySchema)
|
|
71
|
+
: [];
|
|
72
|
+
const operationParameters = [...pathParameters, ...queryParameters];
|
|
73
|
+
const requestBody = hasValue(requestBodySchema)
|
|
74
|
+
? {
|
|
75
|
+
requestBody: {
|
|
76
|
+
description: `${meta.name} Request Body`,
|
|
77
|
+
required: true,
|
|
78
|
+
content: {
|
|
79
|
+
"application/json": {
|
|
80
|
+
schema: z.toJSONSchema(requestBodySchema), // Type assertion
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {};
|
|
86
|
+
// 4. Response Schema Translation
|
|
87
|
+
const responses = Object.entries(responseSchemas)
|
|
88
|
+
.map(([statusCode, responseDef]) => {
|
|
89
|
+
if (isJsonResponseSchema(responseDef)) {
|
|
90
|
+
const zodSchema = responseDef.dataSchema;
|
|
91
|
+
const responseSchema = z.toJSONSchema(zodSchema);
|
|
92
|
+
return {
|
|
93
|
+
[statusCode]: {
|
|
94
|
+
description: `Response for status code ${statusCode}`,
|
|
95
|
+
content: {
|
|
96
|
+
[responseDef.dataType]: {
|
|
97
|
+
schema: responseSchema,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return {
|
|
105
|
+
[statusCode]: {
|
|
106
|
+
description: `Response for status code ${statusCode}`,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.reduce((acc, resp) => {
|
|
112
|
+
return { ...acc, ...resp };
|
|
113
|
+
}, {});
|
|
114
|
+
const operation = {
|
|
115
|
+
operationId: meta.name,
|
|
116
|
+
summary: meta.description,
|
|
117
|
+
tags: [meta.group],
|
|
118
|
+
description: meta.description,
|
|
119
|
+
parameters: operationParameters,
|
|
120
|
+
...requestBody,
|
|
121
|
+
responses,
|
|
122
|
+
};
|
|
123
|
+
const pathItem = {
|
|
124
|
+
[method.toLowerCase()]: operation,
|
|
125
|
+
};
|
|
126
|
+
return [openApiPath, pathItem];
|
|
127
|
+
}
|
|
128
|
+
export async function generateOpenApiDoc() {
|
|
129
|
+
const definitions = await findEndpointDefinitions();
|
|
130
|
+
const paths = definitions.reduce((acc, def) => {
|
|
131
|
+
const [openApiPath, pathItem] = translateToOpenAPIPathItem(def);
|
|
132
|
+
if (acc[openApiPath]) {
|
|
133
|
+
acc[openApiPath] = {
|
|
134
|
+
...acc[openApiPath],
|
|
135
|
+
...pathItem,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
acc[openApiPath] = pathItem;
|
|
140
|
+
}
|
|
141
|
+
return acc;
|
|
142
|
+
}, {});
|
|
143
|
+
const openApiDocument = {
|
|
144
|
+
openapi: "3.0.0",
|
|
145
|
+
info: {
|
|
146
|
+
title: "Generated API",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
},
|
|
149
|
+
paths: paths,
|
|
150
|
+
};
|
|
151
|
+
return openApiDocument;
|
|
152
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const HttpMethods: {
|
|
2
|
+
readonly get: "get";
|
|
3
|
+
readonly post: "post";
|
|
4
|
+
readonly put: "put";
|
|
5
|
+
readonly delete: "delete";
|
|
6
|
+
readonly patch: "patch";
|
|
7
|
+
readonly head: "head";
|
|
8
|
+
readonly options: "options";
|
|
9
|
+
};
|
|
10
|
+
export type HttpMethod = (typeof HttpMethods)[keyof typeof HttpMethods];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const SuccessStatusCodes: {
|
|
2
|
+
readonly Ok_200: 200;
|
|
3
|
+
readonly Created_201: 201;
|
|
4
|
+
readonly NoContent_204: 204;
|
|
5
|
+
};
|
|
6
|
+
export type SuccessStatusCode = (typeof SuccessStatusCodes)[keyof typeof SuccessStatusCodes];
|
|
7
|
+
export declare const ClientErrorStatusCodes: {
|
|
8
|
+
readonly BadRequest_400: 400;
|
|
9
|
+
readonly Unauthorized_401: 401;
|
|
10
|
+
readonly Forbidden_403: 403;
|
|
11
|
+
readonly NotFound_404: 404;
|
|
12
|
+
readonly Conflict_409: 409;
|
|
13
|
+
};
|
|
14
|
+
export type ClientErrorStatusCode = (typeof ClientErrorStatusCodes)[keyof typeof ClientErrorStatusCodes];
|
|
15
|
+
export declare const ServerErrorStatusCodes: {
|
|
16
|
+
readonly InternalServerError_500: 500;
|
|
17
|
+
readonly NotImplemented_501: 501;
|
|
18
|
+
readonly BadGateway_502: 502;
|
|
19
|
+
readonly ServiceUnavailable_503: 503;
|
|
20
|
+
};
|
|
21
|
+
export declare const ErrorStatusCodes: {
|
|
22
|
+
readonly InternalServerError_500: 500;
|
|
23
|
+
readonly NotImplemented_501: 501;
|
|
24
|
+
readonly BadGateway_502: 502;
|
|
25
|
+
readonly ServiceUnavailable_503: 503;
|
|
26
|
+
readonly BadRequest_400: 400;
|
|
27
|
+
readonly Unauthorized_401: 401;
|
|
28
|
+
readonly Forbidden_403: 403;
|
|
29
|
+
readonly NotFound_404: 404;
|
|
30
|
+
readonly Conflict_409: 409;
|
|
31
|
+
};
|
|
32
|
+
export type ErrorStatusCode = (typeof ErrorStatusCodes)[keyof typeof ErrorStatusCodes];
|
|
33
|
+
export type ServerErrorStatusCode = (typeof ServerErrorStatusCodes)[keyof typeof ServerErrorStatusCodes];
|
|
34
|
+
export declare const HttpStatusCodes: {
|
|
35
|
+
readonly InternalServerError_500: 500;
|
|
36
|
+
readonly NotImplemented_501: 501;
|
|
37
|
+
readonly BadGateway_502: 502;
|
|
38
|
+
readonly ServiceUnavailable_503: 503;
|
|
39
|
+
readonly BadRequest_400: 400;
|
|
40
|
+
readonly Unauthorized_401: 401;
|
|
41
|
+
readonly Forbidden_403: 403;
|
|
42
|
+
readonly NotFound_404: 404;
|
|
43
|
+
readonly Conflict_409: 409;
|
|
44
|
+
readonly Ok_200: 200;
|
|
45
|
+
readonly Created_201: 201;
|
|
46
|
+
readonly NoContent_204: 204;
|
|
47
|
+
};
|
|
48
|
+
export type HttpStatusCode = (typeof HttpStatusCodes)[keyof typeof HttpStatusCodes];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const SuccessStatusCodes = {
|
|
2
|
+
Ok_200: 200,
|
|
3
|
+
Created_201: 201,
|
|
4
|
+
NoContent_204: 204,
|
|
5
|
+
};
|
|
6
|
+
export const ClientErrorStatusCodes = {
|
|
7
|
+
BadRequest_400: 400,
|
|
8
|
+
Unauthorized_401: 401,
|
|
9
|
+
Forbidden_403: 403,
|
|
10
|
+
NotFound_404: 404,
|
|
11
|
+
Conflict_409: 409,
|
|
12
|
+
};
|
|
13
|
+
export const ServerErrorStatusCodes = {
|
|
14
|
+
InternalServerError_500: 500,
|
|
15
|
+
NotImplemented_501: 501,
|
|
16
|
+
BadGateway_502: 502,
|
|
17
|
+
ServiceUnavailable_503: 503,
|
|
18
|
+
};
|
|
19
|
+
export const ErrorStatusCodes = {
|
|
20
|
+
...ClientErrorStatusCodes,
|
|
21
|
+
...ServerErrorStatusCodes,
|
|
22
|
+
};
|
|
23
|
+
export const HttpStatusCodes = {
|
|
24
|
+
...SuccessStatusCodes,
|
|
25
|
+
...ClientErrorStatusCodes,
|
|
26
|
+
...ServerErrorStatusCodes,
|
|
27
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { HttpMethod } from "../../constants/HttpMethods";
|
|
3
|
+
import { GenericResponseSchemaMap } from "./responses/index";
|
|
4
|
+
export interface ApiEndpointMeta {
|
|
5
|
+
name: string;
|
|
6
|
+
group: string;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
export type ApiEndpointDefinition<Path extends string, Method extends HttpMethod, RequestBody extends z.ZodType | undefined, Query extends z.ZodType | undefined, ResponseMap extends GenericResponseSchemaMap> = {
|
|
10
|
+
meta: ApiEndpointMeta;
|
|
11
|
+
path: Path;
|
|
12
|
+
method: Method;
|
|
13
|
+
requestBodySchema?: RequestBody;
|
|
14
|
+
querySchema?: Query;
|
|
15
|
+
responseSchemas: ResponseMap;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
import { HttpStatusCodes } from "../../constants/HttpStatusCodes";
|
|
3
|
+
import { GenericResponse } from "./responses";
|
|
4
|
+
export type ApiEndpointHandler<PathParams extends Record<string, string> | undefined = {}, RequestBody = unknown, Query = unknown, Responses extends GenericResponse = never> = (request: Request<PathParams, unknown, RequestBody, Query, Record<string, unknown>>) => Promise<Responses | {
|
|
5
|
+
code: (typeof HttpStatusCodes)["InternalServerError_500"];
|
|
6
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
describe("HandlerFromDefinition", () => {
|
|
4
|
+
it("can infer handler responses correctly (Empty)", () => {
|
|
5
|
+
expectTypeOf().toEqualTypeOf();
|
|
6
|
+
});
|
|
7
|
+
it("can infer handler responses correctly (Json)", () => {
|
|
8
|
+
const _data = z.object({
|
|
9
|
+
a: z.number(),
|
|
10
|
+
});
|
|
11
|
+
expectTypeOf().toEqualTypeOf();
|
|
12
|
+
});
|
|
13
|
+
it("can infer handler responses correctly (Multiple)", () => {
|
|
14
|
+
const _data = z.object({
|
|
15
|
+
a: z.number(),
|
|
16
|
+
});
|
|
17
|
+
expectTypeOf().toEqualTypeOf();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { HttpStatusCode } from "../../constants/HttpStatusCodes";
|
|
3
|
+
import { Prettify } from "../../utils/types";
|
|
4
|
+
import { ApiEndpointHandler } from "./EndpointHandler";
|
|
5
|
+
import { ExtractPathParams } from "./PathParameters";
|
|
6
|
+
import { EmptyResponse, EmptyResponseSchema } from "./responses/emptyResponse";
|
|
7
|
+
import { GenericResponseSchemaMap } from "./responses/index";
|
|
8
|
+
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>, Prettify<{
|
|
10
|
+
[K in keyof ResponsesMap]: K extends HttpStatusCode ? ResponsesMap[K] extends JsonResponseSchema ? JsonResponseSchemaToResponseType<K, ResponsesMap[K]> : ResponsesMap[K] extends EmptyResponseSchema ? EmptyResponse<K> : never : never;
|
|
11
|
+
}[keyof ResponsesMap]>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
2
|
+
describe("ExtractPathParameters", () => {
|
|
3
|
+
it("can infer the parameter types of a path", () => {
|
|
4
|
+
expectTypeOf().toEqualTypeOf();
|
|
5
|
+
});
|
|
6
|
+
it("can infer multiple params", () => {
|
|
7
|
+
expectTypeOf().toEqualTypeOf();
|
|
8
|
+
});
|
|
9
|
+
it("can infer correctly if no param is present", () => {
|
|
10
|
+
expectTypeOf().toEqualTypeOf();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { HttpMethod } from "../../constants/HttpMethods";
|
|
3
|
+
import { ApiEndpointDefinition } from "./EndpointDefinition";
|
|
4
|
+
import { ApiEndpointHandler } from "./EndpointHandler";
|
|
5
|
+
import { HandlerForDefinition } from "./HandlerFromDefinition";
|
|
6
|
+
import { GenericResponseSchemaMap } from "./responses";
|
|
7
|
+
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: ApiEndpointDefinition<Path, Method, RequestBody, Query, ResponsesMap>, handler: HandlerForDefinition<Path, RequestBody, Query, ResponsesMap>): {
|
|
8
|
+
__API_ENDPOINT_DEFINITION__: ApiEndpointDefinition<Path, Method, RequestBody, Query, ResponsesMap>;
|
|
9
|
+
definition: ApiEndpointDefinition<Path, Method, RequestBody, Query, ResponsesMap>;
|
|
10
|
+
handler: HandlerForDefinition<Path, RequestBody, Query, ResponsesMap>;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildApiEndpointHandler(handler: ApiEndpointHandler): import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import expressAsyncHandler from "express-async-handler";
|
|
2
|
+
import { isJsonResponse } from "./responses/jsonResponse";
|
|
3
|
+
export function createApiEndpointHandler(definition, handler) {
|
|
4
|
+
return {
|
|
5
|
+
__API_ENDPOINT_DEFINITION__: definition,
|
|
6
|
+
definition,
|
|
7
|
+
handler,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function buildApiEndpointHandler(handler) {
|
|
11
|
+
return expressAsyncHandler(async (request, response) => {
|
|
12
|
+
const result = await handler(request);
|
|
13
|
+
if (isJsonResponse(result)) {
|
|
14
|
+
response.status(result.code).json(result.json);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
response.status(result.code).send();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HttpStatusCode } from "../../../constants/HttpStatusCodes";
|
|
2
|
+
export interface EmptyResponseSchema {
|
|
3
|
+
}
|
|
4
|
+
export interface EmptyResponse<Code extends HttpStatusCode = HttpStatusCode> {
|
|
5
|
+
code: Code;
|
|
6
|
+
}
|
|
7
|
+
export type EmptyResponseSchemaToResponseType<Code extends HttpStatusCode, _ extends EmptyResponseSchema> = EmptyResponse<Code>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ClientErrorStatusCode, SuccessStatusCode } from "../../../constants/HttpStatusCodes";
|
|
2
|
+
import { EmptyResponse, EmptyResponseSchema } from "./emptyResponse";
|
|
3
|
+
import { JsonResponse, JsonResponseSchema } from "./jsonResponse";
|
|
4
|
+
export type GenericResponseSchemaMap = {
|
|
5
|
+
[K in SuccessStatusCode | ClientErrorStatusCode]?: EmptyResponseSchema | JsonResponseSchema | undefined;
|
|
6
|
+
};
|
|
7
|
+
export type GenericResponse = EmptyResponse | JsonResponse;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { HttpStatusCode } from "../../../constants/HttpStatusCodes";
|
|
3
|
+
export interface JsonResponseSchema<DataSchema extends z.ZodType = z.ZodType> {
|
|
4
|
+
dataType: "application/json";
|
|
5
|
+
dataSchema: DataSchema;
|
|
6
|
+
}
|
|
7
|
+
export interface JsonResponse<Code extends HttpStatusCode = HttpStatusCode, Data = unknown> {
|
|
8
|
+
code: Code;
|
|
9
|
+
dataType: "application/json";
|
|
10
|
+
json: Data;
|
|
11
|
+
}
|
|
12
|
+
export type JsonResponseSchemaToResponseType<Code extends HttpStatusCode, Schema extends JsonResponseSchema> = Schema extends JsonResponseSchema<infer DataSchema> ? JsonResponse<Code, z.infer<DataSchema>> : never;
|
|
13
|
+
export declare function isJsonResponseSchema(value: unknown): value is JsonResponseSchema;
|
|
14
|
+
export declare function isJsonResponse(value: unknown): value is JsonResponse;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { hasValue } from "../../../utils/typeGuards";
|
|
2
|
+
export function isJsonResponseSchema(value) {
|
|
3
|
+
return (typeof value === "object" &&
|
|
4
|
+
hasValue(value) &&
|
|
5
|
+
"dataType" in value &&
|
|
6
|
+
value.dataType === "application/json");
|
|
7
|
+
}
|
|
8
|
+
export function isJsonResponse(value) {
|
|
9
|
+
return (typeof value === "object" &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
"dataType" in value &&
|
|
12
|
+
value.dataType === "application/json");
|
|
13
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { Logger } from "../utils/logging";
|
|
3
|
+
export declare function buildRequestLogger(logger: Logger, isInDevMode: boolean): (req: Request, res: Response, next: NextFunction) => void;
|
|
4
|
+
export declare function buildResponseLogger(logger: Logger, isInDevMode: boolean): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import colors from "colors/safe";
|
|
2
|
+
export function buildRequestLogger(logger, isInDevMode) {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
logger.info(`[Request] ${colors.cyan(req.method)} - ${colors.cyan(req.path)}`);
|
|
5
|
+
if (isInDevMode) {
|
|
6
|
+
logger.info(`[Request - Dev] Headers: ${JSON.stringify(req.headers)}`);
|
|
7
|
+
logger.info(`[Request - Dev] Query: ${JSON.stringify(req.query)}`);
|
|
8
|
+
logger.info(`[Request - Dev] Body: ${JSON.stringify(req.body)}`);
|
|
9
|
+
}
|
|
10
|
+
next();
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function buildResponseLogger(logger, isInDevMode) {
|
|
14
|
+
return (req, res, next) => {
|
|
15
|
+
const originalSend = res.send;
|
|
16
|
+
res.json = function (body) {
|
|
17
|
+
logger.info(`[Response] ${colors.cyan(req.method)} - ${colors.cyan(req.path)} - Status: ${res.statusCode > 299 && res.statusCode < 599 ? colors.red(res.statusCode.toString()) : colors.green(res.statusCode.toString())}`);
|
|
18
|
+
if (isInDevMode) {
|
|
19
|
+
logger.info(`[Response - Dev] Body: ${JSON.stringify(body)}`);
|
|
20
|
+
}
|
|
21
|
+
return originalSend.call(this, body);
|
|
22
|
+
};
|
|
23
|
+
next();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { ZodType } from "zod";
|
|
3
|
+
export declare function buildBodyValidatorMiddleware<Schema extends ZodType>(schema: Schema): (request: Request, response: Response, next: NextFunction) => void;
|
|
4
|
+
export declare function buildQueryValidatorMiddleware<Schema extends ZodType>(schema: Schema): (request: Request, response: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HttpStatusCodes } from "../constants/HttpStatusCodes";
|
|
2
|
+
export function buildBodyValidatorMiddleware(schema) {
|
|
3
|
+
return (request, response, next) => {
|
|
4
|
+
const validationResult = schema.safeParse(request.body);
|
|
5
|
+
if (!validationResult.success) {
|
|
6
|
+
response.status(HttpStatusCodes.BadRequest_400).json({
|
|
7
|
+
message: `Body invalid`,
|
|
8
|
+
error: validationResult.error,
|
|
9
|
+
});
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
next();
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function buildQueryValidatorMiddleware(schema) {
|
|
16
|
+
return (request, response, next) => {
|
|
17
|
+
const validationResult = schema.safeParse(request.query);
|
|
18
|
+
if (!validationResult.success) {
|
|
19
|
+
response.status(HttpStatusCodes.BadRequest_400).json({
|
|
20
|
+
message: `Query parameters invalid`,
|
|
21
|
+
error: validationResult.error,
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
}
|