@objectstack/rest 1.1.0

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