@toapi/server 0.12.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/dist/cache.d.ts +32 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +21 -0
- package/dist/create-local-client.d.ts +6 -0
- package/dist/create-local-client.d.ts.map +1 -0
- package/dist/create-local-client.js +19 -0
- package/dist/create-request-handler.d.ts +17 -0
- package/dist/create-request-handler.d.ts.map +1 -0
- package/dist/create-request-handler.js +289 -0
- package/dist/define-api.d.ts +26 -0
- package/dist/define-api.d.ts.map +1 -0
- package/dist/define-api.js +23 -0
- package/dist/define-api.mock.d.ts +86 -0
- package/dist/define-api.mock.d.ts.map +1 -0
- package/dist/define-api.mock.js +127 -0
- package/dist/define-handler.d.ts +4 -0
- package/dist/define-handler.d.ts.map +1 -0
- package/dist/define-handler.js +6 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/openapi.d.ts +14 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +49 -0
- package/dist/request-handler.mock.d.ts +2 -0
- package/dist/request-handler.mock.d.ts.map +1 -0
- package/dist/request-handler.mock.js +3 -0
- package/dist/revalidation-stream.d.ts +7 -0
- package/dist/revalidation-stream.d.ts.map +1 -0
- package/dist/revalidation-stream.js +39 -0
- package/package.json +46 -0
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Json = string | number | boolean | null | Json[] | {
|
|
2
|
+
[key: string]: Json;
|
|
3
|
+
};
|
|
4
|
+
export interface CacheEntry {
|
|
5
|
+
data?: Json | null;
|
|
6
|
+
attachment?: Uint8Array | null;
|
|
7
|
+
}
|
|
8
|
+
export type Subscription = (tags: string[], meta?: Json) => void;
|
|
9
|
+
export interface Cache {
|
|
10
|
+
get(key: string): Promise<CacheEntry | null>;
|
|
11
|
+
set(input: CacheEntry & {
|
|
12
|
+
key: string;
|
|
13
|
+
ttl: number;
|
|
14
|
+
tags: string[];
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
delete(tags: string[], meta?: Json): Promise<void>;
|
|
17
|
+
subscribe(callback: Subscription): () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare class PubSub implements Cache {
|
|
20
|
+
subscribers: Set<Subscription>;
|
|
21
|
+
get(_key: string): Promise<CacheEntry | null>;
|
|
22
|
+
set(_: CacheEntry & {
|
|
23
|
+
key: string;
|
|
24
|
+
ttl: number;
|
|
25
|
+
tags: string[];
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
delete(tags: string[], meta?: {
|
|
28
|
+
clientId?: string;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
subscribe(callback: Subscription): () => void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GACZ,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,IAAI,EAAE,GACN;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,IAAI,CAAC;AAEjE,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC7C,GAAG,CACD,KAAK,EAAE,UAAU,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI,CAAC;CAC/C;AAED,qBAAa,MAAO,YAAW,KAAK;IAClC,WAAW,oBAA2B;IAEhC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAInD,GAAG,CACD,CAAC,EAAE,UAAU,GAAG;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GACA,OAAO,CAAC,IAAI,CAAC;IAIhB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnE,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;CAO9C"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class PubSub {
|
|
2
|
+
subscribers = new Set();
|
|
3
|
+
async get(_key) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
set(_) {
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
}
|
|
9
|
+
delete(tags, meta) {
|
|
10
|
+
for (const callback of this.subscribers) {
|
|
11
|
+
callback(tags, meta ?? {});
|
|
12
|
+
}
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
}
|
|
15
|
+
subscribe(callback) {
|
|
16
|
+
this.subscribers.add(callback);
|
|
17
|
+
return () => {
|
|
18
|
+
this.subscribers.delete(callback);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ApiDefinition } from "./define-api.js";
|
|
2
|
+
import type { Path } from "@toapi/common";
|
|
3
|
+
import type { BaseRoute } from "@toapi/common";
|
|
4
|
+
import type { MaybePromise } from "@toapi/common";
|
|
5
|
+
export declare function createLocalClient<Routes extends Record<Path, MaybePromise<BaseRoute>>>(api: ApiDefinition<Routes>, init?: RequestInit): import("../../2-toapi-client/dist/index.js").Client<Routes>;
|
|
6
|
+
//# sourceMappingURL=create-local-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-local-client.d.ts","sourceRoot":"","sources":["../src/create-local-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,wBAAgB,iBAAiB,CAC/B,MAAM,SAAS,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,EACpD,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,GAAE,WAAgB,+DAgBnD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFetchClient } from "../../2-toapi-client/dist/index.js";
|
|
2
|
+
import { createRequestHandler } from "./create-request-handler.js";
|
|
3
|
+
export function createLocalClient(api, init = {}) {
|
|
4
|
+
const handler = createRequestHandler(api);
|
|
5
|
+
return createFetchClient("http://localhost", {
|
|
6
|
+
fetch(url, localInit) {
|
|
7
|
+
const headers = new Headers(init.headers);
|
|
8
|
+
new Headers(localInit.headers).forEach((value, key) => {
|
|
9
|
+
headers.set(key, value);
|
|
10
|
+
});
|
|
11
|
+
const req = new Request(url, {
|
|
12
|
+
...init,
|
|
13
|
+
...localInit,
|
|
14
|
+
headers,
|
|
15
|
+
});
|
|
16
|
+
return handler(req);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MaybePromise } from "@toapi/common";
|
|
2
|
+
import type { Path as BasePath } from "@toapi/common";
|
|
3
|
+
import type { BaseRoute } from "@toapi/common";
|
|
4
|
+
import type { ApiDefinition } from "./define-api.js";
|
|
5
|
+
import type { Handler } from "@toapi/common";
|
|
6
|
+
import type { TRequest } from "@toapi/common";
|
|
7
|
+
interface Options {
|
|
8
|
+
/** the root path for all API routes */
|
|
9
|
+
basePath?: string;
|
|
10
|
+
/** the default maximum time-to-live (TTL) for cached responses */
|
|
11
|
+
defaultTTL?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function createRequestHandler(api: ApiDefinition<Record<BasePath, MaybePromise<BaseRoute>>>, options?: Options): (req: Request) => Promise<Response>;
|
|
14
|
+
export declare function compilePathRegex(path: string): RegExp;
|
|
15
|
+
export declare function executeHandler<Body>(handler: Handler<any, any, any, Body>, req: TRequest<any, any, any, Body>): Promise<import("@toapi/common").TResponse<any>>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=create-request-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-request-handler.d.ts","sourceRoot":"","sources":["../src/create-request-handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAK9C,UAAU,OAAO;IACf,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,EAC7D,OAAO,GAAE,OAAY,IAmBP,KAAK,OAAO,uBAqL3B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAuGD,wBAAsB,cAAc,CAAC,IAAI,EACvC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EACrC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,mDAOnC"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { ZodError, z } from "zod/v4";
|
|
2
|
+
import { INVALIDATIONS_ROUTE, OPENAPI_ROUTE, SESSION_COOKIE_NAME, } from "@toapi/common";
|
|
3
|
+
import { HttpError } from "@toapi/common";
|
|
4
|
+
import { CookieStore } from "@toapi/common";
|
|
5
|
+
import { generateOpenAPISchema } from "./openapi.js";
|
|
6
|
+
import { streamRevalidatedTags } from "./revalidation-stream.js";
|
|
7
|
+
const DEFAULT_TTL = 60 * 60 * 24 * 14;
|
|
8
|
+
const authUsed = Symbol("TApi.authUsed");
|
|
9
|
+
const headersSchema = z
|
|
10
|
+
.tuple([z.string(), z.string()])
|
|
11
|
+
.array()
|
|
12
|
+
.optional()
|
|
13
|
+
.nullable();
|
|
14
|
+
export function createRequestHandler(api, options = {}) {
|
|
15
|
+
const errorLog = api.logger?.error ??
|
|
16
|
+
((error) => {
|
|
17
|
+
console.error(error);
|
|
18
|
+
});
|
|
19
|
+
const basePath = options.basePath ?? "";
|
|
20
|
+
const routes = [];
|
|
21
|
+
for (const [path, route] of Object.entries(api.routes)) {
|
|
22
|
+
const pattern = compilePathRegex(basePath + path);
|
|
23
|
+
routes.push({ pattern, route });
|
|
24
|
+
}
|
|
25
|
+
let openapiJson;
|
|
26
|
+
return async (req) => {
|
|
27
|
+
const url = new URL(req.url);
|
|
28
|
+
if (url.pathname === `${basePath}${INVALIDATIONS_ROUTE}`) {
|
|
29
|
+
return streamRevalidatedTags({
|
|
30
|
+
cache: api.cache,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (api.oas && url.pathname === `${basePath}${OPENAPI_ROUTE}`) {
|
|
34
|
+
if (!openapiJson) {
|
|
35
|
+
const spec = await generateOpenAPISchema(api, { info: api.oas });
|
|
36
|
+
openapiJson = JSON.stringify(spec);
|
|
37
|
+
}
|
|
38
|
+
return new Response(openapiJson, {
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
for (const { pattern, route: routePromise } of routes) {
|
|
43
|
+
const match = url.pathname.match(pattern);
|
|
44
|
+
const route = await routePromise;
|
|
45
|
+
if (match) {
|
|
46
|
+
const params = match.groups || {};
|
|
47
|
+
switch (req.method) {
|
|
48
|
+
case "HEAD":
|
|
49
|
+
case "GET": {
|
|
50
|
+
try {
|
|
51
|
+
const handler = route[req.method];
|
|
52
|
+
if (!handler)
|
|
53
|
+
return new Response("Not Found", { status: 404 });
|
|
54
|
+
// calls the authorizer, throws if unauthorized
|
|
55
|
+
const treq = await prepareRequestWithoutBody(handler, url, params, req, api.cache);
|
|
56
|
+
try {
|
|
57
|
+
// get matching cache entry
|
|
58
|
+
const cached = await api.cache?.get(req.url);
|
|
59
|
+
if (cached) {
|
|
60
|
+
// serve from cache
|
|
61
|
+
const body = req.method === "HEAD"
|
|
62
|
+
? null
|
|
63
|
+
: new ReadableStream({
|
|
64
|
+
start(controller) {
|
|
65
|
+
controller.enqueue(cached.attachment);
|
|
66
|
+
controller.close();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const headers = await headersSchema.parseAsync(cached.data);
|
|
70
|
+
return new Response(body, {
|
|
71
|
+
headers: headers ?? undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// catches errors while retrieving from cache
|
|
77
|
+
// errors are logged but response is still served
|
|
78
|
+
errorLog(error);
|
|
79
|
+
}
|
|
80
|
+
const res = await executeHandler(handler, treq);
|
|
81
|
+
if (res.cache) {
|
|
82
|
+
if (treq[authUsed] !== true) {
|
|
83
|
+
// cache fresh response according to cache options
|
|
84
|
+
try {
|
|
85
|
+
const cloned = res.clone();
|
|
86
|
+
api.cache
|
|
87
|
+
?.set({
|
|
88
|
+
key: req.url,
|
|
89
|
+
data: Array.from(res.headers.entries()),
|
|
90
|
+
attachment: new Uint8Array(await cloned.arrayBuffer()),
|
|
91
|
+
ttl: res.cache.ttl ?? options.defaultTTL ?? DEFAULT_TTL,
|
|
92
|
+
tags: res.cache.tags ?? [],
|
|
93
|
+
})
|
|
94
|
+
// catches errors while caching if cache.set is async (redis cache)
|
|
95
|
+
.catch(errorLog);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// catches errors while caching if cache.set is sync (in-memory cache)
|
|
99
|
+
errorLog(error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// catches errors while actually handling the request
|
|
107
|
+
await errorLog(error);
|
|
108
|
+
return handleError(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
case "DELETE": {
|
|
112
|
+
const handler = route[req.method];
|
|
113
|
+
if (!handler)
|
|
114
|
+
return new Response("Not Found", { status: 404 });
|
|
115
|
+
try {
|
|
116
|
+
const treq = await prepareRequestWithoutBody(handler, url, params, req, api.cache);
|
|
117
|
+
const res = await executeHandler(handler, treq);
|
|
118
|
+
if (res.cache?.tags) {
|
|
119
|
+
try {
|
|
120
|
+
const clientId = await treq
|
|
121
|
+
.cookies()
|
|
122
|
+
.get(SESSION_COOKIE_NAME);
|
|
123
|
+
api.cache
|
|
124
|
+
?.delete(res.cache.tags, clientId ? { clientId: clientId.value } : undefined)
|
|
125
|
+
.catch(errorLog);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
errorLog(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return res;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
await errorLog(error);
|
|
135
|
+
return handleError(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
case "POST":
|
|
139
|
+
case "PUT":
|
|
140
|
+
case "PATCH": {
|
|
141
|
+
const handler = route[req.method];
|
|
142
|
+
if (!handler)
|
|
143
|
+
return new Response("Not Found", {
|
|
144
|
+
status: 404,
|
|
145
|
+
statusText: "Not Found",
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
const treq = await prepareRequestWithBody(handler, url, params, req, api.cache);
|
|
149
|
+
const res = await executeHandler(handler, treq);
|
|
150
|
+
if (res.cache?.tags) {
|
|
151
|
+
try {
|
|
152
|
+
const clientId = await treq
|
|
153
|
+
.cookies()
|
|
154
|
+
.get(SESSION_COOKIE_NAME);
|
|
155
|
+
api.cache
|
|
156
|
+
?.delete(res.cache.tags, clientId ? { clientId: clientId.value } : undefined)
|
|
157
|
+
.catch(errorLog);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
errorLog(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return res;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
await errorLog(error);
|
|
167
|
+
return handleError(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
return new Response("Not Found", {
|
|
172
|
+
status: 404,
|
|
173
|
+
statusText: "Not Found",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return new Response("Not Found", { status: 404, statusText: "Not Found" });
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function compilePathRegex(path) {
|
|
182
|
+
// Handle wildcards: *name captures as named group, * catches all without capturing
|
|
183
|
+
const pattern = path
|
|
184
|
+
.replaceAll(/\*(\w+)/g, "(?<$1>.+)") // *name -> named capture group
|
|
185
|
+
.replaceAll(/\*/g, ".+") // * -> match everything including /
|
|
186
|
+
.replaceAll(/:(\w+)/g, "(?<$1>[^\\/]+)"); // :param -> named capture group
|
|
187
|
+
return new RegExp(`^${pattern}$`);
|
|
188
|
+
}
|
|
189
|
+
async function prepareRequestWithoutBody(handler, url, params, req, cache) {
|
|
190
|
+
const treq = req;
|
|
191
|
+
treq.params = () => {
|
|
192
|
+
const decodedParams = Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
193
|
+
key,
|
|
194
|
+
decodeURIComponent(value),
|
|
195
|
+
]));
|
|
196
|
+
if (handler.schema.params) {
|
|
197
|
+
const schema = z.object(handler.schema.params);
|
|
198
|
+
return schema.parse(decodedParams);
|
|
199
|
+
}
|
|
200
|
+
treq.params = () => decodedParams;
|
|
201
|
+
return decodedParams;
|
|
202
|
+
};
|
|
203
|
+
treq.query = () => {
|
|
204
|
+
const params = collectData(url.searchParams.entries());
|
|
205
|
+
if (handler.schema.query) {
|
|
206
|
+
const schema = z.object(handler.schema.query);
|
|
207
|
+
return schema.parse(params);
|
|
208
|
+
}
|
|
209
|
+
treq.query = () => params;
|
|
210
|
+
return params;
|
|
211
|
+
};
|
|
212
|
+
treq.cookies = () => {
|
|
213
|
+
const cookieStore = new CookieStore(req);
|
|
214
|
+
treq.cookies = () => cookieStore;
|
|
215
|
+
return cookieStore;
|
|
216
|
+
};
|
|
217
|
+
treq.invalidate = async (tags) => {
|
|
218
|
+
const clientId = await treq.cookies().get(SESSION_COOKIE_NAME);
|
|
219
|
+
cache.delete(tags, clientId ? { clientId: clientId.value } : undefined);
|
|
220
|
+
};
|
|
221
|
+
const auth = await handler.schema.authorize(treq);
|
|
222
|
+
if (!auth) {
|
|
223
|
+
throw new HttpError(401, "Unauthorized");
|
|
224
|
+
}
|
|
225
|
+
treq.auth = () => {
|
|
226
|
+
treq[authUsed] = true;
|
|
227
|
+
return auth;
|
|
228
|
+
};
|
|
229
|
+
return treq;
|
|
230
|
+
}
|
|
231
|
+
async function prepareRequestWithBody(handler, url, params, req, cache) {
|
|
232
|
+
const treq = await prepareRequestWithoutBody(handler, url, params, req, cache);
|
|
233
|
+
if (handler.schema.body) {
|
|
234
|
+
treq.data = async () => handler.schema.body?.parseAsync(await req.json());
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
treq.data = () => {
|
|
238
|
+
console.error("Unexpected call to TRequest.data() method: no body parser specified");
|
|
239
|
+
throw new HttpError(500, "Internal Server Error");
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return treq;
|
|
243
|
+
}
|
|
244
|
+
function collectData(input) {
|
|
245
|
+
const params = {};
|
|
246
|
+
for (const [key, value] of input) {
|
|
247
|
+
if (params[key]) {
|
|
248
|
+
if (Array.isArray(params[key])) {
|
|
249
|
+
params[key].push(value);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
params[key] = [params[key], value];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
params[key] = value;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return params;
|
|
260
|
+
}
|
|
261
|
+
export async function executeHandler(handler, req) {
|
|
262
|
+
const res = await handler.handler(req);
|
|
263
|
+
if (handler.schema.response) {
|
|
264
|
+
await handler.schema.response.parseAsync(res.data);
|
|
265
|
+
}
|
|
266
|
+
return res;
|
|
267
|
+
}
|
|
268
|
+
function handleError(error) {
|
|
269
|
+
if (error instanceof ZodError) {
|
|
270
|
+
return Response.json(error.issues, {
|
|
271
|
+
status: 400,
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": "application/json+zodissues",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (error instanceof HttpError) {
|
|
278
|
+
return Response.json({
|
|
279
|
+
message: error.message,
|
|
280
|
+
data: error.data,
|
|
281
|
+
}, {
|
|
282
|
+
status: error.status,
|
|
283
|
+
headers: {
|
|
284
|
+
"Content-Type": "application/json+httperror",
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
289
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MaybePromise } from "@toapi/common";
|
|
2
|
+
import type { Logger } from "@toapi/common";
|
|
3
|
+
import type { Path as BasePath, StrictParams } from "@toapi/common";
|
|
4
|
+
import type { Route } from "@toapi/common";
|
|
5
|
+
import { type Cache } from "./cache.js";
|
|
6
|
+
export interface OasInfo {
|
|
7
|
+
title: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
interface Options {
|
|
11
|
+
cache?: Cache;
|
|
12
|
+
oas?: OasInfo;
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
}
|
|
15
|
+
export declare function defineApi(options?: Options): ApiDefinition<{}>;
|
|
16
|
+
export declare class ApiDefinition<Routes extends Record<BasePath, unknown>> {
|
|
17
|
+
routes: Routes;
|
|
18
|
+
cache: Cache;
|
|
19
|
+
oas?: OasInfo | undefined;
|
|
20
|
+
logger?: Logger | undefined;
|
|
21
|
+
constructor(routes: Routes, cache: Cache, oas?: OasInfo | undefined, logger?: Logger | undefined);
|
|
22
|
+
invalidate(tags: string[]): Promise<void>;
|
|
23
|
+
route<Path extends BasePath, GetResponse = never, GetQuery extends Record<string, unknown> = never, PostResponse = never, PostQuery extends Record<string, unknown> = never, PostBody = never, DeleteResponse = never, DeleteQuery extends Record<string, unknown> = never, PutResponse = never, PutQuery extends Record<string, unknown> = never, PutBody = never, PatchResponse = never, PatchQuery extends Record<string, unknown> = never, PatchBody = never>(path: Path, route: MaybePromise<Route<StrictParams<Path>, GetResponse, GetQuery, PostResponse, PostQuery, PostBody, DeleteResponse, DeleteQuery, PutResponse, PutQuery, PutBody, PatchResponse, PatchQuery, PatchBody>>): ApiDefinition<Routes & { [path in Path]: MaybePromise<Route<StrictParams<Path>, GetResponse, GetQuery, PostResponse, PostQuery, PostBody, DeleteResponse, DeleteQuery, PutResponse, PutQuery, PutBody, PatchResponse, PatchQuery, PatchBody>>; }>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=define-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-api.d.ts","sourceRoot":"","sources":["../src/define-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,IAAI,IAAI,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,KAAK,KAAK,EAAU,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,OAAO;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,OAAO,GAAE,OAAY,qBAE9C;AAED,qBAAa,aAAa,CAAC,MAAM,SAAS,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;IAExD,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,KAAK;IACZ,GAAG,CAAC,EAAE,OAAO;IACb,MAAM,CAAC,EAAE,MAAM;gBAHf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,GAAG,CAAC,EAAE,OAAO,YAAA,EACb,MAAM,CAAC,EAAE,MAAM,YAAA;IAGlB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE;IAI/B,KAAK,CACH,IAAI,SAAS,QAAQ,EACrB,WAAW,GAAG,KAAK,EACnB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,EAChD,YAAY,GAAG,KAAK,EACpB,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,EACjD,QAAQ,GAAG,KAAK,EAChB,cAAc,GAAG,KAAK,EACtB,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,EACnD,WAAW,GAAG,KAAK,EACnB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,EAChD,OAAO,GAAG,KAAK,EACf,aAAa,GAAG,KAAK,EACrB,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,EAClD,SAAS,GAAG,KAAK,EAEjB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,YAAY,CACjB,KAAK,CACH,YAAY,CAAC,IAAI,CAAC,EAClB,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,cAAc,EACd,WAAW,EACX,WAAW,EACX,QAAQ,EACR,OAAO,EACP,aAAa,EACb,UAAU,EACV,SAAS,CACV,CACF,GAGyB,aAAa,CACrC,MAAM,GAAG,GACN,IAAI,IAAI,IAAI,GAAG,YAAY,CAC1B,KAAK,CACH,YAAY,CAAC,IAAI,CAAC,EAClB,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,cAAc,EACd,WAAW,EACX,WAAW,EACX,QAAQ,EACR,OAAO,EACP,aAAa,EACb,UAAU,EACV,SAAS,CACV,CACF,GACF,CACF;CAEJ"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PubSub } from "./cache.js";
|
|
2
|
+
export function defineApi(options = {}) {
|
|
3
|
+
return new ApiDefinition({}, options?.cache ?? new PubSub(), options?.oas, options?.logger);
|
|
4
|
+
}
|
|
5
|
+
export class ApiDefinition {
|
|
6
|
+
routes;
|
|
7
|
+
cache;
|
|
8
|
+
oas;
|
|
9
|
+
logger;
|
|
10
|
+
constructor(routes, cache, oas, logger) {
|
|
11
|
+
this.routes = routes;
|
|
12
|
+
this.cache = cache;
|
|
13
|
+
this.oas = oas;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
async invalidate(tags) {
|
|
17
|
+
await this.cache.delete(tags);
|
|
18
|
+
}
|
|
19
|
+
route(path, route) {
|
|
20
|
+
this.routes[path] = route;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export declare const api: import("./define-api.js").ApiDefinition<{
|
|
2
|
+
"/books": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}[], {}, {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}, {}, {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}, never, never, never, never, never, never, never, never>>;
|
|
12
|
+
} & {
|
|
13
|
+
"/books/:id": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{
|
|
14
|
+
id: string;
|
|
15
|
+
}, {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
}, {
|
|
19
|
+
test: string;
|
|
20
|
+
}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
21
|
+
} & {
|
|
22
|
+
"/movies/:id": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{
|
|
23
|
+
id: string;
|
|
24
|
+
}, {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
}, {
|
|
28
|
+
test: string;
|
|
29
|
+
}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
30
|
+
} & {
|
|
31
|
+
"/movies": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, never, never, {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
}, {}, {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
}, never, never, never, never, never, never, never, never>>;
|
|
38
|
+
} & {
|
|
39
|
+
"/formData": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, never, never, {
|
|
40
|
+
[k: string]: FormDataEntryValue;
|
|
41
|
+
}, {}, undefined, never, never, never, never, never, never, never, never>>;
|
|
42
|
+
} & {
|
|
43
|
+
"/authorized": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
44
|
+
authorized: boolean;
|
|
45
|
+
}, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
46
|
+
} & {
|
|
47
|
+
"/files/*path": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{
|
|
48
|
+
path: string;
|
|
49
|
+
}, {
|
|
50
|
+
path: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
53
|
+
} & {
|
|
54
|
+
"/wildcard/*": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
55
|
+
pathname: string;
|
|
56
|
+
}, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
57
|
+
} & {
|
|
58
|
+
"/method": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
59
|
+
method: string;
|
|
60
|
+
}, {}, {
|
|
61
|
+
method: string;
|
|
62
|
+
}, {}, undefined, {
|
|
63
|
+
method: string;
|
|
64
|
+
}, {}, {
|
|
65
|
+
method: string;
|
|
66
|
+
}, {}, undefined, {
|
|
67
|
+
method: string;
|
|
68
|
+
}, {}, undefined>>;
|
|
69
|
+
} & {
|
|
70
|
+
"/error/not-found": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, unknown, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
71
|
+
} & {
|
|
72
|
+
"/cached": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
73
|
+
data: string;
|
|
74
|
+
}, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
75
|
+
} & {
|
|
76
|
+
"/stream": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, AsyncGenerator<{
|
|
77
|
+
value: number;
|
|
78
|
+
}, void, unknown>, {}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
79
|
+
} & {
|
|
80
|
+
"/optionalQuery": import("@toapi/common").MaybePromise<import("@toapi/common").Route<{}, {
|
|
81
|
+
optional: string | undefined;
|
|
82
|
+
}, {
|
|
83
|
+
optional: string | undefined;
|
|
84
|
+
}, never, never, never, never, never, never, never, never, never, never, never>>;
|
|
85
|
+
}>;
|
|
86
|
+
//# sourceMappingURL=define-api.mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-api.mock.d.ts","sourceRoot":"","sources":["../src/define-api.mock.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwLZ,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { defineApi } from "./define-api.js";
|
|
3
|
+
import { defineHandler } from "./define-handler.js";
|
|
4
|
+
import { TResponse } from "@toapi/common";
|
|
5
|
+
import { HttpError } from "@toapi/common";
|
|
6
|
+
export const api = defineApi()
|
|
7
|
+
.route("/books", {
|
|
8
|
+
GET: defineHandler({ authorize: () => true }, async () => TResponse.json([
|
|
9
|
+
{ id: "1", title: "Book 1" },
|
|
10
|
+
{ id: "2", title: "Book 2" },
|
|
11
|
+
])),
|
|
12
|
+
POST: defineHandler({
|
|
13
|
+
authorize: () => true,
|
|
14
|
+
body: z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
title: z.string(),
|
|
17
|
+
}),
|
|
18
|
+
}, async (req) => TResponse.json(await req.data())),
|
|
19
|
+
})
|
|
20
|
+
.route("/books/:id", {
|
|
21
|
+
GET: defineHandler({
|
|
22
|
+
params: { id: z.string() },
|
|
23
|
+
query: { test: z.string() },
|
|
24
|
+
authorize: () => true,
|
|
25
|
+
}, async (req) => TResponse.json({
|
|
26
|
+
id: req.params().id,
|
|
27
|
+
title: `Book ${req.params().id}`,
|
|
28
|
+
})),
|
|
29
|
+
})
|
|
30
|
+
.route("/movies/:id", {
|
|
31
|
+
GET: defineHandler({
|
|
32
|
+
params: { id: z.string() },
|
|
33
|
+
query: { test: z.string() },
|
|
34
|
+
authorize: () => true,
|
|
35
|
+
}, async (req) => TResponse.json({
|
|
36
|
+
id: req.params().id,
|
|
37
|
+
title: `Movie ${req.params().id}`,
|
|
38
|
+
}, {
|
|
39
|
+
cache: {
|
|
40
|
+
tags: ["movies"],
|
|
41
|
+
},
|
|
42
|
+
})),
|
|
43
|
+
})
|
|
44
|
+
.route("/movies", {
|
|
45
|
+
POST: defineHandler({
|
|
46
|
+
body: z.object({ id: z.string(), title: z.string() }),
|
|
47
|
+
authorize: () => true,
|
|
48
|
+
}, async (req) => TResponse.json(await req.data(), { cache: { tags: ["movies"] } })),
|
|
49
|
+
})
|
|
50
|
+
.route("/formData", {
|
|
51
|
+
POST: defineHandler({
|
|
52
|
+
authorize: () => true,
|
|
53
|
+
}, async (req) => TResponse.json(Object.fromEntries(await req.formData()), {
|
|
54
|
+
cache: { tags: ["movies"] },
|
|
55
|
+
})),
|
|
56
|
+
})
|
|
57
|
+
.route("/authorized", {
|
|
58
|
+
GET: defineHandler({
|
|
59
|
+
authorize: (req) => {
|
|
60
|
+
const header = req.headers.get("Authorization");
|
|
61
|
+
return header === "Bearer token";
|
|
62
|
+
},
|
|
63
|
+
}, async () => TResponse.json({ authorized: true })),
|
|
64
|
+
})
|
|
65
|
+
.route("/files/*path", {
|
|
66
|
+
GET: defineHandler({
|
|
67
|
+
params: { path: z.string() },
|
|
68
|
+
authorize: () => true,
|
|
69
|
+
}, async (req) => TResponse.json({
|
|
70
|
+
path: req.params().path,
|
|
71
|
+
message: `Accessing file: ${req.params().path}`,
|
|
72
|
+
})),
|
|
73
|
+
})
|
|
74
|
+
.route("/wildcard/*", {
|
|
75
|
+
GET: defineHandler({
|
|
76
|
+
authorize: () => true,
|
|
77
|
+
}, async (req) => TResponse.json({ pathname: req.url })),
|
|
78
|
+
})
|
|
79
|
+
.route("/method", {
|
|
80
|
+
GET: defineHandler({
|
|
81
|
+
authorize: () => true,
|
|
82
|
+
}, async () => TResponse.json({ method: "GET" })),
|
|
83
|
+
POST: defineHandler({
|
|
84
|
+
authorize: () => true,
|
|
85
|
+
}, async () => TResponse.json({ method: "POST" })),
|
|
86
|
+
PUT: defineHandler({
|
|
87
|
+
authorize: () => true,
|
|
88
|
+
}, async () => TResponse.json({ method: "PUT" })),
|
|
89
|
+
PATCH: defineHandler({
|
|
90
|
+
authorize: () => true,
|
|
91
|
+
}, async () => TResponse.json({ method: "PATCH" })),
|
|
92
|
+
DELETE: defineHandler({
|
|
93
|
+
authorize: () => true,
|
|
94
|
+
}, async () => TResponse.json({ method: "DELETE" })),
|
|
95
|
+
})
|
|
96
|
+
.route("/error/not-found", {
|
|
97
|
+
GET: defineHandler({
|
|
98
|
+
authorize: () => true,
|
|
99
|
+
}, async () => {
|
|
100
|
+
throw new HttpError(404, "Not Found");
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
.route("/cached", {
|
|
104
|
+
GET: defineHandler({ authorize: () => true }, async () => TResponse.json({ data: "cached" }, { cache: { tags: ["cached"], ttl: 60 } })),
|
|
105
|
+
})
|
|
106
|
+
.route("/stream", {
|
|
107
|
+
GET: defineHandler({
|
|
108
|
+
authorize: () => true,
|
|
109
|
+
}, async () => {
|
|
110
|
+
async function* stream() {
|
|
111
|
+
for (let i = 0; i < 5; i++) {
|
|
112
|
+
yield { value: i };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return TResponse.ndjson(stream());
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
.route("/optionalQuery", {
|
|
119
|
+
GET: defineHandler({
|
|
120
|
+
query: {
|
|
121
|
+
optional: z.string().optional(),
|
|
122
|
+
},
|
|
123
|
+
authorize: () => true,
|
|
124
|
+
}, async (req) => {
|
|
125
|
+
return TResponse.json(req.query());
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Handler, HandlerFn } from "@toapi/common";
|
|
2
|
+
import type { Schema } from "@toapi/common";
|
|
3
|
+
export declare function defineHandler<Response, AuthData, Params extends Record<string, string> = {}, Query extends Record<string, unknown> = {}, Body = undefined>(schema: Schema<Response, AuthData, Params, Query, Body>, handler: HandlerFn<Response, AuthData, Params, Query, Body>): Handler<Response, Params, Query, Body>;
|
|
4
|
+
//# sourceMappingURL=define-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-handler.d.ts","sourceRoot":"","sources":["../src/define-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,wBAAgB,aAAa,CAC3B,QAAQ,EACR,QAAQ,EACR,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,EAC1C,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,EAC1C,IAAI,GAAG,SAAS,EAEhB,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,EACvD,OAAO,EAAE,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,GAC1D,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAKxC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { createRequestHandler } from "./create-request-handler.js";
|
|
2
|
+
export { createLocalClient } from "./create-local-client.js";
|
|
3
|
+
export { defineApi } from "./define-api.js";
|
|
4
|
+
export { defineHandler } from "./define-handler.js";
|
|
5
|
+
export { HttpError } from "@toapi/common";
|
|
6
|
+
export { TResponse } from "@toapi/common";
|
|
7
|
+
export type { TRequest } from "@toapi/common";
|
|
8
|
+
export { generateOpenAPISchema } from "./openapi.js";
|
|
9
|
+
export type { ApiDefinition } from "./define-api.js";
|
|
10
|
+
export type { CookieStore } from "@toapi/common";
|
|
11
|
+
export { streamRevalidatedTags } from "./revalidation-stream.js";
|
|
12
|
+
export { PubSub, type Cache } from "./cache.js";
|
|
13
|
+
export { INVALIDATIONS_ROUTE } from "@toapi/common";
|
|
14
|
+
export type { Logger } from "@toapi/common";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createRequestHandler } from "./create-request-handler.js";
|
|
2
|
+
export { createLocalClient } from "./create-local-client.js";
|
|
3
|
+
export { defineApi } from "./define-api.js";
|
|
4
|
+
export { defineHandler } from "./define-handler.js";
|
|
5
|
+
export { HttpError } from "@toapi/common";
|
|
6
|
+
export { TResponse } from "@toapi/common";
|
|
7
|
+
export { generateOpenAPISchema } from "./openapi.js";
|
|
8
|
+
export { streamRevalidatedTags } from "./revalidation-stream.js";
|
|
9
|
+
export { PubSub } from "./cache.js";
|
|
10
|
+
export { INVALIDATIONS_ROUTE } from "@toapi/common";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createDocument } from "zod-openapi";
|
|
2
|
+
import type { MaybePromise } from "@toapi/common";
|
|
3
|
+
import type { Path } from "@toapi/common";
|
|
4
|
+
import type { BaseRoute } from "@toapi/common";
|
|
5
|
+
import { ApiDefinition } from "./define-api.js";
|
|
6
|
+
interface Options {
|
|
7
|
+
info: {
|
|
8
|
+
title: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function generateOpenAPISchema(apiDefinition: ApiDefinition<Record<Path, MaybePromise<BaseRoute>>>, options: Options): Promise<ReturnType<typeof createDocument>>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,UAAU,OAAO;IACf,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,EACnE,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,CAgD5C"}
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createDocument } from "zod-openapi";
|
|
2
|
+
import z from "zod/v4";
|
|
3
|
+
import { ApiDefinition } from "./define-api.js";
|
|
4
|
+
export async function generateOpenAPISchema(apiDefinition, options) {
|
|
5
|
+
return createDocument({
|
|
6
|
+
openapi: "3.1.1",
|
|
7
|
+
info: options.info,
|
|
8
|
+
paths: Object.fromEntries(await Promise.all(Object.entries(apiDefinition.routes).map(async ([path, route]) => [
|
|
9
|
+
transformPath(path),
|
|
10
|
+
Object.fromEntries(Object.entries(await route)
|
|
11
|
+
.filter((entry) => entry[1] != null &&
|
|
12
|
+
typeof entry[1] === "object" &&
|
|
13
|
+
"schema" in entry[1] &&
|
|
14
|
+
"handler" in entry[1])
|
|
15
|
+
.map(([method, handler]) => [
|
|
16
|
+
method.toLowerCase(),
|
|
17
|
+
{
|
|
18
|
+
requestParams: {
|
|
19
|
+
path: z.object(handler.schema.params),
|
|
20
|
+
query: z.object(handler.schema.query),
|
|
21
|
+
},
|
|
22
|
+
requestBody: handler.schema.body && {
|
|
23
|
+
content: {
|
|
24
|
+
"application/json": {
|
|
25
|
+
schema: handler.schema.body,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
responses: {
|
|
30
|
+
"200": {
|
|
31
|
+
description: "200 OK",
|
|
32
|
+
content: {
|
|
33
|
+
"application/json": {
|
|
34
|
+
schema: handler.schema.response || z.any(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
])),
|
|
41
|
+
]))),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function transformPath(path) {
|
|
45
|
+
return path
|
|
46
|
+
.split("/")
|
|
47
|
+
.map((segment) => segment.replace(/^:(.+)$/, "{$1}"))
|
|
48
|
+
.join("/");
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-handler.mock.d.ts","sourceRoot":"","sources":["../src/request-handler.mock.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,qCAAkD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revalidation-stream.d.ts","sourceRoot":"","sources":["../src/revalidation-stream.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAIxC,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAED,wBAAgB,qBAAqB,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,YAwCvD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SESSION_COOKIE_NAME, TAGS_CONTENT_TYPE, } from "@toapi/common";
|
|
2
|
+
const KEEPALIVE_INTERVAL = 10 * 1000;
|
|
3
|
+
export function streamRevalidatedTags({ cache }) {
|
|
4
|
+
const id = crypto.randomUUID();
|
|
5
|
+
let interval = null;
|
|
6
|
+
let unsubscribe = () => { };
|
|
7
|
+
const stream = new ReadableStream({
|
|
8
|
+
async start(controller) {
|
|
9
|
+
const textEncoder = new TextEncoder();
|
|
10
|
+
// subscribe to tag invalidations
|
|
11
|
+
unsubscribe = cache.subscribe((tags, meta) => {
|
|
12
|
+
// ignore our own invalidations
|
|
13
|
+
if (meta &&
|
|
14
|
+
typeof meta === "object" &&
|
|
15
|
+
"clientId" in meta &&
|
|
16
|
+
meta.clientId === id)
|
|
17
|
+
return;
|
|
18
|
+
// send tags to client
|
|
19
|
+
controller.enqueue(textEncoder.encode(`${tags.join(" ")}\n`));
|
|
20
|
+
});
|
|
21
|
+
// keepalive
|
|
22
|
+
interval = setInterval(() => {
|
|
23
|
+
controller.enqueue("\n");
|
|
24
|
+
}, KEEPALIVE_INTERVAL);
|
|
25
|
+
},
|
|
26
|
+
cancel() {
|
|
27
|
+
if (interval)
|
|
28
|
+
clearInterval(interval);
|
|
29
|
+
unsubscribe();
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const headers = new Headers({
|
|
33
|
+
"Set-Cookie": `${SESSION_COOKIE_NAME}=${id}; Path=/; HttpOnly; SameSite=Strict`,
|
|
34
|
+
"Content-Type": TAGS_CONTENT_TYPE,
|
|
35
|
+
});
|
|
36
|
+
return new Response(stream, {
|
|
37
|
+
headers,
|
|
38
|
+
});
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toapi/server",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Michel Smola",
|
|
6
|
+
"email": "michel.smola@farbenmeer.de"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"private": false,
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"zod-openapi": "^6.0.0",
|
|
24
|
+
"@toapi/client": "^0.12.1",
|
|
25
|
+
"@toapi/common": "^0.12.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.0.3",
|
|
29
|
+
"vitest": "^4.0.16",
|
|
30
|
+
"zod": "^4.0.17",
|
|
31
|
+
"@farbenmeer/tag-based-cache": "0.3.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"typescript": "^5 || ^6.0.0",
|
|
35
|
+
"zod": "^4.0.17"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/farbenmeer/tapi.git"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc --noEmit false",
|
|
43
|
+
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
44
|
+
"test": "vitest"
|
|
45
|
+
}
|
|
46
|
+
}
|