@objectql/server 0.2.0

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/server.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ObjectQLServer = void 0;
4
+ class ObjectQLServer {
5
+ constructor(app) {
6
+ this.app = app;
7
+ }
8
+ /**
9
+ * The core handler that processes a JSON request object and returns a result.
10
+ * This is framework-agnostic.
11
+ */
12
+ async handle(req) {
13
+ try {
14
+ // 1. Build Context
15
+ // TODO: integrate with real session/auth
16
+ const contextOptions = {
17
+ userId: req.user?.id,
18
+ roles: req.user?.roles || [],
19
+ // TODO: spaceId
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;
25
+ if (typeof app.createContext !== 'function') {
26
+ throw new Error("The provided ObjectQL instance does not support createContext.");
27
+ }
28
+ const ctx = app.createContext(contextOptions);
29
+ const repo = ctx.object(req.object);
30
+ let result;
31
+ switch (req.op) {
32
+ case 'find':
33
+ result = await repo.find(req.args);
34
+ break;
35
+ case 'findOne':
36
+ result = await repo.findOne(req.args);
37
+ break;
38
+ case 'create':
39
+ result = await repo.create(req.args);
40
+ break;
41
+ case 'update':
42
+ result = await repo.update(req.args.id, req.args.data);
43
+ break;
44
+ case 'delete':
45
+ result = await repo.delete(req.args.id);
46
+ break;
47
+ case 'count':
48
+ result = await repo.count(req.args);
49
+ break;
50
+ case 'action':
51
+ // TODO: The repo interface might not expose 'executeAction' directly yet,
52
+ // usually it's on the app level or via special method.
53
+ // For now, let's assume app.executeAction
54
+ result = await app.executeAction(req.object, req.args.action, {
55
+ ...ctx, // Pass context with user info
56
+ params: req.args.params
57
+ });
58
+ break;
59
+ default:
60
+ throw new Error(`Unknown operation: ${req.op}`);
61
+ }
62
+ return { data: result };
63
+ }
64
+ catch (e) {
65
+ console.error('[ObjectQL Server] Error:', e);
66
+ return {
67
+ error: {
68
+ code: 'INTERNAL_ERROR',
69
+ message: e.message || 'An error occurred'
70
+ }
71
+ };
72
+ }
73
+ }
74
+ }
75
+ exports.ObjectQLServer = ObjectQLServer;
76
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;AAGA,MAAa,cAAc;IACvB,YAAoB,GAAc;QAAd,QAAG,GAAH,GAAG,CAAW;IAAG,CAAC;IAEtC;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAoB;QAC7B,IAAI,CAAC;YACD,mBAAmB;YACnB,yCAAyC;YACzC,MAAM,cAAc,GAAG;gBACnB,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE;gBACpB,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;gBAC5B,gBAAgB;aACnB,CAAC;YAEF,wEAAwE;YACxE,uGAAuG;YACvG,oFAAoF;YACpF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAU,CAAC;YAC5B,IAAI,OAAO,GAAG,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;YACtF,CAAC;YAED,MAAM,GAAG,GAAoB,GAAG,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;YAC/D,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAEpC,IAAI,MAAW,CAAC;YAEhB,QAAQ,GAAG,CAAC,EAAE,EAAE,CAAC;gBACb,KAAK,MAAM;oBACP,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACnC,MAAM;gBACV,KAAK,SAAS;oBACV,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACtC,MAAM;gBACV,KAAK,QAAQ;oBACT,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACrC,MAAM;gBACV,KAAK,QAAQ;oBACT,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACvD,MAAM;gBACV,KAAK,QAAQ;oBACT,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACxC,MAAM;gBACV,KAAK,OAAO;oBACR,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACpC,MAAM;gBACV,KAAK,QAAQ;oBACT,0EAA0E;oBAC1E,uDAAuD;oBACvD,0CAA0C;oBAC1C,MAAM,GAAG,MAAM,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE;wBACzD,GAAG,GAAG,EAAE,8BAA8B;wBACtC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM;qBAC3B,CAAC,CAAC;oBACH,MAAM;gBACV;oBACI,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YACxD,CAAC;YAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAE5B,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,CAAC,CAAC,CAAC;YAC7C,OAAO;gBACH,KAAK,EAAE;oBACH,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,mBAAmB;iBAC5C;aACJ,CAAC;QACN,CAAC;IACL,CAAC;CACJ;AA1ED,wCA0EC"}
@@ -0,0 +1,17 @@
1
+ export interface ObjectQLRequest {
2
+ user?: {
3
+ id: string;
4
+ roles: string[];
5
+ [key: string]: any;
6
+ };
7
+ op: 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'count' | 'action';
8
+ object: string;
9
+ args: any;
10
+ }
11
+ export interface ObjectQLResponse {
12
+ data?: any;
13
+ error?: {
14
+ code: string;
15
+ message: string;
16
+ };
17
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/jest.config.js ADDED
@@ -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
+ };
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@objectql/server",
3
+ "version": "0.2.0",
4
+ "description": "HTTP Server Adapter for ObjectQL",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "@objectql/core": "1.2.0",
9
+ "@objectql/types": "1.2.0"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "^5.3.0",
13
+ "@types/node": "^20.10.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "jest"
18
+ }
19
+ }
@@ -0,0 +1,70 @@
1
+ import { IObjectQL } from '@objectql/types';
2
+ import { ObjectQLServer } from '../server';
3
+ import { ObjectQLRequest } from '../types';
4
+ import { IncomingMessage, ServerResponse } from 'http';
5
+ import { generateOpenAPI } from '../openapi';
6
+
7
+ /**
8
+ * Creates a standard Node.js HTTP request handler.
9
+ */
10
+ export function createNodeHandler(app: IObjectQL) {
11
+ const server = new ObjectQLServer(app);
12
+
13
+
14
+ return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
15
+ // Handle OpenAPI spec request
16
+ if (req.method === 'GET' && req.url?.endsWith('/openapi.json')) {
17
+ const spec = generateOpenAPI(app);
18
+ res.setHeader('Content-Type', 'application/json');
19
+ res.statusCode = 200;
20
+ res.end(JSON.stringify(spec));
21
+ return;
22
+ }
23
+
24
+ if (req.method !== 'POST') {
25
+ res.statusCode = 405;
26
+ res.end('Method Not Allowed');
27
+ return;
28
+ }
29
+
30
+ const handleRequest = async (json: any) => {
31
+ try {
32
+ // TODO: Parse user from header or request override
33
+ const qlReq: ObjectQLRequest = {
34
+ op: json.op,
35
+ object: json.object,
36
+ args: json.args,
37
+ user: json.user // For dev/testing, allowing user injection
38
+ };
39
+
40
+ const result = await server.handle(qlReq);
41
+
42
+ res.setHeader('Content-Type', 'application/json');
43
+ res.statusCode = result.error ? 500 : 200;
44
+ res.end(JSON.stringify(result));
45
+ } catch (e) {
46
+ res.statusCode = 500;
47
+ res.end(JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Internal Server Error' }}));
48
+ }
49
+ };
50
+
51
+ // 1. Check if body is already parsed (e.g. by express.json())
52
+ if (req.body && typeof req.body === 'object') {
53
+ await handleRequest(req.body);
54
+ return;
55
+ }
56
+
57
+ // 2. Parse Body from stream
58
+ let body = '';
59
+ req.on('data', chunk => body += chunk);
60
+ req.on('end', async () => {
61
+ try {
62
+ const json = JSON.parse(body);
63
+ await handleRequest(json);
64
+ } catch (e) {
65
+ res.statusCode = 400;
66
+ res.end('Invalid JSON');
67
+ }
68
+ });
69
+ };
70
+ }
package/src/console.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Creates a handler to serve the console UI static files.
7
+ */
8
+ export function createConsoleHandler() {
9
+ // Try to find the built console files
10
+ const possiblePaths = [
11
+ path.join(__dirname, '../../console/dist'),
12
+ path.join(process.cwd(), 'node_modules/@objectql/console/dist'),
13
+ path.join(process.cwd(), 'packages/console/dist'),
14
+ ];
15
+
16
+ let distPath: string | null = null;
17
+ for (const p of possiblePaths) {
18
+ if (fs.existsSync(p)) {
19
+ distPath = p;
20
+ break;
21
+ }
22
+ }
23
+
24
+ return async (req: IncomingMessage, res: ServerResponse) => {
25
+ if (!distPath) {
26
+ // Return placeholder page if console is not built
27
+ const html = getPlaceholderPage();
28
+ res.setHeader('Content-Type', 'text/html');
29
+ res.statusCode = 200;
30
+ res.end(html);
31
+ return;
32
+ }
33
+
34
+ // Parse the URL and remove /console prefix
35
+ let urlPath = (req.url || '').replace(/^\/console/, '') || '/';
36
+
37
+ // Default to index.html for SPA routing
38
+ if (urlPath === '/' || !urlPath.includes('.')) {
39
+ urlPath = '/index.html';
40
+ }
41
+
42
+ const filePath = path.join(distPath, urlPath);
43
+
44
+ // Security check: ensure the file is within distPath
45
+ if (!filePath.startsWith(distPath)) {
46
+ res.statusCode = 403;
47
+ res.end('Forbidden');
48
+ return;
49
+ }
50
+
51
+ // Check if file exists
52
+ if (!fs.existsSync(filePath)) {
53
+ // For SPA, return index.html for any non-existent routes
54
+ const indexPath = path.join(distPath, 'index.html');
55
+ if (fs.existsSync(indexPath)) {
56
+ const content = fs.readFileSync(indexPath);
57
+ res.setHeader('Content-Type', 'text/html');
58
+ res.statusCode = 200;
59
+ res.end(content);
60
+ return;
61
+ }
62
+
63
+ res.statusCode = 404;
64
+ res.end('Not Found');
65
+ return;
66
+ }
67
+
68
+ // Read and serve the file
69
+ const content = fs.readFileSync(filePath);
70
+ const ext = path.extname(filePath);
71
+ const contentType = getContentType(ext);
72
+
73
+ res.setHeader('Content-Type', contentType);
74
+ res.statusCode = 200;
75
+ res.end(content);
76
+ };
77
+ }
78
+
79
+ function getContentType(ext: string): string {
80
+ const types: Record<string, string> = {
81
+ '.html': 'text/html',
82
+ '.js': 'application/javascript',
83
+ '.css': 'text/css',
84
+ '.json': 'application/json',
85
+ '.png': 'image/png',
86
+ '.jpg': 'image/jpeg',
87
+ '.gif': 'image/gif',
88
+ '.svg': 'image/svg+xml',
89
+ '.ico': 'image/x-icon',
90
+ };
91
+ return types[ext] || 'application/octet-stream';
92
+ }
93
+
94
+ function getPlaceholderPage(): string {
95
+ return `<!DOCTYPE html>
96
+ <html lang="en">
97
+ <head>
98
+ <meta charset="UTF-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
100
+ <title>ObjectQL Console</title>
101
+ <style>
102
+ * { margin: 0; padding: 0; box-sizing: border-box; }
103
+ body {
104
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
105
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
106
+ min-height: 100vh;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ color: white;
111
+ }
112
+ .container {
113
+ text-align: center;
114
+ max-width: 600px;
115
+ padding: 2rem;
116
+ }
117
+ h1 { font-size: 3rem; margin-bottom: 1rem; }
118
+ p { font-size: 1.25rem; opacity: 0.9; line-height: 1.6; }
119
+ .info {
120
+ background: rgba(255, 255, 255, 0.1);
121
+ padding: 1.5rem;
122
+ border-radius: 8px;
123
+ margin-top: 2rem;
124
+ backdrop-filter: blur(10px);
125
+ }
126
+ code {
127
+ background: rgba(0, 0, 0, 0.2);
128
+ padding: 0.25rem 0.5rem;
129
+ border-radius: 4px;
130
+ font-family: monospace;
131
+ }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <div class="container">
136
+ <h1>ObjectQL Console</h1>
137
+ <p>Web-based admin console for database management</p>
138
+ <div class="info">
139
+ <p style="margin-bottom: 1rem;">
140
+ The console is available but needs to be built separately.
141
+ </p>
142
+ <p style="font-size: 1rem;">
143
+ To use the full console UI, run:<br>
144
+ <code>cd packages/console && pnpm run build</code>
145
+ </p>
146
+ </div>
147
+ </div>
148
+ </body>
149
+ </html>`;
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './types';
2
+ export * from './openapi';
3
+ export * from './server';
4
+ export * from './metadata';
5
+ export * from './console';
6
+ // We export createNodeHandler from root for convenience,
7
+ // but in the future we might encourage 'import ... from @objectql/server/node'
8
+ export * from './adapters/node';
@@ -0,0 +1,81 @@
1
+ import { IObjectQL } from '@objectql/types';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+
4
+ /**
5
+ * Creates a handler for metadata endpoints.
6
+ * These endpoints expose information about registered objects.
7
+ */
8
+ export function createMetadataHandler(app: IObjectQL) {
9
+ return async (req: IncomingMessage, res: ServerResponse) => {
10
+ // Parse the URL
11
+ const url = req.url || '';
12
+
13
+ // CORS headers for development
14
+ res.setHeader('Access-Control-Allow-Origin', '*');
15
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
16
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
17
+
18
+ if (req.method === 'OPTIONS') {
19
+ res.statusCode = 200;
20
+ res.end();
21
+ return;
22
+ }
23
+
24
+ try {
25
+ // GET /api/metadata/objects - List all objects
26
+ if (url === '/api/metadata/objects') {
27
+ const configs = app.getConfigs();
28
+ const objects = Object.values(configs).map(obj => ({
29
+ name: obj.name,
30
+ label: obj.label || obj.name,
31
+ icon: obj.icon,
32
+ fields: obj.fields ? Object.keys(obj.fields) : []
33
+ }));
34
+
35
+ res.setHeader('Content-Type', 'application/json');
36
+ res.statusCode = 200;
37
+ res.end(JSON.stringify({ objects }));
38
+ return;
39
+ }
40
+
41
+ // GET /api/metadata/objects/:name - Get object details
42
+ const match = url.match(/^\/api\/metadata\/objects\/([^\/]+)$/);
43
+ if (match) {
44
+ const objectName = match[1];
45
+ const metadata = app.getObject(objectName);
46
+ if (!metadata) {
47
+ res.statusCode = 404;
48
+ res.end(JSON.stringify({ error: 'Object not found' }));
49
+ return;
50
+ }
51
+
52
+ // Convert fields object to array
53
+ const fields = metadata.fields
54
+ ? Object.entries(metadata.fields).map(([key, field]) => ({
55
+ name: field.name || key,
56
+ type: field.type,
57
+ label: field.label,
58
+ required: field.required,
59
+ defaultValue: field.defaultValue
60
+ }))
61
+ : [];
62
+
63
+ res.setHeader('Content-Type', 'application/json');
64
+ res.statusCode = 200;
65
+ res.end(JSON.stringify({
66
+ ...metadata,
67
+ fields
68
+ }));
69
+ return;
70
+ }
71
+
72
+ // Not found
73
+ res.statusCode = 404;
74
+ res.end('Not Found');
75
+ } catch (e: any) {
76
+ console.error('[Metadata Handler] Error:', e);
77
+ res.statusCode = 500;
78
+ res.end(JSON.stringify({ error: 'Internal Server Error' }));
79
+ }
80
+ };
81
+ }
package/src/openapi.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { IObjectQL, ObjectConfig, FieldType, FieldConfig } from '@objectql/types';
2
+
3
+ interface OpenAPISchema {
4
+ openapi: string;
5
+ info: {
6
+ title: string;
7
+ version: string;
8
+ };
9
+ paths: Record<string, any>;
10
+ components: {
11
+ schemas: Record<string, any>;
12
+ };
13
+ }
14
+
15
+ export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
16
+ const registry = (app as any).metadata; // Direct access or via interface
17
+ const objects = registry.list('object') as ObjectConfig[];
18
+
19
+ const paths: Record<string, any> = {};
20
+ const schemas: Record<string, any> = {};
21
+
22
+
23
+ // 1. Generate Schemas
24
+ for (const obj of objects) {
25
+ const schemaName = obj.name;
26
+ const properties: Record<string, any> = {};
27
+
28
+ for (const [fieldName, field] of Object.entries(obj.fields)) {
29
+ properties[fieldName] = mapFieldTypeToOpenAPI(field);
30
+ }
31
+
32
+ schemas[schemaName] = {
33
+ type: 'object',
34
+ properties
35
+ };
36
+
37
+ // 2. Generate Paths (RPC Style representation for documentation purposes)
38
+ // Since we only have one endpoint, we might document operations as descriptions
39
+ // Or if we support REST style in the future, we would add /object paths here.
40
+ // For now, let's document the "Virtual" REST API that could exist via a gateway
41
+ // OR just document the schema.
42
+ // Let's assume the user might want to see standard CRUD paths even if implementation is RPC,
43
+ // so they can pass it to frontend generators?
44
+ // No, that would be misleading if the server doesn't support it.
45
+
46
+ // Let's DOCUMENT the RPC operations as if they were paths,
47
+ // OR clearer: One path /api/objectql with polymorphic body?
48
+ // Swagger UI handles oneOf poorly for top level operations sometimes.
49
+ }
50
+
51
+ // Let's do a "Virtual" REST path generation for better visualization
52
+ // Assuming we WILL support REST mapping in this update.
53
+ for (const obj of objects) {
54
+ const name = obj.name;
55
+
56
+ // GET /name (List)
57
+ paths[`/${name}`] = {
58
+ get: {
59
+ summary: `List ${name}s`,
60
+ tags: [name],
61
+ parameters: [
62
+ { name: 'filter', in: 'query', schema: { type: 'string' }, description: 'JSON filter args' }
63
+ ],
64
+ responses: {
65
+ 200: {
66
+ description: `List of ${name}`,
67
+ content: {
68
+ 'application/json': {
69
+ schema: {
70
+ type: 'object',
71
+ properties: {
72
+ data: { type: 'array', items: { $ref: `#/components/schemas/${name}` } }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ },
80
+ post: {
81
+ summary: `Create ${name}`,
82
+ tags: [name],
83
+ requestBody: {
84
+ content: {
85
+ 'application/json': {
86
+ schema: {
87
+ type: 'object',
88
+ properties: {
89
+ data: { $ref: `#/components/schemas/${name}` }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
95
+ responses: {
96
+ 200: { description: 'Created' }
97
+ }
98
+ }
99
+ };
100
+
101
+ // GET /name/{id}
102
+ paths[`/${name}/{id}`] = {
103
+ get: {
104
+ summary: `Get ${name}`,
105
+ tags: [name],
106
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
107
+ responses: { 200: { description: 'Item' } }
108
+ },
109
+ patch: {
110
+ summary: `Update ${name}`,
111
+ tags: [name],
112
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
113
+ requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { data: { type: 'object' }} } } } },
114
+ responses: { 200: { description: 'Updated' } }
115
+ },
116
+ delete: {
117
+ summary: `Delete ${name}`,
118
+ tags: [name],
119
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
120
+ responses: { 200: { description: 'Deleted' } }
121
+ }
122
+ };
123
+ }
124
+
125
+ return {
126
+ openapi: '3.0.0',
127
+ info: {
128
+ title: 'ObjectQL API',
129
+ version: '1.0.0'
130
+ },
131
+ paths,
132
+ components: {
133
+ schemas
134
+ }
135
+ };
136
+ }
137
+
138
+ function mapFieldTypeToOpenAPI(field: FieldConfig | string): any {
139
+ const type = typeof field === 'string' ? field : field.type;
140
+
141
+ switch (type) {
142
+ case 'string': return { type: 'string' };
143
+ case 'integer': return { type: 'integer' };
144
+ case 'float': return { type: 'number' };
145
+ case 'boolean': return { type: 'boolean' };
146
+ case 'date': return { type: 'string', format: 'date-time' };
147
+ case 'json': return { type: 'object' };
148
+ default: return { type: 'string' }; // Fallback or relationship ID
149
+ }
150
+ }
package/src/server.ts ADDED
@@ -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
+ }