@objectstack/runtime 0.6.1 → 0.7.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.
@@ -0,0 +1,305 @@
1
+ import { RouteHandler, IHttpServer } from '@objectstack/core';
2
+ import { System, Shared } from '@objectstack/spec';
3
+
4
+ type RouteHandlerMetadata = System.RouteHandlerMetadata;
5
+ type HttpMethod = Shared.HttpMethod;
6
+
7
+ /**
8
+ * Route Entry
9
+ * Internal representation of registered routes
10
+ */
11
+ export interface RouteEntry {
12
+ method: HttpMethod;
13
+ path: string;
14
+ handler: RouteHandler;
15
+ metadata?: RouteHandlerMetadata['metadata'];
16
+ security?: RouteHandlerMetadata['security'];
17
+ }
18
+
19
+ /**
20
+ * RouteManager
21
+ *
22
+ * Manages route registration and organization for HTTP servers.
23
+ * Provides:
24
+ * - Route registration with metadata
25
+ * - Route lookup and querying
26
+ * - Bulk route registration
27
+ * - Route grouping by prefix
28
+ *
29
+ * @example
30
+ * const manager = new RouteManager(server);
31
+ *
32
+ * // Register individual route
33
+ * manager.register({
34
+ * method: 'GET',
35
+ * path: '/api/users/:id',
36
+ * handler: getUserHandler,
37
+ * metadata: {
38
+ * summary: 'Get user by ID',
39
+ * tags: ['users']
40
+ * }
41
+ * });
42
+ *
43
+ * // Register route group
44
+ * manager.group('/api/users', (group) => {
45
+ * group.get('/', listUsersHandler);
46
+ * group.post('/', createUserHandler);
47
+ * group.get('/:id', getUserHandler);
48
+ * });
49
+ */
50
+ export class RouteManager {
51
+ private server: IHttpServer;
52
+ private routes: Map<string, RouteEntry>;
53
+
54
+ constructor(server: IHttpServer) {
55
+ this.server = server;
56
+ this.routes = new Map();
57
+ }
58
+
59
+ /**
60
+ * Register a route
61
+ * @param entry - Route entry with method, path, handler, and metadata
62
+ */
63
+ register(entry: Omit<RouteEntry, 'handler'> & { handler: RouteHandler | string }): void {
64
+ // Validate handler type - string handlers not yet supported
65
+ if (typeof entry.handler === 'string') {
66
+ throw new Error(
67
+ `String-based route handlers are not supported yet. ` +
68
+ `Received handler identifier "${entry.handler}". ` +
69
+ `Please provide a RouteHandler function instead.`
70
+ );
71
+ }
72
+
73
+ const handler: RouteHandler = entry.handler;
74
+
75
+ const routeEntry: RouteEntry = {
76
+ method: entry.method,
77
+ path: entry.path,
78
+ handler,
79
+ metadata: entry.metadata,
80
+ security: entry.security,
81
+ };
82
+
83
+ const key = this.getRouteKey(entry.method, entry.path);
84
+ this.routes.set(key, routeEntry);
85
+
86
+ // Register with underlying server
87
+ this.registerWithServer(routeEntry);
88
+ }
89
+
90
+ /**
91
+ * Register multiple routes
92
+ * @param entries - Array of route entries
93
+ */
94
+ registerMany(entries: Array<Omit<RouteEntry, 'handler'> & { handler: RouteHandler | string }>): void {
95
+ entries.forEach(entry => this.register(entry));
96
+ }
97
+
98
+ /**
99
+ * Unregister a route
100
+ * @param method - HTTP method
101
+ * @param path - Route path
102
+ */
103
+ unregister(method: HttpMethod, path: string): void {
104
+ const key = this.getRouteKey(method, path);
105
+ this.routes.delete(key);
106
+ // Note: Most server frameworks don't support unregistering routes at runtime
107
+ // This just removes it from our registry
108
+ }
109
+
110
+ /**
111
+ * Get route by method and path
112
+ * @param method - HTTP method
113
+ * @param path - Route path
114
+ */
115
+ get(method: HttpMethod, path: string): RouteEntry | undefined {
116
+ const key = this.getRouteKey(method, path);
117
+ return this.routes.get(key);
118
+ }
119
+
120
+ /**
121
+ * Get all routes
122
+ */
123
+ getAll(): RouteEntry[] {
124
+ return Array.from(this.routes.values());
125
+ }
126
+
127
+ /**
128
+ * Get routes by method
129
+ * @param method - HTTP method
130
+ */
131
+ getByMethod(method: HttpMethod): RouteEntry[] {
132
+ return this.getAll().filter(route => route.method === method);
133
+ }
134
+
135
+ /**
136
+ * Get routes by path prefix
137
+ * @param prefix - Path prefix
138
+ */
139
+ getByPrefix(prefix: string): RouteEntry[] {
140
+ return this.getAll().filter(route => route.path.startsWith(prefix));
141
+ }
142
+
143
+ /**
144
+ * Get routes by tag
145
+ * @param tag - Tag name
146
+ */
147
+ getByTag(tag: string): RouteEntry[] {
148
+ return this.getAll().filter(route =>
149
+ route.metadata?.tags?.includes(tag)
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Create a route group with common prefix
155
+ * @param prefix - Common path prefix
156
+ * @param configure - Function to configure routes in the group
157
+ */
158
+ group(prefix: string, configure: (group: RouteGroupBuilder) => void): void {
159
+ const builder = new RouteGroupBuilder(this, prefix);
160
+ configure(builder);
161
+ }
162
+
163
+ /**
164
+ * Get route count
165
+ */
166
+ count(): number {
167
+ return this.routes.size;
168
+ }
169
+
170
+ /**
171
+ * Clear all routes
172
+ */
173
+ clear(): void {
174
+ this.routes.clear();
175
+ }
176
+
177
+ /**
178
+ * Get route key for storage
179
+ */
180
+ private getRouteKey(method: HttpMethod, path: string): string {
181
+ return `${method}:${path}`;
182
+ }
183
+
184
+ /**
185
+ * Register route with underlying server
186
+ */
187
+ private registerWithServer(entry: RouteEntry): void {
188
+ const { method, path, handler } = entry;
189
+
190
+ switch (method) {
191
+ case 'GET':
192
+ this.server.get(path, handler);
193
+ break;
194
+ case 'POST':
195
+ this.server.post(path, handler);
196
+ break;
197
+ case 'PUT':
198
+ this.server.put(path, handler);
199
+ break;
200
+ case 'DELETE':
201
+ this.server.delete(path, handler);
202
+ break;
203
+ case 'PATCH':
204
+ this.server.patch(path, handler);
205
+ break;
206
+ default:
207
+ throw new Error(`Unsupported HTTP method: ${method}`);
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * RouteGroupBuilder
214
+ *
215
+ * Builder for creating route groups with common prefix
216
+ */
217
+ export class RouteGroupBuilder {
218
+ private manager: RouteManager;
219
+ private prefix: string;
220
+
221
+ constructor(manager: RouteManager, prefix: string) {
222
+ this.manager = manager;
223
+ this.prefix = prefix;
224
+ }
225
+
226
+ /**
227
+ * Register GET route in group
228
+ */
229
+ get(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this {
230
+ this.manager.register({
231
+ method: 'GET',
232
+ path: this.resolvePath(path),
233
+ handler,
234
+ metadata,
235
+ });
236
+ return this;
237
+ }
238
+
239
+ /**
240
+ * Register POST route in group
241
+ */
242
+ post(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this {
243
+ this.manager.register({
244
+ method: 'POST',
245
+ path: this.resolvePath(path),
246
+ handler,
247
+ metadata,
248
+ });
249
+ return this;
250
+ }
251
+
252
+ /**
253
+ * Register PUT route in group
254
+ */
255
+ put(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this {
256
+ this.manager.register({
257
+ method: 'PUT',
258
+ path: this.resolvePath(path),
259
+ handler,
260
+ metadata,
261
+ });
262
+ return this;
263
+ }
264
+
265
+ /**
266
+ * Register PATCH route in group
267
+ */
268
+ patch(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this {
269
+ this.manager.register({
270
+ method: 'PATCH',
271
+ path: this.resolvePath(path),
272
+ handler,
273
+ metadata,
274
+ });
275
+ return this;
276
+ }
277
+
278
+ /**
279
+ * Register DELETE route in group
280
+ */
281
+ delete(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this {
282
+ this.manager.register({
283
+ method: 'DELETE',
284
+ path: this.resolvePath(path),
285
+ handler,
286
+ metadata,
287
+ });
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Resolve full path with prefix
293
+ */
294
+ private resolvePath(path: string): string {
295
+ // Normalize slashes
296
+ const normalizedPrefix = this.prefix.endsWith('/')
297
+ ? this.prefix.slice(0, -1)
298
+ : this.prefix;
299
+ const normalizedPath = path.startsWith('/')
300
+ ? path
301
+ : '/' + path;
302
+
303
+ return normalizedPrefix + normalizedPath;
304
+ }
305
+ }