@objectql/server 1.4.0 → 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 +12 -0
- package/dist/adapters/node.js +116 -42
- package/dist/adapters/node.js.map +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/metadata.js +101 -115
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.js +73 -24
- package/dist/openapi.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/node.ts +124 -45
- package/src/metadata.ts +112 -125
- package/src/openapi.ts +80 -32
- package/tsconfig.json +5 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/metadata.ts
CHANGED
|
@@ -20,7 +20,7 @@ function readBody(req: IncomingMessage): Promise<any> {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Creates a handler for metadata endpoints.
|
|
23
|
-
* These endpoints expose information about registered objects.
|
|
23
|
+
* These endpoints expose information about registered objects and other metadata.
|
|
24
24
|
*/
|
|
25
25
|
export function createMetadataHandler(app: IObjectQL) {
|
|
26
26
|
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
@@ -40,7 +40,22 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
|
-
//
|
|
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
|
|
44
59
|
if (method === 'GET' && (url === '/api/metadata' || url === '/api/metadata/objects')) {
|
|
45
60
|
const configs = app.getConfigs();
|
|
46
61
|
const objects = Object.values(configs).map(obj => ({
|
|
@@ -50,88 +65,108 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
50
65
|
description: obj.description,
|
|
51
66
|
fields: obj.fields || {}
|
|
52
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.
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
const entries = app.metadata.list(type);
|
|
78
|
+
// Return simple list
|
|
79
|
+
return sendJson({
|
|
80
|
+
[type]: entries
|
|
81
|
+
});
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
);
|
|
74
150
|
}
|
|
75
|
-
|
|
76
|
-
// Convert fields object to array
|
|
77
|
-
const fields = metadata.fields
|
|
78
|
-
? Object.entries(metadata.fields).map(([key, field]) => ({
|
|
79
|
-
name: field.name || key,
|
|
80
|
-
type: field.type,
|
|
81
|
-
label: field.label,
|
|
82
|
-
required: field.required,
|
|
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
|
|
91
|
-
}))
|
|
92
|
-
: [];
|
|
93
|
-
|
|
94
|
-
res.setHeader('Content-Type', 'application/json');
|
|
95
|
-
res.statusCode = 200;
|
|
96
|
-
res.end(JSON.stringify({
|
|
97
|
-
...metadata,
|
|
98
|
-
fields
|
|
99
|
-
}));
|
|
100
|
-
return;
|
|
101
151
|
}
|
|
102
152
|
|
|
103
|
-
//
|
|
153
|
+
// ---------------------------------------------------------
|
|
154
|
+
// 4. Object Sub-resources (Fields, Actions)
|
|
155
|
+
// ---------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
// GET /api/metadata/objects/:name/fields/:field
|
|
158
|
+
// Legacy path support.
|
|
104
159
|
const fieldMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/fields\/([^\/\?]+)$/);
|
|
105
160
|
if (method === 'GET' && fieldMatch) {
|
|
106
161
|
const [, objectName, fieldName] = fieldMatch;
|
|
107
162
|
const metadata = app.getObject(objectName);
|
|
108
163
|
|
|
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
|
-
}
|
|
164
|
+
if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
|
|
119
165
|
|
|
120
166
|
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
|
-
}
|
|
167
|
+
if (!field) return sendError(ErrorCode.NOT_FOUND, `Field '${fieldName}' not found`, 404);
|
|
131
168
|
|
|
132
|
-
|
|
133
|
-
res.statusCode = 200;
|
|
134
|
-
res.end(JSON.stringify({
|
|
169
|
+
return sendJson({
|
|
135
170
|
name: field.name || fieldName,
|
|
136
171
|
type: field.type,
|
|
137
172
|
label: field.label,
|
|
@@ -144,36 +179,20 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
144
179
|
min_length: field.min_length,
|
|
145
180
|
max_length: field.max_length,
|
|
146
181
|
regex: field.regex
|
|
147
|
-
})
|
|
148
|
-
return;
|
|
182
|
+
});
|
|
149
183
|
}
|
|
150
184
|
|
|
151
|
-
// GET /api/metadata/objects/:name/actions
|
|
185
|
+
// GET /api/metadata/objects/:name/actions
|
|
152
186
|
const actionsMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/actions$/);
|
|
153
187
|
if (method === 'GET' && actionsMatch) {
|
|
154
188
|
const [, objectName] = actionsMatch;
|
|
155
189
|
const metadata = app.getObject(objectName);
|
|
156
190
|
|
|
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
|
-
}
|
|
191
|
+
if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
|
|
167
192
|
|
|
168
193
|
const actions = metadata.actions || {};
|
|
169
194
|
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
|
-
};
|
|
195
|
+
const actionConfig = action as any;
|
|
177
196
|
const hasFields = !!actionConfig.fields && Object.keys(actionConfig.fields).length > 0;
|
|
178
197
|
return {
|
|
179
198
|
name: key,
|
|
@@ -184,44 +203,12 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
184
203
|
};
|
|
185
204
|
});
|
|
186
205
|
|
|
187
|
-
|
|
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;
|
|
206
|
+
return sendJson({ actions: formattedActions });
|
|
215
207
|
}
|
|
216
208
|
|
|
217
209
|
// Not found
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
error: {
|
|
221
|
-
code: ErrorCode.NOT_FOUND,
|
|
222
|
-
message: 'Not Found'
|
|
223
|
-
}
|
|
224
|
-
}));
|
|
210
|
+
sendError(ErrorCode.NOT_FOUND, 'Not Found', 404);
|
|
211
|
+
|
|
225
212
|
} catch (e: any) {
|
|
226
213
|
console.error('[Metadata Handler] Error:', e);
|
|
227
214
|
res.statusCode = 500;
|
package/src/openapi.ts
CHANGED
|
@@ -20,7 +20,41 @@ export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
|
|
|
20
20
|
const schemas: Record<string, any> = {};
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
// 1.
|
|
23
|
+
// 1. JSON-RPC Endpoint
|
|
24
|
+
paths['/api/objectql'] = {
|
|
25
|
+
post: {
|
|
26
|
+
summary: 'JSON-RPC Entry Point',
|
|
27
|
+
description: 'Execute any ObjectQL operation via a JSON body.',
|
|
28
|
+
tags: ['System'],
|
|
29
|
+
requestBody: {
|
|
30
|
+
content: {
|
|
31
|
+
'application/json': {
|
|
32
|
+
schema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
op: { type: 'string', enum: ['find', 'findOne', 'create', 'update', 'delete', 'count', 'action'] },
|
|
36
|
+
object: { type: 'string' },
|
|
37
|
+
args: { type: 'object' }
|
|
38
|
+
},
|
|
39
|
+
required: ['op', 'object']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
responses: {
|
|
45
|
+
200: {
|
|
46
|
+
description: 'Operation Result',
|
|
47
|
+
content: {
|
|
48
|
+
'application/json': {
|
|
49
|
+
schema: { type: 'object' } // Dynamic result
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// 2. Generate Schemas
|
|
24
58
|
for (const obj of objects) {
|
|
25
59
|
const schemaName = obj.name;
|
|
26
60
|
const properties: Record<string, any> = {};
|
|
@@ -33,33 +67,23 @@ export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
|
|
|
33
67
|
type: 'object',
|
|
34
68
|
properties
|
|
35
69
|
};
|
|
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
70
|
}
|
|
50
71
|
|
|
51
|
-
//
|
|
52
|
-
// Assuming we WILL support REST mapping in this update.
|
|
72
|
+
// 3. REST API Paths
|
|
53
73
|
for (const obj of objects) {
|
|
54
74
|
const name = obj.name;
|
|
75
|
+
const basePath = `/api/data/${name}`; // Standard REST Path
|
|
55
76
|
|
|
56
|
-
// GET /name (List)
|
|
57
|
-
paths[
|
|
77
|
+
// GET /api/data/:name (List)
|
|
78
|
+
paths[basePath] = {
|
|
58
79
|
get: {
|
|
59
|
-
summary: `List ${name}
|
|
80
|
+
summary: `List ${name}`,
|
|
60
81
|
tags: [name],
|
|
61
82
|
parameters: [
|
|
62
|
-
{ name: 'filter', in: 'query', schema: { type: 'string' }, description: 'JSON filter args' }
|
|
83
|
+
{ name: 'filter', in: 'query', schema: { type: 'string' }, description: 'JSON filter args' },
|
|
84
|
+
{ name: 'fields', in: 'query', schema: { type: 'string' }, description: 'Comma-separated fields to return' },
|
|
85
|
+
{ name: 'top', in: 'query', schema: { type: 'integer' }, description: 'Limit' },
|
|
86
|
+
{ name: 'skip', in: 'query', schema: { type: 'integer' }, description: 'Offset' }
|
|
63
87
|
],
|
|
64
88
|
responses: {
|
|
65
89
|
200: {
|
|
@@ -98,26 +122,50 @@ export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
|
|
|
98
122
|
}
|
|
99
123
|
};
|
|
100
124
|
|
|
101
|
-
//
|
|
102
|
-
paths[
|
|
125
|
+
// /api/data/:name/:id
|
|
126
|
+
paths[`${basePath}/{id}`] = {
|
|
103
127
|
get: {
|
|
104
128
|
summary: `Get ${name}`,
|
|
105
129
|
tags: [name],
|
|
106
130
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
107
|
-
responses: {
|
|
131
|
+
responses: {
|
|
132
|
+
200: {
|
|
133
|
+
description: 'Item',
|
|
134
|
+
content: {
|
|
135
|
+
'application/json': {
|
|
136
|
+
schema: { $ref: `#/components/schemas/${name}` }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
108
141
|
},
|
|
109
142
|
patch: {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
summary: `Update ${name}`,
|
|
144
|
+
tags: [name],
|
|
145
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
146
|
+
requestBody: {
|
|
147
|
+
content: {
|
|
148
|
+
'application/json': {
|
|
149
|
+
schema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
data: { type: 'object' }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
responses: {
|
|
159
|
+
200: { description: 'Updated' }
|
|
160
|
+
}
|
|
115
161
|
},
|
|
116
162
|
delete: {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
summary: `Delete ${name}`,
|
|
164
|
+
tags: [name],
|
|
165
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
166
|
+
responses: {
|
|
167
|
+
200: { description: 'Deleted' }
|
|
168
|
+
}
|
|
121
169
|
}
|
|
122
170
|
};
|
|
123
171
|
}
|