@objectql/server 1.3.1 → 1.5.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.
@@ -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,18 +1,36 @@
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.
6
- * These endpoints expose information about registered objects.
23
+ * These endpoints expose information about registered objects and other metadata.
7
24
  */
8
25
  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,60 +40,184 @@ 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
+ // Helper to send JSON
44
+ const sendJson = (data: any, status = 200) => {
45
+ res.setHeader('Content-Type', 'application/json');
46
+ res.statusCode = status;
47
+ res.end(JSON.stringify(data));
48
+ };
49
+
50
+ const sendError = (code: ErrorCode, message: string, status = 400) => {
51
+ sendJson({ error: { code, message } }, status);
52
+ };
53
+
54
+ // ---------------------------------------------------------
55
+ // 1. List Entries (GET /api/metadata/:type)
56
+ // ---------------------------------------------------------
57
+
58
+ // Legacy/Alias: /api/metadata or /api/metadata/objects -> list objects
59
+ if (method === 'GET' && (url === '/api/metadata' || url === '/api/metadata/objects')) {
27
60
  const configs = app.getConfigs();
28
61
  const objects = Object.values(configs).map(obj => ({
29
62
  name: obj.name,
30
63
  label: obj.label || obj.name,
31
64
  icon: obj.icon,
32
- fields: obj.fields ? Object.keys(obj.fields) : []
65
+ description: obj.description,
66
+ fields: obj.fields || {}
33
67
  }));
68
+ return sendJson({ objects });
69
+ }
70
+
71
+ // Generic List: /api/metadata/:type
72
+ const listMatch = url.match(/^\/api\/metadata\/([^\/]+)$/);
73
+ if (method === 'GET' && listMatch) {
74
+ let [, type] = listMatch;
75
+ if (type === 'objects') type = 'object'; // Should not hit due to order, but safe to keep.
34
76
 
35
- res.setHeader('Content-Type', 'application/json');
36
- res.statusCode = 200;
37
- res.end(JSON.stringify({ objects }));
38
- return;
77
+ const entries = app.metadata.list(type);
78
+ // Return simple list
79
+ return sendJson({
80
+ [type]: entries
81
+ });
39
82
  }
