Skip to content

Commit c3b4f1f

Browse files
committed
Add OpenAPI spec generation and endpoint
Introduces a new `generateOpenAPI` function to produce OpenAPI 3.0 specs from ObjectQL metadata. Adds an endpoint `/openapi.json` to the Node.js adapter for serving the spec, and exports the OpenAPI generator from the server package index.
1 parent fe222d9 commit c3b4f1f

3 files changed

Lines changed: 161 additions & 0 deletions

File tree

packages/server/src/adapters/node.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IObjectQL } from '@objectql/types';
22
import { ObjectQLServer } from '../server';
33
import { ObjectQLRequest } from '../types';
44
import { IncomingMessage, ServerResponse } from 'http';
5+
import { generateOpenAPI } from '../openapi';
56

67
/**
78
* Creates a standard Node.js HTTP request handler.
@@ -11,6 +12,15 @@ export function createNodeHandler(app: IObjectQL) {
1112

1213

1314
return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
15+
// Handle OpenAPI spec request
16+
if (req.method === 'GET' && req.url?.endsWith('/openapi.json')) {
17+
const spec = generateOpenAPI(app);
18+
res.setHeader('Content-Type', 'application/json');
19+
res.statusCode = 200;
20+
res.end(JSON.stringify(spec));
21+
return;
22+
}
23+
1424
if (req.method !== 'POST') {
1525
res.statusCode = 405;
1626
res.end('Method Not Allowed');

packages/server/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './types';
2+
export * from './openapi';
23
export * from './server';
34
// We export createNodeHandler from root for convenience,
45
// but in the future we might encourage 'import ... from @objectql/server/node'

packages/server/src/openapi.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { IObjectQL, ObjectConfig, FieldType, FieldConfig } from '@objectql/types';
2+
3+
interface OpenAPISchema {
4+
openapi: string;
5+
info: {
6+
title: string;
7+
version: string;
8+
};
9+
paths: Record<string, any>;
10+
components: {
11+
schemas: Record<string, any>;
12+
};
13+
}
14+
15+
export function generateOpenAPI(app: IObjectQL): OpenAPISchema {
16+
const registry = (app as any).metadata; // Direct access or via interface
17+
const objects = registry.list('object') as ObjectConfig[];
18+
19+
const paths: Record<string, any> = {};
20+
const schemas: Record<string, any> = {};
21+
22+
23+
// 1. Generate Schemas
24+
for (const obj of objects) {
25+
const schemaName = obj.name;
26+
const properties: Record<string, any> = {};
27+
28+
for (const [fieldName, field] of Object.entries(obj.fields)) {
29+
properties[fieldName] = mapFieldTypeToOpenAPI(field);
30+
}
31+
32+
schemas[schemaName] = {
33+
type: 'object',
34+
properties
35+
};
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+
}
50+
51+
// Let's do a "Virtual" REST path generation for better visualization
52+
// Assuming we WILL support REST mapping in this update.
53+
for (const obj of objects) {
54+
const name = obj.name;
55+
56+
// GET /name (List)
57+
paths[`/${name}`] = {
58+
get: {
59+
summary: `List ${name}s`,
60+
tags: [name],
61+
parameters: [
62+
{ name: 'filter', in: 'query', schema: { type: 'string' }, description: 'JSON filter args' }
63+
],
64+
responses: {
65+
200: {
66+
description: `List of ${name}`,
67+
content: {
68+
'application/json': {
69+
schema: {
70+
type: 'object',
71+
properties: {
72+
data: { type: 'array', items: { $ref: `#/components/schemas/${name}` } }
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
},
80+
post: {
81+
summary: `Create ${name}`,
82+
tags: [name],
83+
requestBody: {
84+
content: {
85+
'application/json': {
86+
schema: {
87+
type: 'object',
88+
properties: {
89+
data: { $ref: `#/components/schemas/${name}` }
90+
}
91+
}
92+
}
93+
}
94+
},
95+
responses: {
96+
200: { description: 'Created' }
97+
}
98+
}
99+
};
100+
101+
// GET /name/{id}
102+
paths[`/${name}/{id}`] = {
103+
get: {
104+
summary: `Get ${name}`,
105+
tags: [name],
106+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
107+
responses: { 200: { description: 'Item' } }
108+
},
109+
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' } }
115+
},
116+
delete: {
117+
summary: `Delete ${name}`,
118+
tags: [name],
119+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
120+
responses: { 200: { description: 'Deleted' } }
121+
}
122+
};
123+
}
124+
125+
return {
126+
openapi: '3.0.0',
127+
info: {
128+
title: 'ObjectQL API',
129+
version: '1.0.0'
130+
},
131+
paths,
132+
components: {
133+
schemas
134+
}
135+
};
136+
}
137+
138+
function mapFieldTypeToOpenAPI(field: FieldConfig | string): any {
139+
const type = typeof field === 'string' ? field : field.type;
140+
141+
switch (type) {
142+
case 'string': return { type: 'string' };
143+
case 'integer': return { type: 'integer' };
144+
case 'float': return { type: 'number' };
145+
case 'boolean': return { type: 'boolean' };
146+
case 'date': return { type: 'string', format: 'date-time' };
147+
case 'json': return { type: 'object' };
148+
default: return { type: 'string' }; // Fallback or relationship ID
149+
}
150+
}

0 commit comments

Comments
 (0)