Skip to content

Commit caaff6e

Browse files
committed
Add @objectql/server package with Node adapter
Introduces the @objectql/server package, including a generic HTTP server adapter for ObjectQL, Node.js handler, types, tests, and configuration files. Also updates pnpm-lock.yaml to register new dependencies and linkages for the server and example projects.
1 parent 960e016 commit caaff6e

11 files changed

Lines changed: 902 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "src",
6+
"esModuleInterop": true
7+
},
8+
"include": ["src/**/*"]
9+
}

packages/server/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# @objectql/server
2+
3+
Generic HTTP Server Adapter for ObjectQL.
4+
Allows running ObjectQL on Node.js, Express, Next.js, etc.
5+
6+
## Installation
7+
8+
```bash
9+
pnpm add @objectql/server
10+
```
11+
12+
## Usage
13+
14+
### Node.js (Raw HTTP)
15+
16+
```typescript
17+
import { createNodeHandler } from '@objectql/server';
18+
import { app } from './objectql'; // Your initialized ObjectQL instance
19+
import { createServer } from 'http';
20+
21+
const handler = createNodeHandler(app);
22+
const server = createServer(handler);
23+
server.listen(3000);
24+
```
25+
26+
### Express
27+
28+
```typescript
29+
import express from 'express';
30+
import { createNodeHandler } from '@objectql/server';
31+
import { app } from './objectql';
32+
33+
const server = express();
34+
35+
// Optional: Mount express.json() if you want, but ObjectQL handles parsing too.
36+
// server.use(express.json());
37+
38+
// Mount the handler
39+
server.all('/api/objectql', createNodeHandler(app));
40+
41+
server.listen(3000);
42+
```
43+
44+
### Next.js (API Routes)
45+
46+
```typescript
47+
// pages/api/objectql.ts
48+
import { createNodeHandler } from '@objectql/server';
49+
import { app } from '../../lib/objectql';
50+
51+
export const config = {
52+
api: {
53+
bodyParser: false, // ObjectQL handles body parsing
54+
},
55+
};
56+
57+
export default createNodeHandler(app);
58+
```

packages/server/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testMatch: ['**/*.test.ts'],
5+
moduleNameMapper: {
6+
'^@objectql/(.*)$': '<rootDir>/../$1/src'
7+
}
8+
};

packages/server/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@objectql/server",
3+
"version": "0.1.0",
4+
"description": "HTTP Server Adapter for ObjectQL",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"scripts": {
8+
"build": "tsc",
9+
"test": "jest"
10+
},
11+
"dependencies": {
12+
"@objectql/core": "workspace:*",
13+
"@objectql/types": "workspace:*"
14+
},
15+
"devDependencies": {
16+
"typescript": "^5.3.0",
17+
"@types/node": "^20.10.0"
18+
}
19+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { IObjectQL } from '@objectql/types';
2+
import { ObjectQLServer } from '../server';
3+
import { ObjectQLRequest } from '../types';
4+
import { IncomingMessage, ServerResponse } from 'http';
5+
6+
/**
7+
* Creates a standard Node.js HTTP request handler.
8+
*/
9+
export function createNodeHandler(app: IObjectQL) {
10+
const server = new ObjectQLServer(app);
11+
12+
13+
return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
14+
if (req.method !== 'POST') {
15+
res.statusCode = 405;
16+
res.end('Method Not Allowed');
17+
return;
18+
}
19+
20+
const handleRequest = async (json: any) => {
21+
try {
22+
// TODO: Parse user from header or request override
23+
const qlReq: ObjectQLRequest = {
24+
op: json.op,
25+
object: json.object,
26+
args: json.args,
27+
user: json.user // For dev/testing, allowing user injection
28+
};
29+
30+
const result = await server.handle(qlReq);
31+
32+
res.setHeader('Content-Type', 'application/json');
33+
res.statusCode = result.error ? 500 : 200;
34+
res.end(JSON.stringify(result));
35+
} catch (e) {
36+
res.statusCode = 500;
37+
res.end(JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Internal Server Error' }}));
38+
}
39+
};
40+
41+
// 1. Check if body is already parsed (e.g. by express.json())
42+
if (req.body && typeof req.body === 'object') {
43+
await handleRequest(req.body);
44+
return;
45+
}
46+
47+
// 2. Parse Body from stream
48+
let body = '';
49+
req.on('data', chunk => body += chunk);
50+
req.on('end', async () => {
51+
try {
52+
const json = JSON.parse(body);
53+
await handleRequest(json);
54+
} catch (e) {
55+
res.statusCode = 400;
56+
res.end('Invalid JSON');
57+
}
58+
});
59+
};
60+
}

packages/server/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './types';
2+
export * from './server';
3+
// We export createNodeHandler from root for convenience,
4+
// but in the future we might encourage 'import ... from @objectql/server/node'
5+
export * from './adapters/node';

