Skip to content

Commit 894b447

Browse files
committed
feat: add JSONRPCPlugin for JSON-RPC 2.0 support; update package.json and pnpm-lock.yaml for new dependency
1 parent a43a448 commit 894b447

6 files changed

Lines changed: 70 additions & 230 deletions

File tree

objectql.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ObjectQLSecurityPlugin } from '@objectql/plugin-security';
1010
import { ValidatorPlugin } from '@objectql/plugin-validator';
1111
import { GraphQLPlugin } from '@objectql/protocol-graphql';
1212
import { ODataV4Plugin } from '@objectql/protocol-odata-v4';
13+
import { JSONRPCPlugin } from '@objectql/protocol-json-rpc';
1314
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
1415
import { ObjectQLPlugin } from '@objectql/core';
1516
import * as fs from 'fs';
@@ -67,6 +68,11 @@ export default {
6768
enableBatch: true,
6869
enableSearch: true,
6970
enableETags: true
71+
}),
72+
new JSONRPCPlugin({
73+
basePath: '/rpc',
74+
enableIntrospection: true,
75+
enableSessions: true
7076
})
7177
]
7278
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@objectql/plugin-security": "workspace:*",
3131
"@objectql/plugin-validator": "workspace:*",
3232
"@objectql/protocol-graphql": "workspace:*",
33+
"@objectql/protocol-json-rpc": "workspace:*",
3334
"@objectql/protocol-odata-v4": "workspace:*",
3435
"@objectstack/cli": "0.8.1",
3536
"@objectstack/core": "0.8.1",
@@ -55,6 +56,7 @@
5556
},
5657
"version": "4.0.0",
5758
"dependencies": {
59+
"@objectql/protocol-json-rpc": "workspace:*",
5860
"build": "^0.1.4"
5961
}
6062
}

