@objectql/server 1.3.1 → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  import { IObjectQL } from '@objectql/types';
2
2
  import { ObjectQLServer } from '../server';
3
- import { ObjectQLRequest } from '../types';
3
+ import { ObjectQLRequest, ErrorCode } from '../types';
4
4
  import { IncomingMessage, ServerResponse } from 'http';
5
5
  import { generateOpenAPI } from '../openapi';
6
6
 
@@ -21,12 +21,6 @@ export function createNodeHandler(app: IObjectQL) {
21
21
  return;
22
22
  }
23
23
 
24
- if (req.method !== 'POST') {
25
- res.statusCode = 405;
26
- res.end('Method Not Allowed');
27
- return;
28
- }
29
-
30
24
  const handleRequest = async (json: any) => {
31
25
  try {
32
26
  // TODO: Parse user from header or request override
@@ -34,20 +28,82 @@ export function createNodeHandler(app: IObjectQL) {
34
28
  op: json.op,
35
29
  object: json.object,
36
30
  args: json.args,
37
- user: json.user // For dev/testing, allowing user injection
31
+ user: json.user, // For dev/testing, allowing user injection
32
+ ai_context: json.ai_context // Support AI context
38
33
  };
39
34
 
40
35
  const result = await server.handle(qlReq);
41
36
 
37
+ // Determine HTTP status code based on error
38
+ let statusCode = 200;
39
+ if (result.error) {
40
+ switch (result.error.code) {
41
+ case ErrorCode.INVALID_REQUEST:
42
+ case ErrorCode.VALIDATION_ERROR:
43
+ statusCode = 400;
44
+ break;
45
+ case ErrorCode.UNAUTHORIZED:
46
+ statusCode = 401;
47
+ break;
48
+ case ErrorCode.FORBIDDEN:
49
+ statusCode = 403;
50
+ break;
51
+ case ErrorCode.NOT_FOUND:
52
+ statusCode = 404;
53
+ break;
54
+ case ErrorCode.CONFLICT:
55
+ statusCode = 409;
56
+ break;
57
+ case ErrorCode.RATE_LIMIT_EXCEEDED:
58
+ statusCode = 429;
59
+ break;
60
+ default:
61
+ statusCode = 500;
62
+ }
63
+ }
64
+
42
65
  res.setHeader('Content-Type', 'application/json');
43
- res.statusCode = result.error ? 500 : 200;
66
+ res.statusCode = statusCode;
44
67
  res.end(JSON.stringify(result));
45
68
  } catch (e) {
46
69
  res.statusCode = 500;
47
- res.end(JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Internal Server Error' }}));
70
+ res.end(JSON.stringify({
71
+ error: {
72
+ code: ErrorCode.INTERNAL_ERROR,
73
+ message: 'Internal Server Error'
74
+ }
75
+ }));
48
76
  }
49
77
  };
50
78
 
79
+ if (req.method !== 'POST') {
80
+ // Attempt to handle GET requests for simple queries like /api/objectql/table
81
+ // We map this to a find operation
82
+ // URL pattern: /api/objectql/:objectName
83
+ const match = req.url?.match(/\/([^\/?]+)(\?.*)?$/);
84
+ if (req.method === 'GET' && match) {
85
+ const objectName = match[1];
86
+ // Ignore special paths
87
+ if (objectName !== 'openapi.json' && objectName !== 'metadata') {
88
+ await handleRequest({
89
+ op: 'find',
90
+ object: objectName,
91
+ args: {} // TODO: Parse query params to args
92
+ });
93
+ return;
94
+ }
95
+ }
96
+
97
+ res.statusCode = 405;
98
+ res.end(JSON.stringify({
99
+ error: {
100
+ code: ErrorCode.INVALID_REQUEST,
101
+ message: 'Method Not Allowed'
102
+ }
103
+ }));
104
+ return;
105
+ }
106
+
51
107
  // 1. Check if body is already parsed (e.g. by express.json())
52
108
  if (req.body && typeof req.body === 'object') {
53
109
  await handleRequest(req.body);
@@ -63,7 +119,12 @@ export function createNodeHandler(app: IObjectQL) {
63
119
  await handleRequest(json);
64
120
  } catch (e) {
65
121
  res.statusCode = 400;
66
- res.end('Invalid JSON');
122
+ res.end(JSON.stringify({
123
+ error: {
124
+ code: ErrorCode.INVALID_REQUEST,
125
+ message: 'Invalid JSON'
126
+ }
127
+ }));
67
128
  }
68
129
  });
69
130
  };
