navis.js 5.3.2 → 5.4.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,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
+