@objectql/server 1.7.3 → 1.8.1

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.
@@ -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
+ });