@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.
@@ -0,0 +1,188 @@
1
+ // McpTransport — Model Context Protocol transport adapter
2
+ // Exposes ORM entities as MCP tools and resources for AI agents (Claude, GPT, etc.)
3
+ // Uses the official @modelcontextprotocol/sdk
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import { z } from 'zod';
8
+ export class McpTransport {
9
+ name = 'mcp';
10
+ config = null;
11
+ schemas = [];
12
+ middlewares = [];
13
+ ormHandler = null;
14
+ stats = { requests: 0, errors: 0, startedAt: 0 };
15
+ setHandler(handler) { this.ormHandler = handler; }
16
+ use(mw) { this.middlewares.push(mw); }
17
+ registerEntity(schema) { this.schemas.push(schema); }
18
+ async start(config) {
19
+ this.config = config;
20
+ this.stats.startedAt = Date.now();
21
+ // MCP server is created per-request in handleRequest() (stateless mode)
22
+ }
23
+ async stop() {
24
+ this.config = null;
25
+ }
26
+ getInfo() {
27
+ return {
28
+ name: this.name,
29
+ status: this.config ? 'running' : 'stopped',
30
+ url: this.config?.path || '/mcp',
31
+ entities: this.schemas.map(s => s.name),
32
+ stats: { ...this.stats },
33
+ };
34
+ }
35
+ getPath() {
36
+ return this.config?.path || '/mcp';
37
+ }
38
+ /**
39
+ * Handle an incoming MCP HTTP request.
40
+ * Called by the main Fastify server via route handler.
41
+ */
42
+ /**
43
+ * Handle an incoming MCP HTTP request.
44
+ * Creates a new McpServer + Transport per session for stateless mode.
45
+ */
46
+ async handleRequest(req, res, body) {
47
+ // Create a fresh MCP server per request (stateless mode)
48
+ const server = new McpServer({ name: '@mostajs/orm', version: '1.5.0' }, { capabilities: { tools: {}, resources: {} } });
49
+ // Register tools and resources
50
+ for (const schema of this.schemas) {
51
+ this.registerEntityToolsOn(server, schema);
52
+ this.registerEntityResourcesOn(server, schema);
53
+ }
54
+ // Create transport and connect
55
+ const transport = new StreamableHTTPServerTransport({
56
+ sessionIdGenerator: undefined, // stateless
57
+ });
58
+ await server.connect(transport);
59
+ await transport.handleRequest(req, res, body);
60
+ }
61
+ // ============================================================
62
+ // Tool registration (CRUD operations per entity)
63
+ // ============================================================
64
+ registerEntityToolsOn(server, schema) {
65
+ const name = schema.name;
66
+ // Tool: {Entity}_findAll — query entities
67
+ server.registerTool(`${name}_findAll`, {
68
+ description: `Find all ${name} entities. Optionally filter, sort, limit.`,
69
+ inputSchema: {
70
+ filter: z.string().optional().describe('JSON filter object (MongoDB-style)'),
71
+ sort: z.string().optional().describe('JSON sort object, e.g. {"name":1}'),
72
+ limit: z.number().optional().describe('Max results to return'),
73
+ skip: z.number().optional().describe('Number of results to skip'),
74
+ },
75
+ }, async (params) => {
76
+ this.stats.requests++;
77
+ const res = await this.callOrm({
78
+ op: 'findAll',
79
+ entity: name,
80
+ filter: params.filter ? JSON.parse(params.filter) : {},
81
+ options: {
82
+ sort: params.sort ? JSON.parse(params.sort) : undefined,
83
+ limit: params.limit,
84
+ skip: params.skip,
85
+ },
86
+ });
87
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
88
+ });
89
+ // Tool: {Entity}_findById — get one entity by ID
90
+ server.registerTool(`${name}_findById`, {
91
+ description: `Find a ${name} entity by its ID.`,
92
+ inputSchema: {
93
+ id: z.string().describe(`The ${name} ID`),
94
+ },
95
+ }, async (params) => {
96
+ this.stats.requests++;
97
+ const res = await this.callOrm({ op: 'findById', entity: name, id: params.id });
98
+ if (!res.data) {
99
+ return { content: [{ type: 'text', text: `${name} with id "${params.id}" not found` }], isError: true };
100
+ }
101
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
102
+ });
103
+ // Tool: {Entity}_create — create a new entity
104
+ server.registerTool(`${name}_create`, {
105
+ description: `Create a new ${name} entity.`,
106
+ inputSchema: {
107
+ data: z.string().describe(`JSON object with ${name} fields: ${Object.keys(schema.fields).join(', ')}`),
108
+ },
109
+ }, async (params) => {
110
+ this.stats.requests++;
111
+ const data = JSON.parse(params.data);
112
+ const res = await this.callOrm({ op: 'create', entity: name, data });
113
+ if (res.status === 'error') {
114
+ return { content: [{ type: 'text', text: `Error: ${res.error?.message}` }], isError: true };
115
+ }
116
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
117
+ });
118
+ // Tool: {Entity}_update — update an entity
119
+ server.registerTool(`${name}_update`, {
120
+ description: `Update a ${name} entity by ID.`,
121
+ inputSchema: {
122
+ id: z.string().describe(`The ${name} ID to update`),
123
+ data: z.string().describe('JSON object with fields to update'),
124
+ },
125
+ }, async (params) => {
126
+ this.stats.requests++;
127
+ const data = JSON.parse(params.data);
128
+ const res = await this.callOrm({ op: 'update', entity: name, id: params.id, data });
129
+ if (res.status === 'error') {
130
+ return { content: [{ type: 'text', text: `Error: ${res.error?.message}` }], isError: true };
131
+ }
132
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
133
+ });
134
+ // Tool: {Entity}_delete — delete an entity
135
+ server.registerTool(`${name}_delete`, {
136
+ description: `Delete a ${name} entity by ID.`,
137
+ inputSchema: {
138
+ id: z.string().describe(`The ${name} ID to delete`),
139
+ },
140
+ }, async (params) => {
141
+ this.stats.requests++;
142
+ const res = await this.callOrm({ op: 'delete', entity: name, id: params.id });
143
+ return { content: [{ type: 'text', text: res.data ? 'Deleted' : 'Not found' }] };
144
+ });
145
+ // Tool: {Entity}_count — count entities
146
+ server.registerTool(`${name}_count`, {
147
+ description: `Count ${name} entities. Optionally filter.`,
148
+ inputSchema: {
149
+ filter: z.string().optional().describe('JSON filter object'),
150
+ },
151
+ }, async (params) => {
152
+ this.stats.requests++;
153
+ const res = await this.callOrm({
154
+ op: 'count',
155
+ entity: name,
156
+ filter: params.filter ? JSON.parse(params.filter) : {},
157
+ });
158
+ return { content: [{ type: 'text', text: String(res.data) }] };
159
+ });
160
+ }
161
+ // ============================================================
162
+ // Resource registration (schema info per entity)
163
+ // ============================================================
164
+ registerEntityResourcesOn(server, schema) {
165
+ // Resource: entity schema definition
166
+ server.registerResource(`${schema.name} Schema`, `entity://${schema.name}/schema`, { description: `Schema definition for ${schema.name} entity`, mimeType: 'application/json' }, async () => ({
167
+ contents: [{
168
+ uri: `entity://${schema.name}/schema`,
169
+ text: JSON.stringify(schema, null, 2),
170
+ mimeType: 'application/json',
171
+ }],
172
+ }));
173
+ }
174
+ // ============================================================
175
+ // ORM call helper
176
+ // ============================================================
177
+ async callOrm(req) {
178
+ if (!this.ormHandler) {
179
+ return { status: 'error', error: { code: 'NO_HANDLER', message: 'ORM handler not initialized' } };
180
+ }
181
+ const ctx = { transport: this.name };
182
+ return this.ormHandler(req, ctx);
183
+ }
184
+ }
185
+ /** Factory */
186
+ export function createTransport() {
187
+ return new McpTransport();
188
+ }
@@ -0,0 +1,29 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
3
+ import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
4
+ /** Handler function that executes OrmRequest (set by the server) */
5
+ type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
6
+ export declare class RestTransport implements ITransport {
7
+ readonly name = "rest";
8
+ private app;
9
+ private config;
10
+ private schemas;
11
+ private middlewares;
12
+ private ormHandler;
13
+ private stats;
14
+ /** Set the ORM handler (called by the server after creating EntityService) */
15
+ setHandler(handler: OrmHandler): void;
16
+ use(middleware: TransportMiddleware): void;
17
+ registerEntity(schema: EntitySchema): void;
18
+ start(config: TransportConfig): Promise<void>;
19
+ stop(): Promise<void>;
20
+ getInfo(): TransportInfo;
21
+ /** Get the Fastify instance (for mounting in the shared server) */
22
+ getApp(): FastifyInstance | null;
23
+ private registerRoutes;
24
+ private handle;
25
+ private mapErrorToHttpStatus;
26
+ }
27
+ /** Factory function (used by transport loader) */
28
+ export declare function createTransport(): ITransport;
29
+ export {};
@@ -0,0 +1,184 @@
1
+ // RestTransport — REST API transport adapter
2
+ // Generates CRUD routes from EntitySchema, translates HTTP → OrmRequest → OrmResponse → HTTP
3
+ // Mirror of MongoDialect / PostgresDialect in @mostajs/orm
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ import Fastify from 'fastify';
6
+ import { composeMiddleware } from '../core/middleware.js';
7
+ export class RestTransport {
8
+ name = 'rest';
9
+ app = null;
10
+ config = null;
11
+ schemas = [];
12
+ middlewares = [];
13
+ ormHandler = null;
14
+ stats = { requests: 0, errors: 0, startedAt: 0 };
15
+ /** Set the ORM handler (called by the server after creating EntityService) */
16
+ setHandler(handler) {
17
+ this.ormHandler = handler;
18
+ }
19
+ use(middleware) {
20
+ this.middlewares.push(middleware);
21
+ }
22
+ registerEntity(schema) {
23
+ this.schemas.push(schema);
24
+ }
25
+ async start(config) {
26
+ this.config = config;
27
+ this.stats.startedAt = Date.now();
28
+ // Fastify instance is created here but NOT listened —
29
+ // the main server.ts will inject it into the shared Fastify instance
30
+ this.app = Fastify({ logger: false });
31
+ // Register routes for each entity
32
+ for (const schema of this.schemas) {
33
+ this.registerRoutes(schema);
34
+ }
35
+ }
36
+ async stop() {
37
+ this.app = null;
38
+ }
39
+ getInfo() {
40
+ return {
41
+ name: this.name,
42
+ status: this.app ? 'running' : 'stopped',
43
+ url: this.config?.path || '/api/v1',
44
+ entities: this.schemas.map(s => s.name),
45
+ stats: { ...this.stats },
46
+ };
47
+ }
48
+ /** Get the Fastify instance (for mounting in the shared server) */
49
+ getApp() {
50
+ return this.app;
51
+ }
52
+ // ============================================================
53
+ // Route generation
54
+ // ============================================================
55
+ registerRoutes(schema) {
56
+ if (!this.app)
57
+ return;
58
+ const prefix = this.config?.path || '/api/v1';
59
+ const collection = schema.collection;
60
+ // GET /api/v1/{collection} → findAll
61
+ this.app.get(`${prefix}/${collection}`, async (req, reply) => {
62
+ const query = req.query;
63
+ const ormReq = {
64
+ op: 'findAll',
65
+ entity: schema.name,
66
+ filter: query.filter ? JSON.parse(query.filter) : {},
67
+ options: {
68
+ sort: query.sort ? JSON.parse(query.sort) : undefined,
69
+ limit: query.limit ? parseInt(query.limit, 10) : undefined,
70
+ skip: query.skip ? parseInt(query.skip, 10) : undefined,
71
+ select: query.select ? query.select.split(',') : undefined,
72
+ },
73
+ };
74
+ return this.handle(ormReq, reply);
75
+ });
76
+ // GET /api/v1/{collection}/:id → findById
77
+ this.app.get(`${prefix}/${collection}/:id`, async (req, reply) => {
78
+ const { id } = req.params;
79
+ const query = req.query;
80
+ const ormReq = {
81
+ op: 'findById',
82
+ entity: schema.name,
83
+ id,
84
+ relations: query.include ? query.include.split(',') : undefined,
85
+ };
86
+ return this.handle(ormReq, reply);
87
+ });
88
+ // POST /api/v1/{collection} → create
89
+ this.app.post(`${prefix}/${collection}`, async (req, reply) => {
90
+ const ormReq = {
91
+ op: 'create',
92
+ entity: schema.name,
93
+ data: req.body,
94
+ };
95
+ const res = await this.handle(ormReq, reply);
96
+ if (reply.statusCode < 400)
97
+ reply.status(201);
98
+ return res;
99
+ });
100
+ // PUT /api/v1/{collection}/:id → update
101
+ this.app.put(`${prefix}/${collection}/:id`, async (req, reply) => {
102
+ const { id } = req.params;
103
+ const ormReq = {
104
+ op: 'update',
105
+ entity: schema.name,
106
+ id,
107
+ data: req.body,
108
+ };
109
+ return this.handle(ormReq, reply);
110
+ });
111
+ // DELETE /api/v1/{collection}/:id → delete
112
+ this.app.delete(`${prefix}/${collection}/:id`, async (req, reply) => {
113
+ const { id } = req.params;
114
+ const ormReq = {
115
+ op: 'delete',
116
+ entity: schema.name,
117
+ id,
118
+ };
119
+ return this.handle(ormReq, reply);
120
+ });
121
+ // GET /api/v1/{collection}/count → count
122
+ this.app.get(`${prefix}/${collection}/count`, async (req, reply) => {
123
+ const query = req.query;
124
+ const ormReq = {
125
+ op: 'count',
126
+ entity: schema.name,
127
+ filter: query.filter ? JSON.parse(query.filter) : {},
128
+ };
129
+ return this.handle(ormReq, reply);
130
+ });
131
+ // POST /api/v1/{collection}/search → search
132
+ this.app.post(`${prefix}/${collection}/search`, async (req, reply) => {
133
+ const body = req.body;
134
+ const ormReq = {
135
+ op: 'search',
136
+ entity: schema.name,
137
+ query: body.query,
138
+ searchFields: body.fields,
139
+ options: body.options,
140
+ };
141
+ return this.handle(ormReq, reply);
142
+ });
143
+ }
144
+ // ============================================================
145
+ // Request handling (OrmRequest → middleware → OrmResponse → HTTP)
146
+ // ============================================================
147
+ async handle(ormReq, reply) {
148
+ this.stats.requests++;
149
+ if (!this.ormHandler) {
150
+ this.stats.errors++;
151
+ reply.status(503);
152
+ return { status: 'error', error: { code: 'NO_HANDLER', message: 'ORM handler not initialized' } };
153
+ }
154
+ const ctx = { transport: this.name };
155
+ // Compose middleware chain → final handler
156
+ const chain = composeMiddleware(this.middlewares, this.ormHandler);
157
+ const res = await chain(ormReq, ctx);
158
+ if (res.status === 'error') {
159
+ this.stats.errors++;
160
+ const code = this.mapErrorToHttpStatus(res.error?.code);
161
+ reply.status(code);
162
+ }
163
+ return res;
164
+ }
165
+ mapErrorToHttpStatus(code) {
166
+ switch (code) {
167
+ case 'ENTITY_NOT_FOUND': return 404;
168
+ case 'EntityNotFoundError': return 404;
169
+ case 'MISSING_ID': return 400;
170
+ case 'MISSING_DATA': return 400;
171
+ case 'MISSING_QUERY': return 400;
172
+ case 'MISSING_PARAMS': return 400;
173
+ case 'MISSING_STAGES': return 400;
174
+ case 'UNKNOWN_OP': return 400;
175
+ case 'ValidationError': return 422;
176
+ case 'ConnectionError': return 503;
177
+ default: return 500;
178
+ }
179
+ }
180
+ }
181
+ /** Factory function (used by transport loader) */
182
+ export function createTransport() {
183
+ return new RestTransport();
184
+ }
@@ -0,0 +1,45 @@
1
+ import type { EntitySchema } from '@mostajs/orm';
2
+ import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware } from '../core/types.js';
3
+ import type { ServerResponse } from 'http';
4
+ /**
5
+ * SSETransport streams entity change events to clients via Server-Sent Events.
6
+ *
7
+ * Clients connect to GET /events (or configured path) and receive:
8
+ * event: entity.created
9
+ * data: {"entity":"User","data":{...}}
10
+ *
11
+ * event: entity.updated
12
+ * data: {"entity":"User","id":"123","data":{...}}
13
+ *
14
+ * event: entity.deleted
15
+ * data: {"entity":"User","id":"123"}
16
+ */
17
+ export declare class SSETransport implements ITransport {
18
+ readonly name = "sse";
19
+ private config;
20
+ private schemas;
21
+ private middlewares;
22
+ private clients;
23
+ private stats;
24
+ use(middleware: TransportMiddleware): void;
25
+ registerEntity(schema: EntitySchema): void;
26
+ start(config: TransportConfig): Promise<void>;
27
+ stop(): Promise<void>;
28
+ getInfo(): TransportInfo;
29
+ /** Get the SSE path for Fastify route registration */
30
+ getPath(): string;
31
+ /** Get connected client count */
32
+ getClientCount(): number;
33
+ /**
34
+ * Handle a new SSE client connection.
35
+ * Called by the server when a GET request arrives at the SSE path.
36
+ */
37
+ addClient(res: ServerResponse): void;
38
+ /**
39
+ * Broadcast an event to all connected SSE clients.
40
+ * Called by the server when EntityService emits change events.
41
+ */
42
+ broadcast(eventName: string, data: Record<string, unknown>): void;
43
+ }
44
+ /** Factory function */
45
+ export declare function createTransport(): ITransport;
@@ -0,0 +1,99 @@
1
+ // SSETransport — Server-Sent Events transport adapter
2
+ // Pushes entity change notifications (created, updated, deleted) to connected clients
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ /**
5
+ * SSETransport streams entity change events to clients via Server-Sent Events.
6
+ *
7
+ * Clients connect to GET /events (or configured path) and receive:
8
+ * event: entity.created
9
+ * data: {"entity":"User","data":{...}}
10
+ *
11
+ * event: entity.updated
12
+ * data: {"entity":"User","id":"123","data":{...}}
13
+ *
14
+ * event: entity.deleted
15
+ * data: {"entity":"User","id":"123"}
16
+ */
17
+ export class SSETransport {
18
+ name = 'sse';
19
+ config = null;
20
+ schemas = [];
21
+ middlewares = [];
22
+ clients = new Set();
23
+ stats = { requests: 0, errors: 0, startedAt: 0 };
24
+ use(middleware) {
25
+ this.middlewares.push(middleware);
26
+ }
27
+ registerEntity(schema) {
28
+ this.schemas.push(schema);
29
+ }
30
+ async start(config) {
31
+ this.config = config;
32
+ this.stats.startedAt = Date.now();
33
+ }
34
+ async stop() {
35
+ // Close all connected clients
36
+ for (const client of this.clients) {
37
+ client.end();
38
+ }
39
+ this.clients.clear();
40
+ }
41
+ getInfo() {
42
+ return {
43
+ name: this.name,
44
+ status: this.config ? 'running' : 'stopped',
45
+ url: this.config?.path || '/events',
46
+ entities: this.schemas.map(s => s.name),
47
+ stats: { ...this.stats },
48
+ };
49
+ }
50
+ /** Get the SSE path for Fastify route registration */
51
+ getPath() {
52
+ return this.config?.path || '/events';
53
+ }
54
+ /** Get connected client count */
55
+ getClientCount() {
56
+ return this.clients.size;
57
+ }
58
+ /**
59
+ * Handle a new SSE client connection.
60
+ * Called by the server when a GET request arrives at the SSE path.
61
+ */
62
+ addClient(res) {
63
+ this.stats.requests++;
64
+ // SSE headers
65
+ res.writeHead(200, {
66
+ 'Content-Type': 'text/event-stream',
67
+ 'Cache-Control': 'no-cache',
68
+ 'Connection': 'keep-alive',
69
+ 'X-Accel-Buffering': 'no', // Disable nginx buffering
70
+ });
71
+ // Send initial comment (keep-alive)
72
+ res.write(':ok\n\n');
73
+ // Send connected event with available entities
74
+ const connectData = JSON.stringify({
75
+ entities: this.schemas.map(s => s.name),
76
+ connectedAt: new Date().toISOString(),
77
+ });
78
+ res.write(`event: connected\ndata: ${connectData}\n\n`);
79
+ this.clients.add(res);
80
+ // Remove client on disconnect
81
+ res.on('close', () => {
82
+ this.clients.delete(res);
83
+ });
84
+ }
85
+ /**
86
+ * Broadcast an event to all connected SSE clients.
87
+ * Called by the server when EntityService emits change events.
88
+ */
89
+ broadcast(eventName, data) {
90
+ const payload = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
91
+ for (const client of this.clients) {
92
+ client.write(payload);
93
+ }
94
+ }
95
+ }
96
+ /** Factory function */
97
+ export function createTransport() {
98
+ return new SSETransport();
99
+ }
@@ -0,0 +1,33 @@
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 WebSocketTransport implements ITransport {
5
+ readonly name = "ws";
6
+ private wss;
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
+ /** Get connected client count */
19
+ getClientCount(): number;
20
+ /**
21
+ * Attach WebSocket server to an existing HTTP server.
22
+ * Called by server.ts after Fastify starts listening.
23
+ */
24
+ attachToServer(httpServer: import('http').Server): void;
25
+ /**
26
+ * Broadcast an entity change event to all connected WebSocket clients.
27
+ */
28
+ broadcast(eventName: string, data: Record<string, unknown>): void;
29
+ private handleRequest;
30
+ }
31
+ /** Factory */
32
+ export declare function createTransport(): ITransport;
33
+ export {};
@@ -0,0 +1,100 @@
1
+ // WebSocketTransport — Bidirectional WebSocket transport
2
+ // Clients send OrmRequest as JSON, receive OrmResponse + entity change events
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ import { WebSocketServer, WebSocket } from 'ws';
5
+ export class WebSocketTransport {
6
+ name = 'ws';
7
+ wss = null;
8
+ config = null;
9
+ schemas = [];
10
+ middlewares = [];
11
+ ormHandler = null;
12
+ stats = { requests: 0, errors: 0, startedAt: 0 };
13
+ setHandler(handler) { this.ormHandler = handler; }
14
+ use(mw) { this.middlewares.push(mw); }
15
+ registerEntity(schema) { this.schemas.push(schema); }
16
+ async start(config) {
17
+ this.config = config;
18
+ this.stats.startedAt = Date.now();
19
+ // WSS is created later when the HTTP server is available (see attachToServer)
20
+ }
21
+ async stop() {
22
+ if (this.wss) {
23
+ for (const client of this.wss.clients) {
24
+ client.close(1000, 'Server shutting down');
25
+ }
26
+ this.wss.close();
27
+ this.wss = null;
28
+ }
29
+ }
30
+ getInfo() {
31
+ return {
32
+ name: this.name,
33
+ status: this.wss ? 'running' : 'stopped',
34
+ url: this.config?.path || '/ws',
35
+ entities: this.schemas.map(s => s.name),
36
+ stats: { ...this.stats },
37
+ };
38
+ }
39
+ /** Get connected client count */
40
+ getClientCount() {
41
+ return this.wss?.clients.size || 0;
42
+ }
43
+ /**
44
+ * Attach WebSocket server to an existing HTTP server.
45
+ * Called by server.ts after Fastify starts listening.
46
+ */
47
+ attachToServer(httpServer) {
48
+ const path = this.config?.path || '/ws';
49
+ this.wss = new WebSocketServer({ server: httpServer, path });
50
+ this.wss.on('connection', (socket) => {
51
+ // Send welcome message
52
+ socket.send(JSON.stringify({
53
+ type: 'connected',
54
+ entities: this.schemas.map(s => s.name),
55
+ connectedAt: new Date().toISOString(),
56
+ }));
57
+ // Handle incoming messages (OrmRequest)
58
+ socket.on('message', async (raw) => {
59
+ this.stats.requests++;
60
+ try {
61
+ const req = JSON.parse(raw.toString());
62
+ const res = await this.handleRequest(req);
63
+ socket.send(JSON.stringify({ type: 'response', ...res }));
64
+ }
65
+ catch (err) {
66
+ this.stats.errors++;
67
+ socket.send(JSON.stringify({
68
+ type: 'response',
69
+ status: 'error',
70
+ error: { code: 'PARSE_ERROR', message: err.message },
71
+ }));
72
+ }
73
+ });
74
+ });
75
+ }
76
+ /**
77
+ * Broadcast an entity change event to all connected WebSocket clients.
78
+ */
79
+ broadcast(eventName, data) {
80
+ if (!this.wss)
81
+ return;
82
+ const payload = JSON.stringify({ type: 'event', event: eventName, ...data });
83
+ for (const client of this.wss.clients) {
84
+ if (client.readyState === WebSocket.OPEN) {
85
+ client.send(payload);
86
+ }
87
+ }
88
+ }
89
+ async handleRequest(req) {
90
+ if (!this.ormHandler) {
91
+ return { status: 'error', error: { code: 'NO_HANDLER', message: 'ORM handler not initialized' } };
92
+ }
93
+ const ctx = { transport: this.name };
94
+ return this.ormHandler(req, ctx);
95
+ }
96
+ }
97
+ /** Factory */
98
+ export function createTransport() {
99
+ return new WebSocketTransport();
100
+ }