navis.js 5.3.2 → 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 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.3.0
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 (Current)
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 ✅ (Current)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navis.js",
3
- "version": "5.3.2",
3
+ "version": "5.4.0",
4
4
  "description": "A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -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 {