Skip to content

Commit 3fd9c84

Browse files
authored
Fix query from AppSync with an empty filter object (#61)
Build the query/mutation's selection set using the `graphql` utilities, instead of building manually. This will avoid any edge cases with the provided arguments. The schema is assumed to be valid: validation should be taken care of ahead of time in `schemaModelValidator` and we also currently do not define the built-in directives (e.g. `@id`, `@relationship`) required to make it a valid schema. - break out lookup functions for type/field definitions - update `outputReference` resolvers - add unit tests - fix query result tests that were sensitive to object key order (use `toEqual` instead of strict JSON string equality) - also check output file size of `.cjs` resolver in test `Case01.02` - fix running test `Case01.04` (use `.cjs` extension) closes #44 * Change `resolveGraphDBQuery` to accept AST param Callers can now pass in GraphQL ASTs directly. Saves a redundant print/parse in `resolveGraphDBQueryFromAppSyncEvent`. For backwards-compatibility, callers *can* continue to pass in a query string. - update `outputReference` resolvers - update tests to pass in ASTs instead of strings see #61 (comment)
1 parent e4b8021 commit 3fd9c84

File tree

8 files changed

+433
-140
lines changed

8 files changed

+433
-140
lines changed

templates/JSResolverOCHTTPSTemplate.js

+90-33
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ express or implied. See the License for the specific language governing
1010
permissions and limitations under the License.
1111
*/
1212

13+
const { astFromValue, buildASTSchema, typeFromAST } = require('graphql');
1314
const gql = require('graphql-tag'); // GraphQL library to parse the GraphQL query
1415

1516
const useCallSubquery = false;
@@ -20,36 +21,50 @@ const schemaDataModelJSON = `INSERT SCHEMA DATA MODEL HERE`;
2021

2122
const schemaDataModel = JSON.parse(schemaDataModelJSON);
2223

24+
const schema = buildASTSchema(schemaDataModel, { assumeValidSDL: true });
25+
2326

2427
function resolveGraphDBQueryFromAppSyncEvent(event) {
25-
let query = '{\n';
26-
let args = '';
27-
28-
Object.keys(event.arguments).forEach(key => {
29-
if (typeof event.arguments[key] === 'object') {
30-
args += key + ': {';
31-
let obj = event.arguments[key];
32-
Object.keys(obj).forEach(key2 => {
33-
args += key2 + ': "' + obj[key2] + '", '
28+
const fieldDef = getFieldDef(event.field);
29+
30+
const args = [];
31+
for (const inputDef of fieldDef.arguments) {
32+
const value = event.arguments[inputDef.name.value];
33+
34+
if (value) {
35+
const inputType = typeFromAST(schema, inputDef.type);
36+
args.push({
37+
kind: 'Argument',
38+
name: { kind: 'Name', value: inputDef.name.value },
39+
value: astFromValue(value, inputType)
3440
});
35-
args = args.substring(0, args.length - 2);
36-
args += '}';
37-
} else {
38-
args += key + ': "' + event.arguments[key] + '", '
39-
args = args.substring(0, args.length - 2);
4041
}
41-
});
42-
43-
if (args != '') {
44-
query += event.field + '(' + args + ') ';
45-
} else {
46-
query += event.field + ' ';
47-
}
48-
49-
query += event.selectionSetGraphQL;
50-
query += '\n}';
51-
52-
const graphQuery = resolveGraphDBQuery(query);
42+
}
43+
44+
const fieldNode = {
45+
kind: 'Field',
46+
name: { kind: 'Name', value: event.field },
47+
arguments: args,
48+
selectionSet:
49+
event.selectionSetGraphQL !== ''
50+
? gql`${event.selectionSetGraphQL}`.definitions[0].selectionSet
51+
: undefined,
52+
};
53+
const obj = {
54+
kind: 'Document',
55+
definitions: [
56+
{
57+
kind: 'OperationDefinition',
58+
operation: 'query',
59+
selectionSet: {
60+
kind: 'SelectionSet',
61+
selections: [fieldNode]
62+
}
63+
}
64+
]
65+
};
66+
67+
const graphQuery = resolveGraphDBQuery(obj);
5368
return graphQuery;
5469
}
5570

@@ -65,6 +80,35 @@ const returnString = []; // openCypher return statements
6580
let parameters = {}; // openCypher query parameters
6681

6782

83+
function getRootTypeDefs() {
84+
return getTypeDefs(['Query', 'Mutation']);
85+
}
86+
87+
88+
function getTypeDefs(typeNameOrNames) {
89+
if (!Array.isArray(typeNameOrNames)) {
90+
typeNameOrNames = [typeNameOrNames];
91+
}
92+
93+
return schemaDataModel.definitions.filter(
94+
def => def.kind === 'ObjectTypeDefinition' && typeNameOrNames.includes(def.name.value)
95+
);
96+
}
97+
98+
99+
function getFieldDef(fieldName) {
100+
const rootTypeDefs = getRootTypeDefs();
101+
102+
for (const rootDef of rootTypeDefs) {
103+
const fieldDef = rootDef.fields.find(def => def.name.value === fieldName);
104+
105+
if (fieldDef) {
106+
return fieldDef;
107+
}
108+
}
109+
}
110+
111+
68112
function getTypeAlias(typeName) {
69113
let alias = null;
70114
schemaDataModel.definitions.forEach(def => {
@@ -1018,14 +1062,27 @@ function resolveGremlinQuery(obj, querySchemaInfo) {
10181062
}
10191063

10201064

1021-
// Function takes the graphql query and output the graphDB query
1022-
function resolveGraphDBQuery(query) {
1065+
function parseQueryInput(queryObjOrStr) {
1066+
// Backwards compatibility
1067+
if (typeof queryObjOrStr === 'string') {
1068+
return gql(queryObjOrStr);
1069+
}
1070+
1071+
// Already in AST format
1072+
return queryObjOrStr;
1073+
}
1074+
1075+
1076+
/**
1077+
* Accepts a GraphQL document or query string and outputs the graphDB query.
1078+
*
1079+
* @param {(Object|string)} queryObjOrStr the GraphQL document containing an operation to resolve
1080+
* @returns {string}
1081+
*/
1082+
function resolveGraphDBQuery(queryObjOrStr) {
10231083
let executeQuery = { query:'', parameters: {}, language: 'opencypher', refactorOutput: null };
1024-
1025-
// create a gql object from the query, gql is GraphQL Query Language
1026-
const obj = gql`
1027-
${query}
1028-
`;
1084+
1085+
const obj = parseQueryInput(queryObjOrStr);
10291086

10301087
const querySchemaInfo = getSchemaQueryInfo(obj.definitions[0].selectionSet.selections[0].name.value);
10311088

test/TestCases/Case01/Case01.04.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { readJSONFile, testResolverQueriesResults } from '../../testLib';
44

55
const casetest = readJSONFile('./test/TestCases/Case01/case.json');
66

7-
await testResolverQueriesResults( './TestCases/Case01/output/output.resolver.graphql.js',
7+
await testResolverQueriesResults( './TestCases/Case01/output/output.resolver.graphql.cjs',
88
'./test/TestCases/Case01/queries',
99
casetest.host,
1010
casetest.port);
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
import { jest } from '@jest/globals';
3+
import { loadResolver } from '../../testLib';
4+
5+
describe('AppSync resolver', () => {
6+
let resolverModule;
7+
8+
beforeAll(async () => {
9+
resolverModule = await loadResolver('./TestCases/Case01/output/output.resolver.graphql.cjs');
10+
});
11+
12+
test('should resolve queries with a filter', () => {
13+
const result = resolve({
14+
field: 'getNodeAirport',
15+
arguments: { filter: { code: 'SEA' } },
16+
selectionSetGraphQL: '{ city }'
17+
});
18+
19+
expect(result).toEqual({
20+
query: 'MATCH (getNodeAirport_Airport:`airport`{code: $getNodeAirport_Airport_code})\n' +
21+
'RETURN {city: getNodeAirport_Airport.`city`} LIMIT 1',
22+
parameters: { getNodeAirport_Airport_code: 'SEA' },
23+
language: 'opencypher',
24+
refactorOutput: null
25+
});
26+
});
27+
28+
test('should resolve queries with an empty filter object', () => {
29+
const result = resolve({
30+
field: 'getNodeAirport',
31+
arguments: { filter: {} },
32+
selectionSetGraphQL: '{ city }'
33+
});
34+
35+
expect(result).toEqual({
36+
query: 'MATCH (getNodeAirport_Airport:`airport`)\n' +
37+
'RETURN {city: getNodeAirport_Airport.`city`} LIMIT 1',
38+
parameters: {},
39+
language: 'opencypher',
40+
refactorOutput: null
41+
});
42+
});
43+
44+
test('should resolve queries without a filter', () => {
45+
const result = resolve({
46+
field: 'getNodeAirport',
47+
arguments: {},
48+
selectionSetGraphQL: '{ city }'
49+
});
50+
51+
expect(result).toEqual({
52+
query: 'MATCH (getNodeAirport_Airport:`airport`)\n' +
53+
'RETURN {city: getNodeAirport_Airport.`city`} LIMIT 1',
54+
parameters: {},
55+
language: 'opencypher',
56+
refactorOutput: null
57+
});
58+
});
59+
60+
function resolve(event) {
61+
return resolverModule.resolveGraphDBQueryFromAppSyncEvent(event);
62+
}
63+
});

test/TestCases/Case01/case.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"--output-no-lambda-zip"],
1010
"host": "<AIR_ROUTES_DB_HOST>",
1111
"port": "<AIR_ROUTES_DB_PORT>",
12-
"testOutputFilesSize": ["output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"],
12+
"testOutputFilesSize": ["output.resolver.graphql.cjs", "output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"],
1313
"testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"]
1414
}

0 commit comments

Comments
 (0)