40
83
 
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;
84
+ // ---------------------------------------------------------
85
+ // 2. Get Single Entry (GET /api/metadata/:type/:id)
86
+ // ---------------------------------------------------------
87
+
88
+ const detailMatch = url.match(/^\/api\/metadata\/([^\/]+)\/([^\/\?]+)$/);
89
+
90
+ if (method === 'GET' && detailMatch) {
91
+ let [, type, id] = detailMatch;
92
+
93
+ // Handle Object Special Logic (Field Formatting)
94
+ if (type === 'objects' || type === 'object') {
95
+ const metadata = app.getObject(id);
96
+ if (!metadata) {
97
+ return sendError(ErrorCode.NOT_FOUND, `Object '${id}' not found`, 404);
98
+ }
99
+
100
+ // Convert fields object to array (Standard Object Response)
101
+ const fields = metadata.fields
102
+ ? Object.entries(metadata.fields).map(([key, field]) => ({
103
+ name: field.name || key,
104
+ type: field.type,
105
+ label: field.label,
106
+ required: field.required,
107
+ defaultValue: field.defaultValue,
108
+ unique: field.unique,
109
+ options: field.options,
110
+ min: field.min,
111
+ max: field.max,
112
+ min_length: field.min_length,
113
+ max_length: field.max_length,
114
+ regex: field.regex
115
+ }))
116
+ : [];
117
+
118
+ return sendJson({
119
+ ...metadata,
120
+ fields
121
+ });
122
+ } else {
123
+ // Generic Metadata (View, Form, etc.)
124
+ const content = app.metadata.get(type, id);
125
+ if (!content) {
126
+ return sendError(ErrorCode.NOT_FOUND, `${type} '${id}' not found`, 404);
127
+ }
128
+ return sendJson(content);
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------
133
+ // 3. Update Entry (POST/PUT /api/metadata/:type/:id)
134
+ // ---------------------------------------------------------
135
+ if ((method === 'POST' || method === 'PUT') && detailMatch) {
136
+ let [, type, id] = detailMatch;
137
+ if (type === 'objects') type = 'object';
138
+
139
+ const body = await readBody(req);
140
+ try {
141
+ await app.updateMetadata(type, id, body);
142
+ return sendJson({ success: true });
143
+ } catch (e: any) {
144
+ const isUserError = e.message.startsWith('Cannot update') || e.message.includes('not found');
145
+ return sendError(
146
+ isUserError ? ErrorCode.INVALID_REQUEST : ErrorCode.INTERNAL_ERROR,
147
+ e.message,
148
+ isUserError ? 400 : 500
149
+ );
50
150
  }
151
+ }
152
+
153
+ // ---------------------------------------------------------
154
+ // 4. Object Sub-resources (Fields, Actions)
155
+ // ---------------------------------------------------------
156
+
157
+ // GET /api/metadata/objects/:name/fields/:field
158
+ // Legacy path support.
159
+ const fieldMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/fields\/([^\/\?]+)$/);
160
+ if (method === 'GET' && fieldMatch) {
161
+ const [, objectName, fieldName] = fieldMatch;
162
+ const metadata = app.getObject(objectName);
51
163
 
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
- : [];
164
+ if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
165
+
166
+ const field = metadata.fields?.[fieldName];
167
+ if (!field) return sendError(ErrorCode.NOT_FOUND, `Field '${fieldName}' not found`, 404);
168
+
169
+ return sendJson({
170
+ name: field.name || fieldName,
171
+ type: field.type,
172
+ label: field.label,
173
+ required: field.required,
174
+ unique: field.unique,
175
+ defaultValue: field.defaultValue,
176
+ options: field.options,
177
+ min: field.min,
178
+ max: field.max,
179
+ min_length: field.min_length,
180
+ max_length: field.max_length,
181
+ regex: field.regex
182
+ });
183
+ }
184
+
185
+ // GET /api/metadata/objects/:name/actions
186
+ const actionsMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/actions$/);
187
+ if (method === 'GET' && actionsMatch) {
188
+ const [, objectName] = actionsMatch;
189
+ const metadata = app.getObject(objectName);
62
190
 
63
- res.setHeader('Content-Type', 'application/json');
64
- res.statusCode = 200;
65
- res.end(JSON.stringify({
66
- ...metadata,
67
- fields
68
- }));
69
- return;
191
+ if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
192
+
193
+ const actions = metadata.actions || {};
194
+ const formattedActions = Object.entries(actions).map(([key, action]) => {
195
+ const actionConfig = action as any;
196
+ const hasFields = !!actionConfig.fields && Object.keys(actionConfig.fields).length > 0;
197
+ return {
198
+ name: key,
199
+ type: actionConfig.type || (hasFields ? 'record' : 'global'),
200
+ label: actionConfig.label || key,
201
+ params: actionConfig.params || {},
202
+ description: actionConfig.description
203
+ };
204
+ });
205
+
206
+ return sendJson({ actions: formattedActions });
70
207
  }
71
208
 
72
209
  // Not found
73
- res.statusCode = 404;
74
- res.end('Not Found');
210
+ sendError(ErrorCode.NOT_FOUND, 'Not Found', 404);
211
+
75
212
  } catch (e: any) {
76
213
  console.error('[Metadata Handler] Error:', e);
77
214
  res.statusCode = 500;
78
- res.end(JSON.stringify({ error: 'Internal Server Error' }));
215
+ res.end(JSON.stringify({
216
+ error: {
217
+ code: ErrorCode.INTERNAL_ERROR,
218
+ message: 'Internal Server Error'
219
+ }
220
+ }));
79
221
  }
80
222
  };
81
223
  }