@mostajs/net 1.0.0-alpha.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.
- package/dist/auth/apikey-middleware.d.ts +12 -0
- package/dist/auth/apikey-middleware.js +62 -0
- package/dist/auth/apikeys.d.ts +34 -0
- package/dist/auth/apikeys.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +60 -0
- package/dist/core/factory.d.ts +15 -0
- package/dist/core/factory.js +60 -0
- package/dist/core/middleware.d.ts +12 -0
- package/dist/core/middleware.js +32 -0
- package/dist/core/types.d.ts +62 -0
- package/dist/core/types.js +4 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +20 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +221 -0
- package/dist/transports/graphql.transport.d.ts +29 -0
- package/dist/transports/graphql.transport.js +164 -0
- package/dist/transports/jsonrpc.transport.d.ts +41 -0
- package/dist/transports/jsonrpc.transport.js +116 -0
- package/dist/transports/mcp.transport.d.ts +34 -0
- package/dist/transports/mcp.transport.js +188 -0
- package/dist/transports/rest.transport.d.ts +29 -0
- package/dist/transports/rest.transport.js +184 -0
- package/dist/transports/sse.transport.d.ts +45 -0
- package/dist/transports/sse.transport.js +99 -0
- package/dist/transports/ws.transport.d.ts +33 -0
- package/dist/transports/ws.transport.js +100 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @mostajs/net — Multi-protocol transport layer for @mostajs/orm
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
// Config
|
|
4
|
+
export { loadNetConfig, getEnabledTransports, TRANSPORT_NAMES } from './core/config.js';
|
|
5
|
+
// Factory
|
|
6
|
+
export { getTransport, getActiveTransports, stopAllTransports } from './core/factory.js';
|
|
7
|
+
// Middleware
|
|
8
|
+
export { composeMiddleware, loggingMiddleware } from './core/middleware.js';
|
|
9
|
+
// Transports
|
|
10
|
+
export { RestTransport } from './transports/rest.transport.js';
|
|
11
|
+
export { SSETransport } from './transports/sse.transport.js';
|
|
12
|
+
export { GraphQLTransport } from './transports/graphql.transport.js';
|
|
13
|
+
export { WebSocketTransport } from './transports/ws.transport.js';
|
|
14
|
+
export { JsonRpcTransport } from './transports/jsonrpc.transport.js';
|
|
15
|
+
export { McpTransport } from './transports/mcp.transport.js';
|
|
16
|
+
// Auth / API Keys
|
|
17
|
+
export { readApiKeys, writeApiKeys, generateApiKey, hashApiKey, createSubscription, revokeSubscription, validateApiKey, checkPermission, } from './auth/apikeys.js';
|
|
18
|
+
export { apiKeyMiddleware } from './auth/apikey-middleware.js';
|
|
19
|
+
// Server
|
|
20
|
+
export { startServer } from './server.js';
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { EntityService } from '@mostajs/orm';
|
|
3
|
+
export interface NetServer {
|
|
4
|
+
app: FastifyInstance;
|
|
5
|
+
entityService: EntityService;
|
|
6
|
+
stop: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Start the @mostajs/net server.
|
|
10
|
+
*
|
|
11
|
+
* 1. Load config from .env.local (MOSTA_NET_*)
|
|
12
|
+
* 2. Connect to ORM via @mostajs/orm (reads MOSTA_ORM_* / DB_DIALECT + SGBD_URI)
|
|
13
|
+
* 3. Create EntityService
|
|
14
|
+
* 4. Load and start enabled transports
|
|
15
|
+
* 5. Wire transports to EntityService
|
|
16
|
+
* 6. Listen on MOSTA_NET_PORT
|
|
17
|
+
*/
|
|
18
|
+
export declare function startServer(): Promise<NetServer>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// @mostajs/net — Main server orchestrator
|
|
2
|
+
// Loads config, connects ORM, starts transports, wires everything together
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
import Fastify from 'fastify';
|
|
5
|
+
import { EntityService, getAllSchemas, getDialect } from '@mostajs/orm';
|
|
6
|
+
import { loadNetConfig, getEnabledTransports } from './core/config.js';
|
|
7
|
+
import { getTransport, stopAllTransports } from './core/factory.js';
|
|
8
|
+
import { loggingMiddleware } from './core/middleware.js';
|
|
9
|
+
import { RestTransport } from './transports/rest.transport.js';
|
|
10
|
+
import { SSETransport } from './transports/sse.transport.js';
|
|
11
|
+
import { GraphQLTransport } from './transports/graphql.transport.js';
|
|
12
|
+
import { WebSocketTransport } from './transports/ws.transport.js';
|
|
13
|
+
import { JsonRpcTransport } from './transports/jsonrpc.transport.js';
|
|
14
|
+
import { McpTransport } from './transports/mcp.transport.js';
|
|
15
|
+
/**
|
|
16
|
+
* Start the @mostajs/net server.
|
|
17
|
+
*
|
|
18
|
+
* 1. Load config from .env.local (MOSTA_NET_*)
|
|
19
|
+
* 2. Connect to ORM via @mostajs/orm (reads MOSTA_ORM_* / DB_DIALECT + SGBD_URI)
|
|
20
|
+
* 3. Create EntityService
|
|
21
|
+
* 4. Load and start enabled transports
|
|
22
|
+
* 5. Wire transports to EntityService
|
|
23
|
+
* 6. Listen on MOSTA_NET_PORT
|
|
24
|
+
*/
|
|
25
|
+
export async function startServer() {
|
|
26
|
+
// 1. Load net config
|
|
27
|
+
const config = loadNetConfig();
|
|
28
|
+
// 2. Connect ORM
|
|
29
|
+
const dialect = await getDialect();
|
|
30
|
+
// 3. Create EntityService
|
|
31
|
+
const entityService = new EntityService(dialect);
|
|
32
|
+
// 4. Get schemas
|
|
33
|
+
const schemas = getAllSchemas();
|
|
34
|
+
// 5. Create shared Fastify instance
|
|
35
|
+
const app = Fastify({ logger: false });
|
|
36
|
+
// 6. ORM handler (OrmRequest → EntityService → OrmResponse)
|
|
37
|
+
const ormHandler = async (req, _ctx) => {
|
|
38
|
+
return entityService.execute(req);
|
|
39
|
+
};
|
|
40
|
+
// 7. Load and start enabled transports
|
|
41
|
+
const enabledNames = getEnabledTransports(config);
|
|
42
|
+
for (const name of enabledNames) {
|
|
43
|
+
const transportConfig = config.transports[name];
|
|
44
|
+
const transport = await getTransport(name, transportConfig);
|
|
45
|
+
if (!transport)
|
|
46
|
+
continue;
|
|
47
|
+
// Register all schemas
|
|
48
|
+
for (const schema of schemas) {
|
|
49
|
+
transport.registerEntity(schema);
|
|
50
|
+
}
|
|
51
|
+
// Add logging middleware
|
|
52
|
+
transport.use(loggingMiddleware);
|
|
53
|
+
// Wire ORM handler
|
|
54
|
+
if (transport instanceof RestTransport) {
|
|
55
|
+
transport.setHandler(ormHandler);
|
|
56
|
+
}
|
|
57
|
+
// Start the transport
|
|
58
|
+
await transport.start(transportConfig);
|
|
59
|
+
// Mount REST routes directly on the shared Fastify instance
|
|
60
|
+
if (transport instanceof RestTransport) {
|
|
61
|
+
for (const schema of schemas) {
|
|
62
|
+
registerRestRoutes(app, schema, ormHandler);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Mount SSE route and wire EntityService events → broadcast
|
|
66
|
+
if (transport instanceof SSETransport) {
|
|
67
|
+
const sseTransport = transport;
|
|
68
|
+
const ssePath = sseTransport.getPath();
|
|
69
|
+
// SSE endpoint: GET /events
|
|
70
|
+
app.get(ssePath, async (req, reply) => {
|
|
71
|
+
// Use raw Node.js response for SSE streaming
|
|
72
|
+
reply.hijack();
|
|
73
|
+
sseTransport.addClient(reply.raw);
|
|
74
|
+
});
|
|
75
|
+
// Wire EntityService change events → SSE broadcast
|
|
76
|
+
entityService.on('entity.created', (data) => sseTransport.broadcast('entity.created', data));
|
|
77
|
+
entityService.on('entity.updated', (data) => sseTransport.broadcast('entity.updated', data));
|
|
78
|
+
entityService.on('entity.deleted', (data) => sseTransport.broadcast('entity.deleted', data));
|
|
79
|
+
entityService.on('entity.upserted', (data) => sseTransport.broadcast('entity.upserted', data));
|
|
80
|
+
}
|
|
81
|
+
// Mount GraphQL via mercurius
|
|
82
|
+
if (transport instanceof GraphQLTransport) {
|
|
83
|
+
const gqlTransport = transport;
|
|
84
|
+
gqlTransport.setHandler(ormHandler);
|
|
85
|
+
const schema = gqlTransport.generateSchema();
|
|
86
|
+
const resolvers = gqlTransport.generateResolvers();
|
|
87
|
+
const mercurius = (await import('mercurius')).default;
|
|
88
|
+
await app.register(mercurius, {
|
|
89
|
+
schema,
|
|
90
|
+
resolvers,
|
|
91
|
+
path: gqlTransport.getInfo().url || '/graphql',
|
|
92
|
+
graphiql: true, // Enable GraphiQL IDE at the same path
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Mount JSON-RPC endpoint
|
|
96
|
+
if (transport instanceof JsonRpcTransport) {
|
|
97
|
+
const rpcTransport = transport;
|
|
98
|
+
rpcTransport.setHandler(ormHandler);
|
|
99
|
+
const rpcPath = rpcTransport.getPath();
|
|
100
|
+
app.post(rpcPath, async (req, reply) => {
|
|
101
|
+
const result = await rpcTransport.handleBody(req.body);
|
|
102
|
+
return result;
|
|
103
|
+
});
|
|
104
|
+
// Discovery: list available methods
|
|
105
|
+
app.get(rpcPath, async () => ({
|
|
106
|
+
jsonrpc: '2.0',
|
|
107
|
+
methods: rpcTransport.listMethods(),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
// Mount MCP endpoint (Streamable HTTP)
|
|
111
|
+
if (transport instanceof McpTransport) {
|
|
112
|
+
const mcpTransport = transport;
|
|
113
|
+
mcpTransport.setHandler(ormHandler);
|
|
114
|
+
const mcpPath = mcpTransport.getPath();
|
|
115
|
+
// MCP uses raw Node.js request/response for streaming
|
|
116
|
+
app.all(mcpPath, async (req, reply) => {
|
|
117
|
+
reply.hijack();
|
|
118
|
+
await mcpTransport.handleRequest(req.raw, reply.raw, req.body);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const info = transport.getInfo();
|
|
122
|
+
console.log(` \x1b[32m●\x1b[0m ${info.name.charAt(0).toUpperCase() + info.name.slice(1)}Transport${info.url ? ' ' + info.url : ''}${info.port ? ' :' + info.port : ''}`);
|
|
123
|
+
}
|
|
124
|
+
// 8. Health check
|
|
125
|
+
app.get('/health', async () => ({ status: 'ok', transports: enabledNames, entities: schemas.map(s => s.name) }));
|
|
126
|
+
// 9. Listen
|
|
127
|
+
await app.listen({ port: config.port, host: '0.0.0.0' });
|
|
128
|
+
// 10. Attach WebSocket to the HTTP server (must be after listen)
|
|
129
|
+
for (const name of enabledNames) {
|
|
130
|
+
const transport = (await import('./core/factory.js')).getActiveTransports().find(t => t.name === name);
|
|
131
|
+
if (transport instanceof WebSocketTransport) {
|
|
132
|
+
const wsTransport = transport;
|
|
133
|
+
wsTransport.setHandler(ormHandler);
|
|
134
|
+
wsTransport.attachToServer(app.server);
|
|
135
|
+
// Wire change events → WS broadcast
|
|
136
|
+
entityService.on('entity.created', (data) => wsTransport.broadcast('entity.created', data));
|
|
137
|
+
entityService.on('entity.updated', (data) => wsTransport.broadcast('entity.updated', data));
|
|
138
|
+
entityService.on('entity.deleted', (data) => wsTransport.broadcast('entity.deleted', data));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log(`\n \x1b[36mReady.\x1b[0m ${schemas.length} entities × ${enabledNames.length} transports = ${schemas.length * enabledNames.length} endpoints\n`);
|
|
142
|
+
return {
|
|
143
|
+
app,
|
|
144
|
+
entityService,
|
|
145
|
+
stop: async () => {
|
|
146
|
+
await stopAllTransports();
|
|
147
|
+
await app.close();
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Register REST routes directly on the main Fastify instance.
|
|
153
|
+
* This avoids the complexity of merging two Fastify instances.
|
|
154
|
+
*/
|
|
155
|
+
function registerRestRoutes(app, schema, ormHandler) {
|
|
156
|
+
const prefix = '/api/v1';
|
|
157
|
+
const col = schema.collection;
|
|
158
|
+
const handle = async (ormReq, reply) => {
|
|
159
|
+
const ctx = { transport: 'rest' };
|
|
160
|
+
const res = await ormHandler(ormReq, ctx);
|
|
161
|
+
if (res.status === 'error') {
|
|
162
|
+
reply.status(res.error?.code === 'ENTITY_NOT_FOUND' || res.error?.code === 'EntityNotFoundError' ? 404 :
|
|
163
|
+
res.error?.code?.startsWith('MISSING') ? 400 : 500);
|
|
164
|
+
}
|
|
165
|
+
return res;
|
|
166
|
+
};
|
|
167
|
+
// GET /api/v1/{collection}
|
|
168
|
+
app.get(`${prefix}/${col}`, async (req, reply) => {
|
|
169
|
+
const q = req.query;
|
|
170
|
+
return handle({
|
|
171
|
+
op: 'findAll', entity: schema.name,
|
|
172
|
+
filter: q.filter ? JSON.parse(q.filter) : {},
|
|
173
|
+
options: {
|
|
174
|
+
sort: q.sort ? JSON.parse(q.sort) : undefined,
|
|
175
|
+
limit: q.limit ? parseInt(q.limit, 10) : undefined,
|
|
176
|
+
skip: q.skip ? parseInt(q.skip, 10) : undefined,
|
|
177
|
+
},
|
|
178
|
+
}, reply);
|
|
179
|
+
});
|
|
180
|
+
// GET /api/v1/{collection}/count
|
|
181
|
+
app.get(`${prefix}/${col}/count`, async (req, reply) => {
|
|
182
|
+
const q = req.query;
|
|
183
|
+
return handle({ op: 'count', entity: schema.name, filter: q.filter ? JSON.parse(q.filter) : {} }, reply);
|
|
184
|
+
});
|
|
185
|
+
// GET /api/v1/{collection}/:id
|
|
186
|
+
app.get(`${prefix}/${col}/:id`, async (req, reply) => {
|
|
187
|
+
const { id } = req.params;
|
|
188
|
+
const q = req.query;
|
|
189
|
+
return handle({
|
|
190
|
+
op: 'findById', entity: schema.name, id,
|
|
191
|
+
relations: q.include ? q.include.split(',') : undefined,
|
|
192
|
+
}, reply);
|
|
193
|
+
});
|
|
194
|
+
// POST /api/v1/{collection}
|
|
195
|
+
app.post(`${prefix}/${col}`, async (req, reply) => {
|
|
196
|
+
const res = await handle({ op: 'create', entity: schema.name, data: req.body }, reply);
|
|
197
|
+
if (reply.statusCode < 400)
|
|
198
|
+
reply.status(201);
|
|
199
|
+
return res;
|
|
200
|
+
});
|
|
201
|
+
// PUT /api/v1/{collection}/:id
|
|
202
|
+
app.put(`${prefix}/${col}/:id`, async (req, reply) => {
|
|
203
|
+
const { id } = req.params;
|
|
204
|
+
return handle({ op: 'update', entity: schema.name, id, data: req.body }, reply);
|
|
205
|
+
});
|
|
206
|
+
// DELETE /api/v1/{collection}/:id
|
|
207
|
+
app.delete(`${prefix}/${col}/:id`, async (req, reply) => {
|
|
208
|
+
const { id } = req.params;
|
|
209
|
+
return handle({ op: 'delete', entity: schema.name, id }, reply);
|
|
210
|
+
});
|
|
211
|
+
// POST /api/v1/{collection}/search
|
|
212
|
+
app.post(`${prefix}/${col}/search`, async (req, reply) => {
|
|
213
|
+
const body = req.body;
|
|
214
|
+
return handle({
|
|
215
|
+
op: 'search', entity: schema.name,
|
|
216
|
+
query: body.query,
|
|
217
|
+
searchFields: body.fields,
|
|
218
|
+
options: body.options,
|
|
219
|
+
}, reply);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
|
|
2
|
+
import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
|
|
3
|
+
type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
|
|
4
|
+
export declare class GraphQLTransport implements ITransport {
|
|
5
|
+
readonly name = "graphql";
|
|
6
|
+
private config;
|
|
7
|
+
private schemas;
|
|
8
|
+
private middlewares;
|
|
9
|
+
private ormHandler;
|
|
10
|
+
private stats;
|
|
11
|
+
setHandler(handler: OrmHandler): void;
|
|
12
|
+
use(mw: TransportMiddleware): void;
|
|
13
|
+
registerEntity(schema: EntitySchema): void;
|
|
14
|
+
start(config: TransportConfig): Promise<void>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
getInfo(): TransportInfo;
|
|
17
|
+
/**
|
|
18
|
+
* Generate the GraphQL SDL schema string from registered EntitySchemas.
|
|
19
|
+
*/
|
|
20
|
+
generateSchema(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Generate resolvers that call OrmHandler.
|
|
23
|
+
*/
|
|
24
|
+
generateResolvers(): Record<string, Record<string, Function>>;
|
|
25
|
+
private callOrm;
|
|
26
|
+
}
|
|
27
|
+
/** Factory */
|
|
28
|
+
export declare function createTransport(): ITransport;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// GraphQLTransport — Auto-generates GraphQL schema from EntitySchemas
|
|
2
|
+
// Queries and mutations call EntityService via OrmRequest/OrmResponse
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
export class GraphQLTransport {
|
|
5
|
+
name = 'graphql';
|
|
6
|
+
config = null;
|
|
7
|
+
schemas = [];
|
|
8
|
+
middlewares = [];
|
|
9
|
+
ormHandler = null;
|
|
10
|
+
stats = { requests: 0, errors: 0, startedAt: 0 };
|
|
11
|
+
setHandler(handler) { this.ormHandler = handler; }
|
|
12
|
+
use(mw) { this.middlewares.push(mw); }
|
|
13
|
+
registerEntity(schema) { this.schemas.push(schema); }
|
|
14
|
+
async start(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.stats.startedAt = Date.now();
|
|
17
|
+
}
|
|
18
|
+
async stop() { this.config = null; }
|
|
19
|
+
getInfo() {
|
|
20
|
+
return {
|
|
21
|
+
name: this.name,
|
|
22
|
+
status: this.config ? 'running' : 'stopped',
|
|
23
|
+
url: this.config?.path || '/graphql',
|
|
24
|
+
entities: this.schemas.map(s => s.name),
|
|
25
|
+
stats: { ...this.stats },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate the GraphQL SDL schema string from registered EntitySchemas.
|
|
30
|
+
*/
|
|
31
|
+
generateSchema() {
|
|
32
|
+
const types = [];
|
|
33
|
+
const queries = [];
|
|
34
|
+
const mutations = [];
|
|
35
|
+
for (const schema of this.schemas) {
|
|
36
|
+
const name = schema.name;
|
|
37
|
+
const fields = Object.entries(schema.fields);
|
|
38
|
+
// Type definition
|
|
39
|
+
const fieldDefs = [
|
|
40
|
+
' id: ID!',
|
|
41
|
+
...fields.map(([fname, fdef]) => ` ${fname}: ${gqlType(fdef.type, fdef.required)}`),
|
|
42
|
+
...(schema.timestamps ? [' createdAt: String', ' updatedAt: String'] : []),
|
|
43
|
+
];
|
|
44
|
+
types.push(`type ${name} {\n${fieldDefs.join('\n')}\n}`);
|
|
45
|
+
// Input type for create/update
|
|
46
|
+
const inputFields = fields.map(([fname, fdef]) => ` ${fname}: ${gqlInputType(fdef.type, fdef.required && fname !== 'id')}`);
|
|
47
|
+
types.push(`input ${name}Input {\n${inputFields.join('\n')}\n}`);
|
|
48
|
+
// Queries
|
|
49
|
+
queries.push(` ${lcfirst(name)}s(filter: String, limit: Int, skip: Int, sort: String): [${name}!]!`);
|
|
50
|
+
queries.push(` ${lcfirst(name)}(id: ID!): ${name}`);
|
|
51
|
+
queries.push(` ${lcfirst(name)}Count(filter: String): Int!`);
|
|
52
|
+
// Mutations
|
|
53
|
+
mutations.push(` create${name}(input: ${name}Input!): ${name}!`);
|
|
54
|
+
mutations.push(` update${name}(id: ID!, input: ${name}Input!): ${name}`);
|
|
55
|
+
mutations.push(` delete${name}(id: ID!): Boolean!`);
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
...types,
|
|
59
|
+
`type Query {\n${queries.join('\n')}\n}`,
|
|
60
|
+
`type Mutation {\n${mutations.join('\n')}\n}`,
|
|
61
|
+
].join('\n\n');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate resolvers that call OrmHandler.
|
|
65
|
+
*/
|
|
66
|
+
generateResolvers() {
|
|
67
|
+
const Query = {};
|
|
68
|
+
const Mutation = {};
|
|
69
|
+
for (const schema of this.schemas) {
|
|
70
|
+
const name = schema.name;
|
|
71
|
+
// Query: users(filter, limit, skip, sort)
|
|
72
|
+
Query[`${lcfirst(name)}s`] = async (_, args) => {
|
|
73
|
+
this.stats.requests++;
|
|
74
|
+
const req = {
|
|
75
|
+
op: 'findAll',
|
|
76
|
+
entity: name,
|
|
77
|
+
filter: args.filter ? JSON.parse(args.filter) : {},
|
|
78
|
+
options: {
|
|
79
|
+
limit: args.limit,
|
|
80
|
+
skip: args.skip,
|
|
81
|
+
sort: args.sort ? JSON.parse(args.sort) : undefined,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const res = await this.callOrm(req);
|
|
85
|
+
return res.data || [];
|
|
86
|
+
};
|
|
87
|
+
// Query: user(id)
|
|
88
|
+
Query[lcfirst(name)] = async (_, args) => {
|
|
89
|
+
this.stats.requests++;
|
|
90
|
+
const res = await this.callOrm({ op: 'findById', entity: name, id: args.id });
|
|
91
|
+
return res.data;
|
|
92
|
+
};
|
|
93
|
+
// Query: userCount(filter)
|
|
94
|
+
Query[`${lcfirst(name)}Count`] = async (_, args) => {
|
|
95
|
+
this.stats.requests++;
|
|
96
|
+
const res = await this.callOrm({
|
|
97
|
+
op: 'count', entity: name,
|
|
98
|
+
filter: args.filter ? JSON.parse(args.filter) : {},
|
|
99
|
+
});
|
|
100
|
+
return res.data || 0;
|
|
101
|
+
};
|
|
102
|
+
// Mutation: createUser(input)
|
|
103
|
+
Mutation[`create${name}`] = async (_, args) => {
|
|
104
|
+
this.stats.requests++;
|
|
105
|
+
const res = await this.callOrm({ op: 'create', entity: name, data: args.input });
|
|
106
|
+
if (res.status === 'error')
|
|
107
|
+
throw new Error(res.error?.message);
|
|
108
|
+
return res.data;
|
|
109
|
+
};
|
|
110
|
+
// Mutation: updateUser(id, input)
|
|
111
|
+
Mutation[`update${name}`] = async (_, args) => {
|
|
112
|
+
this.stats.requests++;
|
|
113
|
+
const res = await this.callOrm({ op: 'update', entity: name, id: args.id, data: args.input });
|
|
114
|
+
if (res.status === 'error')
|
|
115
|
+
throw new Error(res.error?.message);
|
|
116
|
+
return res.data;
|
|
117
|
+
};
|
|
118
|
+
// Mutation: deleteUser(id)
|
|
119
|
+
Mutation[`delete${name}`] = async (_, args) => {
|
|
120
|
+
this.stats.requests++;
|
|
121
|
+
const res = await this.callOrm({ op: 'delete', entity: name, id: args.id });
|
|
122
|
+
return res.data === true;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { Query, Mutation };
|
|
126
|
+
}
|
|
127
|
+
async callOrm(req) {
|
|
128
|
+
if (!this.ormHandler) {
|
|
129
|
+
return { status: 'error', error: { code: 'NO_HANDLER', message: 'ORM handler not initialized' } };
|
|
130
|
+
}
|
|
131
|
+
const ctx = { transport: this.name };
|
|
132
|
+
return this.ormHandler(req, ctx);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ============================================================
|
|
136
|
+
// Helpers
|
|
137
|
+
// ============================================================
|
|
138
|
+
function gqlType(type, required) {
|
|
139
|
+
const base = fieldTypeToGql(type);
|
|
140
|
+
return required ? `${base}!` : base;
|
|
141
|
+
}
|
|
142
|
+
function gqlInputType(type, required) {
|
|
143
|
+
const base = fieldTypeToGql(type);
|
|
144
|
+
return required ? `${base}!` : base;
|
|
145
|
+
}
|
|
146
|
+
function fieldTypeToGql(type) {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'string':
|
|
149
|
+
case 'text': return 'String';
|
|
150
|
+
case 'number': return 'Float';
|
|
151
|
+
case 'boolean': return 'Boolean';
|
|
152
|
+
case 'date': return 'String';
|
|
153
|
+
case 'json': return 'String';
|
|
154
|
+
case 'array': return '[String]';
|
|
155
|
+
default: return 'String';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function lcfirst(s) {
|
|
159
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
160
|
+
}
|
|
161
|
+
/** Factory */
|
|
162
|
+
export function createTransport() {
|
|
163
|
+
return new GraphQLTransport();
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
|
|
2
|
+
import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
|
|
3
|
+
type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
|
|
4
|
+
interface JsonRpcResponse {
|
|
5
|
+
jsonrpc: '2.0';
|
|
6
|
+
result?: unknown;
|
|
7
|
+
error?: {
|
|
8
|
+
code: number;
|
|
9
|
+
message: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
};
|
|
12
|
+
id: string | number | null;
|
|
13
|
+
}
|
|
14
|
+
export declare class JsonRpcTransport implements ITransport {
|
|
15
|
+
readonly name = "jsonrpc";
|
|
16
|
+
private config;
|
|
17
|
+
private schemas;
|
|
18
|
+
private middlewares;
|
|
19
|
+
private ormHandler;
|
|
20
|
+
private stats;
|
|
21
|
+
setHandler(handler: OrmHandler): void;
|
|
22
|
+
use(mw: TransportMiddleware): void;
|
|
23
|
+
registerEntity(schema: EntitySchema): void;
|
|
24
|
+
start(config: TransportConfig): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
getInfo(): TransportInfo;
|
|
27
|
+
getPath(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Handle a JSON-RPC 2.0 request body.
|
|
30
|
+
* Supports single requests and batch (array of requests).
|
|
31
|
+
*/
|
|
32
|
+
handleBody(body: unknown): Promise<JsonRpcResponse | JsonRpcResponse[]>;
|
|
33
|
+
private handleSingleRequest;
|
|
34
|
+
/**
|
|
35
|
+
* List available methods (for discovery).
|
|
36
|
+
*/
|
|
37
|
+
listMethods(): string[];
|
|
38
|
+
}
|
|
39
|
+
/** Factory */
|
|
40
|
+
export declare function createTransport(): ITransport;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// JsonRpcTransport — JSON-RPC 2.0 over HTTP
|
|
2
|
+
// Implements the JSON-RPC 2.0 specification (jsonrpc.org)
|
|
3
|
+
// Foundation for MCP transport (MCP uses JSON-RPC 2.0)
|
|
4
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
5
|
+
// JSON-RPC 2.0 error codes
|
|
6
|
+
const PARSE_ERROR = -32700;
|
|
7
|
+
const INVALID_REQUEST = -32600;
|
|
8
|
+
const METHOD_NOT_FOUND = -32601;
|
|
9
|
+
const INVALID_PARAMS = -32602;
|
|
10
|
+
const INTERNAL_ERROR = -32603;
|
|
11
|
+
export class JsonRpcTransport {
|
|
12
|
+
name = 'jsonrpc';
|
|
13
|
+
config = null;
|
|
14
|
+
schemas = [];
|
|
15
|
+
middlewares = [];
|
|
16
|
+
ormHandler = null;
|
|
17
|
+
stats = { requests: 0, errors: 0, startedAt: 0 };
|
|
18
|
+
setHandler(handler) { this.ormHandler = handler; }
|
|
19
|
+
use(mw) { this.middlewares.push(mw); }
|
|
20
|
+
registerEntity(schema) { this.schemas.push(schema); }
|
|
21
|
+
async start(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.stats.startedAt = Date.now();
|
|
24
|
+
}
|
|
25
|
+
async stop() { this.config = null; }
|
|
26
|
+
getInfo() {
|
|
27
|
+
return {
|
|
28
|
+
name: this.name,
|
|
29
|
+
status: this.config ? 'running' : 'stopped',
|
|
30
|
+
url: this.config?.path || '/rpc',
|
|
31
|
+
entities: this.schemas.map(s => s.name),
|
|
32
|
+
stats: { ...this.stats },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
getPath() {
|
|
36
|
+
return this.config?.path || '/rpc';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Handle a JSON-RPC 2.0 request body.
|
|
40
|
+
* Supports single requests and batch (array of requests).
|
|
41
|
+
*/
|
|
42
|
+
async handleBody(body) {
|
|
43
|
+
// Batch request
|
|
44
|
+
if (Array.isArray(body)) {
|
|
45
|
+
return Promise.all(body.map(req => this.handleSingleRequest(req)));
|
|
46
|
+
}
|
|
47
|
+
return this.handleSingleRequest(body);
|
|
48
|
+
}
|
|
49
|
+
async handleSingleRequest(raw) {
|
|
50
|
+
this.stats.requests++;
|
|
51
|
+
// Validate JSON-RPC envelope
|
|
52
|
+
const req = raw;
|
|
53
|
+
if (!req || req.jsonrpc !== '2.0' || typeof req.method !== 'string') {
|
|
54
|
+
this.stats.errors++;
|
|
55
|
+
return { jsonrpc: '2.0', error: { code: INVALID_REQUEST, message: 'Invalid JSON-RPC 2.0 request' }, id: req?.id ?? null };
|
|
56
|
+
}
|
|
57
|
+
// Parse method: "entity.{op}" or "{entityName}.{op}"
|
|
58
|
+
const parts = req.method.split('.');
|
|
59
|
+
if (parts.length < 2) {
|
|
60
|
+
this.stats.errors++;
|
|
61
|
+
return { jsonrpc: '2.0', error: { code: METHOD_NOT_FOUND, message: `Invalid method format: "${req.method}". Expected: "Entity.operation"` }, id: req.id ?? null };
|
|
62
|
+
}
|
|
63
|
+
const entityName = parts[0];
|
|
64
|
+
const op = parts[1];
|
|
65
|
+
const params = (req.params || {});
|
|
66
|
+
// Build OrmRequest
|
|
67
|
+
const ormReq = {
|
|
68
|
+
op: op,
|
|
69
|
+
entity: entityName,
|
|
70
|
+
id: params.id,
|
|
71
|
+
filter: params.filter,
|
|
72
|
+
data: params.data,
|
|
73
|
+
options: params.options,
|
|
74
|
+
relations: params.relations,
|
|
75
|
+
query: params.query,
|
|
76
|
+
searchFields: params.searchFields,
|
|
77
|
+
stages: params.stages,
|
|
78
|
+
};
|
|
79
|
+
if (!this.ormHandler) {
|
|
80
|
+
this.stats.errors++;
|
|
81
|
+
return { jsonrpc: '2.0', error: { code: INTERNAL_ERROR, message: 'ORM handler not initialized' }, id: req.id ?? null };
|
|
82
|
+
}
|
|
83
|
+
const ctx = { transport: this.name };
|
|
84
|
+
const res = await this.ormHandler(ormReq, ctx);
|
|
85
|
+
if (res.status === 'error') {
|
|
86
|
+
this.stats.errors++;
|
|
87
|
+
return {
|
|
88
|
+
jsonrpc: '2.0',
|
|
89
|
+
error: { code: INTERNAL_ERROR, message: res.error?.message || 'Unknown error', data: res.error },
|
|
90
|
+
id: req.id ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
jsonrpc: '2.0',
|
|
95
|
+
result: { data: res.data, metadata: res.metadata },
|
|
96
|
+
id: req.id ?? null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List available methods (for discovery).
|
|
101
|
+
*/
|
|
102
|
+
listMethods() {
|
|
103
|
+
const methods = [];
|
|
104
|
+
const ops = ['findAll', 'findOne', 'findById', 'create', 'update', 'delete', 'count', 'search'];
|
|
105
|
+
for (const schema of this.schemas) {
|
|
106
|
+
for (const op of ops) {
|
|
107
|
+
methods.push(`${schema.name}.${op}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return methods;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Factory */
|
|
114
|
+
export function createTransport() {
|
|
115
|
+
return new JsonRpcTransport();
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
|
|
3
|
+
import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
|
|
4
|
+
type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
|
|
5
|
+
export declare class McpTransport implements ITransport {
|
|
6
|
+
readonly name = "mcp";
|
|
7
|
+
private config;
|
|
8
|
+
private schemas;
|
|
9
|
+
private middlewares;
|
|
10
|
+
private ormHandler;
|
|
11
|
+
private stats;
|
|
12
|
+
setHandler(handler: OrmHandler): void;
|
|
13
|
+
use(mw: TransportMiddleware): void;
|
|
14
|
+
registerEntity(schema: EntitySchema): void;
|
|
15
|
+
start(config: TransportConfig): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
getInfo(): TransportInfo;
|
|
18
|
+
getPath(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Handle an incoming MCP HTTP request.
|
|
21
|
+
* Called by the main Fastify server via route handler.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Handle an incoming MCP HTTP request.
|
|
25
|
+
* Creates a new McpServer + Transport per session for stateless mode.
|
|
26
|
+
*/
|
|
27
|
+
handleRequest(req: IncomingMessage, res: ServerResponse, body?: unknown): Promise<void>;
|
|
28
|
+
private registerEntityToolsOn;
|
|
29
|
+
private registerEntityResourcesOn;
|
|
30
|
+
private callOrm;
|
|
31
|
+
}
|
|
32
|
+
/** Factory */
|
|
33
|
+
export declare function createTransport(): ITransport;
|
|
34
|
+
export {};
|