@typokit/core 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/adapters/database.d.ts +28 -0
  2. package/dist/adapters/database.d.ts.map +1 -0
  3. package/dist/adapters/database.js +2 -0
  4. package/dist/adapters/database.js.map +1 -0
  5. package/dist/adapters/server.d.ts +35 -0
  6. package/dist/adapters/server.d.ts.map +1 -0
  7. package/dist/adapters/server.js +2 -0
  8. package/dist/adapters/server.js.map +1 -0
  9. package/dist/app.d.ts +36 -0
  10. package/dist/app.d.ts.map +1 -0
  11. package/dist/app.js +55 -0
  12. package/dist/app.js.map +1 -0
  13. package/dist/error-middleware.d.ts +17 -0
  14. package/dist/error-middleware.d.ts.map +1 -0
  15. package/dist/error-middleware.js +138 -0
  16. package/dist/error-middleware.js.map +1 -0
  17. package/dist/handler.d.ts +41 -0
  18. package/dist/handler.d.ts.map +1 -0
  19. package/dist/handler.js +22 -0
  20. package/dist/handler.js.map +1 -0
  21. package/dist/hooks.d.ts +48 -0
  22. package/dist/hooks.d.ts.map +1 -0
  23. package/dist/hooks.js +64 -0
  24. package/dist/hooks.js.map +1 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +6 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/middleware.d.ts +35 -0
  30. package/dist/middleware.d.ts.map +1 -0
  31. package/dist/middleware.js +54 -0
  32. package/dist/middleware.js.map +1 -0
  33. package/dist/plugin.d.ts +74 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +3 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/package.json +29 -0
  38. package/src/adapters/database.ts +37 -0
  39. package/src/adapters/server.ts +55 -0
  40. package/src/app.test.ts +438 -0
  41. package/src/app.ts +118 -0
  42. package/src/error-middleware.test.ts +263 -0
  43. package/src/error-middleware.ts +186 -0
  44. package/src/handler.test.ts +346 -0
  45. package/src/handler.ts +64 -0
  46. package/src/hooks.test.ts +419 -0
  47. package/src/hooks.ts +114 -0
  48. package/src/index.ts +51 -0
  49. package/src/middleware.test.ts +253 -0
  50. package/src/middleware.ts +100 -0
  51. package/src/plugin.ts +108 -0
