vector-framework 0.8.1
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/LICENSE +21 -0
- package/README.md +508 -0
- package/dist/auth/protected.d.ts +9 -0
- package/dist/auth/protected.d.ts.map +1 -0
- package/dist/auth/protected.js +26 -0
- package/dist/auth/protected.js.map +1 -0
- package/dist/cache/manager.d.ts +21 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +92 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +142 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/constants/index.d.ts +84 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +88 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/core/router.d.ts +26 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +208 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/server.d.ts +18 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +89 -0
- package/dist/core/server.js.map +1 -0
- package/dist/core/vector.d.ts +43 -0
- package/dist/core/vector.d.ts.map +1 -0
- package/dist/core/vector.js +179 -0
- package/dist/core/vector.js.map +1 -0
- package/dist/dev/route-generator.d.ts +8 -0
- package/dist/dev/route-generator.d.ts.map +1 -0
- package/dist/dev/route-generator.js +77 -0
- package/dist/dev/route-generator.js.map +1 -0
- package/dist/dev/route-scanner.d.ts +9 -0
- package/dist/dev/route-scanner.d.ts.map +1 -0
- package/dist/dev/route-scanner.js +85 -0
- package/dist/dev/route-scanner.js.map +1 -0
- package/dist/errors/index.d.ts +24 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +73 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http.d.ts +73 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +143 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +21 -0
- package/dist/middleware/manager.d.ts +11 -0
- package/dist/middleware/manager.d.ts.map +1 -0
- package/dist/middleware/manager.js +35 -0
- package/dist/middleware/manager.js.map +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +68 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +48 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +110 -0
- package/src/auth/protected.ts +41 -0
- package/src/cache/manager.ts +133 -0
- package/src/cli/index.ts +157 -0
- package/src/constants/index.ts +93 -0
- package/src/core/router.ts +258 -0
- package/src/core/server.ts +107 -0
- package/src/core/vector.ts +228 -0
- package/src/dev/route-generator.ts +93 -0
- package/src/dev/route-scanner.ts +97 -0
- package/src/errors/index.ts +91 -0
- package/src/http.ts +331 -0
- package/src/index.ts +19 -0
- package/src/middleware/manager.ts +53 -0
- package/src/types/index.ts +126 -0
- package/src/utils/logger.ts +87 -0
- package/src/utils/validation.ts +58 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import type { GeneratedRoute } from '../types';
|
|
4
|
+
|
|
5
|
+
export class RouteScanner {
|
|
6
|
+
private routesDir: string;
|
|
7
|
+
|
|
8
|
+
constructor(routesDir = './routes') {
|
|
9
|
+
this.routesDir = resolve(process.cwd(), routesDir);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async scan(): Promise<GeneratedRoute[]> {
|
|
13
|
+
const routes: GeneratedRoute[] = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await this.scanDirectory(this.routesDir, routes);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if ((error as any).code === 'ENOENT') {
|
|
19
|
+
console.warn(`Routes directory not found: ${this.routesDir}`);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return routes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
|
|
29
|
+
const entries = await readdir(dir);
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = join(dir, entry);
|
|
33
|
+
const stats = await stat(fullPath);
|
|
34
|
+
|
|
35
|
+
if (stats.isDirectory()) {
|
|
36
|
+
const newBasePath = basePath ? `${basePath}/${entry}` : entry;
|
|
37
|
+
await this.scanDirectory(fullPath, routes, newBasePath);
|
|
38
|
+
} else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
|
|
39
|
+
const routePath = relative(this.routesDir, fullPath)
|
|
40
|
+
.replace(/\.(ts|js)$/, '')
|
|
41
|
+
.split(sep)
|
|
42
|
+
.join('/');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Convert Windows paths to URLs for import
|
|
46
|
+
const importPath =
|
|
47
|
+
process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
|
|
48
|
+
|
|
49
|
+
const module = await import(importPath);
|
|
50
|
+
|
|
51
|
+
if (module.default && typeof module.default === 'function') {
|
|
52
|
+
routes.push({
|
|
53
|
+
name: 'default',
|
|
54
|
+
path: fullPath,
|
|
55
|
+
method: 'GET',
|
|
56
|
+
options: {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: `/${routePath}`,
|
|
59
|
+
expose: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [name, value] of Object.entries(module)) {
|
|
65
|
+
if (name === 'default') continue;
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value) && value.length >= 4) {
|
|
68
|
+
const [method, , , path] = value;
|
|
69
|
+
routes.push({
|
|
70
|
+
name,
|
|
71
|
+
path: fullPath,
|
|
72
|
+
method: method as string,
|
|
73
|
+
options: {
|
|
74
|
+
method: method as string,
|
|
75
|
+
path: path as string,
|
|
76
|
+
expose: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`Failed to load route from ${fullPath}:`, error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
enableWatch(callback: () => void) {
|
|
89
|
+
if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
|
|
90
|
+
console.log(`Watching for route changes in ${this.routesDir}`);
|
|
91
|
+
|
|
92
|
+
setInterval(async () => {
|
|
93
|
+
await callback();
|
|
94
|
+
}, 1000);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class VectorError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
public readonly code: string,
|
|
5
|
+
public readonly statusCode?: number
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'VectorError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AuthenticationError extends VectorError {
|
|
13
|
+
constructor(message = 'Authentication failed') {
|
|
14
|
+
super(message, 'AUTH_ERROR', 401);
|
|
15
|
+
this.name = 'AuthenticationError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ValidationError extends VectorError {
|
|
20
|
+
constructor(
|
|
21
|
+
message: string,
|
|
22
|
+
public readonly field?: string
|
|
23
|
+
) {
|
|
24
|
+
super(message, 'VALIDATION_ERROR', 400);
|
|
25
|
+
this.name = 'ValidationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class RouteNotFoundError extends VectorError {
|
|
30
|
+
constructor(path: string) {
|
|
31
|
+
super(`Route not found: ${path}`, 'ROUTE_NOT_FOUND', 404);
|
|
32
|
+
this.name = 'RouteNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ConfigurationError extends VectorError {
|
|
37
|
+
constructor(message: string) {
|
|
38
|
+
super(message, 'CONFIG_ERROR');
|
|
39
|
+
this.name = 'ConfigurationError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ServerError extends VectorError {
|
|
44
|
+
constructor(message = 'Internal server error') {
|
|
45
|
+
super(message, 'SERVER_ERROR', 500);
|
|
46
|
+
this.name = 'ServerError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isVectorError(error: unknown): error is VectorError {
|
|
51
|
+
return error instanceof VectorError;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function handleError(error: unknown): Response {
|
|
55
|
+
if (isVectorError(error)) {
|
|
56
|
+
return new Response(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
error: error.message,
|
|
59
|
+
code: error.code,
|
|
60
|
+
}),
|
|
61
|
+
{
|
|
62
|
+
status: error.statusCode || 500,
|
|
63
|
+
headers: { 'content-type': 'application/json' },
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error instanceof Error) {
|
|
69
|
+
return new Response(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
error: error.message,
|
|
72
|
+
code: 'UNKNOWN_ERROR',
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
status: 500,
|
|
76
|
+
headers: { 'content-type': 'application/json' },
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Response(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
error: 'An unknown error occurred',
|
|
84
|
+
code: 'UNKNOWN_ERROR',
|
|
85
|
+
}),
|
|
86
|
+
{
|
|
87
|
+
status: 500,
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cors,
|
|
3
|
+
type IRequest,
|
|
4
|
+
type RouteEntry,
|
|
5
|
+
withContent,
|
|
6
|
+
withCookies,
|
|
7
|
+
} from "itty-router";
|
|
8
|
+
import { CONTENT_TYPES, HTTP_STATUS } from "./constants";
|
|
9
|
+
import type {
|
|
10
|
+
DefaultVectorTypes,
|
|
11
|
+
GetAuthType,
|
|
12
|
+
VectorRequest,
|
|
13
|
+
VectorTypes,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
export interface ProtectedRequest<
|
|
17
|
+
TTypes extends VectorTypes = DefaultVectorTypes
|
|
18
|
+
> extends IRequest {
|
|
19
|
+
authUser?: GetAuthType<TTypes>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const { preflight, corsify } = cors({
|
|
23
|
+
origin: "*",
|
|
24
|
+
credentials: true,
|
|
25
|
+
allowHeaders: "Content-Type, Authorization",
|
|
26
|
+
allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
27
|
+
exposeHeaders: "Authorization",
|
|
28
|
+
maxAge: 86_400,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
interface ExtendedApiOptions extends ApiOptions {
|
|
32
|
+
method: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function route<TTypes extends VectorTypes = DefaultVectorTypes>(
|
|
37
|
+
options: ExtendedApiOptions,
|
|
38
|
+
fn: (req: VectorRequest<TTypes>) => Promise<unknown>
|
|
39
|
+
): RouteEntry {
|
|
40
|
+
const handler = api(options, fn);
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
options.method.toUpperCase(),
|
|
44
|
+
RegExp(
|
|
45
|
+
`^${
|
|
46
|
+
options.path
|
|
47
|
+
.replace(/\/+(\/|$)/g, "$1") // strip double & trailing splash
|
|
48
|
+
.replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>*))") // greedy params
|
|
49
|
+
.replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))") // named params and image format
|
|
50
|
+
.replace(/\./g, "\\.") // dot in path
|
|
51
|
+
.replace(/(\/?)\*/g, "($1.*)?") // wildcard
|
|
52
|
+
}/*$`
|
|
53
|
+
),
|
|
54
|
+
[handler],
|
|
55
|
+
options.path,
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stringifyData(data: unknown): string {
|
|
60
|
+
return JSON.stringify(data ?? null, (_key, value) =>
|
|
61
|
+
typeof value === "bigint" ? value.toString() : value
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ApiResponse = {
|
|
66
|
+
success: <T>(data: T, contentType?: string) =>
|
|
67
|
+
createResponse(HTTP_STATUS.OK, data, contentType),
|
|
68
|
+
created: <T>(data: T, contentType?: string) =>
|
|
69
|
+
createResponse(HTTP_STATUS.CREATED, data, contentType),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function createErrorResponse(
|
|
73
|
+
code: number,
|
|
74
|
+
message: string,
|
|
75
|
+
contentType?: string
|
|
76
|
+
): Response {
|
|
77
|
+
const errorBody = {
|
|
78
|
+
error: true,
|
|
79
|
+
message,
|
|
80
|
+
statusCode: code,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return createResponse(code, errorBody, contentType);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const APIError = {
|
|
88
|
+
// 4xx Client Errors
|
|
89
|
+
badRequest: (msg = "Bad Request", contentType?: string) =>
|
|
90
|
+
createErrorResponse(HTTP_STATUS.BAD_REQUEST, msg, contentType),
|
|
91
|
+
|
|
92
|
+
unauthorized: (msg = "Unauthorized", contentType?: string) =>
|
|
93
|
+
createErrorResponse(HTTP_STATUS.UNAUTHORIZED, msg, contentType),
|
|
94
|
+
|
|
95
|
+
paymentRequired: (msg = "Payment Required", contentType?: string) =>
|
|
96
|
+
createErrorResponse(402, msg, contentType),
|
|
97
|
+
|
|
98
|
+
forbidden: (msg = "Forbidden", contentType?: string) =>
|
|
99
|
+
createErrorResponse(HTTP_STATUS.FORBIDDEN, msg, contentType),
|
|
100
|
+
|
|
101
|
+
notFound: (msg = "Not Found", contentType?: string) =>
|
|
102
|
+
createErrorResponse(HTTP_STATUS.NOT_FOUND, msg, contentType),
|
|
103
|
+
|
|
104
|
+
methodNotAllowed: (msg = "Method Not Allowed", contentType?: string) =>
|
|
105
|
+
createErrorResponse(405, msg, contentType),
|
|
106
|
+
|
|
107
|
+
notAcceptable: (msg = "Not Acceptable", contentType?: string) =>
|
|
108
|
+
createErrorResponse(406, msg, contentType),
|
|
109
|
+
|
|
110
|
+
requestTimeout: (msg = "Request Timeout", contentType?: string) =>
|
|
111
|
+
createErrorResponse(408, msg, contentType),
|
|
112
|
+
|
|
113
|
+
conflict: (msg = "Conflict", contentType?: string) =>
|
|
114
|
+
createErrorResponse(HTTP_STATUS.CONFLICT, msg, contentType),
|
|
115
|
+
|
|
116
|
+
gone: (msg = "Gone", contentType?: string) =>
|
|
117
|
+
createErrorResponse(410, msg, contentType),
|
|
118
|
+
|
|
119
|
+
lengthRequired: (msg = "Length Required", contentType?: string) =>
|
|
120
|
+
createErrorResponse(411, msg, contentType),
|
|
121
|
+
|
|
122
|
+
preconditionFailed: (msg = "Precondition Failed", contentType?: string) =>
|
|
123
|
+
createErrorResponse(412, msg, contentType),
|
|
124
|
+
|
|
125
|
+
payloadTooLarge: (msg = "Payload Too Large", contentType?: string) =>
|
|
126
|
+
createErrorResponse(413, msg, contentType),
|
|
127
|
+
|
|
128
|
+
uriTooLong: (msg = "URI Too Long", contentType?: string) =>
|
|
129
|
+
createErrorResponse(414, msg, contentType),
|
|
130
|
+
|
|
131
|
+
unsupportedMediaType: (
|
|
132
|
+
msg = "Unsupported Media Type",
|
|
133
|
+
contentType?: string
|
|
134
|
+
) => createErrorResponse(415, msg, contentType),
|
|
135
|
+
|
|
136
|
+
rangeNotSatisfiable: (msg = "Range Not Satisfiable", contentType?: string) =>
|
|
137
|
+
createErrorResponse(416, msg, contentType),
|
|
138
|
+
|
|
139
|
+
expectationFailed: (msg = "Expectation Failed", contentType?: string) =>
|
|
140
|
+
createErrorResponse(417, msg, contentType),
|
|
141
|
+
|
|
142
|
+
imATeapot: (msg = "I'm a teapot", contentType?: string) =>
|
|
143
|
+
createErrorResponse(418, msg, contentType),
|
|
144
|
+
|
|
145
|
+
misdirectedRequest: (msg = "Misdirected Request", contentType?: string) =>
|
|
146
|
+
createErrorResponse(421, msg, contentType),
|
|
147
|
+
|
|
148
|
+
unprocessableEntity: (msg = "Unprocessable Entity", contentType?: string) =>
|
|
149
|
+
createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
|
|
150
|
+
|
|
151
|
+
locked: (msg = "Locked", contentType?: string) =>
|
|
152
|
+
createErrorResponse(423, msg, contentType),
|
|
153
|
+
|
|
154
|
+
failedDependency: (msg = "Failed Dependency", contentType?: string) =>
|
|
155
|
+
createErrorResponse(424, msg, contentType),
|
|
156
|
+
|
|
157
|
+
tooEarly: (msg = "Too Early", contentType?: string) =>
|
|
158
|
+
createErrorResponse(425, msg, contentType),
|
|
159
|
+
|
|
160
|
+
upgradeRequired: (msg = "Upgrade Required", contentType?: string) =>
|
|
161
|
+
createErrorResponse(426, msg, contentType),
|
|
162
|
+
|
|
163
|
+
preconditionRequired: (msg = "Precondition Required", contentType?: string) =>
|
|
164
|
+
createErrorResponse(428, msg, contentType),
|
|
165
|
+
|
|
166
|
+
tooManyRequests: (msg = "Too Many Requests", contentType?: string) =>
|
|
167
|
+
createErrorResponse(429, msg, contentType),
|
|
168
|
+
|
|
169
|
+
requestHeaderFieldsTooLarge: (
|
|
170
|
+
msg = "Request Header Fields Too Large",
|
|
171
|
+
contentType?: string
|
|
172
|
+
) => createErrorResponse(431, msg, contentType),
|
|
173
|
+
|
|
174
|
+
unavailableForLegalReasons: (
|
|
175
|
+
msg = "Unavailable For Legal Reasons",
|
|
176
|
+
contentType?: string
|
|
177
|
+
) => createErrorResponse(451, msg, contentType),
|
|
178
|
+
|
|
179
|
+
// 5xx Server Errors
|
|
180
|
+
internalServerError: (msg = "Internal Server Error", contentType?: string) =>
|
|
181
|
+
createErrorResponse(HTTP_STATUS.INTERNAL_SERVER_ERROR, msg, contentType),
|
|
182
|
+
|
|
183
|
+
notImplemented: (msg = "Not Implemented", contentType?: string) =>
|
|
184
|
+
createErrorResponse(501, msg, contentType),
|
|
185
|
+
|
|
186
|
+
badGateway: (msg = "Bad Gateway", contentType?: string) =>
|
|
187
|
+
createErrorResponse(502, msg, contentType),
|
|
188
|
+
|
|
189
|
+
serviceUnavailable: (msg = "Service Unavailable", contentType?: string) =>
|
|
190
|
+
createErrorResponse(503, msg, contentType),
|
|
191
|
+
|
|
192
|
+
gatewayTimeout: (msg = "Gateway Timeout", contentType?: string) =>
|
|
193
|
+
createErrorResponse(504, msg, contentType),
|
|
194
|
+
|
|
195
|
+
httpVersionNotSupported: (
|
|
196
|
+
msg = "HTTP Version Not Supported",
|
|
197
|
+
contentType?: string
|
|
198
|
+
) => createErrorResponse(505, msg, contentType),
|
|
199
|
+
|
|
200
|
+
variantAlsoNegotiates: (
|
|
201
|
+
msg = "Variant Also Negotiates",
|
|
202
|
+
contentType?: string
|
|
203
|
+
) => createErrorResponse(506, msg, contentType),
|
|
204
|
+
|
|
205
|
+
insufficientStorage: (msg = "Insufficient Storage", contentType?: string) =>
|
|
206
|
+
createErrorResponse(507, msg, contentType),
|
|
207
|
+
|
|
208
|
+
loopDetected: (msg = "Loop Detected", contentType?: string) =>
|
|
209
|
+
createErrorResponse(508, msg, contentType),
|
|
210
|
+
|
|
211
|
+
notExtended: (msg = "Not Extended", contentType?: string) =>
|
|
212
|
+
createErrorResponse(510, msg, contentType),
|
|
213
|
+
|
|
214
|
+
networkAuthenticationRequired: (
|
|
215
|
+
msg = "Network Authentication Required",
|
|
216
|
+
contentType?: string
|
|
217
|
+
) => createErrorResponse(511, msg, contentType),
|
|
218
|
+
|
|
219
|
+
// Aliases for common use cases
|
|
220
|
+
invalidArgument: (msg = "Invalid Argument", contentType?: string) =>
|
|
221
|
+
createErrorResponse(HTTP_STATUS.UNPROCESSABLE_ENTITY, msg, contentType),
|
|
222
|
+
|
|
223
|
+
rateLimitExceeded: (msg = "Rate Limit Exceeded", contentType?: string) =>
|
|
224
|
+
createErrorResponse(429, msg, contentType),
|
|
225
|
+
|
|
226
|
+
maintenance: (msg = "Service Under Maintenance", contentType?: string) =>
|
|
227
|
+
createErrorResponse(503, msg, contentType),
|
|
228
|
+
|
|
229
|
+
// Helper to create custom error with any status code
|
|
230
|
+
custom: (statusCode: number, msg: string, contentType?: string) =>
|
|
231
|
+
createErrorResponse(statusCode, msg, contentType),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export function createResponse(
|
|
235
|
+
statusCode: number,
|
|
236
|
+
data?: unknown,
|
|
237
|
+
contentType: string = CONTENT_TYPES.JSON
|
|
238
|
+
): Response {
|
|
239
|
+
const body = contentType === CONTENT_TYPES.JSON ? stringifyData(data) : data;
|
|
240
|
+
|
|
241
|
+
return new Response(body as string, {
|
|
242
|
+
status: statusCode,
|
|
243
|
+
headers: { "content-type": contentType },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const protectedRoute = async <
|
|
248
|
+
TTypes extends VectorTypes = DefaultVectorTypes
|
|
249
|
+
>(
|
|
250
|
+
request: VectorRequest<TTypes>,
|
|
251
|
+
responseContentType?: string
|
|
252
|
+
) => {
|
|
253
|
+
// Get the Vector instance to access the protected handler
|
|
254
|
+
const vector = (await import("./core/vector")).default;
|
|
255
|
+
|
|
256
|
+
if (!vector.protected) {
|
|
257
|
+
throw APIError.unauthorized(
|
|
258
|
+
"Authentication not configured",
|
|
259
|
+
responseContentType
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const authUser = await vector.protected(request as any);
|
|
265
|
+
request.authUser = authUser as GetAuthType<TTypes>;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw APIError.unauthorized(
|
|
268
|
+
error instanceof Error ? error.message : "Authentication failed",
|
|
269
|
+
responseContentType
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export interface ApiOptions {
|
|
275
|
+
auth?: boolean;
|
|
276
|
+
expose?: boolean;
|
|
277
|
+
rawRequest?: boolean;
|
|
278
|
+
rawResponse?: boolean;
|
|
279
|
+
cache?: number | null;
|
|
280
|
+
responseContentType?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function api<TTypes extends VectorTypes = DefaultVectorTypes>(
|
|
284
|
+
options: ApiOptions,
|
|
285
|
+
fn: (request: VectorRequest<TTypes>) => Promise<unknown>
|
|
286
|
+
) {
|
|
287
|
+
const {
|
|
288
|
+
auth = false,
|
|
289
|
+
expose = false,
|
|
290
|
+
rawRequest = false,
|
|
291
|
+
rawResponse = false,
|
|
292
|
+
responseContentType = CONTENT_TYPES.JSON,
|
|
293
|
+
} = options;
|
|
294
|
+
|
|
295
|
+
return async (request: IRequest) => {
|
|
296
|
+
if (!expose) {
|
|
297
|
+
return APIError.forbidden("Forbidden");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
if (auth) {
|
|
302
|
+
await protectedRoute(
|
|
303
|
+
request as any as VectorRequest<TTypes>,
|
|
304
|
+
responseContentType
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!rawRequest) {
|
|
309
|
+
await withContent(request);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
withCookies(request);
|
|
313
|
+
|
|
314
|
+
// Cache handling is now done in the router
|
|
315
|
+
const result = await fn(request as any as VectorRequest<TTypes>);
|
|
316
|
+
|
|
317
|
+
return rawResponse
|
|
318
|
+
? result
|
|
319
|
+
: ApiResponse.success(result, responseContentType);
|
|
320
|
+
} catch (err: unknown) {
|
|
321
|
+
// Ensure we return a Response object
|
|
322
|
+
if (err instanceof Response) {
|
|
323
|
+
return err;
|
|
324
|
+
}
|
|
325
|
+
// For non-Response errors, wrap them
|
|
326
|
+
return APIError.internalServerError(String(err), responseContentType);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export default ApiResponse;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Vector } from './core/vector';
|
|
2
|
+
import { route } from './http';
|
|
3
|
+
import type { DefaultVectorTypes, VectorTypes } from './types';
|
|
4
|
+
|
|
5
|
+
export { route, Vector };
|
|
6
|
+
export { AuthManager } from './auth/protected';
|
|
7
|
+
export { CacheManager } from './cache/manager';
|
|
8
|
+
export { APIError, createResponse } from './http';
|
|
9
|
+
export { MiddlewareManager } from './middleware/manager';
|
|
10
|
+
export * from './types';
|
|
11
|
+
|
|
12
|
+
// Create a typed Vector instance with custom types
|
|
13
|
+
export function createVector<TTypes extends VectorTypes = DefaultVectorTypes>(): Vector<TTypes> {
|
|
14
|
+
return Vector.getInstance<TTypes>();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Default vector instance with default AuthUser type
|
|
18
|
+
const vector = Vector.getInstance();
|
|
19
|
+
export default vector;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AfterMiddlewareHandler,
|
|
3
|
+
BeforeMiddlewareHandler,
|
|
4
|
+
DefaultVectorTypes,
|
|
5
|
+
VectorRequest,
|
|
6
|
+
VectorTypes,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
export class MiddlewareManager<TTypes extends VectorTypes = DefaultVectorTypes> {
|
|
10
|
+
private beforeHandlers: BeforeMiddlewareHandler<TTypes>[] = [];
|
|
11
|
+
private finallyHandlers: AfterMiddlewareHandler<TTypes>[] = [];
|
|
12
|
+
|
|
13
|
+
addBefore(...handlers: BeforeMiddlewareHandler<TTypes>[]): void {
|
|
14
|
+
this.beforeHandlers.push(...handlers);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
addFinally(...handlers: AfterMiddlewareHandler<TTypes>[]): void {
|
|
18
|
+
this.finallyHandlers.push(...handlers);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async executeBefore(request: VectorRequest<TTypes>): Promise<VectorRequest<TTypes> | Response> {
|
|
22
|
+
let currentRequest = request;
|
|
23
|
+
|
|
24
|
+
for (const handler of this.beforeHandlers) {
|
|
25
|
+
const result = await handler(currentRequest);
|
|
26
|
+
|
|
27
|
+
if (result instanceof Response) {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
currentRequest = result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return currentRequest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async executeFinally(response: Response, request: VectorRequest<TTypes>): Promise<Response> {
|
|
38
|
+
let currentResponse = response;
|
|
39
|
+
|
|
40
|
+
for (const handler of this.finallyHandlers) {
|
|
41
|
+
currentResponse = await handler(currentResponse, request);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return currentResponse;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clone(): MiddlewareManager<TTypes> {
|
|
48
|
+
const manager = new MiddlewareManager<TTypes>();
|
|
49
|
+
manager.beforeHandlers = [...this.beforeHandlers];
|
|
50
|
+
manager.finallyHandlers = [...this.finallyHandlers];
|
|
51
|
+
return manager;
|
|
52
|
+
}
|
|
53
|
+
}
|