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

feat: replace GraphQL Apollo with GraphQL Yoga #7967

Merged
merged 11 commits into from
May 18, 2022
1,372 changes: 319 additions & 1,053 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"@apollo/client": "3.5.10",
"@apollographql/graphql-playground-html": "1.6.29",
"@graphql-tools/links": "8.2.11",
"@graphql-tools/stitch": "6.2.4",
"@graphql-tools/utils": "6.2.4",
"@graphql-yoga/node": "2.6.0",
"@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.2",
"apollo-server-express": "2.25.2",
"bcryptjs": "2.4.3",
"body-parser": "1.20.0",
"commander": "5.1.0",
Expand All @@ -38,7 +35,6 @@
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.7.0",
"graphql-tag": "2.12.6",
"graphql-upload": "11.0.0",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jwks-rsa": "2.0.5",
Expand All @@ -63,6 +59,7 @@
},
"devDependencies": {
"@actions/core": "1.2.6",
"@apollo/client": "3.6.1",
"@babel/cli": "7.10.0",
"@babel/core": "7.10.0",
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
Expand Down Expand Up @@ -98,7 +95,7 @@
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "4.8.1",
"mongodb-version-list": "1.0.0",
"node-fetch": "3.1.1",
mtrezza marked this conversation as resolved.
Show resolved Hide resolved
"node-fetch": "3.2.4",
"nyc": "15.1.0",
"prettier": "2.0.5",
"semantic-release": "17.4.6",
Expand Down
188 changes: 133 additions & 55 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ describe('ParseGraphQLServer', () => {
});
});

describe('_getServer', () => {
it('should only return new server on schema changes', async () => {
parseGraphQLServer.server = undefined;
const server1 = await parseGraphQLServer._getServer();
const server2 = await parseGraphQLServer._getServer();
expect(server1).toBe(server2);

// Trigger a schema change
const obj = new Parse.Object('SomeClass');
await obj.save();

const server3 = await parseGraphQLServer._getServer();
const server4 = await parseGraphQLServer._getServer();
expect(server3).not.toBe(server2);
expect(server3).toBe(server4);
});
});

