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:
Mark Kim
2023-11-23 11:12:36 -05:00
committed by GitHub
parent da01223886
commit 83a3fab4c4
6 changed files with 224 additions and 13 deletions

View File

@@ -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

View File

@@ -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,
},
},
}),
});

View File

@@ -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');

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {