navis.js 5.3.1 → 5.4.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/README.md +146 -5
- package/examples/graphql-demo.js +130 -0
- package/package.json +1 -1
- package/src/graphql/graphql-server.js +362 -0
- package/src/graphql/resolver.js +200 -0
- package/src/graphql/schema.js +188 -0
- package/src/index.js +27 -0
- package/types/index.d.ts +130 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js.
|
|
4
4
|
|
|
5
5
|
**Author:** Syed Imran Ali
|
|
6
|
-
**Version:** 5.
|
|
6
|
+
**Version:** 5.4.0
|
|
7
7
|
**License:** MIT
|
|
8
8
|
|
|
9
9
|
## Philosophy
|
|
@@ -173,12 +173,18 @@ navis metrics
|
|
|
173
173
|
- ✅ **Server-Sent Events** - One-way real-time streaming
|
|
174
174
|
- ✅ **Database integration** - Connection pooling for PostgreSQL, MySQL, MongoDB
|
|
175
175
|
|
|
176
|
-
### v5.3
|
|
177
|
-
|
|
176
|
+
### v5.3 ✅
|
|
178
177
|
- ✅ **TypeScript support** - Full type definitions for all features
|
|
179
178
|
- ✅ **Type-safe API** - Complete IntelliSense and type checking
|
|
180
179
|
- ✅ **TypeScript examples** - Ready-to-use TypeScript examples
|
|
181
180
|
|
|
181
|
+
### v5.4 (Current)
|
|
182
|
+
- ✅ **GraphQL support** - Lightweight GraphQL server implementation
|
|
183
|
+
- ✅ **GraphQL queries & mutations** - Full query and mutation support
|
|
184
|
+
- ✅ **GraphQL resolvers** - Flexible resolver system with utilities
|
|
185
|
+
- ✅ **GraphQL schema builder** - Schema definition helpers
|
|
186
|
+
- ✅ **GraphQL middleware** - Easy integration with Navis.js routes
|
|
187
|
+
|
|
182
188
|
## API Reference
|
|
183
189
|
|
|
184
190
|
### NavisApp
|
|
@@ -267,6 +273,79 @@ response.success(res, { data: 'value' }, 200);
|
|
|
267
273
|
response.error(res, 'Error message', 500);
|
|
268
274
|
```
|
|
269
275
|
|
|
276
|
+
### GraphQL Support (v5.4)
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
const {
|
|
280
|
+
NavisApp,
|
|
281
|
+
graphql,
|
|
282
|
+
createGraphQLServer,
|
|
283
|
+
createResolver,
|
|
284
|
+
createSchema,
|
|
285
|
+
combineResolvers,
|
|
286
|
+
} = require('navis.js');
|
|
287
|
+
|
|
288
|
+
// Basic GraphQL setup
|
|
289
|
+
const resolvers = {
|
|
290
|
+
Query: {
|
|
291
|
+
users: createResolver(async (variables, context) => {
|
|
292
|
+
return [{ id: '1', name: 'Alice' }];
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
Mutation: {
|
|
296
|
+
createUser: createResolver(async (variables, context) => {
|
|
297
|
+
const { name, email } = variables;
|
|
298
|
+
return { id: '2', name, email };
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
app.use(graphql({
|
|
304
|
+
path: '/graphql',
|
|
305
|
+
resolvers,
|
|
306
|
+
context: (req) => ({ userId: req.headers['x-user-id'] }),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// Advanced: Custom GraphQL server
|
|
310
|
+
const server = createGraphQLServer({
|
|
311
|
+
resolvers,
|
|
312
|
+
context: async (req) => ({ user: await getUser(req) }),
|
|
313
|
+
formatError: (error) => ({ message: error.message, code: 'CUSTOM_ERROR' }),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
app.use(server.handler({ path: '/graphql', enableGET: true }));
|
|
317
|
+
|
|
318
|
+
// Schema builder
|
|
319
|
+
const schema = createSchema();
|
|
320
|
+
schema
|
|
321
|
+
.type('User', { id: 'ID!', name: 'String!', email: 'String!' })
|
|
322
|
+
.query('users', { type: '[User!]!' })
|
|
323
|
+
.mutation('createUser', { args: { name: 'String!', email: 'String!' }, type: 'User!' });
|
|
324
|
+
const schemaString = schema.build();
|
|
325
|
+
|
|
326
|
+
// Resolver utilities
|
|
327
|
+
const userResolver = createResolver(
|
|
328
|
+
async (variables, context) => { /* resolver logic */ },
|
|
329
|
+
{
|
|
330
|
+
validate: async (vars) => ({ valid: true }),
|
|
331
|
+
authorize: async (ctx) => true,
|
|
332
|
+
cache: { get: async (k) => null, set: async (k, v) => {}, key: (v, c) => 'key' },
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Combine multiple resolvers
|
|
337
|
+
const allResolvers = combineResolvers(userResolvers, postResolvers, commentResolvers);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**GraphQL Features:**
|
|
341
|
+
- Query and mutation support
|
|
342
|
+
- Resolver utilities (validation, authorization, caching)
|
|
343
|
+
- Schema builder for type definitions
|
|
344
|
+
- Context injection for request-specific data
|
|
345
|
+
- Error formatting and handling
|
|
346
|
+
- GET and POST request support
|
|
347
|
+
- TypeScript support included
|
|
348
|
+
|
|
270
349
|
### Observability (v3)
|
|
271
350
|
|
|
272
351
|
```javascript
|
|
@@ -356,6 +435,65 @@ exports.handler = async (event, context) => {
|
|
|
356
435
|
};
|
|
357
436
|
```
|
|
358
437
|
|
|
438
|
+
### GraphQL Support (v5.4)
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
const { NavisApp, graphql, createResolver } = require('navis.js');
|
|
442
|
+
|
|
443
|
+
const app = new NavisApp();
|
|
444
|
+
|
|
445
|
+
// Define resolvers
|
|
446
|
+
const resolvers = {
|
|
447
|
+
Query: {
|
|
448
|
+
// Get all users
|
|
449
|
+
users: createResolver(async (variables, context) => {
|
|
450
|
+
return [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }];
|
|
451
|
+
}),
|
|
452
|
+
|
|
453
|
+
// Get user by ID
|
|
454
|
+
user: createResolver(async (variables, context) => {
|
|
455
|
+
const { id } = variables;
|
|
456
|
+
return { id, name: 'Alice', email: 'alice@example.com' };
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
Mutation: {
|
|
461
|
+
// Create user
|
|
462
|
+
createUser: createResolver(async (variables, context) => {
|
|
463
|
+
const { name, email } = variables;
|
|
464
|
+
return { id: '3', name, email };
|
|
465
|
+
}),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Add GraphQL middleware
|
|
470
|
+
app.use(graphql({
|
|
471
|
+
path: '/graphql',
|
|
472
|
+
resolvers,
|
|
473
|
+
context: (req) => ({
|
|
474
|
+
userId: req.headers['x-user-id'] || null,
|
|
475
|
+
}),
|
|
476
|
+
}));
|
|
477
|
+
|
|
478
|
+
app.listen(3000);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**GraphQL Query Example:**
|
|
482
|
+
```bash
|
|
483
|
+
curl -X POST http://localhost:3000/graphql \
|
|
484
|
+
-H "Content-Type: application/json" \
|
|
485
|
+
-d '{"query": "query { users { id name } }"}'
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**GraphQL Mutation Example:**
|
|
489
|
+
```bash
|
|
490
|
+
curl -X POST http://localhost:3000/graphql \
|
|
491
|
+
-H "Content-Type: application/json" \
|
|
492
|
+
-d '{"query": "mutation { createUser(name: \"Charlie\", email: \"charlie@example.com\") { id name email } }"}'
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
See `examples/graphql-demo.js` for a complete GraphQL example.
|
|
496
|
+
|
|
359
497
|
## Examples
|
|
360
498
|
|
|
361
499
|
See the `examples/` directory:
|
|
@@ -372,6 +510,7 @@ See the `examples/` directory:
|
|
|
372
510
|
- `v5-features-demo.js` - v5 features demonstration (caching, CORS, security, compression, health checks, etc.)
|
|
373
511
|
- `v5.1-features-demo.js` - v5.1 features demonstration (Swagger, versioning, upload, testing)
|
|
374
512
|
- `v5.2-features-demo.js` - v5.2 features demonstration (WebSocket, SSE, database)
|
|
513
|
+
- `graphql-demo.js` - GraphQL server example with queries and mutations (v5.4)
|
|
375
514
|
- `service-client-demo.js` - ServiceClient usage example
|
|
376
515
|
|
|
377
516
|
## Roadmap
|
|
@@ -397,13 +536,15 @@ Developer experience: OpenAPI/Swagger, API versioning, file upload, testing util
|
|
|
397
536
|
### v5.2 ✅
|
|
398
537
|
Real-time features: WebSocket, Server-Sent Events, database integration
|
|
399
538
|
|
|
400
|
-
### v5.3 ✅
|
|
539
|
+
### v5.3 ✅
|
|
401
540
|
TypeScript support: Full type definitions, type-safe API, IntelliSense
|
|
402
541
|
|
|
542
|
+
### v5.4 ✅ (Current)
|
|
543
|
+
GraphQL support: Lightweight GraphQL server, queries, mutations, resolvers, schema builder
|
|
544
|
+
|
|
403
545
|
## What's Next?
|
|
404
546
|
|
|
405
547
|
Future versions may include:
|
|
406
|
-
- GraphQL support
|
|
407
548
|
- gRPC integration
|
|
408
549
|
- Advanced caching strategies
|
|
409
550
|
- More database adapters
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Demo - Navis.js
|
|
3
|
+
* Demonstrates GraphQL query and mutation support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { NavisApp, graphql, createSchema, createResolver, response } = require('../src/index');
|
|
7
|
+
|
|
8
|
+
const app = new NavisApp();
|
|
9
|
+
|
|
10
|
+
// Sample data store
|
|
11
|
+
const users = [
|
|
12
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com', age: 30 },
|
|
13
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com', age: 25 },
|
|
14
|
+
{ id: '3', name: 'Charlie', email: 'charlie@example.com', age: 35 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const posts = [
|
|
18
|
+
{ id: '1', title: 'First Post', content: 'Content 1', authorId: '1' },
|
|
19
|
+
{ id: '2', title: 'Second Post', content: 'Content 2', authorId: '2' },
|
|
20
|
+
{ id: '3', title: 'Third Post', content: 'Content 3', authorId: '1' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Define resolvers
|
|
24
|
+
const resolvers = {
|
|
25
|
+
Query: {
|
|
26
|
+
// Get all users
|
|
27
|
+
users: createResolver(async (variables, context) => {
|
|
28
|
+
return users;
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
// Get user by ID
|
|
32
|
+
user: createResolver(async (variables, context) => {
|
|
33
|
+
const { id } = variables;
|
|
34
|
+
const user = users.find(u => u.id === id);
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new Error(`User with id ${id} not found`);
|
|
37
|
+
}
|
|
38
|
+
return user;
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
// Get all posts
|
|
42
|
+
posts: createResolver(async (variables, context) => {
|
|
43
|
+
return posts;
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
// Get posts by author
|
|
47
|
+
postsByAuthor: createResolver(async (variables, context) => {
|
|
48
|
+
const { authorId } = variables;
|
|
49
|
+
return posts.filter(p => p.authorId === authorId);
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
Mutation: {
|
|
54
|
+
// Create user
|
|
55
|
+
createUser: createResolver(async (variables, context) => {
|
|
56
|
+
const { name, email, age } = variables;
|
|
57
|
+
const newUser = {
|
|
58
|
+
id: String(users.length + 1),
|
|
59
|
+
name,
|
|
60
|
+
email,
|
|
61
|
+
age: age || 0,
|
|
62
|
+
};
|
|
63
|
+
users.push(newUser);
|
|
64
|
+
return newUser;
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
// Update user
|
|
68
|
+
updateUser: createResolver(async (variables, context) => {
|
|
69
|
+
const { id, name, email, age } = variables;
|
|
70
|
+
const user = users.find(u => u.id === id);
|
|
71
|
+
if (!user) {
|
|
72
|
+
throw new Error(`User with id ${id} not found`);
|
|
73
|
+
}
|
|
74
|
+
if (name) user.name = name;
|
|
75
|
+
if (email) user.email = email;
|
|
76
|
+
if (age !== undefined) user.age = age;
|
|
77
|
+
return user;
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
// Delete user
|
|
81
|
+
deleteUser: createResolver(async (variables, context) => {
|
|
82
|
+
const { id } = variables;
|
|
83
|
+
const index = users.findIndex(u => u.id === id);
|
|
84
|
+
if (index === -1) {
|
|
85
|
+
throw new Error(`User with id ${id} not found`);
|
|
86
|
+
}
|
|
87
|
+
const deleted = users.splice(index, 1)[0];
|
|
88
|
+
return deleted;
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Add GraphQL middleware
|
|
94
|
+
app.use(graphql({
|
|
95
|
+
path: '/graphql',
|
|
96
|
+
resolvers,
|
|
97
|
+
context: (req) => {
|
|
98
|
+
// Add custom context (e.g., authentication info)
|
|
99
|
+
return {
|
|
100
|
+
userId: req.headers['x-user-id'] || null,
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// Health check endpoint
|
|
107
|
+
app.get('/health', (req, res) => {
|
|
108
|
+
response.success(res, { status: 'ok', graphql: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Start server
|
|
112
|
+
const PORT = process.env.PORT || 3000;
|
|
113
|
+
app.listen(PORT, () => {
|
|
114
|
+
console.log(`🚀 Navis.js GraphQL server running on http://localhost:${PORT}`);
|
|
115
|
+
console.log(`📊 GraphQL endpoint: http://localhost:${PORT}/graphql`);
|
|
116
|
+
console.log('\n📝 Example queries:');
|
|
117
|
+
console.log('\n1. Get all users:');
|
|
118
|
+
console.log(`curl -X POST http://localhost:${PORT}/graphql \\`);
|
|
119
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
120
|
+
console.log(` -d '{"query": "query { users { id name email age } }"}'`);
|
|
121
|
+
console.log('\n2. Get user by ID:');
|
|
122
|
+
console.log(`curl -X POST http://localhost:${PORT}/graphql \\`);
|
|
123
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
124
|
+
console.log(` -d '{"query": "query { user(id: \\"1\\") { id name email } }"}'`);
|
|
125
|
+
console.log('\n3. Create user:');
|
|
126
|
+
console.log(`curl -X POST http://localhost:${PORT}/graphql \\`);
|
|
127
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
128
|
+
console.log(` -d '{"query": "mutation { createUser(name: \\"Dave\\", email: \\"dave@example.com\\", age: 28) { id name email } }"}'`);
|
|
129
|
+
});
|
|
130
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Server for Navis.js
|
|
3
|
+
* Lightweight GraphQL implementation for serverless and microservice architectures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class GraphQLError extends Error {
|
|
7
|
+
constructor(message, code = 'INTERNAL_ERROR', extensions = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'GraphQLError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.extensions = extensions;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GraphQL Server
|
|
17
|
+
*/
|
|
18
|
+
class GraphQLServer {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.schema = options.schema || null;
|
|
21
|
+
this.resolvers = options.resolvers || {};
|
|
22
|
+
this.context = options.context || (() => ({}));
|
|
23
|
+
this.formatError = options.formatError || this._defaultFormatError;
|
|
24
|
+
this.introspection = options.introspection !== false; // Enable by default
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create GraphQL handler middleware
|
|
29
|
+
* @param {Object} options - Handler options
|
|
30
|
+
* @returns {Function} - Middleware function
|
|
31
|
+
*/
|
|
32
|
+
handler(options = {}) {
|
|
33
|
+
const path = options.path || '/graphql';
|
|
34
|
+
const method = options.method || 'POST';
|
|
35
|
+
const enableGET = options.enableGET !== false; // Enable GET by default
|
|
36
|
+
|
|
37
|
+
return async (req, res, next) => {
|
|
38
|
+
// Check if this is a GraphQL request
|
|
39
|
+
const isGraphQLPath = req.path === path || req.url?.startsWith(path);
|
|
40
|
+
if (!isGraphQLPath) {
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check method
|
|
45
|
+
if (req.method === method || (enableGET && req.method === 'GET')) {
|
|
46
|
+
try {
|
|
47
|
+
// Parse GraphQL request
|
|
48
|
+
const graphqlRequest = await this._parseRequest(req, enableGET);
|
|
49
|
+
|
|
50
|
+
// Execute GraphQL query
|
|
51
|
+
const result = await this._execute(graphqlRequest, req);
|
|
52
|
+
|
|
53
|
+
// Send response
|
|
54
|
+
this._sendResponse(res, result);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this._sendError(res, error);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
next();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse GraphQL request from HTTP request
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
async _parseRequest(req, enableGET) {
|
|
69
|
+
let query = null;
|
|
70
|
+
let variables = {};
|
|
71
|
+
let operationName = null;
|
|
72
|
+
|
|
73
|
+
if (req.method === 'GET') {
|
|
74
|
+
// Parse from query string
|
|
75
|
+
query = req.query.query || req.query.q;
|
|
76
|
+
variables = req.query.variables ? JSON.parse(req.query.variables) : {};
|
|
77
|
+
operationName = req.query.operationName || null;
|
|
78
|
+
} else {
|
|
79
|
+
// Parse from body
|
|
80
|
+
let body = req.body;
|
|
81
|
+
|
|
82
|
+
// If body is string, parse it
|
|
83
|
+
if (typeof body === 'string') {
|
|
84
|
+
try {
|
|
85
|
+
body = JSON.parse(body);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
throw new GraphQLError('Invalid JSON in request body', 'BAD_REQUEST');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
query = body.query;
|
|
92
|
+
variables = body.variables || {};
|
|
93
|
+
operationName = body.operationName || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!query) {
|
|
97
|
+
throw new GraphQLError('GraphQL query is required', 'BAD_REQUEST');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { query, variables, operationName };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute GraphQL query
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
async _execute(request, httpReq) {
|
|
108
|
+
const { query, variables, operationName } = request;
|
|
109
|
+
|
|
110
|
+
// Build context
|
|
111
|
+
const context = await this._buildContext(httpReq);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Simple query execution (without full GraphQL parser)
|
|
115
|
+
// This is a lightweight implementation
|
|
116
|
+
const result = await this._executeQuery(query, variables, operationName, context);
|
|
117
|
+
return { data: result, errors: null };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
data: null,
|
|
121
|
+
errors: [this.formatError(error)],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Execute GraphQL query (simplified implementation)
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
async _executeQuery(query, variables, operationName, context) {
|
|
131
|
+
// Parse operation type and name
|
|
132
|
+
const operation = this._parseOperation(query, operationName);
|
|
133
|
+
|
|
134
|
+
if (!operation) {
|
|
135
|
+
throw new GraphQLError('Invalid GraphQL query', 'BAD_REQUEST');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Execute based on operation type
|
|
139
|
+
if (operation.type === 'query') {
|
|
140
|
+
return await this._executeQueryOperation(operation, variables, context);
|
|
141
|
+
} else if (operation.type === 'mutation') {
|
|
142
|
+
return await this._executeMutationOperation(operation, variables, context);
|
|
143
|
+
} else {
|
|
144
|
+
throw new GraphQLError(`Unsupported operation type: ${operation.type}`, 'BAD_REQUEST');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse GraphQL operation (simplified)
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
_parseOperation(query, operationName) {
|
|
153
|
+
// Simple regex-based parsing (for lightweight implementation)
|
|
154
|
+
// In production, use a proper GraphQL parser like graphql-js
|
|
155
|
+
|
|
156
|
+
const queryMatch = query.match(/^\s*(query|mutation|subscription)\s+(\w+)?/i);
|
|
157
|
+
if (!queryMatch) {
|
|
158
|
+
// Try to find operation by name
|
|
159
|
+
if (operationName) {
|
|
160
|
+
const namedMatch = query.match(new RegExp(`(query|mutation|subscription)\\s+${operationName}`, 'i'));
|
|
161
|
+
if (namedMatch) {
|
|
162
|
+
return {
|
|
163
|
+
type: namedMatch[1].toLowerCase(),
|
|
164
|
+
name: operationName,
|
|
165
|
+
query,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: queryMatch[1].toLowerCase(),
|
|
174
|
+
name: queryMatch[2] || operationName || 'default',
|
|
175
|
+
query,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Execute query operation
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
async _executeQueryOperation(operation, variables, context) {
|
|
184
|
+
const { name, query } = operation;
|
|
185
|
+
|
|
186
|
+
// Get resolver for this query
|
|
187
|
+
const resolver = this.resolvers.Query?.[name] || this.resolvers[name];
|
|
188
|
+
|
|
189
|
+
if (!resolver) {
|
|
190
|
+
throw new GraphQLError(`Resolver not found for query: ${name}`, 'RESOLVER_NOT_FOUND');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Extract field selections (simplified)
|
|
194
|
+
const fields = this._extractFields(query);
|
|
195
|
+
|
|
196
|
+
// Execute resolver
|
|
197
|
+
const result = await this._executeResolver(resolver, variables, context, fields);
|
|
198
|
+
|
|
199
|
+
return { [name]: result };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute mutation operation
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
async _executeMutationOperation(operation, variables, context) {
|
|
207
|
+
const { name, query } = operation;
|
|
208
|
+
|
|
209
|
+
// Get resolver for this mutation
|
|
210
|
+
const resolver = this.resolvers.Mutation?.[name] || this.resolvers[name];
|
|
211
|
+
|
|
212
|
+
if (!resolver) {
|
|
213
|
+
throw new GraphQLError(`Resolver not found for mutation: ${name}`, 'RESOLVER_NOT_FOUND');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Extract field selections (simplified)
|
|
217
|
+
const fields = this._extractFields(query);
|
|
218
|
+
|
|
219
|
+
// Execute resolver
|
|
220
|
+
const result = await this._executeResolver(resolver, variables, context, fields);
|
|
221
|
+
|
|
222
|
+
return { [name]: result };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Execute resolver function
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
async _executeResolver(resolver, variables, context, fields) {
|
|
230
|
+
if (typeof resolver === 'function') {
|
|
231
|
+
return await resolver(variables, context, fields);
|
|
232
|
+
} else if (typeof resolver === 'object' && resolver.resolve) {
|
|
233
|
+
return await resolver.resolve(variables, context, fields);
|
|
234
|
+
} else {
|
|
235
|
+
throw new GraphQLError('Invalid resolver', 'INVALID_RESOLVER');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract fields from GraphQL query (simplified)
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
_extractFields(query) {
|
|
244
|
+
// Simple field extraction (for lightweight implementation)
|
|
245
|
+
// In production, use proper GraphQL AST parser
|
|
246
|
+
const fieldMatch = query.match(/\{\s*([^}]+)\s*\}/);
|
|
247
|
+
if (fieldMatch) {
|
|
248
|
+
return fieldMatch[1]
|
|
249
|
+
.split(/\s+/)
|
|
250
|
+
.filter(f => f && !f.startsWith('('))
|
|
251
|
+
.map(f => f.replace(/[,\n\r]/g, '').trim())
|
|
252
|
+
.filter(f => f);
|
|
253
|
+
}
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build context for GraphQL execution
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
async _buildContext(httpReq) {
|
|
262
|
+
const baseContext = {
|
|
263
|
+
req: httpReq,
|
|
264
|
+
headers: httpReq.headers || {},
|
|
265
|
+
query: httpReq.query || {},
|
|
266
|
+
params: httpReq.params || {},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (typeof this.context === 'function') {
|
|
270
|
+
const customContext = await this.context(httpReq);
|
|
271
|
+
return { ...baseContext, ...customContext };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return baseContext;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Send GraphQL response
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
_sendResponse(res, result) {
|
|
282
|
+
res.statusCode = 200;
|
|
283
|
+
res.setHeader('Content-Type', 'application/json');
|
|
284
|
+
|
|
285
|
+
if (res.end) {
|
|
286
|
+
res.end(JSON.stringify(result));
|
|
287
|
+
} else {
|
|
288
|
+
res.body = result;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Send GraphQL error response
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_sendError(res, error) {
|
|
297
|
+
const formatted = this.formatError(error);
|
|
298
|
+
|
|
299
|
+
res.statusCode = error.code === 'BAD_REQUEST' ? 400 :
|
|
300
|
+
error.code === 'UNAUTHORIZED' ? 401 :
|
|
301
|
+
error.code === 'FORBIDDEN' ? 403 :
|
|
302
|
+
error.code === 'NOT_FOUND' ? 404 : 500;
|
|
303
|
+
res.setHeader('Content-Type', 'application/json');
|
|
304
|
+
|
|
305
|
+
const response = {
|
|
306
|
+
data: null,
|
|
307
|
+
errors: [formatted],
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (res.end) {
|
|
311
|
+
res.end(JSON.stringify(response));
|
|
312
|
+
} else {
|
|
313
|
+
res.body = response;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Default error formatter
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
_defaultFormatError(error) {
|
|
322
|
+
if (error instanceof GraphQLError) {
|
|
323
|
+
return {
|
|
324
|
+
message: error.message,
|
|
325
|
+
code: error.code,
|
|
326
|
+
extensions: error.extensions,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
message: error.message || 'Internal server error',
|
|
332
|
+
code: 'INTERNAL_ERROR',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create GraphQL server instance
|
|
339
|
+
* @param {Object} options - Server options
|
|
340
|
+
* @returns {GraphQLServer} - GraphQL server instance
|
|
341
|
+
*/
|
|
342
|
+
function createGraphQLServer(options = {}) {
|
|
343
|
+
return new GraphQLServer(options);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* GraphQL handler middleware factory
|
|
348
|
+
* @param {Object} options - Handler options
|
|
349
|
+
* @returns {Function} - Middleware function
|
|
350
|
+
*/
|
|
351
|
+
function graphql(options = {}) {
|
|
352
|
+
const server = new GraphQLServer(options);
|
|
353
|
+
return server.handler();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
GraphQLServer,
|
|
358
|
+
GraphQLError,
|
|
359
|
+
createGraphQLServer,
|
|
360
|
+
graphql,
|
|
361
|
+
};
|
|
362
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Resolver Utilities
|
|
3
|
+
* Helper functions for building GraphQL resolvers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a resolver function
|
|
8
|
+
*/
|
|
9
|
+
function createResolver(resolverFn, options = {}) {
|
|
10
|
+
const {
|
|
11
|
+
validate = null,
|
|
12
|
+
authorize = null,
|
|
13
|
+
cache = null,
|
|
14
|
+
errorHandler = null,
|
|
15
|
+
} = options;
|
|
16
|
+
|
|
17
|
+
return async (variables, context, fields) => {
|
|
18
|
+
try {
|
|
19
|
+
// Authorization check
|
|
20
|
+
if (authorize) {
|
|
21
|
+
const authorized = await authorize(context);
|
|
22
|
+
if (!authorized) {
|
|
23
|
+
throw new Error('Unauthorized');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Validation
|
|
28
|
+
if (validate) {
|
|
29
|
+
const validationResult = await validate(variables);
|
|
30
|
+
if (!validationResult.valid) {
|
|
31
|
+
throw new Error(`Validation failed: ${validationResult.errors.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Cache check
|
|
36
|
+
if (cache && cache.get) {
|
|
37
|
+
const cacheKey = cache.key ? cache.key(variables, context) : null;
|
|
38
|
+
if (cacheKey) {
|
|
39
|
+
const cached = await cache.get(cacheKey);
|
|
40
|
+
if (cached !== null && cached !== undefined) {
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Execute resolver
|
|
47
|
+
const result = await resolverFn(variables, context, fields);
|
|
48
|
+
|
|
49
|
+
// Cache result
|
|
50
|
+
if (cache && cache.set && cache.key) {
|
|
51
|
+
const cacheKey = cache.key(variables, context);
|
|
52
|
+
await cache.set(cacheKey, result, cache.ttl || 3600);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (errorHandler) {
|
|
58
|
+
return await errorHandler(error, variables, context);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a field resolver
|
|
67
|
+
*/
|
|
68
|
+
function fieldResolver(fieldName, resolverFn) {
|
|
69
|
+
return {
|
|
70
|
+
[fieldName]: resolverFn,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Combine multiple resolvers
|
|
76
|
+
*/
|
|
77
|
+
function combineResolvers(...resolvers) {
|
|
78
|
+
const combined = {
|
|
79
|
+
Query: {},
|
|
80
|
+
Mutation: {},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
resolvers.forEach(resolver => {
|
|
84
|
+
if (resolver.Query) {
|
|
85
|
+
Object.assign(combined.Query, resolver.Query);
|
|
86
|
+
}
|
|
87
|
+
if (resolver.Mutation) {
|
|
88
|
+
Object.assign(combined.Mutation, resolver.Mutation);
|
|
89
|
+
}
|
|
90
|
+
// Also support flat resolver structure
|
|
91
|
+
Object.keys(resolver).forEach(key => {
|
|
92
|
+
if (key !== 'Query' && key !== 'Mutation') {
|
|
93
|
+
combined.Query[key] = resolver[key];
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return combined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create async resolver with retry logic
|
|
103
|
+
*/
|
|
104
|
+
function createAsyncResolver(resolverFn, options = {}) {
|
|
105
|
+
const {
|
|
106
|
+
maxRetries = 3,
|
|
107
|
+
retryDelay = 1000,
|
|
108
|
+
retryCondition = null,
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
return async (variables, context, fields) => {
|
|
112
|
+
let lastError;
|
|
113
|
+
|
|
114
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
115
|
+
try {
|
|
116
|
+
return await resolverFn(variables, context, fields);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
lastError = error;
|
|
119
|
+
|
|
120
|
+
// Check if we should retry
|
|
121
|
+
if (attempt < maxRetries) {
|
|
122
|
+
if (retryCondition && !retryCondition(error)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Wait before retry
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw lastError;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create batch resolver (for N+1 query problem)
|
|
138
|
+
*/
|
|
139
|
+
function createBatchResolver(resolverFn, options = {}) {
|
|
140
|
+
const {
|
|
141
|
+
batchKey = (variables) => JSON.stringify(variables),
|
|
142
|
+
batchSize = 100,
|
|
143
|
+
waitTime = 10,
|
|
144
|
+
} = options;
|
|
145
|
+
|
|
146
|
+
let batch = [];
|
|
147
|
+
let batchTimer = null;
|
|
148
|
+
|
|
149
|
+
const processBatch = async () => {
|
|
150
|
+
if (batch.length === 0) return;
|
|
151
|
+
|
|
152
|
+
const currentBatch = batch.splice(0, batchSize);
|
|
153
|
+
batchTimer = null;
|
|
154
|
+
|
|
155
|
+
// Group by batch key
|
|
156
|
+
const groups = {};
|
|
157
|
+
currentBatch.forEach(({ variables, context, fields, resolve, reject }) => {
|
|
158
|
+
const key = batchKey(variables);
|
|
159
|
+
if (!groups[key]) {
|
|
160
|
+
groups[key] = [];
|
|
161
|
+
}
|
|
162
|
+
groups[key].push({ variables, context, fields, resolve, reject });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Execute each group
|
|
166
|
+
Object.keys(groups).forEach(async key => {
|
|
167
|
+
const group = groups[key];
|
|
168
|
+
try {
|
|
169
|
+
const result = await resolverFn(group[0].variables, group[0].context, group[0].fields);
|
|
170
|
+
group.forEach(({ resolve }) => resolve(result));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
group.forEach(({ reject }) => reject(error));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return async (variables, context, fields) => {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
batch.push({ variables, context, fields, resolve, reject });
|
|
180
|
+
|
|
181
|
+
if (!batchTimer) {
|
|
182
|
+
batchTimer = setTimeout(processBatch, waitTime);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (batch.length >= batchSize) {
|
|
186
|
+
clearTimeout(batchTimer);
|
|
187
|
+
processBatch();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
createResolver,
|
|
195
|
+
fieldResolver,
|
|
196
|
+
combineResolvers,
|
|
197
|
+
createAsyncResolver,
|
|
198
|
+
createBatchResolver,
|
|
199
|
+
};
|
|
200
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL Schema Utilities
|
|
3
|
+
* Lightweight schema definition helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GraphQL Schema Builder
|
|
8
|
+
*/
|
|
9
|
+
class GraphQLSchema {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.types = {};
|
|
12
|
+
this.queries = {};
|
|
13
|
+
this.mutations = {};
|
|
14
|
+
this.subscriptions = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Define a type
|
|
19
|
+
*/
|
|
20
|
+
type(name, definition) {
|
|
21
|
+
this.types[name] = definition;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Define a query
|
|
27
|
+
*/
|
|
28
|
+
query(name, definition) {
|
|
29
|
+
this.queries[name] = definition;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Define a mutation
|
|
35
|
+
*/
|
|
36
|
+
mutation(name, definition) {
|
|
37
|
+
this.mutations[name] = definition;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build schema string (GraphQL SDL format)
|
|
43
|
+
*/
|
|
44
|
+
build() {
|
|
45
|
+
let schema = '';
|
|
46
|
+
|
|
47
|
+
// Build type definitions
|
|
48
|
+
Object.keys(this.types).forEach(name => {
|
|
49
|
+
schema += this._buildType(name, this.types[name]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Build Query type
|
|
53
|
+
if (Object.keys(this.queries).length > 0) {
|
|
54
|
+
schema += '\ntype Query {\n';
|
|
55
|
+
Object.keys(this.queries).forEach(name => {
|
|
56
|
+
schema += ` ${name}${this._buildFieldDefinition(this.queries[name])}\n`;
|
|
57
|
+
});
|
|
58
|
+
schema += '}\n';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build Mutation type
|
|
62
|
+
if (Object.keys(this.mutations).length > 0) {
|
|
63
|
+
schema += '\ntype Mutation {\n';
|
|
64
|
+
Object.keys(this.mutations).forEach(name => {
|
|
65
|
+
schema += ` ${name}${this._buildFieldDefinition(this.mutations[name])}\n`;
|
|
66
|
+
});
|
|
67
|
+
schema += '}\n';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return schema;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build type definition
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_buildType(name, definition) {
|
|
78
|
+
if (typeof definition === 'string') {
|
|
79
|
+
return `type ${name} ${definition}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let typeDef = `type ${name} {\n`;
|
|
83
|
+
if (typeof definition === 'object') {
|
|
84
|
+
Object.keys(definition).forEach(field => {
|
|
85
|
+
typeDef += ` ${field}: ${definition[field]}\n`;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
typeDef += '}\n';
|
|
89
|
+
return typeDef;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build field definition
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
_buildFieldDefinition(definition) {
|
|
97
|
+
if (typeof definition === 'string') {
|
|
98
|
+
return `: ${definition}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof definition === 'object' && definition.type) {
|
|
102
|
+
let def = '(';
|
|
103
|
+
if (definition.args && Object.keys(definition.args).length > 0) {
|
|
104
|
+
const args = Object.keys(definition.args).map(arg => {
|
|
105
|
+
return `${arg}: ${definition.args[arg]}`;
|
|
106
|
+
}).join(', ');
|
|
107
|
+
def += args;
|
|
108
|
+
}
|
|
109
|
+
def += `): ${definition.type}`;
|
|
110
|
+
return def;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ': String';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create schema builder
|
|
119
|
+
*/
|
|
120
|
+
function createSchema() {
|
|
121
|
+
return new GraphQLSchema();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Define GraphQL type
|
|
126
|
+
*/
|
|
127
|
+
function type(name, definition) {
|
|
128
|
+
const schema = new GraphQLSchema();
|
|
129
|
+
return schema.type(name, definition);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Common GraphQL scalar types
|
|
134
|
+
*/
|
|
135
|
+
const scalars = {
|
|
136
|
+
String: 'String',
|
|
137
|
+
Int: 'Int',
|
|
138
|
+
Float: 'Float',
|
|
139
|
+
Boolean: 'Boolean',
|
|
140
|
+
ID: 'ID',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Common GraphQL type helpers
|
|
145
|
+
*/
|
|
146
|
+
const types = {
|
|
147
|
+
/**
|
|
148
|
+
* Create input type definition
|
|
149
|
+
*/
|
|
150
|
+
input(name, fields) {
|
|
151
|
+
let input = `input ${name} {\n`;
|
|
152
|
+
Object.keys(fields).forEach(field => {
|
|
153
|
+
input += ` ${field}: ${fields[field]}\n`;
|
|
154
|
+
});
|
|
155
|
+
input += '}';
|
|
156
|
+
return input;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create list type
|
|
161
|
+
*/
|
|
162
|
+
list(type) {
|
|
163
|
+
return `[${type}]`;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create non-null type
|
|
168
|
+
*/
|
|
169
|
+
required(type) {
|
|
170
|
+
return `${type}!`;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create list of non-null items
|
|
175
|
+
*/
|
|
176
|
+
requiredList(type) {
|
|
177
|
+
return `[${type}!]`;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
GraphQLSchema,
|
|
183
|
+
createSchema,
|
|
184
|
+
type,
|
|
185
|
+
scalars,
|
|
186
|
+
types,
|
|
187
|
+
};
|
|
188
|
+
|
package/src/index.js
CHANGED
|
@@ -68,6 +68,17 @@ const WebSocketServer = require('./websocket/websocket-server');
|
|
|
68
68
|
const { SSEServer, createSSEServer, sse } = require('./sse/server-sent-events');
|
|
69
69
|
const { DatabasePool, createPool } = require('./db/db-pool');
|
|
70
70
|
|
|
71
|
+
// v5.4: GraphQL Support
|
|
72
|
+
const { GraphQLServer, GraphQLError, createGraphQLServer, graphql } = require('./graphql/graphql-server');
|
|
73
|
+
const { GraphQLSchema, createSchema, type, scalars, types } = require('./graphql/schema');
|
|
74
|
+
const {
|
|
75
|
+
createResolver,
|
|
76
|
+
fieldResolver,
|
|
77
|
+
combineResolvers,
|
|
78
|
+
createAsyncResolver,
|
|
79
|
+
createBatchResolver,
|
|
80
|
+
} = require('./graphql/resolver');
|
|
81
|
+
|
|
71
82
|
module.exports = {
|
|
72
83
|
// Core
|
|
73
84
|
NavisApp,
|
|
@@ -151,6 +162,22 @@ module.exports = {
|
|
|
151
162
|
DatabasePool,
|
|
152
163
|
createPool,
|
|
153
164
|
|
|
165
|
+
// v5.4: GraphQL Support
|
|
166
|
+
GraphQLServer,
|
|
167
|
+
GraphQLError,
|
|
168
|
+
createGraphQLServer,
|
|
169
|
+
graphql,
|
|
170
|
+
GraphQLSchema,
|
|
171
|
+
createSchema,
|
|
172
|
+
type,
|
|
173
|
+
scalars,
|
|
174
|
+
types,
|
|
175
|
+
createResolver,
|
|
176
|
+
fieldResolver,
|
|
177
|
+
combineResolvers,
|
|
178
|
+
createAsyncResolver,
|
|
179
|
+
createBatchResolver,
|
|
180
|
+
|
|
154
181
|
// Utilities
|
|
155
182
|
response: {
|
|
156
183
|
success,
|
package/types/index.d.ts
CHANGED
|
@@ -656,6 +656,123 @@ export const DatabasePool: {
|
|
|
656
656
|
new (options?: DatabasePoolOptions): DatabasePool;
|
|
657
657
|
};
|
|
658
658
|
|
|
659
|
+
// ============================================
|
|
660
|
+
// GraphQL Types (v5.4)
|
|
661
|
+
// ============================================
|
|
662
|
+
|
|
663
|
+
export interface GraphQLRequest {
|
|
664
|
+
query: string;
|
|
665
|
+
variables?: Record<string, any>;
|
|
666
|
+
operationName?: string;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export interface GraphQLResponse {
|
|
670
|
+
data?: any;
|
|
671
|
+
errors?: GraphQLError[];
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export interface GraphQLError {
|
|
675
|
+
message: string;
|
|
676
|
+
code?: string;
|
|
677
|
+
extensions?: Record<string, any>;
|
|
678
|
+
locations?: Array<{ line: number; column: number }>;
|
|
679
|
+
path?: Array<string | number>;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export interface GraphQLContext {
|
|
683
|
+
req: NavisRequest;
|
|
684
|
+
headers: Record<string, string>;
|
|
685
|
+
query: Record<string, string>;
|
|
686
|
+
params: Record<string, string>;
|
|
687
|
+
[key: string]: any;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export type GraphQLResolver = (
|
|
691
|
+
variables: Record<string, any>,
|
|
692
|
+
context: GraphQLContext,
|
|
693
|
+
fields?: string[]
|
|
694
|
+
) => Promise<any> | any;
|
|
695
|
+
|
|
696
|
+
export interface GraphQLResolverOptions {
|
|
697
|
+
validate?: (variables: Record<string, any>) => Promise<{ valid: boolean; errors?: string[] }>;
|
|
698
|
+
authorize?: (context: GraphQLContext) => Promise<boolean>;
|
|
699
|
+
cache?: {
|
|
700
|
+
get: (key: string) => Promise<any>;
|
|
701
|
+
set: (key: string, value: any, ttl?: number) => Promise<void>;
|
|
702
|
+
key?: (variables: Record<string, any>, context: GraphQLContext) => string;
|
|
703
|
+
ttl?: number;
|
|
704
|
+
};
|
|
705
|
+
errorHandler?: (error: Error, variables: Record<string, any>, context: GraphQLContext) => Promise<any>;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export interface GraphQLServerOptions {
|
|
709
|
+
schema?: any;
|
|
710
|
+
resolvers?: {
|
|
711
|
+
Query?: Record<string, GraphQLResolver | { resolve: GraphQLResolver }>;
|
|
712
|
+
Mutation?: Record<string, GraphQLResolver | { resolve: GraphQLResolver }>;
|
|
713
|
+
[key: string]: any;
|
|
714
|
+
};
|
|
715
|
+
context?: (req: NavisRequest) => Promise<Record<string, any>> | Record<string, any>;
|
|
716
|
+
formatError?: (error: Error) => GraphQLError;
|
|
717
|
+
introspection?: boolean;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export interface GraphQLHandlerOptions {
|
|
721
|
+
path?: string;
|
|
722
|
+
method?: string;
|
|
723
|
+
enableGET?: boolean;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export interface GraphQLSchema {
|
|
727
|
+
type(name: string, definition: any): GraphQLSchema;
|
|
728
|
+
query(name: string, definition: any): GraphQLSchema;
|
|
729
|
+
mutation(name: string, definition: any): GraphQLSchema;
|
|
730
|
+
build(): string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export interface GraphQLTypes {
|
|
734
|
+
input(name: string, fields: Record<string, string>): string;
|
|
735
|
+
list(type: string): string;
|
|
736
|
+
required(type: string): string;
|
|
737
|
+
requiredList(type: string): string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export interface GraphQLScalars {
|
|
741
|
+
String: string;
|
|
742
|
+
Int: string;
|
|
743
|
+
Float: string;
|
|
744
|
+
Boolean: string;
|
|
745
|
+
ID: string;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface GraphQLAsyncResolverOptions {
|
|
749
|
+
maxRetries?: number;
|
|
750
|
+
retryDelay?: number;
|
|
751
|
+
retryCondition?: (error: Error) => boolean;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export interface GraphQLBatchResolverOptions {
|
|
755
|
+
batchKey?: (variables: Record<string, any>) => string;
|
|
756
|
+
batchSize?: number;
|
|
757
|
+
waitTime?: number;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export const GraphQLServer: {
|
|
761
|
+
new (options?: GraphQLServerOptions): GraphQLServer;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
export interface GraphQLServer {
|
|
765
|
+
handler(options?: GraphQLHandlerOptions): Middleware;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export const GraphQLSchema: {
|
|
769
|
+
new (): GraphQLSchema;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
export const GraphQLError: {
|
|
773
|
+
new (message: string, code?: string, extensions?: Record<string, any>): Error;
|
|
774
|
+
};
|
|
775
|
+
|
|
659
776
|
// Functions
|
|
660
777
|
export function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
661
778
|
export function shouldRetryHttpStatus(statusCode: number): boolean;
|
|
@@ -686,6 +803,19 @@ export function gracefulShutdown(server: any, options?: GracefulShutdownOptions)
|
|
|
686
803
|
export function getPool(): ServiceClientPool;
|
|
687
804
|
export function createLazyInit(initFn: () => Promise<any>, options?: LazyInitOptions): LazyInit;
|
|
688
805
|
export function coldStartTracker(): Middleware;
|
|
806
|
+
export function createGraphQLServer(options?: GraphQLServerOptions): GraphQLServer;
|
|
807
|
+
export function graphql(options?: GraphQLServerOptions): Middleware;
|
|
808
|
+
export function createSchema(): GraphQLSchema;
|
|
809
|
+
export function type(name: string, definition: any): GraphQLSchema;
|
|
810
|
+
export function createResolver(resolverFn: GraphQLResolver, options?: GraphQLResolverOptions): GraphQLResolver;
|
|
811
|
+
export function fieldResolver(fieldName: string, resolverFn: GraphQLResolver): Record<string, GraphQLResolver>;
|
|
812
|
+
export function combineResolvers(...resolvers: any[]): any;
|
|
813
|
+
export function createAsyncResolver(resolverFn: GraphQLResolver, options?: GraphQLAsyncResolverOptions): GraphQLResolver;
|
|
814
|
+
export function createBatchResolver(resolverFn: GraphQLResolver, options?: GraphQLBatchResolverOptions): GraphQLResolver;
|
|
815
|
+
|
|
816
|
+
// GraphQL Constants
|
|
817
|
+
export const scalars: GraphQLScalars;
|
|
818
|
+
export const types: GraphQLTypes;
|
|
689
819
|
|
|
690
820
|
// Error Classes
|
|
691
821
|
export class ValidationError extends Error {
|