@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/dist/adapters/node.js +69 -9
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/rest.d.ts +15 -0
- package/dist/adapters/rest.js +252 -0
- package/dist/adapters/rest.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.js +152 -11
- package/dist/metadata.js.map +1 -1
- package/dist/server.d.ts +8 -0
- package/dist/server.js +75 -15
- package/dist/server.js.map +1 -1
- package/dist/studio.d.ts +5 -0
- package/dist/{console.js → studio.js} +35 -23
- package/dist/studio.js.map +1 -0
- package/dist/types.d.ts +49 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -5
- package/src/adapters/node.ts +72 -11
- package/src/adapters/rest.ts +271 -0
- package/src/index.ts +3 -1
- package/src/metadata.ts +166 -11
- package/src/server.ts +119 -16
- package/src/{console.ts → studio.ts} +35 -22
- package/src/types.ts +56 -2
- package/test/rest.test.ts +164 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/console.d.ts +0 -5
- package/dist/console.js.map +0 -1
package/src/adapters/node.ts
CHANGED
|
@@ -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 =
|
|
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({
|
|
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(
|
|
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 './
|
|
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
|
-
|
|
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
|
|
43
|
-
if (
|
|
44
|
-
const objectName =
|
|
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({
|
|
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(
|
|
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({
|
|
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
|
}
|