@@ -0,0 +1,271 @@
1
+ import { IObjectQL } from '@objectql/types';
2
+ import { ObjectQLServer } from '../server';
3
+ import { ObjectQLRequest, ErrorCode } from '../types';
4
+ import { IncomingMessage, ServerResponse } from 'http';
5
+
6
+ /**
7
+ * Parse query string parameters
8
+ */
9
+ function parseQueryParams(url: string): Record<string, any> {
10
+ const params: Record<string, any> = {};
11
+ const queryIndex = url.indexOf('?');
12
+ if (queryIndex === -1) return params;
13
+
14
+ const queryString = url.substring(queryIndex + 1);
15
+ const pairs = queryString.split('&');
16
+
17
+ for (const pair of pairs) {
18
+ const [key, value] = pair.split('=');
19
+ if (!key) continue;
20
+
21
+ const decodedKey = decodeURIComponent(key);
22
+ const decodedValue = decodeURIComponent(value || '');
23
+
24
+ // Try to parse JSON values
25
+ try {
26
+ params[decodedKey] = JSON.parse(decodedValue);
27
+ } catch {
28
+ params[decodedKey] = decodedValue;
29
+ }
30
+ }
31
+
32
+ return params;
33
+ }
34
+
35
+ /**
36
+ * Read request body as JSON
37
+ */
38
+ function readBody(req: IncomingMessage): Promise<any> {
39
+ return new Promise((resolve, reject) => {
40
+ let body = '';
41
+ req.on('data', chunk => body += chunk.toString());
42
+ req.on('end', () => {
43
+ if (!body) return resolve({});
44
+ try {
45
+ resolve(JSON.parse(body));
46
+ } catch (e) {
47
+ reject(new Error('Invalid JSON'));
48
+ }
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Send JSON response
56
+ */
57
+ function sendJSON(res: ServerResponse, statusCode: number, data: any) {
58
+ res.setHeader('Content-Type', 'application/json');
59
+ res.statusCode = statusCode;
60
+ res.end(JSON.stringify(data));
61
+ }
62
+
63
+ /**
64
+ * Creates a REST-style HTTP request handler for ObjectQL
65
+ *
66
+ * Endpoints:
67
+ * - GET /api/data/:object - List records
68
+ * - GET /api/data/:object/:id - Get single record
69
+ * - POST /api/data/:object - Create record
70
+ * - PUT /api/data/:object/:id - Update record
71
+ * - DELETE /api/data/:object/:id - Delete record
72
+ */
73
+ export function createRESTHandler(app: IObjectQL) {
74
+ const server = new ObjectQLServer(app);
75
+
76
+ return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
77
+ try {
78
+ // CORS headers
79
+ const requestOrigin = req.headers.origin;
80
+ const configuredOrigin = process.env.OBJECTQL_CORS_ORIGIN;
81
+ const isProduction = process.env.NODE_ENV === 'production';
82
+
83
+ // In development, allow all origins by default (or use configured override).
84
+ // In production, require an explicit OBJECTQL_CORS_ORIGIN to be set.
85
+ if (!isProduction) {
86
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin || '*');
87
+ } else if (configuredOrigin && (!requestOrigin || requestOrigin === configuredOrigin)) {
88
+ res.setHeader('Access-Control-Allow-Origin', configuredOrigin);
89
+ }
90
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
91
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
92
+
93
+ if (req.method === 'OPTIONS') {
94
+ res.statusCode = 200;
95
+ res.end();
96
+ return;
97
+ }
98
+
99
+ const url = req.url || '';
100
+ const method = req.method || 'GET';
101
+
102
+ // Parse URL: /api/data/:object or /api/data/:object/:id
103
+ const match = url.match(/^\/api\/data\/([^\/\?]+)(?:\/([^\/\?]+))?(\?.*)?$/);
104
+
105
+ if (!match) {
106
+ sendJSON(res, 404, {
107
+ error: {
108
+ code: ErrorCode.NOT_FOUND,
109
+ message: 'Invalid REST API endpoint'
110
+ }
111
+ });
112
+ return;
113
+ }
114
+
115
+ const [, objectName, id, queryString] = match;
116
+ const queryParams = queryString ? parseQueryParams(queryString) : {};
117
+
118
+ let qlRequest: ObjectQLRequest;
119
+
120
+ switch (method) {
121
+ case 'GET':
122
+ if (id) {
123
+ // GET /api/data/:object/:id - Get single record
124
+ qlRequest = {
125
+ op: 'findOne',
126
+ object: objectName,
127
+ args: id
128
+ };
129
+ } else {
130
+ // GET /api/data/:object - List records
131
+ const args: any = {};
132
+
133
+ // Parse query parameters
134
+ if (queryParams.filter) {
135
+ args.filters = queryParams.filter;
136
+ }
137
+ if (queryParams.fields) {
138
+ args.fields = queryParams.fields;
139
+ }
140
+ if (queryParams.sort) {
141
+ args.sort = Array.isArray(queryParams.sort)
142
+ ? queryParams.sort
143
+ : [[queryParams.sort, 'asc']];
144
+ }
145
+ if (queryParams.top || queryParams.limit) {
146
+ args.top = queryParams.top || queryParams.limit;
147
+ }
148
+ if (queryParams.skip || queryParams.offset) {
149
+ args.skip = queryParams.skip || queryParams.offset;
150
+ }
151
+ if (queryParams.expand) {
152
+ args.expand = queryParams.expand;
153
+ }
154
+
155
+ qlRequest = {
156
+ op: 'find',
157
+ object: objectName,
158
+ args
159
+ };
160
+ }
161
+ break;
162
+
163
+ case 'POST':
164
+ // POST /api/data/:object - Create record
165
+ const createBody = req.body || await readBody(req);
166
+ qlRequest = {
167
+ op: 'create',
168
+ object: objectName,
169
+ args: createBody
170
+ };
171
+ break;
172
+
173
+ case 'PUT':
174
+ case 'PATCH':
175
+ // PUT /api/data/:object/:id - Update record
176
+ if (!id) {
177
+ sendJSON(res, 400, {
178
+ error: {
179
+ code: ErrorCode.INVALID_REQUEST,
180
+ message: 'ID is required for update operation'
181
+ }
182
+ });
183
+ return;
184
+ }
185
+
186
+ const updateBody = req.body || await readBody(req);
187
+ qlRequest = {
188
+ op: 'update',
189
+ object: objectName,
190
+ args: {
191
+ id,
192
+ data: updateBody
193
+ }
194
+ };
195
+ break;
196
+
197
+ case 'DELETE':
198
+ // DELETE /api/data/:object/:id - Delete record
199
+ if (!id) {
200
+ sendJSON(res, 400, {
201
+ error: {
202
+ code: ErrorCode.INVALID_REQUEST,
203
+ message: 'ID is required for delete operation'
204
+ }
205
+ });
206
+ return;
207
+ }
208
+
209
+ qlRequest = {
210
+ op: 'delete',
211
+ object: objectName,
212
+ args: { id }
213
+ };
214
+ break;
215
+
216
+ default:
217
+ sendJSON(res, 405, {
218
+ error: {
219
+ code: ErrorCode.INVALID_REQUEST,
220
+ message: 'Method not allowed'
221
+ }
222
+ });
223
+ return;
224
+ }
225
+
226
+ // Execute the request
227
+ const result = await server.handle(qlRequest);
228
+
229
+ // Determine HTTP status code
230
+ let statusCode = 200;
231
+ if (result.error) {
232
+ switch (result.error.code) {
233
+ case ErrorCode.INVALID_REQUEST:
234
+ case ErrorCode.VALIDATION_ERROR:
235
+ statusCode = 400;
236
+ break;
237
+ case ErrorCode.UNAUTHORIZED:
238
+ statusCode = 401;
239
+ break;
240
+ case ErrorCode.FORBIDDEN:
241
+ statusCode = 403;
242
+ break;
243
+ case ErrorCode.NOT_FOUND:
244
+ statusCode = 404;
245
+ break;
246
+ case ErrorCode.CONFLICT:
247
+ statusCode = 409;
248
+ break;
249
+ case ErrorCode.RATE_LIMIT_EXCEEDED:
250
+ statusCode = 429;
251
+ break;
252
+ default:
253
+ statusCode = 500;
254
+ }
255
+ } else if (method === 'POST') {
256
+ statusCode = 201; // Created
257
+ }
258
+
259
+ sendJSON(res, statusCode, result);
260
+
261
+ } catch (e: any) {
262
+ console.error('[REST Handler] Error:', e);
263
+ sendJSON(res, 500, {
264
+ error: {
265
+ code: ErrorCode.INTERNAL_ERROR,
266
+ message: 'Internal server error'
267
+ }
268
+ });
269
+ }
270
+ };
271
+ }
package/src/index.ts CHANGED
@@ -2,7 +2,9 @@ export * from './types';
2
2
  export * from './openapi';
3
3
  export * from './server';
4
4
  export * from './metadata';
5
- export * from './console';
5
+ export * from './studio';
6
6
  // We export createNodeHandler from root for convenience,
7
7
  // but in the future we might encourage 'import ... from @objectql/server/node'
8
8
  export * from './adapters/node';
9
+ // Export REST adapter
10
+ export * from './adapters/rest';
package/src/metadata.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  import { IObjectQL } from '@objectql/types';
2
2
  import { IncomingMessage, ServerResponse } from 'http';
3
+ import { ErrorCode } from './types';
4
+
5
+ function readBody(req: IncomingMessage): Promise<any> {
6
+ return new Promise((resolve, reject) => {
7
+ let body = '';
8
+ req.on('data', chunk => body += chunk.toString());
9
+ req.on('end', () => {
10
+ if (!body) return resolve({});
11
+ try {
12
+ resolve(JSON.parse(body));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ });
17
+ req.on('error', reject);
18
+ });
19
+ }
3
20
 
4
21
  /**
5
22
  * Creates a handler for metadata endpoints.
@@ -9,10 +26,11 @@ export function createMetadataHandler(app: IObjectQL) {
9
26
  return async (req: IncomingMessage, res: ServerResponse) => {
10
27
  // Parse the URL
11
28
  const url = req.url || '';
29
+ const method = req.method;
12
30
 
13
31
  // CORS headers for development
14
32
  res.setHeader('Access-Control-Allow-Origin', '*');
15
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
33
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
16
34
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
17
35
 
18
36
  if (req.method === 'OPTIONS') {
@@ -22,14 +40,15 @@ export function createMetadataHandler(app: IObjectQL) {
22
40
  }
23
41
 
24
42
  try {
25
- // GET /api/metadata/objects - List all objects
26
- if (url === '/api/metadata/objects') {
43
+ // GET /api/metadata or /api/metadata/objects - List all objects
44
+ if (method === 'GET' && (url === '/api/metadata' || url === '/api/metadata/objects')) {
27
45
  const configs = app.getConfigs();
28
46
  const objects = Object.values(configs).map(obj => ({
29
47
  name: obj.name,
30
48
  label: obj.label || obj.name,
31
49
  icon: obj.icon,
32
- fields: obj.fields ? Object.keys(obj.fields) : []
50
+ description: obj.description,
51
+ fields: obj.fields || {}
33
52
  }));
34
53
 
35
54
  res.setHeader('Content-Type', 'application/json');
@@ -39,13 +58,18 @@ export function createMetadataHandler(app: IObjectQL) {
39
58
  }
40
59
 
41
60
  // GET /api/metadata/objects/:name - Get object details
42
- const match = url.match(/^\/api\/metadata\/objects\/([^\/]+)$/);
43
- if (match) {
44
- const objectName = match[1];
61
+ const objectMatch = url.match(/^\/api\/metadata\/objects\/([^\/\?]+)$/);
62
+ if (method === 'GET' && objectMatch) {
63
+ const objectName = objectMatch[1];
45
64
  const metadata = app.getObject(objectName);
46
65
  if (!metadata) {
47
66
  res.statusCode = 404;
48
- res.end(JSON.stringify({ error: 'Object not found' }));
67
+ res.end(JSON.stringify({
68
+ error: {
69
+ code: ErrorCode.NOT_FOUND,
70
+ message: `Object '${objectName}' not found`
71
+ }
72
+ }));
49
73
  return;
50
74
  }
51
75
 
@@ -56,7 +80,14 @@ export function createMetadataHandler(app: IObjectQL) {
56
80
  type: field.type,
57
81
  label: field.label,
58
82
  required: field.required,
59
- defaultValue: field.defaultValue
83
+ defaultValue: field.defaultValue,
84
+ unique: field.unique,
85
+ options: field.options,
86
+ min: field.min,
87
+ max: field.max,
88
+ min_length: field.min_length,
89
+ max_length: field.max_length,
90
+ regex: field.regex
60
91
  }))
61
92
  : [];
62
93
 
@@ -69,13 +100,137 @@ export function createMetadataHandler(app: IObjectQL) {
69
100
  return;
70
101
  }
71
102
 
103
+ // GET /api/metadata/objects/:name/fields/:field - Get field metadata
104
+ const fieldMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/fields\/([^\/\?]+)$/);
105
+ if (method === 'GET' && fieldMatch) {
106
+ const [, objectName, fieldName] = fieldMatch;
107
+ const metadata = app.getObject(objectName);
108
+
109
+ if (!metadata) {
110
+ res.statusCode = 404;
111
+ res.end(JSON.stringify({
112
+ error: {
113
+ code: ErrorCode.NOT_FOUND,
114
+ message: `Object '${objectName}' not found`
115
+ }
116
+ }));
117
+ return;
118
+ }
119
+
120
+ const field = metadata.fields?.[fieldName];
121
+ if (!field) {
122
+ res.statusCode = 404;
123
+ res.end(JSON.stringify({
124
+ error: {
125
+ code: ErrorCode.NOT_FOUND,
126
+ message: `Field '${fieldName}' not found in object '${objectName}'`
127
+ }
128
+ }));
129
+ return;
130
+ }
131
+
132
+ res.setHeader('Content-Type', 'application/json');
133
+ res.statusCode = 200;
134
+ res.end(JSON.stringify({
135
+ name: field.name || fieldName,
136
+ type: field.type,
137
+ label: field.label,
138
+ required: field.required,
139
+ unique: field.unique,
140
+ defaultValue: field.defaultValue,
141
+ options: field.options,
142
+ min: field.min,
143
+ max: field.max,
144
+ min_length: field.min_length,
145
+ max_length: field.max_length,
146
+ regex: field.regex
147
+ }));
148
+ return;
149
+ }
150
+
151
+ // GET /api/metadata/objects/:name/actions - List actions
152
+ const actionsMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/actions$/);
153
+ if (method === 'GET' && actionsMatch) {
154
+ const [, objectName] = actionsMatch;
155
+ const metadata = app.getObject(objectName);
156
+
157
+ if (!metadata) {
158
+ res.statusCode = 404;
159
+ res.end(JSON.stringify({
160
+ error: {
161
+ code: ErrorCode.NOT_FOUND,
162
+ message: `Object '${objectName}' not found`
163
+ }
164
+ }));
165
+ return;
166
+ }
167
+
168
+ const actions = metadata.actions || {};
169
+ const formattedActions = Object.entries(actions).map(([key, action]) => {
170
+ const actionConfig = action as {
171
+ type?: string;
172
+ label?: string;
173
+ params?: Record<string, unknown>;
174
+ description?: string;
175
+ fields?: Record<string, unknown>;
176
+ };
177
+ const hasFields = !!actionConfig.fields && Object.keys(actionConfig.fields).length > 0;
178
+ return {
179
+ name: key,
180
+ type: actionConfig.type || (hasFields ? 'record' : 'global'),
181
+ label: actionConfig.label || key,
182
+ params: actionConfig.params || {},
183
+ description: actionConfig.description
184
+ };
185
+ });
186
+
187
+ res.setHeader('Content-Type', 'application/json');
188
+ res.statusCode = 200;
189
+ res.end(JSON.stringify({ actions: formattedActions }));
190
+ return;
191
+ }
192
+
193
+ // POST/PUT /api/metadata/:type/:id - Update metadata
194
+ const updateMatch = url.match(/^\/api\/metadata\/([^\/]+)\/([^\/]+)$/);
195
+ if ((method === 'POST' || method === 'PUT') && updateMatch) {
196
+ const [, type, id] = updateMatch;
197
+ const body = await readBody(req);
198
+
199
+ try {
200
+ await app.updateMetadata(type, id, body);
201
+ res.setHeader('Content-Type', 'application/json');
202
+ res.statusCode = 200;
203
+ res.end(JSON.stringify({ success: true }));
204
+ } catch (e: any) {
205
+ const isUserError = e.message.startsWith('Cannot update') || e.message.includes('not found');
206
+ res.statusCode = isUserError ? 400 : 500;
207
+ res.end(JSON.stringify({
208
+ error: {
209
+ code: isUserError ? ErrorCode.INVALID_REQUEST : ErrorCode.INTERNAL_ERROR,
210
+ message: e.message
211
+ }
212
+ }));
213
+ }
214
+ return;
215
+ }
216
+
72
217
  // Not found
73
218
  res.statusCode = 404;
74
- res.end('Not Found');
219
+ res.end(JSON.stringify({
220
+ error: {
221
+ code: ErrorCode.NOT_FOUND,
222
+ message: 'Not Found'
223
+ }
224
+ }));
75
225
  } catch (e: any) {
76
226
  console.error('[Metadata Handler] Error:', e);
77
227
  res.statusCode = 500;
78
- res.end(JSON.stringify({ error: 'Internal Server Error' }));
228
+ res.end(JSON.stringify({
229
+ error: {
230
+ code: ErrorCode.INTERNAL_ERROR,
231
+ message: 'Internal Server Error'
232
+ }
233
+ }));
79
234
  }
80
235
  };
81
236
  }