@objectql/server 1.7.3 → 1.8.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 +12 -0
- package/LICENSE +21 -118
- package/dist/adapters/graphql.d.ts +17 -0
- package/dist/adapters/graphql.js +460 -0
- package/dist/adapters/graphql.js.map +1 -0
- package/dist/adapters/rest.d.ts +7 -5
- package/dist/adapters/rest.js +51 -14
- package/dist/adapters/rest.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server.js +33 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +6 -4
- package/src/adapters/graphql.ts +523 -0
- package/src/adapters/rest.ts +48 -14
- package/src/index.ts +2 -0
- package/src/server.ts +42 -0
- package/src/types.ts +1 -1
- package/test/graphql.test.ts +439 -0
- package/test/rest-advanced.test.ts +455 -0
- package/test/rest.test.ts +236 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { ObjectQL } from '@objectql/core';
|
|
4
|
+
import { createGraphQLHandler } from '../src/adapters/graphql';
|
|
5
|
+
import { Driver } from '@objectql/types';
|
|
6
|
+
|
|
7
|
+
// Simple Mock Driver
|
|
8
|
+
class MockDriver implements Driver {
|
|
9
|
+
private initialData: Record<string, any[]> = {
|
|
10
|
+
user: [
|
|
11
|
+
{ _id: '1', name: 'Alice', email: 'alice@example.com', age: 30 },
|
|
12
|
+
{ _id: '2', name: 'Bob', email: 'bob@example.com', age: 25 }
|
|
13
|
+
],
|
|
14
|
+
task: [
|
|
15
|
+
{ _id: 'task1', title: 'Task 1', status: 'open', priority: 'high' },
|
|
16
|
+
{ _id: 'task2', title: 'Task 2', status: 'closed', priority: 'low' }
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
private data: Record<string, any[]>;
|
|
20
|
+
private nextId = 3;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
// Create a deep copy of initialData
|
|
24
|
+
this.data = JSON.parse(JSON.stringify(this.initialData));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reset() {
|
|
28
|
+
// Reset data to initial state
|
|
29
|
+
this.data = JSON.parse(JSON.stringify(this.initialData));
|
|
30
|
+
this.nextId = 3;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init() {}
|
|
34
|
+
|
|
35
|
+
async find(objectName: string, query: any) {
|
|
36
|
+
let items = this.data[objectName] || [];
|
|
37
|
+
|
|
38
|
+
// Apply skip and limit if provided
|
|
39
|
+
if (query) {
|
|
40
|
+
if (query.skip) {
|
|
41
|
+
items = items.slice(query.skip);
|
|
42
|
+
}
|
|
43
|
+
if (query.limit) {
|
|
44
|
+
items = items.slice(0, query.limit);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return items;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async findOne(objectName: string, id: string | number, query?: any, options?: any) {
|
|
52
|
+
const items = this.data[objectName] || [];
|
|
53
|
+
if (id !== undefined && id !== null) {
|
|
54
|
+
const found = items.find(item => item._id === String(id));
|
|
55
|
+
return found || null;
|
|
56
|
+
}
|
|
57
|
+
return items[0] || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async create(objectName: string, data: any) {
|
|
61
|
+
const newItem = { _id: String(this.nextId++), ...data };
|
|
62
|
+
if (!this.data[objectName]) {
|
|
63
|
+
this.data[objectName] = [];
|
|
64
|
+
}
|
|
65
|
+
this.data[objectName].push(newItem);
|
|
66
|
+
return newItem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Note: update() returns the updated object to match the behavior of the SQL driver,
|
|
70
|
+
// which returns { id, ...data }. This is different from the affected row count pattern.
|
|
71
|
+
async update(objectName: string, id: string, data: any) {
|
|
72
|
+
const items = this.data[objectName] || [];
|
|
73
|
+
const index = items.findIndex(item => item._id === id);
|
|
74
|
+
if (index >= 0) {
|
|
75
|
+
this.data[objectName][index] = { ...items[index], ...data };
|
|
76
|
+
return this.data[objectName][index];
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delete(objectName: string, id: string) {
|
|
82
|
+
const items = this.data[objectName] || [];
|
|
83
|
+
const index = items.findIndex(item => item._id === id);
|
|
84
|
+
if (index >= 0) {
|
|
85
|
+
this.data[objectName].splice(index, 1);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async count(objectName: string, query: any) {
|
|
92
|
+
return (this.data[objectName] || []).length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async execute(sql: string) {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('GraphQL API Adapter', () => {
|
|
99
|
+
let app: ObjectQL;
|
|
100
|
+
let server: any;
|
|
101
|
+
let handler: any;
|
|
102
|
+
let mockDriver: MockDriver;
|
|
103
|
+
|
|
104
|
+
beforeAll(async () => {
|
|
105
|
+
mockDriver = new MockDriver();
|
|
106
|
+
app = new ObjectQL({
|
|
107
|
+
datasources: {
|
|
108
|
+
default: mockDriver
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Register user object schema
|
|
113
|
+
app.metadata.register('object', {
|
|
114
|
+
type: 'object',
|
|
115
|
+
id: 'user',
|
|
116
|
+
content: {
|
|
117
|
+
name: 'user',
|
|
118
|
+
label: 'User',
|
|
119
|
+
fields: {
|
|
120
|
+
name: { type: 'text', label: 'Name' },
|
|
121
|
+
email: { type: 'email', label: 'Email' },
|
|
122
|
+
age: { type: 'number', label: 'Age' }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Register task object schema
|
|
128
|
+
app.metadata.register('object', {
|
|
129
|
+
type: 'object',
|
|
130
|
+
id: 'task',
|
|
131
|
+
content: {
|
|
132
|
+
name: 'task',
|
|
133
|
+
label: 'Task',
|
|
134
|
+
fields: {
|
|
135
|
+
title: { type: 'text', label: 'Title', required: true },
|
|
136
|
+
status: { type: 'select', label: 'Status', options: ['open', 'closed'] },
|
|
137
|
+
priority: { type: 'select', label: 'Priority', options: ['low', 'medium', 'high'] }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Create handler and server once for all tests
|
|
143
|
+
handler = createGraphQLHandler(app);
|
|
144
|
+
server = createServer(handler);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
// Reset mock data before each test
|
|
149
|
+
mockDriver.reset();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Queries', () => {
|
|
153
|
+
it('should query a single user by ID', async () => {
|
|
154
|
+
const query = `
|
|
155
|
+
query {
|
|
156
|
+
user(id: "1") {
|
|
157
|
+
id
|
|
158
|
+
name
|
|
159
|
+
email
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const response = await request(server)
|
|
165
|
+
.post('/api/graphql')
|
|
166
|
+
.send({ query })
|
|
167
|
+
.set('Accept', 'application/json');
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(200);
|
|
170
|
+
expect(response.body.data).toBeDefined();
|
|
171
|
+
expect(response.body.data.user).toEqual({
|
|
172
|
+
id: '1',
|
|
173
|
+
name: 'Alice',
|
|
174
|
+
email: 'alice@example.com'
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should query list of users', async () => {
|
|
179
|
+
const query = `
|
|
180
|
+
query {
|
|
181
|
+
userList {
|
|
182
|
+
id
|
|
183
|
+
name
|
|
184
|
+
email
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const response = await request(server)
|
|
190
|
+
.post('/api/graphql')
|
|
191
|
+
.send({ query })
|
|
192
|
+
.set('Accept', 'application/json');
|
|
193
|
+
|
|
194
|
+
expect(response.status).toBe(200);
|
|
195
|
+
expect(response.body.data).toBeDefined();
|
|
196
|
+
expect(response.body.data.userList).toHaveLength(2);
|
|
197
|
+
expect(response.body.data.userList[0].name).toBe('Alice');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should support pagination with limit and skip', async () => {
|
|
201
|
+
const query = `
|
|
202
|
+
query {
|
|
203
|
+
userList(limit: 1, skip: 1) {
|
|
204
|
+
id
|
|
205
|
+
name
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
const response = await request(server)
|
|
211
|
+
.post('/api/graphql')
|
|
212
|
+
.send({ query })
|
|
213
|
+
.set('Accept', 'application/json');
|
|
214
|
+
|
|
215
|
+
expect(response.status).toBe(200);
|
|
216
|
+
expect(response.body.data.userList).toHaveLength(1);
|
|
217
|
+
expect(response.body.data.userList[0].name).toBe('Bob');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should support field selection', async () => {
|
|
221
|
+
const query = `
|
|
222
|
+
query {
|
|
223
|
+
userList(fields: ["name"]) {
|
|
224
|
+
id
|
|
225
|
+
name
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
const response = await request(server)
|
|
231
|
+
.post('/api/graphql')
|
|
232
|
+
.send({ query })
|
|
233
|
+
.set('Accept', 'application/json');
|
|
234
|
+
|
|
235
|
+
expect(response.status).toBe(200);
|
|
236
|
+
expect(response.body.data.userList).toBeDefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should query tasks', async () => {
|
|
240
|
+
const query = `
|
|
241
|
+
query {
|
|
242
|
+
taskList {
|
|
243
|
+
id
|
|
244
|
+
title
|
|
245
|
+
status
|
|
246
|
+
priority
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const response = await request(server)
|
|
252
|
+
.post('/api/graphql')
|
|
253
|
+
.send({ query })
|
|
254
|
+
.set('Accept', 'application/json');
|
|
255
|
+
|
|
256
|
+
expect(response.status).toBe(200);
|
|
257
|
+
expect(response.body.data.taskList).toHaveLength(2);
|
|
258
|
+
expect(response.body.data.taskList[0].title).toBe('Task 1');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should support GET requests with query parameter', async () => {
|
|
262
|
+
const query = encodeURIComponent('{ user(id: "1") { id name } }');
|
|
263
|
+
|
|
264
|
+
const response = await request(server)
|
|
265
|
+
.get(`/api/graphql?query=${query}`)
|
|
266
|
+
.set('Accept', 'application/json');
|
|
267
|
+
|
|
268
|
+
expect(response.status).toBe(200);
|
|
269
|
+
expect(response.body.data.user.name).toBe('Alice');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Mutations', () => {
|
|
274
|
+
it('should create a new user', async () => {
|
|
275
|
+
const mutation = `
|
|
276
|
+
mutation {
|
|
277
|
+
createUser(input: { name: "Charlie", email: "charlie@example.com", age: 35 }) {
|
|
278
|
+
id
|
|
279
|
+
name
|
|
280
|
+
email
|
|
281
|
+
age
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
const response = await request(server)
|
|
287
|
+
.post('/api/graphql')
|
|
288
|
+
.send({ query: mutation })
|
|
289
|
+
.set('Accept', 'application/json');
|
|
290
|
+
|
|
291
|
+
expect(response.status).toBe(200);
|
|
292
|
+
expect(response.body.data).toBeDefined();
|
|
293
|
+
expect(response.body.data.createUser.name).toBe('Charlie');
|
|
294
|
+
expect(response.body.data.createUser.email).toBe('charlie@example.com');
|
|
295
|
+
expect(response.body.data.createUser.id).toBeDefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should update an existing user', async () => {
|
|
299
|
+
const mutation = `
|
|
300
|
+
mutation {
|
|
301
|
+
updateUser(id: "1", input: { name: "Alice Updated" }) {
|
|
302
|
+
id
|
|
303
|
+
name
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
const response = await request(server)
|
|
309
|
+
.post('/api/graphql')
|
|
310
|
+
.send({ query: mutation })
|
|
311
|
+
.set('Accept', 'application/json');
|
|
312
|
+
|
|
313
|
+
expect(response.status).toBe(200);
|
|
314
|
+
expect(response.body.data).toBeDefined();
|
|
315
|
+
expect(response.body.data.updateUser.name).toBe('Alice Updated');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should delete a user', async () => {
|
|
319
|
+
const mutation = `
|
|
320
|
+
mutation {
|
|
321
|
+
deleteUser(id: "2") {
|
|
322
|
+
id
|
|
323
|
+
deleted
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
const response = await request(server)
|
|
329
|
+
.post('/api/graphql')
|
|
330
|
+
.send({ query: mutation })
|
|
331
|
+
.set('Accept', 'application/json');
|
|
332
|
+
|
|
333
|
+
expect(response.status).toBe(200);
|
|
334
|
+
expect(response.body.data).toBeDefined();
|
|
335
|
+
expect(response.body.data.deleteUser.id).toBe('2');
|
|
336
|
+
expect(response.body.data.deleteUser.deleted).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should create a task', async () => {
|
|
340
|
+
const mutation = `
|
|
341
|
+
mutation {
|
|
342
|
+
createTask(input: { title: "New Task", status: "open", priority: "medium" }) {
|
|
343
|
+
id
|
|
344
|
+
title
|
|
345
|
+
status
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
const response = await request(server)
|
|
351
|
+
.post('/api/graphql')
|
|
352
|
+
.send({ query: mutation })
|
|
353
|
+
.set('Accept', 'application/json');
|
|
354
|
+
|
|
355
|
+
expect(response.status).toBe(200);
|
|
356
|
+
expect(response.body.data.createTask.title).toBe('New Task');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Error Handling', () => {
|
|
361
|
+
it('should return error for invalid query', async () => {
|
|
362
|
+
const query = `query { invalid }`;
|
|
363
|
+
|
|
364
|
+
const response = await request(server)
|
|
365
|
+
.post('/api/graphql')
|
|
366
|
+
.send({ query })
|
|
367
|
+
.set('Accept', 'application/json');
|
|
368
|
+
|
|
369
|
+
expect(response.status).toBe(200);
|
|
370
|
+
expect(response.body.errors).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should return error when query is missing', async () => {
|
|
374
|
+
const response = await request(server)
|
|
375
|
+
.post('/api/graphql')
|
|
376
|
+
.send({})
|
|
377
|
+
.set('Accept', 'application/json');
|
|
378
|
+
|
|
379
|
+
expect(response.status).toBe(400);
|
|
380
|
+
expect(response.body.errors).toBeDefined();
|
|
381
|
+
expect(response.body.errors[0].message).toContain('query');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should reject non-POST/GET methods', async () => {
|
|
385
|
+
const response = await request(server)
|
|
386
|
+
.put('/api/graphql')
|
|
387
|
+
.send({ query: '{ userList { id } }' })
|
|
388
|
+
.set('Accept', 'application/json');
|
|
389
|
+
|
|
390
|
+
expect(response.status).toBe(405);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('Variables', () => {
|
|
395
|
+
it('should support GraphQL variables', async () => {
|
|
396
|
+
const query = `
|
|
397
|
+
query GetUser($userId: String!) {
|
|
398
|
+
user(id: $userId) {
|
|
399
|
+
id
|
|
400
|
+
name
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
`;
|
|
404
|
+
|
|
405
|
+
const response = await request(server)
|
|
406
|
+
.post('/api/graphql')
|
|
407
|
+
.send({
|
|
408
|
+
query,
|
|
409
|
+
variables: { userId: '1' }
|
|
410
|
+
})
|
|
411
|
+
.set('Accept', 'application/json');
|
|
412
|
+
|
|
413
|
+
expect(response.status).toBe(200);
|
|
414
|
+
expect(response.body.data.user.name).toBe('Alice');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should support variables in GET requests', async () => {
|
|
418
|
+
const query = encodeURIComponent('query GetUser($id: String!) { user(id: $id) { name } }');
|
|
419
|
+
const variables = encodeURIComponent(JSON.stringify({ id: '1' }));
|
|
420
|
+
|
|
421
|
+
const response = await request(server)
|
|
422
|
+
.get(`/api/graphql?query=${query}&variables=${variables}`)
|
|
423
|
+
.set('Accept', 'application/json');
|
|
424
|
+
|
|
425
|
+
expect(response.status).toBe(200);
|
|
426
|
+
expect(response.body.data.user.name).toBe('Alice');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('CORS', () => {
|
|
431
|
+
it('should handle OPTIONS preflight request', async () => {
|
|
432
|
+
const response = await request(server)
|
|
433
|
+
.options('/api/graphql');
|
|
434
|
+
|
|
435
|
+
expect(response.status).toBe(200);
|
|
436
|
+
expect(response.headers['access-control-allow-methods']).toContain('POST');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|