@objectstack/runtime 0.6.0 → 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
+ }
@@ -1,19 +0,0 @@
1
- import { Plugin, PluginContext } from '@objectstack/core';
2
- /**
3
- * AppManifestPlugin
4
- *
5
- * Adapts a static Manifest JSON into a dynamic Kernel Service.
6
- * This allows the ObjectQL Engine to "discover" this app during its start phase.
7
- *
8
- * Flow:
9
- * 1. AppPlugin registers `app.<id>` service (init phase)
10
- * 2. ObjectQL Engine discovers `app.*` services (start phase)
11
- */
12
- export declare class AppManifestPlugin implements Plugin {
13
- name: string;
14
- version?: string;
15
- private manifest;
16
- constructor(manifest: any);
17
- init(ctx: PluginContext): Promise<void>;
18
- start(ctx: PluginContext): Promise<void>;
19
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * AppManifestPlugin
3
- *
4
- * Adapts a static Manifest JSON into a dynamic Kernel Service.
5
- * This allows the ObjectQL Engine to "discover" this app during its start phase.
6
- *
7
- * Flow:
8
- * 1. AppPlugin registers `app.<id>` service (init phase)
9
- * 2. ObjectQL Engine discovers `app.*` services (start phase)
10
- */
11
- export class AppManifestPlugin {
12
- constructor(manifest) {
13
- this.manifest = manifest;
14
- // Support both direct manifest (legacy) and Stack Definition (nested manifest)
15
- const sys = manifest.manifest || manifest;
16
- const appId = sys.id || sys.name || 'unnamed-app';
17
- this.name = `plugin.app.${appId}`; // Unique plugin name
18
- this.version = sys.version;
19
- }
20
- async init(ctx) {
21
- // Support both direct manifest (legacy) and Stack Definition (nested manifest)
22
- const sys = this.manifest.manifest || this.manifest;
23
- const appId = sys.id || sys.name;
24
- ctx.logger.log(`[AppManifestPlugin] Registering App Service: ${appId}`);
25
- // Register the app manifest as a service
26
- const serviceName = `app.${appId}`;
27
- ctx.registerService(serviceName, this.manifest);
28
- }
29
- async start(ctx) {
30
- // No logic needed here.
31
- // Logic is inverted: The Engine will pull data from this service.
32
- }
33
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Test file to verify capability contract interfaces
3
- *
4
- * This file demonstrates how plugins can implement the IHttpServer
5
- * and IDataEngine interfaces without depending on concrete implementations.
6
- */
7
- export {};
@@ -1,138 +0,0 @@
1
- /**
2
- * Test file to verify capability contract interfaces
3
- *
4
- * This file demonstrates how plugins can implement the IHttpServer
5
- * and IDataEngine interfaces without depending on concrete implementations.
6
- */
7
- /**
8
- * Example: Mock HTTP Server Plugin
9
- *
10
- * Shows how a plugin can implement the IHttpServer interface
11
- * without depending on Express, Fastify, or any specific framework.
12
- */
13
- class MockHttpServer {
14
- constructor() {
15
- this.routes = new Map();
16
- }
17
- get(path, handler) {
18
- this.routes.set(`GET:${path}`, { method: 'GET', handler });
19
- console.log(`✅ Registered GET ${path}`);
20
- }
21
- post(path, handler) {
22
- this.routes.set(`POST:${path}`, { method: 'POST', handler });
23
- console.log(`✅ Registered POST ${path}`);
24
- }
25
- put(path, handler) {
26
- this.routes.set(`PUT:${path}`, { method: 'PUT', handler });
27
- console.log(`✅ Registered PUT ${path}`);
28
- }
29
- delete(path, handler) {
30
- this.routes.set(`DELETE:${path}`, { method: 'DELETE', handler });
31
- console.log(`✅ Registered DELETE ${path}`);
32
- }
33
- patch(path, handler) {
34
- this.routes.set(`PATCH:${path}`, { method: 'PATCH', handler });
35
- console.log(`✅ Registered PATCH ${path}`);
36
- }
37
- use(path, handler) {
38
- console.log(`✅ Registered middleware`);
39
- }
40
- async listen(port) {
41
- console.log(`✅ Mock HTTP server listening on port ${port}`);
42
- }
43
- async close() {
44
- console.log(`✅ Mock HTTP server closed`);
45
- }
46
- }
47
- /**
48
- * Example: Mock Data Engine Plugin
49
- *
50
- * Shows how a plugin can implement the IDataEngine interface
51
- * without depending on ObjectQL, Prisma, or any specific database.
52
- */
53
- class MockDataEngine {
54
- constructor() {
55
- this.store = new Map();
56
- this.idCounter = 0;
57
- }
58
- async insert(objectName, data) {
59
- if (!this.store.has(objectName)) {
60
- this.store.set(objectName, new Map());
61
- }
62
- const id = `${objectName}_${++this.idCounter}`;
63
- const record = { id, ...data };
64
- this.store.get(objectName).set(id, record);
65
- console.log(`✅ Inserted into ${objectName}:`, record);
66
- return record;
67
- }
68
- async find(objectName, query) {
69
- const objectStore = this.store.get(objectName);
70
- if (!objectStore) {
71
- return [];
72
- }
73
- const results = Array.from(objectStore.values());
74
- console.log(`✅ Found ${results.length} records in ${objectName}`);
75
- return results;
76
- }
77
- async update(objectName, id, data) {
78
- const objectStore = this.store.get(objectName);
79
- if (!objectStore || !objectStore.has(id)) {
80
- throw new Error(`Record ${id} not found in ${objectName}`);
81
- }
82
- const existing = objectStore.get(id);
83
- const updated = { ...existing, ...data };
84
- objectStore.set(id, updated);
85
- console.log(`✅ Updated ${objectName}/${id}:`, updated);
86
- return updated;
87
- }
88
- async delete(objectName, id) {
89
- const objectStore = this.store.get(objectName);
90
- if (!objectStore) {
91
- return false;
92
- }
93
- const deleted = objectStore.delete(id);
94
- console.log(`✅ Deleted ${objectName}/${id}: ${deleted}`);
95
- return deleted;
96
- }
97
- }
98
- /**
99
- * Test the interfaces
100
- */
101
- async function testInterfaces() {
102
- console.log('\n=== Testing IHttpServer Interface ===\n');
103
- const httpServer = new MockHttpServer();
104
- // Register routes using the interface
105
- httpServer.get('/api/users', async (req, res) => {
106
- res.json({ users: [] });
107
- });
108
- httpServer.post('/api/users', async (req, res) => {
109
- res.status(201).json({ id: 1, ...req.body });
110
- });
111
- await httpServer.listen(3000);
112
- console.log('\n=== Testing IDataEngine Interface ===\n');
113
- const dataEngine = new MockDataEngine();
114
- // Use the data engine interface
115
- const user1 = await dataEngine.insert('user', {
116
- name: 'John Doe',
117
- email: 'john@example.com'
118
- });
119
- const user2 = await dataEngine.insert('user', {
120
- name: 'Jane Smith',
121
- email: 'jane@example.com'
122
- });
123
- const users = await dataEngine.find('user');
124
- console.log(`Found ${users.length} users after inserts`);
125
- const updatedUser = await dataEngine.update('user', user1.id, {
126
- name: 'John Updated'
127
- });
128
- console.log(`Updated user:`, updatedUser);
129
- const deleted = await dataEngine.delete('user', user2.id);
130
- console.log(`Delete result: ${deleted}`);
131
- console.log('\n✅ All interface tests passed!\n');
132
- if (httpServer.close) {
133
- await httpServer.close();
134
- }
135
- }
136
- // Run tests
137
- testInterfaces().catch(console.error);
138
- export {};
@@ -1,48 +0,0 @@
1
- import { Plugin, PluginContext } from '@objectstack/core';
2
-
3
- /**
4
- * AppManifestPlugin
5
- *
6
- * Adapts a static Manifest JSON into a dynamic Kernel Service.
7
- * This allows the ObjectQL Engine to "discover" this app during its start phase.
8
- *
9
- * Flow:
10
- * 1. AppPlugin registers `app.<id>` service (init phase)
11
- * 2. ObjectQL Engine discovers `app.*` services (start phase)
12
- */
13
- export class AppManifestPlugin implements Plugin {
14
- name: string;
15
- version?: string;
16
-
17
- // Dependencies removed: This plugin produces data. It doesn't need to consume the engine to register itself.
18
- // Making it dependency-free allows it to initialize BEFORE the engine if needed.
19
-
20
- private manifest: any;
21
-
22
- constructor(manifest: any) {
23
- this.manifest = manifest;
24
- // Support both direct manifest (legacy) and Stack Definition (nested manifest)
25
- const sys = manifest.manifest || manifest;
26
- const appId = sys.id || sys.name || 'unnamed-app';
27
-
28
- this.name = `plugin.app.${appId}`; // Unique plugin name
29
- this.version = sys.version;
30
- }
31
-
32
- async init(ctx: PluginContext) {
33
- // Support both direct manifest (legacy) and Stack Definition (nested manifest)
34
- const sys = this.manifest.manifest || this.manifest;
35
- const appId = sys.id || sys.name;
36
-
37
- ctx.logger.log(`[AppManifestPlugin] Registering App Service: ${appId}`);
38
-
39
- // Register the app manifest as a service
40
- const serviceName = `app.${appId}`;
41
- ctx.registerService(serviceName, this.manifest);
42
- }
43
-
44
- async start(ctx: PluginContext) {
45
- // No logic needed here.
46
- // Logic is inverted: The Engine will pull data from this service.
47
- }
48
- }