@@ -0,0 +1,346 @@
1
+ import { describe, it, expect } from "@rstest/core";
2
+ import { defineHandlers } from "./handler.js";
3
+ import type {
4
+ RouteContract,
5
+ RequestContext,
6
+ PaginatedResponse,
7
+ } from "@typokit/types";
8
+ import { createRequestContext } from "./middleware.js";
9
+
10
+ // ─── Test Route Contracts ────────────────────────────────────
11
+
12
+ interface TestUser {
13
+ id: string;
14
+ name: string;
15
+ email: string;
16
+ }
17
+
18
+ interface CreateUserInput {
19
+ name: string;
20
+ email: string;
21
+ }
22
+
23
+ type UsersRoutes = {
24
+ "GET /users": RouteContract<
25
+ void,
26
+ { page?: number; pageSize?: number },
27
+ void,
28
+ PaginatedResponse<TestUser>
29
+ >;
30
+
31
+ "POST /users": RouteContract<void, void, CreateUserInput, TestUser>;
32
+
33
+ "GET /users/:id": RouteContract<{ id: string }, void, void, TestUser>;
34
+ };
35
+
36
+ // ─── Tests ───────────────────────────────────────────────────
37
+
38
+ describe("defineHandlers", () => {
39
+ it("returns the handlers object unchanged", () => {
40
+ const handlers = defineHandlers<UsersRoutes>({
41
+ "GET /users": async ({ query }) => ({
42
+ data: [],
43
+ pagination: {
44
+ total: 0,
45
+ page: query.page ?? 1,
46
+ pageSize: query.pageSize ?? 10,
47
+ totalPages: 0,
48
+ },
49
+ }),
50
+ "POST /users": async ({ body }) => ({
51
+ id: "new-id",
52
+ name: body.name,
53
+ email: body.email,
54
+ }),
55
+ "GET /users/:id": async ({ params }) => ({
56
+ id: params.id,
57
+ name: "Test User",
58
+ email: "test@example.com",
59
+ }),
60
+ });
61
+
62
+ expect(handlers["GET /users"]).toBeDefined();
63
+ expect(handlers["POST /users"]).toBeDefined();
64
+ expect(handlers["GET /users/:id"]).toBeDefined();
65
+ expect(typeof handlers["GET /users"]).toBe("function");
66
+ expect(typeof handlers["POST /users"]).toBe("function");
67
+ expect(typeof handlers["GET /users/:id"]).toBe("function");
68
+ });
69
+
70
+ it("handler receives params typed from contract", async () => {
71
+ const handlers = defineHandlers<UsersRoutes>({
72
+ "GET /users": async () => ({
73
+ data: [],
74
+ pagination: { total: 0, page: 1, pageSize: 10, totalPages: 0 },
75
+ }),
76
+ "POST /users": async ({ body }) => ({
77
+ id: "1",
78
+ name: body.name,
79
+ email: body.email,
80
+ }),
81
+ "GET /users/:id": async ({ params }) => {
82
+ return {
83
+ id: params.id,
84
+ name: "Found User",
85
+ email: "found@example.com",
86
+ };
87
+ },
88
+ });
89
+
90
+ const ctx = createRequestContext();
91
+ const result = await handlers["GET /users/:id"]({
92
+ params: { id: "user-42" },
93
+ query: undefined as void,
94
+ body: undefined as void,
95
+ ctx,
96
+ });
97
+
98
+ expect(result.id).toBe("user-42");
99
+ expect(result.name).toBe("Found User");
100
+ });
101
+
102
+ it("handler receives query typed from contract", async () => {
103
+ const handlers = defineHandlers<UsersRoutes>({
104
+ "GET /users": async ({ query }) => ({
105
+ data: [],
106
+ pagination: {
107
+ total: 0,
108
+ page: query.page ?? 1,
109
+ pageSize: query.pageSize ?? 10,
110
+ totalPages: 0,
111
+ },
112
+ }),
113
+ "POST /users": async ({ body }) => ({
114
+ id: "1",
115
+ name: body.name,
116
+ email: body.email,
117
+ }),
118
+ "GET /users/:id": async ({ params }) => ({
119
+ id: params.id,
120
+ name: "User",
121
+ email: "user@example.com",
122
+ }),
123
+ });
124
+
125
+ const ctx = createRequestContext();
126
+ const result = await handlers["GET /users"]({
127
+ params: undefined as void,
128
+ query: { page: 3, pageSize: 25 },
129
+ body: undefined as void,
130
+ ctx,
131
+ });
132
+
133
+ expect(result.pagination.page).toBe(3);
134
+ expect(result.pagination.pageSize).toBe(25);
135
+ });
136
+
137
+ it("handler receives body typed from contract", async () => {
138
+ const handlers = defineHandlers<UsersRoutes>({
139
+ "GET /users": async () => ({
140
+ data: [],
141
+ pagination: { total: 0, page: 1, pageSize: 10, totalPages: 0 },
142
+ }),
143
+ "POST /users": async ({ body }) => ({
144
+ id: "new-123",
145
+ name: body.name,
146
+ email: body.email,
147
+ }),
148
+ "GET /users/:id": async ({ params }) => ({
149
+ id: params.id,
150
+ name: "User",
151
+ email: "user@example.com",
152
+ }),
153
+ });
154
+
155
+ const ctx = createRequestContext();
156
+ const result = await handlers["POST /users"]({
157
+ params: undefined as void,
158
+ query: undefined as void,
159
+ body: { name: "Alice", email: "alice@example.com" },
160
+ ctx,
161
+ });
162
+
163
+ expect(result.id).toBe("new-123");
164
+ expect(result.name).toBe("Alice");
165
+ expect(result.email).toBe("alice@example.com");
166
+ });
167
+
168
+ it("handler receives ctx with RequestContext", async () => {
169
+ let receivedCtx: RequestContext | undefined;
170
+
171
+ const handlers = defineHandlers<UsersRoutes>({
172
+ "GET /users": async ({ ctx }) => {
173
+ receivedCtx = ctx;
174
+ return {
175
+ data: [],
176
+ pagination: { total: 0, page: 1, pageSize: 10, totalPages: 0 },
177
+ };
178
+ },
179
+ "POST /users": async ({ body }) => ({
180
+ id: "1",
181
+ name: body.name,
182
+ email: body.email,
183
+ }),
184
+ "GET /users/:id": async ({ params }) => ({
185
+ id: params.id,
186
+ name: "User",
187
+ email: "user@example.com",
188
+ }),
189
+ });
190
+
191
+ const ctx = createRequestContext({ requestId: "req-test-123" });
192
+ await handlers["GET /users"]({
193
+ params: undefined as void,
194
+ query: undefined as unknown as { page?: number; pageSize?: number },
195
+ body: undefined as void,
196
+ ctx,
197
+ });
198
+
199
+ expect(receivedCtx).toBeDefined();
200
+ expect(receivedCtx!.requestId).toBe("req-test-123");
201
+ expect(typeof receivedCtx!.fail).toBe("function");
202
+ expect(typeof receivedCtx!.log.info).toBe("function");
203
+ });
204
+
205
+ it("handler return type matches contract response", async () => {
206
+ const handlers = defineHandlers<UsersRoutes>({
207
+ "GET /users": async () => ({
208
+ data: [{ id: "1", name: "Test", email: "test@test.com" }],
209
+ pagination: { total: 1, page: 1, pageSize: 10, totalPages: 1 },
210
+ }),
211
+ "POST /users": async ({ body }) => ({
212
+ id: "1",
213
+ name: body.name,
214
+ email: body.email,
215
+ }),
216
+ "GET /users/:id": async ({ params }) => ({
217
+ id: params.id,
218
+ name: "User",
219
+ email: "user@example.com",
220
+ }),
221
+ });
222
+
223
+ const ctx = createRequestContext();
224
+ const listResult = await handlers["GET /users"]({
225
+ params: undefined as void,
226
+ query: undefined as unknown as { page?: number; pageSize?: number },
227
+ body: undefined as void,
228
+ ctx,
229
+ });
230
+
231
+ expect(listResult.data).toHaveLength(1);
232
+ expect(listResult.data[0].name).toBe("Test");
233
+ expect(listResult.pagination.total).toBe(1);
234
+ });
235
+
236
+ it("supports synchronous handlers", () => {
237
+ const handlers = defineHandlers<UsersRoutes>({
238
+ "GET /users": () => ({
239
+ data: [],
240
+ pagination: { total: 0, page: 1, pageSize: 10, totalPages: 0 },
241
+ }),
242
+ "POST /users": ({ body }) => ({
243
+ id: "sync-1",
244
+ name: body.name,
245
+ email: body.email,
246
+ }),
247
+ "GET /users/:id": ({ params }) => ({
248
+ id: params.id,
249
+ name: "Sync User",
250
+ email: "sync@example.com",
251
+ }),
252
+ });
253
+
254
+ const ctx = createRequestContext();
255
+ const result = handlers["GET /users/:id"]({
256
+ params: { id: "sync-42" },
257
+ query: undefined as void,
258
+ body: undefined as void,
259
+ ctx,
260
+ });
261
+
262
+ // Synchronous handler returns value directly (not a Promise)
263
+ expect((result as { id: string }).id).toBe("sync-42");
264
+ });
265
+ });
266
+
267
+ describe("defineHandlers with single-route contract", () => {
268
+ type SingleRoute = {
269
+ "DELETE /items/:id": RouteContract<
270
+ { id: string },
271
+ void,
272
+ void,
273
+ { deleted: boolean }
274
+ >;
275
+ };
276
+
277
+ it("works with a single route contract", async () => {
278
+ const handlers = defineHandlers<SingleRoute>({
279
+ "DELETE /items/:id": async ({ params: _params }) => ({
280
+ deleted: true,
281
+ }),
282
+ });
283
+
284
+ const ctx = createRequestContext();
285
+ const result = await handlers["DELETE /items/:id"]({
286
+ params: { id: "item-1" },
287
+ query: undefined as void,
288
+ body: undefined as void,
289
+ ctx,
290
+ });
291
+
292
+ expect(result.deleted).toBe(true);
293
+ });
294
+ });
295
+
296
+ describe("defineHandlers with complex types", () => {
297
+ interface ComplexBody {
298
+ tags: string[];
299
+ metadata: Record<string, unknown>;
300
+ }
301
+
302
+ interface ComplexResponse {
303
+ id: string;
304
+ tags: string[];
305
+ metadata: Record<string, unknown>;
306
+ createdAt: string;
307
+ }
308
+
309
+ type ComplexRoutes = {
310
+ "POST /items": RouteContract<void, void, ComplexBody, ComplexResponse>;
311
+ "GET /items/:id": RouteContract<
312
+ { id: string },
313
+ { include?: string[] },
314
+ void,
315
+ ComplexResponse
316
+ >;
317
+ };
318
+
319
+ it("handles complex body and response types", async () => {
320
+ const handlers = defineHandlers<ComplexRoutes>({
321
+ "POST /items": async ({ body }) => ({
322
+ id: "item-new",
323
+ tags: body.tags,
324
+ metadata: body.metadata,
325
+ createdAt: "2026-01-01T00:00:00Z",
326
+ }),
327
+ "GET /items/:id": async ({ params, query: _query }) => ({
328
+ id: params.id,
329
+ tags: ["tag1"],
330
+ metadata: {},
331
+ createdAt: "2026-01-01T00:00:00Z",
332
+ }),
333
+ });
334
+
335
+ const ctx = createRequestContext();
336
+ const result = await handlers["POST /items"]({
337
+ params: undefined as void,
338
+ query: undefined as void,
339
+ body: { tags: ["a", "b"], metadata: { key: "value" } },
340
+ ctx,
341
+ });
342
+
343
+ expect(result.tags).toEqual(["a", "b"]);
344
+ expect(result.metadata).toEqual({ key: "value" });
345
+ });
346
+ });
package/src/handler.ts ADDED
@@ -0,0 +1,64 @@
1
+ // @typokit/core — Handler System
2
+
3
+ import type { RouteContract, RequestContext } from "@typokit/types";
4
+
5
+ /**
6
+ * Input received by each handler function.
7
+ * Types are inferred from the corresponding RouteContract.
8
+ */
9
+ export type HandlerInput<
10
+ TContract extends RouteContract<unknown, unknown, unknown, unknown>,
11
+ > = {
12
+ params: TContract["params"];
13
+ query: TContract["query"];
14
+ body: TContract["body"];
15
+ ctx: RequestContext;
16
+ };
17
+
18
+ /**
19
+ * A handler function that receives typed input and returns the contract's response type.
20
+ */
21
+ export type HandlerFn<
22
+ TContract extends RouteContract<unknown, unknown, unknown, unknown>,
23
+ > = (
24
+ input: HandlerInput<TContract>,
25
+ ) => Promise<TContract["response"]> | TContract["response"];
26
+
27
+ /**
28
+ * Maps each route key in TRoutes to a handler function whose input/output
29
+ * types are inferred from the corresponding RouteContract.
30
+ */
31
+ export type HandlerDefs<
32
+ TRoutes extends Record<
33
+ string,
34
+ RouteContract<unknown, unknown, unknown, unknown>
35
+ >,
36
+ > = {
37
+ [K in keyof TRoutes]: HandlerFn<TRoutes[K]>;
38
+ };
39
+
40
+ /**
41
+ * Define typed handler implementations for a set of route contracts.
42
+ * The type system enforces that every route key has a handler and that
43
+ * each handler's signature matches its contract.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * export default defineHandlers<UsersRoutes>({
48
+ * "GET /users": async ({ query, ctx }) => {
49
+ * return userService.list(query, ctx);
50
+ * },
51
+ * "POST /users": async ({ body, ctx }) => {
52
+ * return userService.create(body, ctx);
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export function defineHandlers<
58
+ TRoutes extends Record<
59
+ string,
60
+ RouteContract<unknown, unknown, unknown, unknown>
61
+ >,
62
+ >(handlers: HandlerDefs<TRoutes>): HandlerDefs<TRoutes> {
63
+ return handlers;
64
+ }