@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.
- package/dist/adapters/database.d.ts +28 -0
- package/dist/adapters/database.d.ts.map +1 -0
- package/dist/adapters/database.js +2 -0
- package/dist/adapters/database.js.map +1 -0
- package/dist/adapters/server.d.ts +35 -0
- package/dist/adapters/server.d.ts.map +1 -0
- package/dist/adapters/server.js +2 -0
- package/dist/adapters/server.js.map +1 -0
- package/dist/app.d.ts +36 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +55 -0
- package/dist/app.js.map +1 -0
- package/dist/error-middleware.d.ts +17 -0
- package/dist/error-middleware.d.ts.map +1 -0
- package/dist/error-middleware.js +138 -0
- package/dist/error-middleware.js.map +1 -0
- package/dist/handler.d.ts +41 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +22 -0
- package/dist/handler.js.map +1 -0
- package/dist/hooks.d.ts +48 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +64 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +35 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +54 -0
- package/dist/middleware.js.map +1 -0
- package/dist/plugin.d.ts +74 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +29 -0
- package/src/adapters/database.ts +37 -0
- package/src/adapters/server.ts +55 -0
- package/src/app.test.ts +438 -0
- package/src/app.ts +118 -0
- package/src/error-middleware.test.ts +263 -0
- package/src/error-middleware.ts +186 -0
- package/src/handler.test.ts +346 -0
- package/src/handler.ts +64 -0
- package/src/hooks.test.ts +419 -0
- package/src/hooks.ts +114 -0
- package/src/index.ts +51 -0
- package/src/middleware.test.ts +253 -0
- package/src/middleware.ts +100 -0
- package/src/plugin.ts +108 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect } from "@rstest/core";
|
|
2
|
+
import {
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
executeMiddlewareChain,
|
|
5
|
+
createRequestContext,
|
|
6
|
+
} from "./middleware.js";
|
|
7
|
+
import type { TypoKitRequest } from "@typokit/types";
|
|
8
|
+
import type { AppError } from "@typokit/errors";
|
|
9
|
+
import {
|
|
10
|
+
NotFoundError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
} from "@typokit/errors";
|
|
14
|
+
|
|
15
|
+
function createTestRequest(
|
|
16
|
+
overrides?: Partial<TypoKitRequest>,
|
|
17
|
+
): TypoKitRequest {
|
|
18
|
+
return {
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/test",
|
|
21
|
+
headers: {},
|
|
22
|
+
body: undefined,
|
|
23
|
+
query: {},
|
|
24
|
+
params: {},
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("defineMiddleware", () => {
|
|
30
|
+
it("creates a middleware with the given handler", async () => {
|
|
31
|
+
const mw = defineMiddleware(async () => ({ foo: "bar" }));
|
|
32
|
+
expect(mw.handler).toBeDefined();
|
|
33
|
+
const result = await mw.handler({
|
|
34
|
+
headers: {},
|
|
35
|
+
body: undefined,
|
|
36
|
+
query: {},
|
|
37
|
+
params: {},
|
|
38
|
+
ctx: createRequestContext(),
|
|
39
|
+
});
|
|
40
|
+
expect(result).toEqual({ foo: "bar" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("receives request properties", async () => {
|
|
44
|
+
const mw = defineMiddleware(async ({ headers, params }) => {
|
|
45
|
+
return {
|
|
46
|
+
token: headers["authorization"] as string,
|
|
47
|
+
userId: params["id"],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const result = await mw.handler({
|
|
51
|
+
headers: { authorization: "Bearer abc" },
|
|
52
|
+
body: undefined,
|
|
53
|
+
query: {},
|
|
54
|
+
params: { id: "42" },
|
|
55
|
+
ctx: createRequestContext(),
|
|
56
|
+
});
|
|
57
|
+
expect(result).toEqual({ token: "Bearer abc", userId: "42" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("executeMiddlewareChain", () => {
|
|
62
|
+
it("runs middleware in order and accumulates context", async () => {
|
|
63
|
+
const order: number[] = [];
|
|
64
|
+
|
|
65
|
+
const mw1 = defineMiddleware(async () => {
|
|
66
|
+
order.push(1);
|
|
67
|
+
return { step1: true };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const mw2 = defineMiddleware(async ({ ctx }) => {
|
|
71
|
+
order.push(2);
|
|
72
|
+
expect((ctx as unknown as Record<string, unknown>)["step1"]).toBe(true);
|
|
73
|
+
return { step2: true };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const req = createTestRequest();
|
|
77
|
+
const ctx = createRequestContext();
|
|
78
|
+
|
|
79
|
+
const result = await executeMiddlewareChain(req, ctx, [
|
|
80
|
+
{ name: "mw1", middleware: mw1 },
|
|
81
|
+
{ name: "mw2", middleware: mw2 },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
expect(order).toEqual([1, 2]);
|
|
85
|
+
expect((result as unknown as Record<string, unknown>)["step1"]).toBe(true);
|
|
86
|
+
expect((result as unknown as Record<string, unknown>)["step2"]).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("respects priority ordering (lower runs first)", async () => {
|
|
90
|
+
const order: string[] = [];
|
|
91
|
+
|
|
92
|
+
const mwA = defineMiddleware(async () => {
|
|
93
|
+
order.push("A");
|
|
94
|
+
return {};
|
|
95
|
+
});
|
|
96
|
+
const mwB = defineMiddleware(async () => {
|
|
97
|
+
order.push("B");
|
|
98
|
+
return {};
|
|
99
|
+
});
|
|
100
|
+
const mwC = defineMiddleware(async () => {
|
|
101
|
+
order.push("C");
|
|
102
|
+
return {};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const req = createTestRequest();
|
|
106
|
+
const ctx = createRequestContext();
|
|
107
|
+
|
|
108
|
+
await executeMiddlewareChain(req, ctx, [
|
|
109
|
+
{ name: "A", middleware: mwA, priority: 30 },
|
|
110
|
+
{ name: "B", middleware: mwB, priority: 10 },
|
|
111
|
+
{ name: "C", middleware: mwC, priority: 20 },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
expect(order).toEqual(["B", "C", "A"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("default priority is 0", async () => {
|
|
118
|
+
const order: string[] = [];
|
|
119
|
+
|
|
120
|
+
const mwA = defineMiddleware(async () => {
|
|
121
|
+
order.push("A");
|
|
122
|
+
return {};
|
|
123
|
+
});
|
|
124
|
+
const mwB = defineMiddleware(async () => {
|
|
125
|
+
order.push("B");
|
|
126
|
+
return {};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const req = createTestRequest();
|
|
130
|
+
const ctx = createRequestContext();
|
|
131
|
+
|
|
132
|
+
await executeMiddlewareChain(req, ctx, [
|
|
133
|
+
{ name: "A", middleware: mwA, priority: 10 },
|
|
134
|
+
{ name: "B", middleware: mwB },
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(order).toEqual(["B", "A"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("short-circuits when middleware throws", async () => {
|
|
141
|
+
const order: number[] = [];
|
|
142
|
+
|
|
143
|
+
const mw1 = defineMiddleware(async () => {
|
|
144
|
+
order.push(1);
|
|
145
|
+
return {};
|
|
146
|
+
});
|
|
147
|
+
const mw2 = defineMiddleware(
|
|
148
|
+
async ({ ctx }): Promise<Record<string, unknown>> => {
|
|
149
|
+
order.push(2);
|
|
150
|
+
ctx.fail(403, "FORBIDDEN", "Not allowed");
|
|
151
|
+
return {};
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
const mw3 = defineMiddleware(async () => {
|
|
155
|
+
order.push(3);
|
|
156
|
+
return {};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const req = createTestRequest();
|
|
160
|
+
const ctx = createRequestContext();
|
|
161
|
+
|
|
162
|
+
let caught: unknown;
|
|
163
|
+
try {
|
|
164
|
+
await executeMiddlewareChain(req, ctx, [
|
|
165
|
+
{ name: "mw1", middleware: mw1 },
|
|
166
|
+
{ name: "mw2", middleware: mw2 },
|
|
167
|
+
{ name: "mw3", middleware: mw3 },
|
|
168
|
+
]);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
caught = err;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
expect(caught).toBeInstanceOf(ForbiddenError);
|
|
174
|
+
expect((caught as AppError).status).toBe(403);
|
|
175
|
+
expect((caught as AppError).code).toBe("FORBIDDEN");
|
|
176
|
+
expect(order).toEqual([1, 2]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("handles empty middleware chain", async () => {
|
|
180
|
+
const req = createTestRequest();
|
|
181
|
+
const ctx = createRequestContext();
|
|
182
|
+
const result = await executeMiddlewareChain(req, ctx, []);
|
|
183
|
+
expect(result).toBeDefined();
|
|
184
|
+
expect(result.requestId).toBe(ctx.requestId);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("createRequestContext", () => {
|
|
189
|
+
it("ctx.fail() throws NotFoundError for 404", () => {
|
|
190
|
+
const ctx = createRequestContext();
|
|
191
|
+
let caught: unknown;
|
|
192
|
+
try {
|
|
193
|
+
ctx.fail(404, "NOT_FOUND", "Resource not found");
|
|
194
|
+
} catch (err) {
|
|
195
|
+
caught = err;
|
|
196
|
+
}
|
|
197
|
+
expect(caught).toBeInstanceOf(NotFoundError);
|
|
198
|
+
expect((caught as AppError).status).toBe(404);
|
|
199
|
+
expect((caught as AppError).code).toBe("NOT_FOUND");
|
|
200
|
+
expect((caught as AppError).message).toBe("Resource not found");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("ctx.fail() throws ValidationError for 400 with details", () => {
|
|
204
|
+
const ctx = createRequestContext();
|
|
205
|
+
let caught: unknown;
|
|
206
|
+
try {
|
|
207
|
+
ctx.fail(400, "VALIDATION", "Invalid input", { field: "email" });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
caught = err;
|
|
210
|
+
}
|
|
211
|
+
expect(caught).toBeInstanceOf(ValidationError);
|
|
212
|
+
expect((caught as AppError).details).toEqual({ field: "email" });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("ctx.log is defined with all log levels", () => {
|
|
216
|
+
const ctx = createRequestContext();
|
|
217
|
+
expect(ctx.log).toBeDefined();
|
|
218
|
+
expect(typeof ctx.log.trace).toBe("function");
|
|
219
|
+
expect(typeof ctx.log.debug).toBe("function");
|
|
220
|
+
expect(typeof ctx.log.info).toBe("function");
|
|
221
|
+
expect(typeof ctx.log.warn).toBe("function");
|
|
222
|
+
expect(typeof ctx.log.error).toBe("function");
|
|
223
|
+
expect(typeof ctx.log.fatal).toBe("function");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("ctx.log methods are no-ops (placeholder)", () => {
|
|
227
|
+
const ctx = createRequestContext();
|
|
228
|
+
// Should not throw
|
|
229
|
+
ctx.log.trace("test");
|
|
230
|
+
ctx.log.debug("test", { key: "value" });
|
|
231
|
+
ctx.log.info("test");
|
|
232
|
+
ctx.log.warn("test");
|
|
233
|
+
ctx.log.error("test");
|
|
234
|
+
ctx.log.fatal("test");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("has a requestId", () => {
|
|
238
|
+
const ctx = createRequestContext();
|
|
239
|
+
expect(typeof ctx.requestId).toBe("string");
|
|
240
|
+
expect(ctx.requestId.length).toBeGreaterThan(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("has services object", () => {
|
|
244
|
+
const ctx = createRequestContext();
|
|
245
|
+
expect(ctx.services).toBeDefined();
|
|
246
|
+
expect(typeof ctx.services).toBe("object");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("accepts overrides", () => {
|
|
250
|
+
const ctx = createRequestContext({ requestId: "test-123" });
|
|
251
|
+
expect(ctx.requestId).toBe("test-123");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @typokit/core — Middleware System
|
|
2
|
+
|
|
3
|
+
import type { TypoKitRequest, RequestContext, Logger } from "@typokit/types";
|
|
4
|
+
import { createAppError } from "@typokit/errors";
|
|
5
|
+
|
|
6
|
+
/** Input received by a defineMiddleware handler */
|
|
7
|
+
export interface MiddlewareInput {
|
|
8
|
+
headers: TypoKitRequest["headers"];
|
|
9
|
+
body: TypoKitRequest["body"];
|
|
10
|
+
query: TypoKitRequest["query"];
|
|
11
|
+
params: TypoKitRequest["params"];
|
|
12
|
+
ctx: RequestContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A typed middleware created by defineMiddleware */
|
|
16
|
+
export interface Middleware<
|
|
17
|
+
TAdded extends Record<string, unknown> = Record<string, unknown>,
|
|
18
|
+
> {
|
|
19
|
+
handler: (input: MiddlewareInput) => Promise<TAdded>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An entry in the middleware chain with name and optional priority */
|
|
23
|
+
export interface MiddlewareEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
middleware: Middleware;
|
|
26
|
+
priority?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Define a typed middleware that receives request properties and returns
|
|
31
|
+
* additional context properties. Supports context type narrowing.
|
|
32
|
+
*/
|
|
33
|
+
export function defineMiddleware<TAdded extends Record<string, unknown>>(
|
|
34
|
+
handler: (input: MiddlewareInput) => Promise<TAdded>,
|
|
35
|
+
): Middleware<TAdded> {
|
|
36
|
+
return { handler };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Create a no-op placeholder logger (actual implementation in observability phase) */
|
|
40
|
+
export function createPlaceholderLogger(): Logger {
|
|
41
|
+
const noop = () => {};
|
|
42
|
+
return {
|
|
43
|
+
trace: noop,
|
|
44
|
+
debug: noop,
|
|
45
|
+
info: noop,
|
|
46
|
+
warn: noop,
|
|
47
|
+
error: noop,
|
|
48
|
+
fatal: noop,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Create a RequestContext with ctx.fail() and ctx.log placeholder */
|
|
53
|
+
export function createRequestContext(
|
|
54
|
+
overrides?: Partial<RequestContext>,
|
|
55
|
+
): RequestContext {
|
|
56
|
+
return {
|
|
57
|
+
log: createPlaceholderLogger(),
|
|
58
|
+
fail(
|
|
59
|
+
status: number,
|
|
60
|
+
code: string,
|
|
61
|
+
message: string,
|
|
62
|
+
details?: Record<string, unknown>,
|
|
63
|
+
): never {
|
|
64
|
+
throw createAppError(status, code, message, details);
|
|
65
|
+
},
|
|
66
|
+
services: {},
|
|
67
|
+
requestId:
|
|
68
|
+
Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2),
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Execute a middleware chain in priority order (lower priority runs first).
|
|
75
|
+
* Each middleware's returned properties are accumulated onto the context.
|
|
76
|
+
* Middleware can short-circuit by throwing an error.
|
|
77
|
+
*/
|
|
78
|
+
export async function executeMiddlewareChain(
|
|
79
|
+
req: TypoKitRequest,
|
|
80
|
+
ctx: RequestContext,
|
|
81
|
+
entries: MiddlewareEntry[],
|
|
82
|
+
): Promise<RequestContext> {
|
|
83
|
+
const sorted = [...entries].sort(
|
|
84
|
+
(a, b) => (a.priority ?? 0) - (b.priority ?? 0),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let currentCtx = ctx;
|
|
88
|
+
for (const entry of sorted) {
|
|
89
|
+
const added = await entry.middleware.handler({
|
|
90
|
+
headers: req.headers,
|
|
91
|
+
body: req.body,
|
|
92
|
+
query: req.query,
|
|
93
|
+
params: req.params,
|
|
94
|
+
ctx: currentCtx,
|
|
95
|
+
});
|
|
96
|
+
currentCtx = { ...currentCtx, ...added } as RequestContext;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return currentCtx;
|
|
100
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @typokit/core — Plugin Interface & Build Pipeline
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BuildContext,
|
|
5
|
+
BuildResult,
|
|
6
|
+
CompiledRouteTable,
|
|
7
|
+
GeneratedOutput,
|
|
8
|
+
RequestContext,
|
|
9
|
+
SchemaChange,
|
|
10
|
+
SchemaTypeMap,
|
|
11
|
+
} from "@typokit/types";
|
|
12
|
+
import type { AppError } from "@typokit/errors";
|
|
13
|
+
|
|
14
|
+
// ─── Hook System ─────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Tapable-style async series hook — plugins tap in, calls run in series */
|
|
17
|
+
export interface AsyncSeriesHook<T extends unknown[]> {
|
|
18
|
+
tap(name: string, fn: (...args: T) => void | Promise<void>): void;
|
|
19
|
+
call(...args: T): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Build Pipeline ──────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Tapable build pipeline — plugins hook into build phases via onBuild() */
|
|
25
|
+
export interface BuildPipeline {
|
|
26
|
+
hooks: {
|
|
27
|
+
/** Runs before any transforms — plugins can register additional type sources */
|
|
28
|
+
beforeTransform: AsyncSeriesHook<[BuildContext]>;
|
|
29
|
+
|
|
30
|
+
/** Runs after types are parsed — plugins can inspect/modify the type map */
|
|
31
|
+
afterTypeParse: AsyncSeriesHook<[SchemaTypeMap, BuildContext]>;
|
|
32
|
+
|
|
33
|
+
/** Runs after validators are generated — plugins can add custom validators */
|
|
34
|
+
afterValidators: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
|
|
35
|
+
|
|
36
|
+
/** Runs after the route table is compiled */
|
|
37
|
+
afterRouteTable: AsyncSeriesHook<[CompiledRouteTable, BuildContext]>;
|
|
38
|
+
|
|
39
|
+
/** Runs after all generation — plugins emit their own artifacts */
|
|
40
|
+
emit: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
|
|
41
|
+
|
|
42
|
+
/** Runs after build completes — cleanup, reporting */
|
|
43
|
+
done: AsyncSeriesHook<[BuildResult]>;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── CLI & Introspection Types ───────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** A CLI subcommand exposed by a plugin */
|
|
50
|
+
export interface CliCommand {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
options?: Array<{ name: string; description: string; required?: boolean }>;
|
|
54
|
+
run(args: Record<string, unknown>): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An introspection endpoint exposed by a plugin for the debug sidecar */
|
|
58
|
+
export interface InspectEndpoint {
|
|
59
|
+
path: string;
|
|
60
|
+
description: string;
|
|
61
|
+
handler(): Promise<unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── App Instance ────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/** Represents the running application instance passed to plugin lifecycle hooks */
|
|
67
|
+
export interface AppInstance {
|
|
68
|
+
/** Application name */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Registered plugins */
|
|
71
|
+
plugins: TypoKitPlugin[];
|
|
72
|
+
/** Service container for dependency injection */
|
|
73
|
+
services: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Plugin Interface ────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** Plugin contract — hooks into both build-time and runtime lifecycle events */
|
|
79
|
+
export interface TypoKitPlugin {
|
|
80
|
+
name: string;
|
|
81
|
+
|
|
82
|
+
/** Hook into the build pipeline — tap into specific build phases */
|
|
83
|
+
onBuild?(pipeline: BuildPipeline): void;
|
|
84
|
+
|
|
85
|
+
/** Hook into server startup — register middleware, services, resources */
|
|
86
|
+
onStart?(app: AppInstance): Promise<void>;
|
|
87
|
+
|
|
88
|
+
/** Fires after all routes are registered and the server is listening.
|
|
89
|
+
* Use for service discovery, health check readiness, warmup. */
|
|
90
|
+
onReady?(app: AppInstance): Promise<void>;
|
|
91
|
+
|
|
92
|
+
/** Observe unhandled errors — reporting, transformation (e.g. Sentry).
|
|
93
|
+
* Called after the framework's error middleware serializes the response. */
|
|
94
|
+
onError?(error: AppError, ctx: RequestContext): void;
|
|
95
|
+
|
|
96
|
+
/** Hook into server shutdown — cleanup connections, flush buffers */
|
|
97
|
+
onStop?(app: AppInstance): Promise<void>;
|
|
98
|
+
|
|
99
|
+
/** Dev mode only — fires when schema types change and the build regenerates.
|
|
100
|
+
* Use to refresh cached state (e.g. debug sidecar route map). */
|
|
101
|
+
onSchemaChange?(changes: SchemaChange[]): void;
|
|
102
|
+
|
|
103
|
+
/** Expose CLI subcommands */
|
|
104
|
+
commands?(): CliCommand[];
|
|
105
|
+
|
|
106
|
+
/** Expose introspection endpoints for the debug sidecar */
|
|
107
|
+
inspect?(): InspectEndpoint[];
|
|
108
|
+
}
|