packages/server/src/server.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { IObjectQL, ObjectQLContext } from '@objectql/types';
2+
import { ObjectQLRequest, ObjectQLResponse } from './types';
3+
4+
export class ObjectQLServer {
5+
constructor(private app: IObjectQL) {}
6+
7+
/**
8+
* The core handler that processes a JSON request object and returns a result.
9+
* This is framework-agnostic.
10+
*/
11+
async handle(req: ObjectQLRequest): Promise<ObjectQLResponse> {
12+
try {
13+
// 1. Build Context
14+
// TODO: integrate with real session/auth
15+
const contextOptions = {
16+
userId: req.user?.id,
17+
roles: req.user?.roles || [],
18+
// TODO: spaceId
19+
};
20+
21+
// Note: Currently IObjectQL.createContext implies we have access to it.
22+
// But IObjectQL interface in @objectql/types usually doesn't expose createContext (it's on the class).
23+
// We need to cast or fix the interface. Assuming 'app' behaves like ObjectQL class.
24+
const app = this.app as any;
25+
if (typeof app.createContext !== 'function') {
26+
throw new Error("The provided ObjectQL instance does not support createContext.");
27+
}
28+
29+
const ctx: ObjectQLContext = app.createContext(contextOptions);
30+
const repo = ctx.object(req.object);
31+
32+
let result: any;
33+
34+
switch (req.op) {
35+
case 'find':
36+
result = await repo.find(req.args);
37+
break;
38+
case 'findOne':
39+
result = await repo.findOne(req.args);
40+
break;
41+
case 'create':
42+
result = await repo.create(req.args);
43+
break;
44+
case 'update':
45+
result = await repo.update(req.args.id, req.args.data);
46+
break;
47+
case 'delete':
48+
result = await repo.delete(req.args.id);
49+
break;
50+
case 'count':
51+
result = await repo.count(req.args);
52+
break;
53+
case 'action':
54+
// TODO: The repo interface might not expose 'executeAction' directly yet,
55+
// usually it's on the app level or via special method.
56+
// For now, let's assume app.executeAction
57+
result = await app.executeAction(req.object, req.args.action, {
58+
...ctx, // Pass context with user info
59+
params: req.args.params
60+
});
61+
break;
62+
default:
63+
throw new Error(`Unknown operation: ${req.op}`);
64+
}
65+
66+
return { data: result };
67+
68+
} catch (e: any) {
69+
console.error('[ObjectQL Server] Error:', e);
70+
return {
71+
error: {
72+
code: 'INTERNAL_ERROR',
73+
message: e.message || 'An error occurred'
74+
}
75+
};
76+
}
77+
}
78+
}

packages/server/src/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// src/types.ts
2+
export interface ObjectQLRequest {
3+
// Identity provided by the framework adapter (e.g. from session)
4+
user?: {
5+
id: string;
6+
roles: string[];
7+
[key: string]: any;
8+
};
9+
10+
// The actual operation
11+
op: 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'count' | 'action';
12+
object: string;
13+
14+
// Arguments
15+
args: any;
16+
}
17+
18+
export interface ObjectQLResponse {
19+
data?: any;
20+
error?: {
21+
code: string;
22+
message: string;
23+
}
24+
}

packages/server/test/node.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import request from 'supertest';
2+
import { createServer } from 'http';
3+
import { ObjectQL } from '@objectql/core';
4+
import { createNodeHandler } from '../src/adapters/node';
5+
import { IObjectQL, Driver } from '@objectql/types';
6+
7+
// Simple Mock Driver
8+
class MockDriver implements Driver {
9+
async init() {}
10+
async find(objectName: string) {
11+
if (objectName === 'user') {
12+
return [{ id: 1, name: 'Alice' }];
13+
}
14+
return [];
15+
}
16+
async findOne() { return null; }
17+
async create() { return { id: 2, name: 'Bob' }; }
18+
async update() { return 1; }
19+
async delete() { return 1; }
20+
async count() { return 0; }
21+
async execute() {}
22+
}
23+
24+
describe('Node Adapter', () => {
25+
let app: ObjectQL;
26+
let server: any;
27+
28+
beforeAll(async () => {
29+
app = new ObjectQL({
30+
datasources: {
31+
default: new MockDriver()
32+
}
33+
});
34+
35+
// Manual schema registration
36+
app.metadata.register('object', {
37+
type: 'object',
38+
id: 'user',
39+
content: {
40+
name: 'user',
41+
fields: {
42+
name: { type: 'string' }
43+
}
44+
}
45+
});
46+
});
47+
48+
it('should handle find request', async () => {
49+
const handler = createNodeHandler(app);
50+
server = createServer(handler);
51+
52+
const response = await request(server)
53+
.post('/')
54+
.send({
55+
op: 'find',
56+
object: 'user',
57+
args: {}
58+
})
59+
.set('Accept', 'application/json');
60+
61+
expect(response.status).toBe(200);
62+
expect(response.body).toEqual({
63+
data: [{ id: 1, name: 'Alice' }]
64+
});
65+
});
66+
67+
it('should handle create request', async () => {
68+
const handler = createNodeHandler(app);
69+
server = createServer(handler);
70+
71+
const response = await request(server)
72+
.post('/')
73+
.send({
74+
op: 'create',
75+
object: 'user',
76+
args: { data: { name: 'Bob' } }
77+
});
78+
79+
expect(response.status).toBe(200);
80+
expect(response.body).toEqual({
81+
data: { id: 2, name: 'Bob' }
82+
});
83+
});
84+
85+
it('should return 405 for GET', async () => {
86+
const handler = createNodeHandler(app);
87+
server = createServer(handler);
88+
89+
const response = await request(server).get('/');
90+
expect(response.status).toBe(405);
91+
});
92+
});

packages/server/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "src",
6+
"composite": true
7+
},
8+
"include": ["src/**/*"]
9+
}

0 commit comments

Comments
 (0)