@objectql/server 1.4.0 → 1.6.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 +24 -0
- package/dist/adapters/node.js +118 -43
- package/dist/adapters/node.js.map +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/metadata.js +104 -116
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.js +73 -24
- package/dist/openapi.js.map +1 -1
- package/dist/server.js +5 -3
- package/dist/server.js.map +1 -1
- package/jest.config.js +4 -1
- package/package.json +3 -3
- package/src/adapters/node.ts +124 -45
- package/src/metadata.ts +113 -125
- package/src/openapi.ts +80 -32
- package/tsconfig.json +6 -2
- 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,109 @@ 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
|
+
return sendError(ErrorCode.INTERNAL_ERROR, 'Metadata updates via API are temporarily disabled in this architectural version.', 501);
|
|
144
|
+
} catch (e: any) {
|
|
145
|
+
const isUserError = e.message.startsWith('Cannot update') || e.message.includes('not found');
|
|
146
|
+
return sendError(
|
|
147
|
+
isUserError ? ErrorCode.INVALID_REQUEST : ErrorCode.INTERNAL_ERROR,
|
|
148
|
+
e.message,
|
|
149
|
+
isUserError ? 400 : 500
|
|
150
|
+
);
|
|
74
151
|
}
|
|
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
152
|
}
|
|
102
153
|
|
|
103
|
-
//
|
|
154
|
+
// ---------------------------------------------------------
|
|
155
|
+
// 4. Object Sub-resources (Fields, Actions)
|
|
156
|
+
// ---------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
// GET /api/metadata/objects/:name/fields/:field
|
|
159
|
+
// Legacy path support.
|
|
104
160
|
const fieldMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/fields\/([^\/\?]+)$/);
|
|
105
161
|
if (method === 'GET' && fieldMatch) {
|
|
106
162
|
const [, objectName, fieldName] = fieldMatch;
|
|
107
163
|
const metadata = app.getObject(objectName);
|
|
108
164
|
|
|
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
|
-
}
|
|
165
|
+
if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
|
|
119
166
|
|
|
120
167
|
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
|
-
}
|
|
168
|
+
if (!field) return sendError(ErrorCode.NOT_FOUND, `Field '${fieldName}' not found`, 404);
|
|
131
169
|
|
|
132
|
-
|
|
133
|
-
res.statusCode = 200;
|
|
134
|
-
res.end(JSON.stringify({
|
|
170
|
+
return sendJson({
|
|
135
171
|
name: field.name || fieldName,
|
|
136
172
|
type: field.type,
|
|
137
173
|
label: field.label,
|
|
@@ -144,36 +180,20 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
144
180
|
min_length: field.min_length,
|
|
145
181
|
max_length: field.max_length,
|
|
146
182
|
regex: field.regex
|
|
147
|
-
})
|
|
148
|
-
return;
|
|
183
|
+
});
|
|
149
184
|
}
|
|
150
185
|
|
|
151
|
-
// GET /api/metadata/objects/:name/actions
|
|
186
|
+
// GET /api/metadata/objects/:name/actions
|
|
152
187
|
const actionsMatch = url.match(/^\/api\/metadata\/objects\/([^\/]+)\/actions$/);
|
|
153
188
|
if (method === 'GET' && actionsMatch) {
|
|
154
189
|
const [, objectName] = actionsMatch;
|
|
155
190
|
const metadata = app.getObject(objectName);
|
|
156
191
|
|
|
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
|
-
}
|
|
192
|
+
if (!metadata) return sendError(ErrorCode.NOT_FOUND, `Object '${objectName}' not found`, 404);
|
|
167
193
|
|
|
168
194
|
const actions = metadata.actions || {};
|
|
169
195
|
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
|
-
};
|
|
196
|
+
const actionConfig = action as any;
|
|
177
197
|
const hasFields = !!actionConfig.fields && Object.keys(actionConfig.fields).length > 0;
|
|
178
198
|
return {
|
|
179
199
|
name: key,
|
|
@@ -184,44 +204,12 @@ export function createMetadataHandler(app: IObjectQL) {
|
|
|
184
204
|
};
|
|
185
205
|
});
|
|
186
206
|
|
|
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;
|
|
207
|
+
return sendJson({ actions: formattedActions });
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
// Not found
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
error: {
|
|
221
|
-
code: ErrorCode.NOT_FOUND,
|
|
222
|
-
message: 'Not Found'
|
|
223
|
-
}
|
|
224
|
-
}));
|
|
211
|
+
sendError(ErrorCode.NOT_FOUND, 'Not Found', 404);
|
|
212
|
+
|
|
225
213
|
} catch (e: any) {
|
|
226
214
|
console.error('[Metadata Handler] Error:', e);
|
|
227
215
|
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
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"outDir": "dist",
|
|
5
5
|
"rootDir": "src",
|
|
6
6
|
"composite": true
|
|
7
7
|
},
|
|
8
|
-
"include": ["src/**/*"]
|
|
8
|
+
"include": ["src/**/*"],
|
|
9
|
+
"references": [
|
|
10
|
+
{ "path": "../../foundation/types" },
|
|
11
|
+
{ "path": "../../foundation/core" }
|
|
12
|
+
]
|
|
9
13
|
}
|