Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Update graphql-yoga to 4.X.X #8718

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
938 changes: 463 additions & 475 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"@graphql-tools/merge": "8.4.1",
"@graphql-tools/schema": "9.0.4",
"@graphql-tools/utils": "8.12.0",
"@graphql-yoga/node": "2.6.0",
"@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.2.0",
"@whatwg-node/fetch": "0.9.9",
"bcryptjs": "2.4.3",
"body-parser": "1.20.2",
"commander": "10.0.1",
Expand All @@ -38,6 +38,7 @@
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.10.0",
"graphql-tag": "2.12.6",
"graphql-yoga": "4.0.4",
"intersect": "1.0.1",
"ip-range-check": "0.2.0",
"jsonwebtoken": "9.0.0",
Expand Down Expand Up @@ -126,11 +127,11 @@
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
"posttest:mongodb": "mongodb-runner stop",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test": "npm run testonly",
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
"start": "node ./bin/parse-server",
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
"prepare": "npm run build",
Expand Down
26 changes: 16 additions & 10 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ describe('ParseGraphQLServer', () => {

it("should return schema and context with req's info, config and auth", async () => {
const options = await parseGraphQLServer._getGraphQLOptions();
expect(options.multipart).toEqual({
fileSize: 20971520,
expect(new options.fetchApi.Body().options).toEqual({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an internal property of our implementation. It is not part of Fetch spec. I'm not sure if you should test that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's covered now, it seems that you fetch system rely on this option. We need to ensure on our side that the option is correctly setup. And in case of a fail, it's just a test we will be able to update it easily :) @ardatan

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whatwg-node/fetch implements Fetch API right? But Fetch API doesn't have Body.options. So that means it is an internal field which is NOT intended to be used by user.

And in case of a fail, it's just a test we will be able to update it easily :)

After the long process in my previous PRs, I don't think it should be that easy :) So I'd not recommend to keep this test like that.

formDataLimits: {
fileSize: 20971520,
},
});
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
const contextResponse = options.context({ req });
Expand Down Expand Up @@ -6833,7 +6835,7 @@ describe('ParseGraphQLServer', () => {

describe('Files Mutations', () => {
describe('Create', () => {
it_only_node_version('<17')('should return File object', async () => {
it('should return File object', async () => {
const clientMutationId = uuidv4();

parseServer = await global.reconfigureServer({
Expand Down Expand Up @@ -9299,7 +9301,7 @@ describe('ParseGraphQLServer', () => {
expect(result6[0].node.name).toEqual('imACountry3');
});

it_only_node_version('<17')('should support files', async () => {
it('should support files', async () => {
try {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
Expand Down Expand Up @@ -9450,7 +9452,8 @@ describe('ParseGraphQLServer', () => {
body: body2,
});
expect(res.status).toEqual(200);
const result2 = JSON.parse(await res.text());
const resText = await res.text();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const resText = await res.text();
const result2= await res.json();

res.json already parses it as JSON.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good catch useless JSON.parse

const result2 = JSON.parse(resText);
expect(result2.data.createSomeClass1.someClass.someField.name).toEqual(
jasmine.stringMatching(/_myFileName.txt$/)
);
Expand Down Expand Up @@ -9508,7 +9511,6 @@ describe('ParseGraphQLServer', () => {
id: result2.data.createSomeClass1.someClass.id,
},
});

expect(typeof getResult.data.someClass.someField).toEqual('object');
expect(getResult.data.someClass.someField.name).toEqual(
result.data.createFile.fileInfo.name
Expand Down Expand Up @@ -9547,9 +9549,14 @@ describe('ParseGraphQLServer', () => {
}
});

it_only_node_version('<17')('should not upload if file is too large', async () => {
it('should not upload if file is too large', async () => {
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';

const options = await parseGraphQLServer._getGraphQLOptions();
expect(new options.fetchApi.Body().options).toEqual({
formDataLimits: {
fileSize: 1024,
},
});
const body = new FormData();
body.append(
'operations',
Expand Down Expand Up @@ -9586,9 +9593,8 @@ describe('ParseGraphQLServer', () => {
headers,
body,
});

const result = JSON.parse(await res.text());
expect(res.status).toEqual(500);
expect(res.status).toEqual(413);
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
});

Expand Down
29 changes: 22 additions & 7 deletions src/GraphQL/ParseGraphQLServer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import corsMiddleware from 'cors';
import { createServer, renderGraphiQL } from '@graphql-yoga/node';
import { createYoga, renderGraphiQL } from 'graphql-yoga';
import { createFetch } from '@whatwg-node/fetch';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { handleParseErrors, handleParseHeaders, handleParseSession } from '../middlewares';
Expand Down Expand Up @@ -31,6 +32,11 @@ class ParseGraphQLServer {

async _getGraphQLOptions() {
try {
const formDataLimits = {
fileSize: this._transformMaxUploadSizeToBytes(
this.parseServer.config.maxUploadSize || '20mb'
),
};
return {
schema: await this.parseGraphQLSchema.load(),
context: ({ req: { info, config, auth } }) => ({
Expand All @@ -39,11 +45,20 @@ class ParseGraphQLServer {
auth,
}),
maskedErrors: false,
multipart: {
fileSize: this._transformMaxUploadSizeToBytes(
this.parseServer.config.maxUploadSize || '20mb'
),
},
// Needed to ensure formDataLimits since it seems to not working
// this is a temporary fix until the issue is resolved
// we need to ask graphql-yoga team
plugins: [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tried without the plugin and just the config on the Yoga docs, but i encountered a bug. The format data options seems to be lost during the request processing so the multipart limit don't work. This is why i needed to add the plugin to enforce the option.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said above, it is not part of Fetch API spec. It is VERY specific to our implementation. I'd strongly recommend to avoid this kind of testing. If something is not working as expected, you can help us to reproduce it on our repo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that the issue is quite specific, @ardatan. I'm not particularly fond of relying on this workaround myself. I invested a considerable amount of time investigating into this issue. I've managed to identify a reproducible case within the Parse Server test suite. However, I'll not be able to dedicate time to reproduce the bug on Yoga repo.

Given that Parse is designed to be a user-friendly Backend as a Service (BaaS), it's crucial to ensure the seamless functionality of all its features. Therefore, we conduct integration tests with our dependencies to guarantee the production readiness and stability of the software.

This is precisely why I reached out to the Guild for assistance. It would be greatly appreciated if you could investigate the bugs within our test suite, as I suspect they might all be linked to the 'fetch ponyfill' library also maintenained by the guild team.

If you find that investigating into Yoga-related bugs within the Parse Server test suite isn't within your scope of interest, please don't hesitate to let me know. I completely understand and respect that. Additionally, I intend to experiment with the new Apollo Server v4. Perhaps within the context of the Parse Server, we'll encounter fewer issues.

It's not an ultimatum; perhaps Yoga is well-suited for certain use cases, but in the context of Parse Server, there appear to be some issues.

I also admire all the work that the guild has done on tools like GraphQL codegen :)

{
onRequestParse: ({ request }) => {
request.options.formDataLimits = formDataLimits;
},
},
],
fetchApi: createFetch({
useNodeFetch: true,
formDataLimits,
}),
};
} catch (e) {
this.log.error(e.stack || (typeof e.toString === 'function' && e.toString()) || e);
Expand All @@ -58,7 +73,7 @@ class ParseGraphQLServer {
return this._server;
}
const options = await this._getGraphQLOptions();
this._server = createServer(options);
this._server = createYoga(options);
return this._server;
}

Expand Down
3 changes: 1 addition & 2 deletions src/GraphQL/loaders/filesMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import logger from '../../logger';

const handleUpload = async (upload, config) => {
const data = Buffer.from(await upload.arrayBuffer());
const data = await upload.buffer();
Copy link

@ardatan ardatan Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if you should rely on an implementation specific buffer method which is not part of Fetch spec. You can create a Node.js buffer with the size information like Buffer.from(arrayBuffer, undefined, upload.size) (it is still weird how arrayBuffer doesn't have the correct size) or consume the stream;

let chunks = [];
for await (const chunk of upload.stream()) {
  if (chunk) { chunks.push(chunk); }
}
const buffer = Buffer.concat(chunks);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using arrayBuffer i got some weird behavior and corrupted file. using buffer worked. the buffer() method seems implemented by @whatwg-node if i remember correctly during my investigation @ardatan.

I got some huge issue migrating ot Yoga V4, it didn't worked easily out of the box. I landed with the current implementation to get file upload working with multi part limits.

Copy link

@ardatan ardatan Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand but .buffer is not intended to be used like that. We cannot guarantee it will be always there because it is not part of Fetch spec. I'd not recommend to use that, and prefer the alternatives I mentioned in my previous comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI @ardatan , i tried the arrayBuffer method. But we got actually have something strange. Some content of the request seems to leak into the file content. It do not happen with .buffer() file.
I'm not sure why.

image

Here a example in our test suite

const fileName = upload.name;
const type = upload.type;

if (!data || !data.length) {
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
}
Expand Down
2 changes: 2 additions & 0 deletions src/GraphQL/loaders/parseClassMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG
const parseFields = await transformTypes('create', fields, {
className,
parseGraphQLSchema,
originalFields: args.fields || {},
req: { config, auth, info },
});

Expand Down Expand Up @@ -190,6 +191,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG
const parseFields = await transformTypes('update', fields, {
className,
parseGraphQLSchema,
originalFields: args.fields || {},
req: { config, auth, info },
});

Expand Down
2 changes: 2 additions & 0 deletions src/GraphQL/loaders/usersMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const load = parseGraphQLSchema => {
const parseFields = await transformTypes('create', fields, {
className: '_User',
parseGraphQLSchema,
originalFields: args.fields || {},
req: { config, auth, info },
});

Expand Down Expand Up @@ -114,6 +115,7 @@ const load = parseGraphQLSchema => {
const parseFields = await transformTypes('create', fields, {
className: '_User',
parseGraphQLSchema,
originalFields: args.fields || {},
req: { config, auth, info },
});

Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL/parseGraphQLUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Parse from 'parse/node';
import { GraphQLYogaError } from '@graphql-yoga/node';
import { GraphQLError } from 'graphql';

export function enforceMasterKeyAccess(auth) {
if (!auth.isMaster) {
Expand All @@ -16,7 +16,7 @@ export function toGraphQLError(error) {
code = Parse.Error.INTERNAL_SERVER_ERROR;
message = 'Internal server error';
}
return new GraphQLYogaError(message, { code });
return new GraphQLError(message, { extensions: { code } });
}

export const extractKeysAndInclude = selectedFields => {
Expand Down
13 changes: 9 additions & 4 deletions src/GraphQL/transformers/mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { fromGlobalId } from 'graphql-relay';
import { handleUpload } from '../loaders/filesMutations';
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
import * as objectsMutations from '../helpers/objectsMutations';
import deepcopy from 'deepcopy';

const transformTypes = async (
inputType: 'create' | 'update',
fields,
{ className, parseGraphQLSchema, req }
{ className, parseGraphQLSchema, req, originalFields }
) => {
const {
classGraphQLCreateType,
Expand Down Expand Up @@ -44,7 +45,9 @@ const transformTypes = async (
fields[field] = transformers.polygon(fields[field]);
break;
case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT:
fields[field] = await transformers.file(fields[field], req);
// fields are a deepcopy, but we can't deepcopy a stream so
// we use the original fields from the graphql request
fields[field] = await transformers.file(originalFields[field], req);
break;
case parseClass.fields[field].type === 'Relation':
fields[field] = await transformers.relation(
Expand Down Expand Up @@ -152,9 +155,10 @@ const transformers = {
nestedObjectsToAdd = (
await Promise.all(
value.createAndAdd.map(async input => {
const parseFields = await transformTypes('create', input, {
const parseFields = await transformTypes('create', deepcopy(input), {
className: targetClass,
parseGraphQLSchema,
originalFields: input || {},
req: { config, auth, info },
});
return objectsMutations.createObject(targetClass, parseFields, config, auth, info);
Expand Down Expand Up @@ -213,9 +217,10 @@ const transformers = {

let nestedObjectToAdd;
if (value.createAndLink) {
const parseFields = await transformTypes('create', value.createAndLink, {
const parseFields = await transformTypes('create', deepcopy(value.createAndLink), {
className: targetClass,
parseGraphQLSchema,
originalFields: value.createAndLink || {},
req: { config, auth, info },
});
nestedObjectToAdd = await objectsMutations.createObject(
Expand Down
Loading