@apiquest/plugin-graphql 1.0.4 → 1.0.5
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/package.json +2 -3
- package/src/index.ts +0 -291
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apiquest/plugin-graphql",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "GraphQL protocol plugin for ApiQuest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,8 +53,7 @@
|
|
|
53
53
|
"basic",
|
|
54
54
|
"oauth2",
|
|
55
55
|
"apikey"
|
|
56
|
-
]
|
|
57
|
-
"strictAuthList": false
|
|
56
|
+
]
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
}
|
package/src/index.ts
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import got, { OptionsOfTextResponseBody, Response, RequestError } from 'got';
|
|
2
|
-
import type { IProtocolPlugin, Request, ExecutionContext, ProtocolResponse, ValidationResult, ValidationError, RuntimeOptions, ILogger } from '@apiquest/types';
|
|
3
|
-
|
|
4
|
-
// Helper functions for string validation
|
|
5
|
-
function isNullOrEmpty(value: string | null | undefined): boolean {
|
|
6
|
-
return value === null || value === undefined || value === '';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function isNullOrWhitespace(value: string | null | undefined): boolean {
|
|
10
|
-
return value === null || value === undefined || value.trim() === '';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const graphqlPlugin: IProtocolPlugin = {
|
|
14
|
-
name: 'GraphQL',
|
|
15
|
-
version: '1.0.0',
|
|
16
|
-
description: 'GraphQL query and mutation support',
|
|
17
|
-
protocols: ['graphql'],
|
|
18
|
-
supportedAuthTypes: ['bearer', 'basic', 'apikey', 'oauth2'],
|
|
19
|
-
strictAuthList: false,
|
|
20
|
-
dataSchema: {
|
|
21
|
-
type: 'object',
|
|
22
|
-
required: ['url'],
|
|
23
|
-
properties: {
|
|
24
|
-
url: {
|
|
25
|
-
type: 'string',
|
|
26
|
-
description: 'GraphQL endpoint URL'
|
|
27
|
-
},
|
|
28
|
-
query: {
|
|
29
|
-
type: 'string',
|
|
30
|
-
description: 'GraphQL query'
|
|
31
|
-
},
|
|
32
|
-
mutation: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
description: 'GraphQL mutation'
|
|
35
|
-
},
|
|
36
|
-
variables: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
description: 'GraphQL variables'
|
|
39
|
-
},
|
|
40
|
-
operationName: {
|
|
41
|
-
type: 'string',
|
|
42
|
-
description: 'Operation name (for multi-operation documents)'
|
|
43
|
-
},
|
|
44
|
-
headers: {
|
|
45
|
-
type: 'object',
|
|
46
|
-
description: 'Custom HTTP headers',
|
|
47
|
-
additionalProperties: { type: 'string' }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
// Options schema for runtime configuration
|
|
53
|
-
optionsSchema: {
|
|
54
|
-
timeout: {
|
|
55
|
-
type: 'number',
|
|
56
|
-
default: 30000,
|
|
57
|
-
description: 'Request timeout in milliseconds'
|
|
58
|
-
},
|
|
59
|
-
validateCertificates: {
|
|
60
|
-
type: 'boolean',
|
|
61
|
-
default: true,
|
|
62
|
-
description: 'Validate SSL/TLS certificates'
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
validate(request: Request, options: RuntimeOptions): ValidationResult {
|
|
67
|
-
const errors: ValidationError[] = [];
|
|
68
|
-
|
|
69
|
-
// Check URL
|
|
70
|
-
if (typeof request.data.url !== 'string' || isNullOrWhitespace(request.data.url)) {
|
|
71
|
-
errors.push({
|
|
72
|
-
message: 'GraphQL endpoint URL is required',
|
|
73
|
-
location: '',
|
|
74
|
-
source: 'protocol'
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Check that either query or mutation is present
|
|
79
|
-
const hasQuery = typeof request.data.query === 'string' && !isNullOrEmpty(request.data.query);
|
|
80
|
-
const hasMutation = typeof request.data.mutation === 'string' && !isNullOrEmpty(request.data.mutation);
|
|
81
|
-
|
|
82
|
-
if (!hasQuery && !hasMutation) {
|
|
83
|
-
errors.push({
|
|
84
|
-
message: 'Either query or mutation is required',
|
|
85
|
-
location: '',
|
|
86
|
-
source: 'protocol'
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (hasQuery && hasMutation) {
|
|
91
|
-
errors.push({
|
|
92
|
-
message: 'Cannot have both query and mutation - use one or the other',
|
|
93
|
-
location: '',
|
|
94
|
-
source: 'protocol'
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (errors.length > 0) {
|
|
99
|
-
return {
|
|
100
|
-
valid: false,
|
|
101
|
-
errors
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { valid: true };
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
async execute(
|
|
109
|
-
request: Request,
|
|
110
|
-
context: ExecutionContext,
|
|
111
|
-
options: RuntimeOptions,
|
|
112
|
-
emitEvent?: (eventName: string, eventData: unknown) => Promise<void>,
|
|
113
|
-
logger?: ILogger
|
|
114
|
-
): Promise<ProtocolResponse> {
|
|
115
|
-
const startTime = Date.now();
|
|
116
|
-
const url = String(request.data.url ?? '');
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
if (isNullOrWhitespace(url)) {
|
|
120
|
-
logger?.error('GraphQL request missing URL');
|
|
121
|
-
throw new Error('GraphQL endpoint URL is required');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Request configuration
|
|
125
|
-
const operation = request.data.query ?? request.data.mutation;
|
|
126
|
-
|
|
127
|
-
const graphqlBody: {
|
|
128
|
-
query: string;
|
|
129
|
-
variables?: Record<string, unknown>;
|
|
130
|
-
operationName?: string;
|
|
131
|
-
} = {
|
|
132
|
-
query: String(operation),
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (request.data.variables !== undefined && request.data.variables !== null) {
|
|
136
|
-
graphqlBody.variables = request.data.variables as Record<string, unknown>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (request.data.operationName !== undefined && request.data.operationName !== null) {
|
|
140
|
-
graphqlBody.operationName = String(request.data.operationName);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Headers
|
|
144
|
-
const headers: Record<string, string> = {
|
|
145
|
-
'Content-Type': 'application/json',
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (typeof request.data.headers === 'object' && request.data.headers !== null) {
|
|
149
|
-
Object.entries(request.data.headers as Record<string, unknown>).forEach(([key, value]) => {
|
|
150
|
-
headers[key] = String(value);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const graphqlOptions: Record<string, unknown> = (options.plugins?.graphql as Record<string, unknown> | null | undefined) ?? {};
|
|
155
|
-
const graphqlTimeout = typeof graphqlOptions.timeout === 'number' ? graphqlOptions.timeout : null;
|
|
156
|
-
const timeout = options.timeout?.request ?? graphqlTimeout ?? 60000;
|
|
157
|
-
const graphqlValidateCerts = typeof graphqlOptions.validateCertificates === 'boolean' ? graphqlOptions.validateCertificates : null;
|
|
158
|
-
const validateCerts = options.ssl?.validateCertificates ?? graphqlValidateCerts ?? true;
|
|
159
|
-
|
|
160
|
-
logger?.debug('GraphQL request options resolved', { timeout, validateCerts });
|
|
161
|
-
|
|
162
|
-
// Cookie handling
|
|
163
|
-
const cookieHeader = context.cookieJar.getCookieHeader(url);
|
|
164
|
-
if (cookieHeader !== null) {
|
|
165
|
-
headers['Cookie'] = cookieHeader;
|
|
166
|
-
logger?.trace('Cookie header applied', { url });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const gotOptions: OptionsOfTextResponseBody = {
|
|
170
|
-
method: 'POST',
|
|
171
|
-
headers: { ...headers },
|
|
172
|
-
json: graphqlBody,
|
|
173
|
-
throwHttpErrors: false,
|
|
174
|
-
timeout: { request: timeout },
|
|
175
|
-
followRedirect: true,
|
|
176
|
-
https: {
|
|
177
|
-
rejectUnauthorized: validateCerts
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
// Dispatch
|
|
182
|
-
logger?.debug('GraphQL request dispatch', { url });
|
|
183
|
-
const response: Response = await got(url, gotOptions);
|
|
184
|
-
const duration = Date.now() - startTime;
|
|
185
|
-
|
|
186
|
-
// Response normalization
|
|
187
|
-
const normalizedHeaders: Record<string, string | string[]> = {};
|
|
188
|
-
if (typeof response.headers === 'object' && response.headers !== null) {
|
|
189
|
-
Object.entries(response.headers).forEach(([key, value]) => {
|
|
190
|
-
if (Array.isArray(value)) {
|
|
191
|
-
normalizedHeaders[key.toLowerCase()] = value.map(item => String(item));
|
|
192
|
-
} else if (value !== undefined && value !== null) {
|
|
193
|
-
normalizedHeaders[key.toLowerCase()] = String(value);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (normalizedHeaders['set-cookie'] !== undefined) {
|
|
199
|
-
context.cookieJar.store(normalizedHeaders['set-cookie'], url);
|
|
200
|
-
logger?.trace('Cookies stored from response', { url });
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let errorMsg: string | undefined = undefined;
|
|
204
|
-
try {
|
|
205
|
-
const responseData = JSON.parse(String(response.body)) as { errors?: Array<{ message: string }> };
|
|
206
|
-
if (responseData?.errors !== undefined && responseData.errors !== null && responseData.errors.length > 0) {
|
|
207
|
-
errorMsg = `GraphQL errors: ${responseData.errors.map((e: { message: string }) => e.message).join(', ')}`;
|
|
208
|
-
}
|
|
209
|
-
} catch (parseError) {
|
|
210
|
-
logger?.trace('GraphQL response body not JSON');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
logger?.debug('GraphQL response received', { status: response.statusCode, duration });
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
status: response.statusCode,
|
|
217
|
-
statusText: (response.statusMessage !== null && response.statusMessage !== undefined && response.statusMessage.length > 0) ? response.statusMessage : '',
|
|
218
|
-
headers: normalizedHeaders,
|
|
219
|
-
body: String(response.body),
|
|
220
|
-
duration,
|
|
221
|
-
error: errorMsg
|
|
222
|
-
};
|
|
223
|
-
} catch (err) {
|
|
224
|
-
const duration = Date.now() - startTime;
|
|
225
|
-
const error = err as RequestError;
|
|
226
|
-
|
|
227
|
-
if (error instanceof RequestError) {
|
|
228
|
-
if (error.response !== undefined) {
|
|
229
|
-
const normalizedHeaders: Record<string, string | string[]> = {};
|
|
230
|
-
if (typeof error.response.headers === 'object' && error.response.headers !== null) {
|
|
231
|
-
Object.entries(error.response.headers).forEach(([key, value]) => {
|
|
232
|
-
if (Array.isArray(value)) {
|
|
233
|
-
normalizedHeaders[key.toLowerCase()] = value.map(item => String(item));
|
|
234
|
-
} else if (value !== undefined && value !== null) {
|
|
235
|
-
normalizedHeaders[key.toLowerCase()] = String(value);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (normalizedHeaders['set-cookie'] !== undefined) {
|
|
241
|
-
context.cookieJar.store(normalizedHeaders['set-cookie'], url);
|
|
242
|
-
logger?.trace('Cookies stored from error response', { url });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
let errorResponseMsg: string | undefined = undefined;
|
|
246
|
-
try {
|
|
247
|
-
const responseData = JSON.parse(String(error.response.body)) as { errors?: Array<{ message: string }> };
|
|
248
|
-
if (responseData?.errors !== undefined && responseData.errors !== null && responseData.errors.length > 0) {
|
|
249
|
-
errorResponseMsg = `GraphQL errors: ${responseData.errors.map((e: { message: string }) => e.message).join(', ')}`;
|
|
250
|
-
}
|
|
251
|
-
} catch (parseError) {
|
|
252
|
-
logger?.trace('GraphQL error response body not JSON');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
logger?.debug('GraphQL error response received', { status: error.response.statusCode, duration });
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
status: error.response.statusCode,
|
|
259
|
-
statusText: (error.response.statusMessage !== null && error.response.statusMessage !== undefined && error.response.statusMessage.length > 0) ? error.response.statusMessage : '',
|
|
260
|
-
headers: normalizedHeaders,
|
|
261
|
-
body: String(error.response.body),
|
|
262
|
-
duration,
|
|
263
|
-
error: errorResponseMsg
|
|
264
|
-
};
|
|
265
|
-
} else {
|
|
266
|
-
logger?.warn('GraphQL network error', { message: error.message, duration });
|
|
267
|
-
return {
|
|
268
|
-
status: 0,
|
|
269
|
-
statusText: 'Network Error',
|
|
270
|
-
headers: {},
|
|
271
|
-
body: '',
|
|
272
|
-
duration,
|
|
273
|
-
error: !isNullOrEmpty(error.message) ? error.message : 'Network request failed'
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
logger?.error('GraphQL unexpected error', { error: err instanceof Error ? err.message : String(err), duration });
|
|
279
|
-
return {
|
|
280
|
-
status: 0,
|
|
281
|
-
statusText: 'Error',
|
|
282
|
-
headers: {},
|
|
283
|
-
body: '',
|
|
284
|
-
duration,
|
|
285
|
-
error: err instanceof Error ? err.message : String(err)
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
export default graphqlPlugin;
|