packages/foundation/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@objectql/core",
3-
"version": "4.0.3",
3+
"version": "4.0.4",
44
"description": "Universal runtime engine for ObjectQL - AI-native metadata-driven ORM with validation, repository pattern, and driver orchestration",
55
"keywords": [
66
"objectql",

packages/protocols/json-rpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@objectql/protocol-json-rpc",
3-
"version": "4.0.3",
3+
"version": "4.0.4",
44
"description": "JSON-RPC 2.0 Protocol Plugin for ObjectStack",
55
"type": "module",
66
"main": "./dist/index.js",

packages/protocols/json-rpc/src/index.ts

Lines changed: 55 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77
*/
88

99
import type { RuntimePlugin, RuntimeContext } from '@objectql/types';
10-
import { IncomingMessage, ServerResponse, createServer, Server } from 'http';
1110

1211
/**
1312
* Configuration for the JSON-RPC Plugin
1413
*/
1514
export interface JSONRPCPluginConfig {
16-
/** Port to listen on */
15+
/** Port to listen on (deprecated in favor of shared Hono server) */
1716
port?: number;
1817
/** Base path for JSON-RPC endpoint */
1918
basePath?: string;
20-
/** Enable CORS */
19+
/** Enable CORS (handled by Hono usually, kept for config compatibility) */
2120
enableCORS?: boolean;
2221
/** Enable introspection methods */
2322
enableIntrospection?: boolean;
@@ -97,11 +96,9 @@ interface MethodSignature {
9796
* - Notification support (requests without id)
9897
* - Built-in introspection methods (system.listMethods, system.describe)
9998
* - Session management for stateful operations
100-
* - Progress notifications via Server-Sent Events (SSE)
99+
* - Progress notifications via Server-Sent Events (SSE) (Partial support in Hono adapter)
101100
* - Method call chaining with result references
102101
* - CRUD operations mapped to RPC methods
103-
* - Named and positional parameter support
104-
* - No direct database access - all operations through ObjectStackProtocolImplementation
105102
*
106103
* Available RPC Methods:
107104
* - object.find(objectName, query) - Find multiple records
@@ -113,56 +110,31 @@ interface MethodSignature {
113110
* - metadata.list() - List all objects
114111
* - metadata.get(objectName) - Get object metadata
115112
* - action.execute(actionName, params) - Execute custom action
116-
* - session.create() - Create a new session (if sessions enabled)
117-
* - session.get(key) - Get session value (if sessions enabled)
118-
* - session.set(key, value) - Set session value (if sessions enabled)
119-
* - session.destroy() - Destroy current session (if sessions enabled)
120-
* - system.listMethods() - List available methods (if introspection enabled)
121-
* - system.describe(method) - Describe method signature (if introspection enabled)
122113
*
123114
* @example
124115
* ```typescript
125-
* import { ObjectKernel } from '@objectstack/core';
116+
* import { ObjectStackKernel } from '@objectstack/core';
126117
* import { JSONRPCPlugin } from '@objectql/protocol-json-rpc';
127118
*
128-
* const kernel = new ObjectKernel([
119+
* const kernel = new ObjectStackKernel([
129120
* new JSONRPCPlugin({
130-
* port: 9000,
131121
* basePath: '/rpc',
132-
* enableSessions: true,
133-
* enableProgress: true,
134-
* enableChaining: true
122+
* enableSessions: true
135123
* })
136124
* ]);
137125
* await kernel.start();
138-
*
139-
* // Client request (positional):
140-
* // POST /rpc
141-
* // {"jsonrpc":"2.0","method":"object.find","params":["users",{"where":{"active":true}}],"id":1}
142-
*
143-
* // Client request (named):
144-
* // POST /rpc
145-
* // {"jsonrpc":"2.0","method":"object.find","params":{"objectName":"users","query":{"where":{"active":true}}},"id":1}
146-
*
147-
* // Batch request with chaining:
148-
* // [{"jsonrpc":"2.0","method":"object.create","params":["users",{"name":"John"}],"id":1},
149-
* // {"jsonrpc":"2.0","method":"object.update","params":["users","$1.result.id",{"active":true}],"id":2}]
150-
*
151-
* // Progress notifications (SSE):
152-
* // GET /rpc/progress?session=<session-id>
153126
* ```
154127
*/
155128
export class JSONRPCPlugin implements RuntimePlugin {
156129
name = '@objectql/protocol-json-rpc';
157130
version = '0.2.0';
158131

159-
private server?: Server;
160132
private engine?: any;
161133
private config: Required<JSONRPCPluginConfig>;
162134
private methods: Map<string, Function>;
163135
private methodSignatures: Map<string, MethodSignature>;
164136
private sessions: Map<string, Session> = new Map();
165-
private progressClients: Map<string, ServerResponse> = new Map();
137+
// private progressClients: Map<string, any> = new Map(); // TODO: SSE implementation for Hono
166138

167139
constructor(config: JSONRPCPluginConfig = {}) {
168140
this.config = {
@@ -202,19 +174,7 @@ export class JSONRPCPlugin implements RuntimePlugin {
202174
if (!this.engine) {
203175
throw new Error('Protocol not initialized. Install hook must be called first.');
204176
}
205-
206-
console.log(`[${this.name}] Starting JSON-RPC 2.0 server...`);
207-
208-
// Create HTTP server
209-
this.server = createServer((req, res) => this.handleRequest(req, res));
210-
211-
// Start listening
212-
await new Promise<void>((resolve) => {
213-
this.server!.listen(this.config.port, () => {
214-
console.log(`[${this.name}] JSON-RPC 2.0 server listening on http://localhost:${this.config.port}${this.config.basePath}`);
215-
resolve();
216-
});
217-
});
177+
console.log(`[${this.name}] JSON-RPC protocol ready. Mount at ${this.config.basePath}`);
218178
}
219179

220180
// --- Adapter for @objectstack/core compatibility ---
@@ -225,22 +185,54 @@ export class JSONRPCPlugin implements RuntimePlugin {
225185
async start(ctx: any): Promise<void> {
226186
return this.onStart(ctx);
227187
}
228-
// ---------------------------------------------------
229188

230189
/**
231190
* Stop hook - called when kernel stops
232191
*/
233192
async onStop(ctx: RuntimeContext): Promise<void> {
234-
if (this.server) {
235-
console.log(`[${this.name}] Stopping JSON-RPC 2.0 server...`);
236-
await new Promise<void>((resolve, reject) => {
237-
this.server!.close((err) => {
238-
if (err) reject(err);
239-
else resolve();
240-
});
241-
});
242-
this.server = undefined;
243-
}
193+
// Cleanup logic if needed
194+
}
195+
196+
/**
197+
* Attach to Hono server
198+
*/
199+
attachToHono(app: any) {
200+
const basePath = this.config.basePath;
201+
console.log(`[${this.name}] Attaching JSON-RPC to Hono at ${basePath}`);
202+
203+
// Post handler for RPC requests
204+
app.post(basePath, async (c: any) => {
205+
try {
206+
const body = await c.req.json();
207+
208+
// Handle batch requests with optional chaining
209+
if (Array.isArray(body)) {
210+
if (this.config.enableChaining) {
211+
const responses = await this.processBatchWithChaining(body);
212+
return c.json(responses);
213+
} else {
214+
const responses = await Promise.all(
215+
body.map((request: any) => this.processRequest(request))
216+
);
217+
return c.json(responses);
218+
}
219+
} else {
220+
const response = await this.processRequest(body);
221+
// Don't send response for notifications
222+
if (response) {
223+
return c.json(response);
224+
} else {
225+
return c.body(null, 204);
226+
}
227+
}
228+
} catch (error) {
229+
console.error(`[${this.name}] Request error:`, error);
230+
const errorResponse = this.createErrorResponse(null, -32700, 'Parse error');
231+
return c.json(errorResponse);
232+
}
233+
});
234+
235+
// TODO: Implement GET /rpc/progress if needed using Hono streaming
244236
}
245237

246238
/**
@@ -648,93 +640,6 @@ export class JSONRPCPlugin implements RuntimePlugin {
648640
return signatures[methodName] || { description: 'No description available' };
649641
}
650642

651-
/**
652-
* Main HTTP request handler
653-
*/
654-
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
655-
// Enable CORS if configured
656-
if (this.config.enableCORS) {
657-
res.setHeader('Access-Control-Allow-Origin', '*');
658-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
659-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
660-
661-
if (req.method === 'OPTIONS') {
662-
res.writeHead(204);
663-
res.end();
664-
return;
665-
}
666-
}
667-
668-
const url = req.url || '/';
669-
const basePath = this.config.basePath;
670-
671-
// Check if request is for RPC endpoint
672-
if (!url.startsWith(basePath)) {
673-
this.sendError(res, null, -32600, 'Invalid Request: Wrong endpoint');
674-
return;
675-
}
676-
677-
// Handle progress SSE endpoint (GET /rpc/progress?session=<id>)
678-
if (this.config.enableProgress && req.method === 'GET' && url.includes('/progress')) {
679-
const sessionId = new URL(url, 'http://localhost').searchParams.get('session');
680-
if (sessionId) {
681-
// Setup SSE
682-
res.writeHead(200, {
683-
'Content-Type': 'text/event-stream',
684-
'Cache-Control': 'no-cache',
685-
'Connection': 'keep-alive'
686-
});
687-
688-
this.progressClients.set(sessionId, res);
689-
690-
// Send initial connection message
691-
res.write('data: {"type":"connected"}\n\n');
692-
693-
// Cleanup on close
694-
req.on('close', () => {
695-
this.progressClients.delete(sessionId);
696-
});
697-
698-
return;
699-
}
700-
}
701-
702-
// Only accept POST requests for RPC
703-
if (req.method !== 'POST') {
704-
this.sendError(res, null, -32600, 'Invalid Request: Method must be POST');
705-
return;
706-
}
707-
708-
try {
709-
const body = await this.readBody(req);
710-
711-
// Handle batch requests with optional chaining
712-
if (Array.isArray(body)) {
713-
if (this.config.enableChaining) {
714-
const responses = await this.processBatchWithChaining(body);
715-
this.sendJSON(res, 200, responses);
716-
} else {
717-
const responses = await Promise.all(
718-
body.map(request => this.processRequest(request))
719-
);
720-
this.sendJSON(res, 200, responses);
721-
}
722-
} else {
723-
const response = await this.processRequest(body);
724-
// Don't send response for notifications
725-
if (response) {
726-
this.sendJSON(res, 200, response);
727-
} else {
728-
res.writeHead(204);
729-
res.end();
730-
}
731-
}
732-
} catch (error) {
733-
console.error(`[${this.name}] Request error:`, error);
734-
this.sendError(res, null, -32700, 'Parse error');
735-
}
736-
}
737-
738643
/**
739644
* Process a single JSON-RPC request
740645
*/
@@ -795,6 +700,7 @@ export class JSONRPCPlugin implements RuntimePlugin {
795700
id: request.id
796701
};
797702
} catch (error) {
703+
console.error(error);
798704
if (isNotification) return null;
799705

800706
return this.createErrorResponse(
@@ -849,44 +755,6 @@ export class JSONRPCPlugin implements RuntimePlugin {
849755
};
850756
}
851757

852-
/**
853-
* Read request body as JSON
854-
*/
855-
private readBody(req: IncomingMessage): Promise<any> {
856-
return new Promise((resolve, reject) => {
857-
let body = '';
858-
req.on('data', chunk => body += chunk.toString());
859-
req.on('end', () => {
860-
if (!body) {
861-
reject(new Error('Empty body'));
862-
return;
863-
}
864-
try {
865-
resolve(JSON.parse(body));
866-
} catch (e) {
867-
reject(new Error('Invalid JSON'));
868-
}
869-
});
870-
req.on('error', reject);
871-
});
872-
}
873-
874-
/**
875-
* Send JSON response
876-
*/
877-
private sendJSON(res: ServerResponse, statusCode: number, data: any): void {
878-
res.setHeader('Content-Type', 'application/json');
879-
res.writeHead(statusCode);
880-
res.end(JSON.stringify(data, null, 2));
881-
}
882-
883-
/**
884-
* Send error response
885-
*/
886-
private sendError(res: ServerResponse, id: any, code: number, message: string): void {
887-
this.sendJSON(res, 200, this.createErrorResponse(id, code, message));
888-
}
889-
890758
/**
891759
* Generate a unique session ID
892760
*/
@@ -942,12 +810,9 @@ export class JSONRPCPlugin implements RuntimePlugin {
942810
/**
943811
* Send progress notification to SSE clients
944812
*/
945-
private sendProgress(sessionId: string, progress: ProgressNotification): void {
946-
const client = this.progressClients.get(sessionId);
947-
if (client) {
948-
client.write(`data: ${JSON.stringify(progress)}\n\n`);
949-
}
950-
}
813+
// private sendProgress(sessionId: string, progress: ProgressNotification): void {
814+
// // TODO: Implement for Hono
815+
// }
951816

952817
/**
953818
* Resolve result references in batch requests (e.g., $1.result.id)

0 commit comments

Comments
 (0)