Skip to content

Commit 51b4cda

Browse files
Copilothotlong
andcommitted
WIP: Migrating express-server to use runtime pattern
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 0807e04 commit 51b4cda

3 files changed

Lines changed: 228 additions & 87 deletions

File tree

examples/integrations/express-server/package.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,18 @@
2121
"directory": "examples/integrations/express-server"
2222
},
2323
"scripts": {
24-
"build": "tsc && cp src/*.yml dist/ || true",
24+
"build": "tsc",
2525
"start": "node dist/index.js",
2626
"test": "jest"
2727
},
2828
"dependencies": {
2929
"@objectql/core": "workspace:*",
30-
"@objectql/server": "workspace:*",
31-
"@objectql/types": "workspace:*",
3230
"@objectql/driver-sql": "workspace:*",
33-
"@objectql/platform-node": "workspace:*",
34-
"express": "^4.18.2",
31+
"@objectql/protocol-json-rpc": "workspace:*",
32+
"@objectql/protocol-graphql": "workspace:*",
3533
"sqlite3": "^5.1.7"
3634
},
3735
"devDependencies": {
38-
"@types/express": "^4.17.21",
3936
"@types/jest": "^30.0.0",
4037
"@types/supertest": "^6.0.3",
4138
"jest": "^30.2.0",

examples/integrations/express-server/src/index.ts

Lines changed: 221 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,84 +6,237 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import express from 'express';
10-
import { ObjectQL } from '@objectql/core';
9+
import { ObjectQLPlugin } from '@objectql/core';
1110
import { SqlDriver } from '@objectql/driver-sql';
12-
import { ObjectLoader } from '@objectql/platform-node';
13-
import { createNodeHandler, createMetadataHandler, createRESTHandler } from '@objectql/server';
14-
import * as path from 'path';
11+
import { JSONRPCPlugin } from '@objectql/protocol-json-rpc';
12+
import { GraphQLPlugin } from '@objectql/protocol-graphql';
13+
import { ObjectKernel } from '@objectstack/runtime';
1514

16-
async function main() {
17-
// 1. Init ObjectQL
18-
const app = new ObjectQL({
19-
datasources: {
20-
default: new SqlDriver({
21-
client: 'sqlite3',
22-
connection: {
23-
filename: ':memory:'
24-
},
25-
useNullAsDefault: true
26-
})
15+
// Define application config with objects converted from YAML
16+
const expressServerApp = {
17+
name: 'express-server-app',
18+
label: 'Express Server Example Application',
19+
description: 'Demonstrates ObjectStack Runtime with JSON-RPC and GraphQL protocols',
20+
objects: {
21+
User: {
22+
name: 'User',
23+
label: 'Users',
24+
ai_context: {
25+
intent: 'Manage user accounts and profiles',
26+
domain: 'user_management',
27+
common_queries: [
28+
'Find active users',
29+
'List users by age',
30+
'Search users by email'
31+
]
32+
},
33+
fields: {
34+
name: {
35+
type: 'text',
36+
label: 'Full Name',
37+
required: true,
38+
ai_context: {
39+
intent: "User's full name for display"
40+
}
41+
},
42+
email: {
43+
type: 'email',
44+
label: 'Email Address',
45+
required: true,
46+
ai_context: {
47+
intent: 'Primary contact email and login identifier'
48+
}
49+
},
50+
status: {
51+
type: 'select',
52+
label: 'Status',
53+
options: [
54+
{ label: 'Active', value: 'active' },
55+
{ label: 'Inactive', value: 'inactive' },
56+
{ label: 'Suspended', value: 'suspended' }
57+
],
58+
defaultValue: 'active',
59+
ai_context: {
60+
intent: 'Account status',
61+
is_state_machine: true,
62+
transitions: {
63+
active: ['inactive', 'suspended'],
64+
inactive: ['active'],
65+
suspended: ['active', 'inactive']
66+
}
67+
}
68+
},
69+
age: {
70+
type: 'number',
71+
label: 'Age',
72+
ai_context: {
73+
intent: "User's age for demographic purposes"
74+
}
75+
}
76+
}
77+
},
78+
Task: {
79+
name: 'Task',
80+
label: 'Tasks',
81+
ai_context: {
82+
intent: 'Track individual tasks and their completion status',
83+
domain: 'task_management',
84+
common_queries: [
85+
'Find pending tasks',
86+
'Show overdue tasks',
87+
'List tasks by priority'
88+
]
89+
},
90+
fields: {
91+
title: {
92+
type: 'text',
93+
label: 'Title',
94+
required: true,
95+
ai_context: {
96+
intent: 'Brief description of the task'
97+
}
98+
},
99+
description: {
100+
type: 'textarea',
101+
label: 'Description',
102+
ai_context: {
103+
intent: 'Detailed task information'
104+
}
105+
},
106+
status: {
107+
type: 'select',
108+
label: 'Status',
109+
options: [
110+
{ label: 'Pending', value: 'pending' },
111+
{ label: 'In Progress', value: 'in_progress' },
112+
{ label: 'Completed', value: 'completed' },
113+
{ label: 'Cancelled', value: 'cancelled' }
114+
],
115+
defaultValue: 'pending',
116+
ai_context: {
117+
intent: 'Current state of the task',
118+
is_state_machine: true,
119+
transitions: {
120+
pending: ['in_progress', 'cancelled'],
121+
in_progress: ['completed', 'pending', 'cancelled'],
122+
completed: [],
123+
cancelled: ['pending']
124+
}
125+
}
126+
},
127+
priority: {
128+
type: 'select',
129+
label: 'Priority',
130+
options: [
131+
{ label: 'Low', value: 'low' },
132+
{ label: 'Medium', value: 'medium' },
133+
{ label: 'High', value: 'high' },
134+
{ label: 'Urgent', value: 'urgent' }
135+
],
136+
defaultValue: 'medium',
137+
ai_context: {
138+
intent: 'Task urgency level'
139+
}
140+
},
141+
due_date: {
142+
type: 'date',
143+
label: 'Due Date',
144+
ai_context: {
145+
intent: 'Deadline for task completion'
146+
}
147+
},
148+
completed: {
149+
type: 'boolean',
150+
label: 'Completed',
151+
defaultValue: false,
152+
ai_context: {
153+
intent: 'Quick completion flag'
154+
}
27155
}
28-
});
156+
}
157+
}
158+
}
159+
};
160+
161+
async function main() {
162+
console.log('🚀 Starting ObjectStack Runtime Server...\n');
163+
164+
// Create kernel
165+
const kernel = new ObjectKernel();
166+
167+
// Create driver
168+
const driver = new SqlDriver({
169+
client: 'sqlite3',
170+
connection: {
171+
filename: ':memory:'
172+
},
173+
useNullAsDefault: true
174+
});
29175

30-
// 2. Load Schema
31-
const rootDir = path.resolve(__dirname, '..');
32-
const loader = new ObjectLoader(app.metadata);
33-
loader.load(rootDir);
176+
// Create ObjectQL plugin
177+
const objectQLPlugin = new ObjectQLPlugin({
178+
datasources: {
179+
default: driver
180+
}
181+
});
34182

35-
// 3. Init
36-
app.init().then(async () => {
37-
const objectQLHandler = createNodeHandler(app);
38-
const restHandler = createRESTHandler(app);
39-
const metadataHandler = createMetadataHandler(app);
183+
// Register app metadata with kernel
184+
if (expressServerApp.objects) {
185+
for (const [objName, objDef] of Object.entries(expressServerApp.objects)) {
186+
kernel.metadata.register('object', objName, objDef);
187+
}
188+
}
40189

41-
// 4. Setup Express
42-
const server = express();
43-
const port = 3004;
190+
// Register plugins
191+
kernel.use(objectQLPlugin);
192+
kernel.use(new JSONRPCPlugin({ port: 3004, basePath: '/api/objectql' }));
193+
kernel.use(new GraphQLPlugin({ port: 4000, introspection: true }));
44194

45-
// Enable CORS for development
46-
server.use((req: any, res: any, next: any) => {
47-
res.header('Access-Control-Allow-Origin', '*');
48-
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
49-
res.header('Access-Control-Allow-Headers', 'Content-Type');
50-
if (req.method === 'OPTIONS') {
51-
res.sendStatus(200);
52-
} else {
53-
next();
54-
}
55-
});
195+
// Setup graceful shutdown handlers
196+
const shutdown = async (signal: string) => {
197+
console.log(`\n\n🛑 Received ${signal}, shutting down gracefully...`);
198+
try {
199+
await kernel.shutdown();
200+
console.log('✅ Server stopped successfully. Goodbye!');
201+
process.exit(0);
202+
} catch (error) {
203+
console.error('❌ Error during shutdown:', error);
204+
process.exit(1);
205+
}
206+
};
56207

57-
// Mount handlers
58-
server.all('/api/objectql*', objectQLHandler);
59-
server.all('/api/data/*', restHandler);
60-
server.all('/api/metadata*', metadataHandler);
208+
process.on('SIGINT', () => shutdown('SIGINT'));
209+
process.on('SIGTERM', () => shutdown('SIGTERM'));
61210

62-
// Create some sample data
63-
const ctx = app.createContext({ isSystem: true });
64-
await ctx.object('User').create({ name: 'Alice', email: 'alice@example.com', age: 28, status: 'active' });
65-
await ctx.object('User').create({ name: 'Bob', email: 'bob@example.com', age: 35, status: 'active' });
66-
await ctx.object('User').create({ name: 'Charlie', email: 'charlie@example.com', age: 42, status: 'inactive' });
67-
68-
await ctx.object('Task').create({ title: 'Complete project', description: 'Finish the ObjectQL console', status: 'in-progress', priority: 'high' });
69-
await ctx.object('Task').create({ title: 'Write documentation', description: 'Document the new console feature', status: 'pending', priority: 'medium' });
70-
await ctx.object('Task').create({ title: 'Code review', description: 'Review pull requests', status: 'pending', priority: 'low' });
71-
await ctx.object('Task').create({ title: 'Deploy to production', description: 'Release version 1.0', status: 'pending', priority: 'high', completed: false });
211+
// Handle uncaught errors
212+
process.on('uncaughtException', (error) => {
213+
console.error('❌ Uncaught exception:', error);
214+
shutdown('UNCAUGHT_EXCEPTION').catch(() => process.exit(1));
215+
});
72216

73-
server.listen(port, () => {
74-
console.log(`\n🚀 ObjectQL Server running on http://localhost:${port}`);
75-
console.log(`\n🔌 APIs:`);
76-
console.log(` - JSON-RPC: http://localhost:${port}/api/objectql`);
77-
console.log(` - REST: http://localhost:${port}/api/data`);
78-
console.log(` - Metadata: http://localhost:${port}/api/metadata`);
79-
console.log(`\nTest JSON-RPC:`);
80-
console.log(`curl -X POST http://localhost:${port}/api/objectql -H "Content-Type: application/json" -d '{"op": "find", "object": "User", "args": {}}'`);
81-
console.log(`\nTest REST API:`);
82-
console.log(`curl http://localhost:${port}/api/data/User`);
83-
console.log(`\nTest Metadata API:`);
84-
console.log(`curl http://localhost:${port}/api/metadata/object`);
85-
});
217+
process.on('unhandledRejection', (reason, promise) => {
218+
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason);
219+
shutdown('UNHANDLED_REJECTION').catch(() => process.exit(1));
86220
});
221+
222+
// Bootstrap the kernel
223+
await kernel.bootstrap();
224+
225+
console.log('\n✅ Server started!\n');
226+
console.log('📡 Available endpoints:');
227+
console.log(' - JSON-RPC: http://localhost:3004/api/objectql');
228+
console.log(' - GraphQL: http://localhost:4000/');
229+
console.log('\n💡 Test the APIs:');
230+
console.log('\nJSON-RPC Example:');
231+
console.log('curl -X POST http://localhost:3004/api/objectql \\');
232+
console.log(' -H "Content-Type: application/json" \\');
233+
console.log(' -d \'{"jsonrpc":"2.0","method":"object.find","params":["User",{}],"id":1}\'');
234+
console.log('\nGraphQL Example (open in browser):');
235+
console.log('http://localhost:4000/');
236+
console.log('\n💡 Press Ctrl+C to stop the server\n');
87237
}
88238

89-
main().catch(console.error);
239+
main().catch((error) => {
240+
console.error('❌ Fatal error:', error);
241+
process.exit(1);
242+
});

pnpm-lock.yaml

Lines changed: 4 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)