mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 22:57:59 -04:00
fix: graphql render issue caused by illegal Object.includes() (#6861)
* fix: graphql render issue caused by illegal JSON.includes() * fix: redner gql pane failed * chore: add comment * fix: add tests for gql * fix: add workaround to send graphql variables as object before sending request [n/a] * fix: rm unnecessary bit of code on graphql editor [n/a] --------- Co-authored-by: George He <hexxa@outlook.com> Co-authored-by: Filipe Freire <livrofubia@gmail.com>
This commit is contained in:
@@ -29,6 +29,58 @@ resources:
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
_type: request
|
||||
- _id: req_63e5c8a8d1674bb39ba6df1d0eb452a1
|
||||
parentId: wrk_d8bfe72fd18b42daa55d2ccc08c9eecb
|
||||
modified: 1655289849596
|
||||
created: 1655289826760
|
||||
url: localhost:4010/graphql
|
||||
name: GraphQL request with number
|
||||
description: ""
|
||||
method: POST
|
||||
body:
|
||||
mimeType: application/graphql
|
||||
text: '{"query": "query($inputVar: Int) { echoNum(intVar: $inputVar)}", "variables": "{ \"inputVar\": {{ _.intVar }} }"}'
|
||||
parameters: []
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: application/json
|
||||
id: pair_91da32f74847489ab06da669db0b425f
|
||||
authentication: {}
|
||||
metaSortKey: -1655289826761
|
||||
isPrivate: false
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
_type: request
|
||||
- _id: req_63e5c8a8d1674bb39ba6df1d0eb452a2
|
||||
parentId: wrk_d8bfe72fd18b42daa55d2ccc08c9eecb
|
||||
modified: 1655289849596
|
||||
created: 1655289826760
|
||||
url: localhost:4010/graphql
|
||||
name: GraphQL request with variables
|
||||
description: ""
|
||||
method: POST
|
||||
body:
|
||||
mimeType: application/graphql
|
||||
text: '{"query":"query($vars: VarsInput) {\n\techoVars(vars: $vars) {\n\t\tstringVar \n\t\tintVar\n\t}\n}", "variables":{"vars":{"stringVar":"3","intVar": 3}} }'
|
||||
parameters: []
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: application/json
|
||||
id: pair_91da32f74847489ab06da669db0b425f
|
||||
authentication: {}
|
||||
metaSortKey: -1655289826761
|
||||
isPrivate: false
|
||||
settingStoreCookies: true
|
||||
settingSendCookies: true
|
||||
settingDisableRenderRequestBody: false
|
||||
settingEncodeUrl: true
|
||||
settingRebuildPath: true
|
||||
settingFollowRedirects: global
|
||||
_type: request
|
||||
- _id: wrk_d8bfe72fd18b42daa55d2ccc08c9eecb
|
||||
parentId: null
|
||||
modified: 1655289816535
|
||||
@@ -42,7 +94,8 @@ resources:
|
||||
modified: 1655289816558
|
||||
created: 1655289816558
|
||||
name: Base Environment
|
||||
data: {}
|
||||
data:
|
||||
intVar: 3
|
||||
dataPropertyOrder: null
|
||||
color: null
|
||||
isPrivate: false
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import { GraphQLEnumType, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
|
||||
import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInt, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
|
||||
|
||||
const TypeVars = new GraphQLObjectType({
|
||||
name: 'Vars',
|
||||
fields: () => ({
|
||||
stringVar: { type: GraphQLString },
|
||||
intVar: { type: GraphQLInt },
|
||||
}),
|
||||
});
|
||||
|
||||
const InputVars = new GraphQLInputObjectType({
|
||||
name: 'VarsInput',
|
||||
fields: () => ({
|
||||
stringVar: { type: GraphQLString },
|
||||
intVar: { type: GraphQLInt },
|
||||
}),
|
||||
});
|
||||
|
||||
export const schema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
@@ -28,6 +44,20 @@ export const schema = new GraphQLSchema({
|
||||
}),
|
||||
resolve: () => 3,
|
||||
},
|
||||
echoNum: {
|
||||
type: GraphQLInt,
|
||||
args: {
|
||||
'intVar': { type: GraphQLInt },
|
||||
},
|
||||
resolve: () => 777,
|
||||
},
|
||||
echoVars: {
|
||||
type: TypeVars,
|
||||
args: {
|
||||
'vars': { type: InputVars },
|
||||
},
|
||||
resolve: vars => vars,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke GraphQLjust now').click();
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
@@ -43,6 +44,88 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {
|
||||
await expect(responseBody).toContainText('"bearer": "Gandalf"');
|
||||
});
|
||||
|
||||
test('can render schema and send GraphQL requests with object variables', async ({ app, page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
await page.getByRole('button', { name: 'Create in project' }).click();
|
||||
|
||||
// Copy the collection with the graphql query to clipboard
|
||||
const text = await loadFixture('graphql.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
// Import from clipboard
|
||||
await page.getByRole('menuitemradio', { name: 'Import' }).click();
|
||||
await page.locator('[data-test-id="import-from-clipboard"]').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke GraphQLjust now').click();
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request with variables').press('Enter');
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
|
||||
|
||||
// Assert schema documentation stuff
|
||||
await page.getByRole('button', { name: 'schema' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Show Documentation' }).click();
|
||||
await page.click('a:has-text("Query")');
|
||||
await page.locator('a:has-text("RingBearer")').click();
|
||||
const graphqlExplorer2 = page.locator('.graphql-explorer');
|
||||
await expect(graphqlExplorer2).toContainText('Characters who at any time bore a Ring of Power.');
|
||||
await page.click('text=QueryRingBearer >> button');
|
||||
|
||||
// Send and assert GraphQL request
|
||||
await page.click('[data-testid="request-pane"] >> text=Send');
|
||||
const statusTag2 = page.locator('[data-testid="response-status-tag"]:visible');
|
||||
await expect(statusTag2).toContainText('200 OK');
|
||||
|
||||
const responseBody2 = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
|
||||
has: page.locator('.CodeMirror-activeline'),
|
||||
});
|
||||
await expect(responseBody2).toContainText('"echoVars": null');
|
||||
});
|
||||
|
||||
test('can render numeric environment', async ({ app, page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
await page.getByRole('button', { name: 'Create in project' }).click();
|
||||
|
||||
// Copy the collection with the graphql query to clipboard
|
||||
const text = await loadFixture('graphql.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
// Import from clipboard
|
||||
await page.getByRole('menuitemradio', { name: 'Import' }).click();
|
||||
await page.locator('[data-test-id="import-from-clipboard"]').click();
|
||||
await page.getByRole('button', { name: 'Scan' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByText('CollectionSmoke GraphQLjust now').click();
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request with number').press('Enter');
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
|
||||
|
||||
// Assert schema documentation stuff
|
||||
await page.getByRole('button', { name: 'schema' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Show Documentation' }).click();
|
||||
await page.click('a:has-text("Query")');
|
||||
await page.locator('a:has-text("RingBearer")').click();
|
||||
const graphqlExplorer2 = page.locator('.graphql-explorer');
|
||||
await expect(graphqlExplorer2).toContainText('Characters who at any time bore a Ring of Power.');
|
||||
await page.click('text=QueryRingBearer >> button');
|
||||
|
||||
// Send and assert GraphQL request
|
||||
await page.click('[data-testid="request-pane"] >> text=Send');
|
||||
const statusTag2 = page.locator('[data-testid="response-status-tag"]:visible');
|
||||
await expect(statusTag2).toContainText('200 OK');
|
||||
|
||||
const responseBody2 = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
|
||||
has: page.locator('.CodeMirror-activeline'),
|
||||
});
|
||||
await expect(responseBody2).toContainText('"echoNum": 777');
|
||||
});
|
||||
|
||||
test('can send GraphQL requests after editing and prettifying query', async ({ app, page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
|
||||
@@ -16,6 +16,20 @@ export function getSendRequestCallback() {
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
|
||||
if (renderedRequest && renderedRequest.body?.text && renderedRequest.body?.mimeType === 'application/graphql') {
|
||||
try {
|
||||
const parsedBody = JSON.parse(renderedRequest.body.text);
|
||||
if (typeof parsedBody.variables === 'string') {
|
||||
parsedBody.variables = JSON.parse(parsedBody.variables);
|
||||
renderedRequest.body.text = JSON.stringify(parsedBody, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse GraphQL variables', e);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
|
||||
@@ -340,6 +340,19 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
|
||||
const renderedResult = await tryToInterpolateRequest(req, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult);
|
||||
|
||||
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
|
||||
if (renderedRequest && renderedRequest.body?.text && renderedRequest.body?.mimeType === 'application/graphql') {
|
||||
try {
|
||||
const parsedBody = JSON.parse(renderedRequest.body.text);
|
||||
if (typeof parsedBody.variables === 'string') {
|
||||
parsedBody.variables = JSON.parse(parsedBody.variables);
|
||||
renderedRequest.body.text = JSON.stringify(parsedBody, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse GraphQL variables', e);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SentryError } from '@sentry/utils';
|
||||
|
||||
const STATE_IN_NUN_VAR = 'nunvar';
|
||||
const STATE_IN_NUN_TAG = 'nuntag';
|
||||
const STATE_IN_NUN_COM = 'nuncom';
|
||||
@@ -26,25 +28,41 @@ const NUNJUCKS_CLOSE_STATES: {
|
||||
'#}': STATE_IN_NUN_COM,
|
||||
};
|
||||
|
||||
function ensureStringify(val?: string | Object): string {
|
||||
let defaultVal = '';
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
if (typeof val === 'object') {
|
||||
try {
|
||||
defaultVal = JSON.stringify(val);
|
||||
} catch (error) {
|
||||
SentryError.captureStackTrace(error);
|
||||
}
|
||||
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a JSON string without parsing it as JavaScript.
|
||||
*
|
||||
* Code taken from jsonlint (http://zaa.ch/jsonlint/)
|
||||
*/
|
||||
export const jsonPrettify = (json?: string, indentChars = '\t', replaceUnicode = true) => {
|
||||
if (!json) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!json.includes('{') && !json.includes('[') && !json.includes('"')) {
|
||||
return json;
|
||||
export const jsonPrettify = (json?: string | Object, indentChars = '\t', replaceUnicode = true) => {
|
||||
let prePrettify = ensureStringify(json);
|
||||
if (!prePrettify.includes('{') && !prePrettify.includes('[') && !prePrettify.includes('"')) {
|
||||
return prePrettify;
|
||||
}
|
||||
|
||||
// Convert the unicode. To correctly mimic JSON.stringify(JSON.parse(json), null, indentChars)
|
||||
// we need to convert all escaped unicode characters to proper unicode characters.
|
||||
if (replaceUnicode) {
|
||||
try {
|
||||
json = convertUnicode(json);
|
||||
prePrettify = convertUnicode(prePrettify);
|
||||
} catch (err) {
|
||||
// Just in case (should never happen)
|
||||
console.warn('Prettify failed to handle unicode', err);
|
||||
@@ -52,7 +70,7 @@ export const jsonPrettify = (json?: string, indentChars = '\t', replaceUnicode =
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
const il = json.length;
|
||||
const il = prePrettify.length;
|
||||
const tab = indentChars;
|
||||
let newJson = '';
|
||||
let indentLevel = 0;
|
||||
@@ -62,8 +80,8 @@ export const jsonPrettify = (json?: string, indentChars = '\t', replaceUnicode =
|
||||
let state = STATE_NONE;
|
||||
|
||||
for (; i < il; i += 1) {
|
||||
currentChar = json.charAt(i);
|
||||
nextChar = json.charAt(i + 1) || '';
|
||||
currentChar = prePrettify.charAt(i);
|
||||
nextChar = prePrettify.charAt(i + 1) || '';
|
||||
nextTwo = currentChar + nextChar;
|
||||
|
||||
if (state === STATE_IN_STRING) {
|
||||
|
||||
Reference in New Issue
Block a user