@objectql/server 1.8.3 → 1.9.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 +27 -0
- package/dist/adapters/graphql.js +25 -0
- package/dist/adapters/graphql.js.map +1 -1
- package/dist/adapters/node.d.ts +3 -1
- package/dist/adapters/node.js +52 -29
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/rest.d.ts +20 -10
- package/dist/adapters/rest.js +39 -31
- package/dist/adapters/rest.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +12 -2
- package/dist/metadata.js +31 -23
- package/dist/metadata.js.map +1 -1
- package/dist/openapi.d.ts +2 -2
- package/dist/openapi.js +6 -4
- package/dist/openapi.js.map +1 -1
- package/dist/utils.d.ts +15 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -3
- package/src/adapters/graphql.ts +28 -0
- package/src/adapters/node.ts +44 -19
- package/src/adapters/rest.ts +34 -19
- package/src/index.ts +1 -1
- package/src/metadata.ts +29 -14
- package/src/openapi.ts +6 -5
- package/src/utils.ts +21 -0
- package/test/custom-routes.test.ts +297 -0
- package/test/metadata.test.ts +259 -0
- package/test/openapi.test.ts +354 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/studio.d.ts +0 -5
- package/dist/studio.js +0 -186
- package/dist/studio.js.map +0 -1
- package/src/studio.ts +0 -164
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createMetadataHandler } from '../src/metadata';
|
|
2
|
+
import { ObjectQL } from '@objectql/core';
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
// Mock IncomingMessage
|
|
7
|
+
class MockRequest extends EventEmitter {
|
|
8
|
+
url: string;
|
|
9
|
+
method: string;
|
|
10
|
+
headers: Record<string, string> = {};
|
|
11
|
+
|
|
12
|
+
constructor(method: string, url: string) {
|
|
13
|
+
super();
|
|
14
|
+
this.method = method;
|
|
15
|
+
this.url = url;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Simulate sending data
|
|
19
|
+
sendData(data: string) {
|
|
20
|
+
this.emit('data', Buffer.from(data));
|
|
21
|
+
this.emit('end');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sendJson(data: any) {
|
|
25
|
+
this.sendData(JSON.stringify(data));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mock ServerResponse
|
|
30
|
+
class MockResponse {
|
|
31
|
+
statusCode: number = 200;
|
|
32
|
+
headers: Record<string, string> = {};
|
|
33
|
+
body: string = '';
|
|
34
|
+
ended: boolean = false;
|
|
35
|
+
|
|
36
|
+
setHeader(name: string, value: string) {
|
|
37
|
+
this.headers[name] = value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
end(data?: string) {
|
|
41
|
+
if (data) this.body = data;
|
|
42
|
+
this.ended = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getBody() {
|
|
46
|
+
return this.body ? JSON.parse(this.body) : null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Metadata Handler', () => {
|
|
51
|
+
let app: ObjectQL;
|
|
52
|
+
let handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
app = new ObjectQL({
|
|
56
|
+
datasources: {},
|
|
57
|
+
objects: {
|
|
58
|
+
user: {
|
|
59
|
+
name: 'user',
|
|
60
|
+
label: 'User',
|
|
61
|
+
icon: 'user-icon',
|
|
62
|
+
description: 'User object',
|
|
63
|
+
fields: {
|
|
64
|
+
name: { type: 'text', required: true },
|
|
65
|
+
email: { type: 'text', required: true }
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
task: {
|
|
69
|
+
name: 'task',
|
|
70
|
+
label: 'Task',
|
|
71
|
+
fields: {
|
|
72
|
+
title: { type: 'text' },
|
|
73
|
+
completed: { type: 'boolean' }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
await app.init();
|
|
79
|
+
handler = createMetadataHandler(app);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('OPTIONS Requests', () => {
|
|
83
|
+
it('should handle OPTIONS request for CORS', async () => {
|
|
84
|
+
const req = new MockRequest('OPTIONS', '/api/metadata');
|
|
85
|
+
const res = new MockResponse();
|
|
86
|
+
|
|
87
|
+
await handler(req as any, res as any);
|
|
88
|
+
|
|
89
|
+
expect(res.statusCode).toBe(200);
|
|
90
|
+
expect(res.headers['Access-Control-Allow-Origin']).toBe('*');
|
|
91
|
+
expect(res.headers['Access-Control-Allow-Methods']).toBe('GET, POST, PUT, OPTIONS');
|
|
92
|
+
expect(res.ended).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('GET /api/metadata', () => {
|
|
97
|
+
it('should list all objects (root endpoint)', async () => {
|
|
98
|
+
const req = new MockRequest('GET', '/api/metadata');
|
|
99
|
+
const res = new MockResponse();
|
|
100
|
+
|
|
101
|
+
req.sendData('');
|
|
102
|
+
await handler(req as any, res as any);
|
|
103
|
+
|
|
104
|
+
const body = res.getBody();
|
|
105
|
+
expect(body.items).toBeDefined();
|
|
106
|
+
expect(body.items).toHaveLength(2);
|
|
107
|
+
expect(body.items.find((o: any) => o.name === 'user')).toBeDefined();
|
|
108
|
+
expect(body.items.find((o: any) => o.name === 'task')).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should include object metadata', async () => {
|
|
112
|
+
const req = new MockRequest('GET', '/api/metadata');
|
|
113
|
+
const res = new MockResponse();
|
|
114
|
+
|
|
115
|
+
req.sendData('');
|
|
116
|
+
await handler(req as any, res as any);
|
|
117
|
+
|
|
118
|
+
const body = res.getBody();
|
|
119
|
+
const user = body.items.find((o: any) => o.name === 'user');
|
|
120
|
+
|
|
121
|
+
expect(user.name).toBe('user');
|
|
122
|
+
expect(user.label).toBe('User');
|
|
123
|
+
expect(user.icon).toBe('user-icon');
|
|
124
|
+
expect(user.description).toBe('User object');
|
|
125
|
+
expect(user.fields).toBeDefined();
|
|
126
|
+
expect(user.fields.name).toBeDefined();
|
|
127
|
+
expect(user.fields.email).toBeDefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('GET /api/metadata/:type', () => {
|
|
132
|
+
it('should list objects with /api/metadata/object', async () => {
|
|
133
|
+
const req = new MockRequest('GET', '/api/metadata/object');
|
|
134
|
+
const res = new MockResponse();
|
|
135
|
+
|
|
136
|
+
req.sendData('');
|
|
137
|
+
await handler(req as any, res as any);
|
|
138
|
+
|
|
139
|
+
const body = res.getBody();
|
|
140
|
+
expect(body.items).toBeDefined();
|
|
141
|
+
expect(body.items).toHaveLength(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should list objects with /api/metadata/objects (alias)', async () => {
|
|
145
|
+
const req = new MockRequest('GET', '/api/metadata/objects');
|
|
146
|
+
const res = new MockResponse();
|
|
147
|
+
|
|
148
|
+
req.sendData('');
|
|
149
|
+
await handler(req as any, res as any);
|
|
150
|
+
|
|
151
|
+
const body = res.getBody();
|
|
152
|
+
expect(body.items).toBeDefined();
|
|
153
|
+
expect(body.items).toHaveLength(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should list custom metadata types', async () => {
|
|
157
|
+
// Register custom metadata
|
|
158
|
+
app.metadata.register('action', {
|
|
159
|
+
type: 'action',
|
|
160
|
+
id: 'sendEmail',
|
|
161
|
+
content: { name: 'sendEmail', handler: 'email.handler' }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const req = new MockRequest('GET', '/api/metadata/action');
|
|
165
|
+
const res = new MockResponse();
|
|
166
|
+
|
|
167
|
+
req.sendData('');
|
|
168
|
+
await handler(req as any, res as any);
|
|
169
|
+
|
|
170
|
+
const body = res.getBody();
|
|
171
|
+
expect(body.items).toBeDefined();
|
|
172
|
+
expect(body.items).toHaveLength(1);
|
|
173
|
+
expect(body.items[0].name).toBe('sendEmail');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should return empty array for non-existent type', async () => {
|
|
177
|
+
const req = new MockRequest('GET', '/api/metadata/nonexistent');
|
|
178
|
+
const res = new MockResponse();
|
|
179
|
+
|
|
180
|
+
req.sendData('');
|
|
181
|
+
await handler(req as any, res as any);
|
|
182
|
+
|
|
183
|
+
const body = res.getBody();
|
|
184
|
+
expect(body.items).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('GET /api/metadata/:type/:id', () => {
|
|
189
|
+
it('should get specific object by name', async () => {
|
|
190
|
+
const req = new MockRequest('GET', '/api/metadata/object/user');
|
|
191
|
+
const res = new MockResponse();
|
|
192
|
+
|
|
193
|
+
req.sendData('');
|
|
194
|
+
await handler(req as any, res as any);
|
|
195
|
+
|
|
196
|
+
const body = res.getBody();
|
|
197
|
+
expect(body.name).toBe('user');
|
|
198
|
+
expect(body.label).toBe('User');
|
|
199
|
+
expect(body.fields).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return 404 for non-existent object', async () => {
|
|
203
|
+
const req = new MockRequest('GET', '/api/metadata/object/nonexistent');
|
|
204
|
+
const res = new MockResponse();
|
|
205
|
+
|
|
206
|
+
req.sendData('');
|
|
207
|
+
await handler(req as any, res as any);
|
|
208
|
+
|
|
209
|
+
expect(res.statusCode).toBe(404);
|
|
210
|
+
const body = res.getBody();
|
|
211
|
+
expect(body.error).toBeDefined();
|
|
212
|
+
expect(body.error.code).toBe('NOT_FOUND');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should get custom metadata by id', async () => {
|
|
216
|
+
app.metadata.register('action', {
|
|
217
|
+
type: 'action',
|
|
218
|
+
id: 'sendEmail',
|
|
219
|
+
content: { name: 'sendEmail', description: 'Send email action' }
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const req = new MockRequest('GET', '/api/metadata/action/sendEmail');
|
|
223
|
+
const res = new MockResponse();
|
|
224
|
+
|
|
225
|
+
req.sendData('');
|
|
226
|
+
await handler(req as any, res as any);
|
|
227
|
+
|
|
228
|
+
const body = res.getBody();
|
|
229
|
+
expect(body.name).toBe('sendEmail');
|
|
230
|
+
expect(body.description).toBe('Send email action');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('CORS Headers', () => {
|
|
235
|
+
it('should include CORS headers on all responses', async () => {
|
|
236
|
+
const req = new MockRequest('GET', '/api/metadata');
|
|
237
|
+
const res = new MockResponse();
|
|
238
|
+
|
|
239
|
+
req.sendData('');
|
|
240
|
+
await handler(req as any, res as any);
|
|
241
|
+
|
|
242
|
+
expect(res.headers['Access-Control-Allow-Origin']).toBe('*');
|
|
243
|
+
expect(res.headers['Access-Control-Allow-Methods']).toBeDefined();
|
|
244
|
+
expect(res.headers['Access-Control-Allow-Headers']).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('Content-Type Header', () => {
|
|
249
|
+
it('should set Content-Type to application/json', async () => {
|
|
250
|
+
const req = new MockRequest('GET', '/api/metadata');
|
|
251
|
+
const res = new MockResponse();
|
|
252
|
+
|
|
253
|
+
req.sendData('');
|
|
254
|
+
await handler(req as any, res as any);
|
|
255
|
+
|
|
256
|
+
expect(res.headers['Content-Type']).toBe('application/json');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { generateOpenAPI } from '../src/openapi';
|
|
2
|
+
import { ObjectQL } from '@objectql/core';
|
|
3
|
+
import { ObjectConfig } from '@objectql/types';
|
|
4
|
+
|
|
5
|
+
describe('OpenAPI Generator', () => {
|
|
6
|
+
let app: ObjectQL;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
app = new ObjectQL({
|
|
10
|
+
datasources: {},
|
|
11
|
+
objects: {
|
|
12
|
+
user: {
|
|
13
|
+
name: 'user',
|
|
14
|
+
label: 'User',
|
|
15
|
+
fields: {
|
|
16
|
+
name: { type: 'text', required: true },
|
|
17
|
+
email: { type: 'text', required: true },
|
|
18
|
+
age: { type: 'number' },
|
|
19
|
+
active: { type: 'boolean' }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
task: {
|
|
23
|
+
name: 'task',
|
|
24
|
+
label: 'Task',
|
|
25
|
+
fields: {
|
|
26
|
+
title: { type: 'text' },
|
|
27
|
+
completed: { type: 'boolean' },
|
|
28
|
+
due_date: { type: 'date' }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
await app.init();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('Basic Structure', () => {
|
|
37
|
+
it('should generate valid OpenAPI structure', () => {
|
|
38
|
+
const spec = generateOpenAPI(app);
|
|
39
|
+
|
|
40
|
+
expect(spec.openapi).toBe('3.0.0');
|
|
41
|
+
expect(spec.info).toBeDefined();
|
|
42
|
+
expect(spec.info.title).toBeDefined();
|
|
43
|
+
expect(spec.info.version).toBeDefined();
|
|
44
|
+
expect(spec.paths).toBeDefined();
|
|
45
|
+
expect(spec.components).toBeDefined();
|
|
46
|
+
expect(spec.components.schemas).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should have openapi version 3.0.0', () => {
|
|
50
|
+
const spec = generateOpenAPI(app);
|
|
51
|
+
expect(spec.openapi).toBe('3.0.0');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should have info section', () => {
|
|
55
|
+
const spec = generateOpenAPI(app);
|
|
56
|
+
expect(spec.info.title).toBeTruthy();
|
|
57
|
+
expect(spec.info.version).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('JSON-RPC Endpoint', () => {
|
|
62
|
+
it('should include JSON-RPC endpoint', () => {
|
|
63
|
+
const spec = generateOpenAPI(app);
|
|
64
|
+
|
|
65
|
+
expect(spec.paths['/api/objectql']).toBeDefined();
|
|
66
|
+
expect(spec.paths['/api/objectql'].post).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should define JSON-RPC operations', () => {
|
|
70
|
+
const spec = generateOpenAPI(app);
|
|
71
|
+
const endpoint = spec.paths['/api/objectql'].post;
|
|
72
|
+
|
|
73
|
+
expect(endpoint.summary).toBeDefined();
|
|
74
|
+
expect(endpoint.description).toBeDefined();
|
|
75
|
+
expect(endpoint.tags).toContain('System');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should define JSON-RPC request body schema', () => {
|
|
79
|
+
const spec = generateOpenAPI(app);
|
|
80
|
+
const endpoint = spec.paths['/api/objectql'].post;
|
|
81
|
+
const schema = endpoint.requestBody.content['application/json'].schema;
|
|
82
|
+
|
|
83
|
+
expect(schema.properties.op).toBeDefined();
|
|
84
|
+
expect(schema.properties.object).toBeDefined();
|
|
85
|
+
expect(schema.properties.args).toBeDefined();
|
|
86
|
+
expect(schema.required).toContain('op');
|
|
87
|
+
expect(schema.required).toContain('object');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should define supported operations', () => {
|
|
91
|
+
const spec = generateOpenAPI(app);
|
|
92
|
+
const endpoint = spec.paths['/api/objectql'].post;
|
|
93
|
+
const schema = endpoint.requestBody.content['application/json'].schema;
|
|
94
|
+
const operations = schema.properties.op.enum;
|
|
95
|
+
|
|
96
|
+
expect(operations).toContain('find');
|
|
97
|
+
expect(operations).toContain('findOne');
|
|
98
|
+
expect(operations).toContain('create');
|
|
99
|
+
expect(operations).toContain('update');
|
|
100
|
+
expect(operations).toContain('delete');
|
|
101
|
+
expect(operations).toContain('count');
|
|
102
|
+
expect(operations).toContain('action');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Schemas Generation', () => {
|
|
107
|
+
it('should generate schema for each object', () => {
|
|
108
|
+
const spec = generateOpenAPI(app);
|
|
109
|
+
|
|
110
|
+
expect(spec.components.schemas.user).toBeDefined();
|
|
111
|
+
expect(spec.components.schemas.task).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should include all fields in schema', () => {
|
|
115
|
+
const spec = generateOpenAPI(app);
|
|
116
|
+
const userSchema = spec.components.schemas.user;
|
|
117
|
+
|
|
118
|
+
expect(userSchema.properties.name).toBeDefined();
|
|
119
|
+
expect(userSchema.properties.email).toBeDefined();
|
|
120
|
+
expect(userSchema.properties.age).toBeDefined();
|
|
121
|
+
expect(userSchema.properties.active).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should map text fields to string type', () => {
|
|
125
|
+
const spec = generateOpenAPI(app);
|
|
126
|
+
const userSchema = spec.components.schemas.user;
|
|
127
|
+
|
|
128
|
+
expect(userSchema.properties.name.type).toBe('string');
|
|
129
|
+
expect(userSchema.properties.email.type).toBe('string');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should map number fields to string type (default mapping)', () => {
|
|
133
|
+
const spec = generateOpenAPI(app);
|
|
134
|
+
const userSchema = spec.components.schemas.user;
|
|
135
|
+
|
|
136
|
+
// Number type maps to string by default in current implementation
|
|
137
|
+
expect(userSchema.properties.age.type).toBe('string');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should map boolean fields to boolean type', () => {
|
|
141
|
+
const spec = generateOpenAPI(app);
|
|
142
|
+
const userSchema = spec.components.schemas.user;
|
|
143
|
+
|
|
144
|
+
expect(userSchema.properties.active.type).toBe('boolean');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should have object type for schemas', () => {
|
|
148
|
+
const spec = generateOpenAPI(app);
|
|
149
|
+
const userSchema = spec.components.schemas.user;
|
|
150
|
+
|
|
151
|
+
expect(userSchema.type).toBe('object');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('REST API Paths', () => {
|
|
156
|
+
it('should generate list endpoint for each object', () => {
|
|
157
|
+
const spec = generateOpenAPI(app);
|
|
158
|
+
|
|
159
|
+
expect(spec.paths['/api/data/user']).toBeDefined();
|
|
160
|
+
expect(spec.paths['/api/data/user'].get).toBeDefined();
|
|
161
|
+
expect(spec.paths['/api/data/task']).toBeDefined();
|
|
162
|
+
expect(spec.paths['/api/data/task'].get).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should generate create endpoint for each object', () => {
|
|
166
|
+
const spec = generateOpenAPI(app);
|
|
167
|
+
|
|
168
|
+
expect(spec.paths['/api/data/user'].post).toBeDefined();
|
|
169
|
+
expect(spec.paths['/api/data/task'].post).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should generate get by id endpoint for each object', () => {
|
|
173
|
+
const spec = generateOpenAPI(app);
|
|
174
|
+
|
|
175
|
+
expect(spec.paths['/api/data/user/{id}']).toBeDefined();
|
|
176
|
+
expect(spec.paths['/api/data/user/{id}'].get).toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should generate update endpoint for each object', () => {
|
|
180
|
+
const spec = generateOpenAPI(app);
|
|
181
|
+
|
|
182
|
+
expect(spec.paths['/api/data/user/{id}'].patch).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should generate delete endpoint for each object', () => {
|
|
186
|
+
const spec = generateOpenAPI(app);
|
|
187
|
+
|
|
188
|
+
expect(spec.paths['/api/data/user/{id}'].delete).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Path Parameters', () => {
|
|
193
|
+
it('should define id parameter for detail endpoints', () => {
|
|
194
|
+
const spec = generateOpenAPI(app);
|
|
195
|
+
const endpoint = spec.paths['/api/data/user/{id}'].get;
|
|
196
|
+
|
|
197
|
+
expect(endpoint.parameters).toBeDefined();
|
|
198
|
+
const idParam = endpoint.parameters.find((p: any) => p.name === 'id');
|
|
199
|
+
expect(idParam).toBeDefined();
|
|
200
|
+
expect(idParam.in).toBe('path');
|
|
201
|
+
expect(idParam.required).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('Complex Field Types', () => {
|
|
206
|
+
it('should handle select fields', async () => {
|
|
207
|
+
const app2 = new ObjectQL({
|
|
208
|
+
datasources: {},
|
|
209
|
+
objects: {
|
|
210
|
+
project: {
|
|
211
|
+
name: 'project',
|
|
212
|
+
fields: {
|
|
213
|
+
status: {
|
|
214
|
+
type: 'select',
|
|
215
|
+
options: [
|
|
216
|
+
{ label: 'Active', value: 'active' },
|
|
217
|
+
{ label: 'Completed', value: 'completed' },
|
|
218
|
+
{ label: 'Archived', value: 'archived' }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
await app2.init();
|
|
226
|
+
|
|
227
|
+
const spec = generateOpenAPI(app2);
|
|
228
|
+
const projectSchema = spec.components.schemas.project;
|
|
229
|
+
|
|
230
|
+
expect(projectSchema.properties.status).toBeDefined();
|
|
231
|
+
expect(projectSchema.properties.status.type).toBe('string');
|
|
232
|
+
// Current implementation doesn't extract enum values
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should handle lookup fields', async () => {
|
|
236
|
+
const app2 = new ObjectQL({
|
|
237
|
+
datasources: {},
|
|
238
|
+
objects: {
|
|
239
|
+
task: {
|
|
240
|
+
name: 'task',
|
|
241
|
+
fields: {
|
|
242
|
+
owner: {
|
|
243
|
+
type: 'lookup',
|
|
244
|
+
reference_to: 'users'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
await app2.init();
|
|
251
|
+
|
|
252
|
+
const spec = generateOpenAPI(app2);
|
|
253
|
+
const taskSchema = spec.components.schemas.task;
|
|
254
|
+
|
|
255
|
+
expect(taskSchema.properties.owner).toBeDefined();
|
|
256
|
+
// Lookup fields typically map to string (ID reference)
|
|
257
|
+
expect(taskSchema.properties.owner.type).toBe('string');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle datetime fields', async () => {
|
|
261
|
+
const app2 = new ObjectQL({
|
|
262
|
+
datasources: {},
|
|
263
|
+
objects: {
|
|
264
|
+
event: {
|
|
265
|
+
name: 'event',
|
|
266
|
+
fields: {
|
|
267
|
+
start_time: { type: 'datetime' }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
await app2.init();
|
|
273
|
+
|
|
274
|
+
const spec = generateOpenAPI(app2);
|
|
275
|
+
const eventSchema = spec.components.schemas.event;
|
|
276
|
+
|
|
277
|
+
expect(eventSchema.properties.start_time).toBeDefined();
|
|
278
|
+
expect(eventSchema.properties.start_time.type).toBe('string');
|
|
279
|
+
// OpenAPI implementation may or may not add format
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('Multiple Objects', () => {
|
|
284
|
+
it('should generate paths for all registered objects', () => {
|
|
285
|
+
const spec = generateOpenAPI(app);
|
|
286
|
+
|
|
287
|
+
const pathKeys = Object.keys(spec.paths);
|
|
288
|
+
const userPaths = pathKeys.filter(p => p.includes('/user'));
|
|
289
|
+
const taskPaths = pathKeys.filter(p => p.includes('/task'));
|
|
290
|
+
|
|
291
|
+
expect(userPaths.length).toBeGreaterThan(0);
|
|
292
|
+
expect(taskPaths.length).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should generate schemas for all registered objects', () => {
|
|
296
|
+
const spec = generateOpenAPI(app);
|
|
297
|
+
|
|
298
|
+
expect(Object.keys(spec.components.schemas)).toContain('user');
|
|
299
|
+
expect(Object.keys(spec.components.schemas)).toContain('task');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Empty App', () => {
|
|
304
|
+
it('should generate valid spec even with no objects', async () => {
|
|
305
|
+
const emptyApp = new ObjectQL({
|
|
306
|
+
datasources: {}
|
|
307
|
+
});
|
|
308
|
+
await emptyApp.init();
|
|
309
|
+
|
|
310
|
+
const spec = generateOpenAPI(emptyApp);
|
|
311
|
+
|
|
312
|
+
expect(spec.openapi).toBe('3.0.0');
|
|
313
|
+
expect(spec.paths).toBeDefined();
|
|
314
|
+
expect(spec.paths['/api/objectql']).toBeDefined();
|
|
315
|
+
expect(spec.components.schemas).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('Response Definitions', () => {
|
|
320
|
+
it('should define 200 responses for GET endpoints', () => {
|
|
321
|
+
const spec = generateOpenAPI(app);
|
|
322
|
+
const endpoint = spec.paths['/api/data/user'].get;
|
|
323
|
+
|
|
324
|
+
expect(endpoint.responses).toBeDefined();
|
|
325
|
+
expect(endpoint.responses['200']).toBeDefined();
|
|
326
|
+
expect(endpoint.responses['200'].description).toBeDefined();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should define responses for POST endpoints', () => {
|
|
330
|
+
const spec = generateOpenAPI(app);
|
|
331
|
+
const endpoint = spec.paths['/api/data/user'].post;
|
|
332
|
+
|
|
333
|
+
expect(endpoint.responses).toBeDefined();
|
|
334
|
+
expect(endpoint.responses['200']).toBeDefined();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Tags', () => {
|
|
339
|
+
it('should tag endpoints by object name', () => {
|
|
340
|
+
const spec = generateOpenAPI(app);
|
|
341
|
+
const endpoint = spec.paths['/api/data/user'].get;
|
|
342
|
+
|
|
343
|
+
expect(endpoint.tags).toBeDefined();
|
|
344
|
+
expect(endpoint.tags).toContain('user');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should have System tag for JSON-RPC endpoint', () => {
|
|
348
|
+
const spec = generateOpenAPI(app);
|
|
349
|
+
const endpoint = spec.paths['/api/objectql'].post;
|
|
350
|
+
|
|
351
|
+
expect(endpoint.tags).toContain('System');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|