@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/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,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
- 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
+ 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
- // GET /api/metadata/objects/:name/fields/:field - Get field metadata
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
- res.setHeader('Content-Type', 'application/json');
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 - List 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
- 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;
207
+ return sendJson({ actions: formattedActions });
215
208
  }
216
209
 
217
210
  // Not found
218
- res.statusCode = 404;
219
- res.end(JSON.stringify({
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. 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
@@ -1,9 +1,13 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
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
  }