describe('_getGraphQLOptions', () => {
const req = {
info: new Object(),
Expand All @@ -106,11 +124,15 @@ describe('ParseGraphQLServer', () => {
};

it("should return schema and context with req's info, config and auth", async () => {
const options = await parseGraphQLServer._getGraphQLOptions(req);
const options = await parseGraphQLServer._getGraphQLOptions();
expect(options.multipart).toEqual({
fileSize: 20971520,
});
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
expect(options.context.info).toEqual(req.info);
expect(options.context.config).toEqual(req.config);
expect(options.context.auth).toEqual(req.auth);
const contextResponse = options.context({ req });
expect(contextResponse.info).toEqual(req.info);
expect(contextResponse.config).toEqual(req.config);
expect(contextResponse.auth).toEqual(req.auth);
});

it('should load GraphQL schema in every call', async () => {
Expand Down Expand Up @@ -467,7 +489,7 @@ describe('ParseGraphQLServer', () => {
}
});

it('should be cors enabled', async () => {
it('should be cors enabled and scope the response within the source origin', async () => {
let checked = false;
const apolloClient = new ApolloClient({
link: new ApolloLink((operation, forward) => {
Expand All @@ -476,7 +498,7 @@ describe('ParseGraphQLServer', () => {
const {
response: { headers },
} = context;
expect(headers.get('access-control-allow-origin')).toEqual('*');
expect(headers.get('access-control-allow-origin')).toEqual('http://example.com');
checked = true;
return response;
});
Expand All @@ -486,7 +508,7 @@ describe('ParseGraphQLServer', () => {
fetch,
headers: {
...headers,
Origin: 'http://someorigin.com',
Origin: 'http://example.com',
},
})
),
Expand All @@ -504,14 +526,25 @@ describe('ParseGraphQLServer', () => {
});

it('should handle Parse headers', async () => {
let checked = false;
const test = {
context: ({ req: { info, config, auth } }) => {
expect(req.info).toBeDefined();
expect(req.config).toBeDefined();
expect(req.auth).toBeDefined();
return {
info,
config,
auth,
};
},
};
const contextSpy = spyOn(test, 'context');
const originalGetGraphQLOptions = parseGraphQLServer._getGraphQLOptions;
parseGraphQLServer._getGraphQLOptions = async req => {
expect(req.info).toBeDefined();
expect(req.config).toBeDefined();
expect(req.auth).toBeDefined();
checked = true;
return await originalGetGraphQLOptions.bind(parseGraphQLServer)(req);
parseGraphQLServer._getGraphQLOptions = async () => {
return {
schema: await parseGraphQLServer.parseGraphQLSchema.load(),
context: test.context,
};
};
const health = (
await apolloClient.query({
Expand All @@ -523,7 +556,7 @@ describe('ParseGraphQLServer', () => {
})
).data.health;
expect(health).toBeTruthy();
expect(checked).toBeTruthy();
expect(contextSpy).toHaveBeenCalledTimes(1);
parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions;
});
});
Expand Down Expand Up @@ -6786,7 +6819,7 @@ describe('ParseGraphQLServer', () => {

expect(queryResult.data.customers.edges.length).toEqual(1);
} catch (e) {
console.log(JSON.stringify(e));
console.error(JSON.stringify(e));
}
});
});
Expand Down Expand Up @@ -9107,15 +9140,15 @@ describe('ParseGraphQLServer', () => {
'operations',
JSON.stringify({
query: `
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
}
}
}
}
`,
`,
variables: {
input: {
upload: null,
Expand Down Expand Up @@ -9176,46 +9209,46 @@ describe('ParseGraphQLServer', () => {
'operations',
JSON.stringify({
query: `
mutation CreateSomeObject(
$fields1: CreateSomeClassFieldsInput
$fields2: CreateSomeClassFieldsInput
$fields3: CreateSomeClassFieldsInput
) {
createSomeClass1: createSomeClass(
input: { fields: $fields1 }
mutation CreateSomeObject(
$fields1: CreateSomeClassFieldsInput
$fields2: CreateSomeClassFieldsInput
$fields3: CreateSomeClassFieldsInput
) {
someClass {
id
someField {
name
url
createSomeClass1: createSomeClass(
input: { fields: $fields1 }
) {
someClass {
id
someField {
name
url
}
}
}
}
createSomeClass2: createSomeClass(
input: { fields: $fields2 }
) {
someClass {
id
someField {
name
url
createSomeClass2: createSomeClass(
input: { fields: $fields2 }
) {
someClass {
id
someField {
name
url
}
}
}
}
createSomeClass3: createSomeClass(
input: { fields: $fields3 }
) {
someClass {
id
someField {
name
url
createSomeClass3: createSomeClass(
input: { fields: $fields3 }
) {
someClass {
id
someField {
name
url
}
}
}
}
}
`,
`,
variables: {
fields1: {
someField: { file: someFieldValue },
Expand Down Expand Up @@ -9344,6 +9377,51 @@ describe('ParseGraphQLServer', () => {
}
});

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

const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
}
}
}
`,
variables: {
input: {
upload: null,
},
},
})
);
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
body.append(
'1',
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
{
filename: 'myFileName.txt',
contentType: 'text/plain',
}
);

const res = await fetch('http://localhost:13377/graphql', {
method: 'POST',
headers,
body,
});

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

it('should support object values', async () => {
try {
const someObjectFieldValue = {
Expand Down
20 changes: 15 additions & 5 deletions src/GraphQL/ParseGraphQLSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ class ParseGraphQLSchema {
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
this.appId = params.appId || requiredParameter('You must provide the appId!');
this.schemaCache = SchemaCache;
this.logCache = {};
}

async load() {
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
const parseClassesArray = await this._getClassesForSchema(parseGraphQLConfig);
const functionNames = await this._getFunctionNames();
const functionNamesString = JSON.stringify(functionNames);
const functionNamesString = functionNames.join();

const parseClasses = parseClassesArray.reduce((acc, clazz) => {
acc[clazz.className] = clazz;
Expand Down Expand Up @@ -331,6 +332,14 @@ class ParseGraphQLSchema {
return this.graphQLSchema;
}

_logOnce(severity, message) {
if (this.logCache[message]) {
return;
}
this.log[severity](message);
this.logCache[message] = true;
}

addGraphQLType(type, throwError = false, ignoreReserved = false, ignoreConnection = false) {
if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
Expand All @@ -341,7 +350,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLTypes.push(type);
Expand All @@ -357,7 +366,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLQueries[fieldName] = field;
Expand All @@ -373,7 +382,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLMutations[fieldName] = field;
Expand Down Expand Up @@ -482,7 +491,8 @@ class ParseGraphQLSchema {
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
return true;
} else {
this.log.warn(
this._logOnce(
'warn',
`Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
);
return false;
Expand Down
Loading