@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/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
- // GET /api/metadata or /api/metadata/objects - List all 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
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
- res.setHeader('Content-Type', 'application/json');
55
- res.statusCode = 200;
56
- res.end(JSON.stringify({ objects }));
57
- return;
77
+ const entries = app.metadata.list(type);
78
+ // Return simple list
79
+ return sendJson({
80
+ [type]: entries
81
+ });
58
82
  }
59
83
 
60
- // GET /api/metadata/objects/:name - Get object details
61
- const objectMatch = url.match(/^\/api\/metadata\/objects\/([^\/\?]+)$/);
62
- if (method === 'GET' && objectMatch) {
63
- const objectName = objectMatch[1];
64
- const metadata = app.getObject(objectName);
65
- if (!metadata) {
66
- res.statusCode = 404;
67
- res.end(JSON.stringify({
68
- error: {
69
- code: ErrorCode.NOT_FOUND,
70
- message: `Object '${objectName}' not found`
71
- }
72
- }));
73
- 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
+ );
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
- // GET /api/metadata/objects/:name/fields/:field - Get field metadata
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
- res.setHeader('Content-Type', 'application/json');
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 - List 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
- 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;
206
+ return sendJson({ actions: formattedActions });
215
207
  }
216
208
 
217
209
  // Not found
218
- res.statusCode = 404;
219
- res.end(JSON.stringify({
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. Generate Schemas
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
- // Let's do a "Virtual" REST path generation for better visualization
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[`/${name}`] = {
77
+ // GET /api/data/:name (List)
78
+ paths[basePath] = {
58
79
  get: {
59
- summary: `List ${name}s`,
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
- // GET /name/{id}
102
- paths[`/${name}/{id}`] = {
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: { 200: { description: 'Item' } }
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
- summary: `Update ${name}`,
111
- tags: [name],
112
- parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
113
- requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { data: { type: 'object' }} } } } },
114
- responses: { 200: { description: 'Updated' } }
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
- summary: `Delete ${name}`,
118
- tags: [name],
119
- parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
120
- responses: { 200: { description: 'Deleted' } }
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
@@ -5,5 +5,9 @@
5
5
  "rootDir": "src",
6
6
  "composite": true
7
7
  },
8
- "include": ["src/**/*"]
8
+ "include": ["src/**/*"],
9
+ "references": [
10
+ { "path": "../types" },
11
+ { "path": "../core" }
12
+ ]
9
13
  }