@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/dist/adapters/node.js +157 -23
- 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.d.ts +1 -1
- package/dist/metadata.js +165 -38
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.js +73 -24
- package/dist/openapi.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 +166 -26
- package/src/adapters/rest.ts +271 -0
- package/src/index.ts +3 -1
- package/src/metadata.ts +180 -38
- package/src/openapi.ts +80 -32
- 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.json +5 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/console.d.ts +0 -5
- package/dist/console.js.map +0 -1
|
@@ -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,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
|
-
//
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
77
|
+
const entries = app.metadata.list(type);
|
|
78
|
+
// Return simple list
|
|
79
|
+
return sendJson({
|
|
80
|
+
[type]: entries
|
|
81
|
+
});
|
|
39
82
|
}
|
|
40
83
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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({
|
|
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
|
}
|