Skip to content

Commit 5d11f2f

Browse files
committed
All directives should now be properly handled
2 parents 7c5414a + e11e029 commit 5d11f2f

File tree

8 files changed

+2333
-26
lines changed

8 files changed

+2333
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ __pycache__/
55
venv*/
66
.cache/
77
generated-example-server/
8+
node_modules/

graphql-api-generator/generator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ def cmd(args):
4343
# write to file or stdout
4444
if args.output:
4545
with open(args.output, 'w') as out:
46-
out.write(printSchemaWithDirectives(schema))
46+
out.write(print_schema_with_directives(schema))
4747
else:
48-
print(printSchemaWithDirectives(schema))
48+
print(print_schema_with_directives(schema))
4949

5050

5151
def run(schema: GraphQLSchema, config: dict):
52+
5253
# validate
5354
if config.get('validate'):
5455
validate_names(schema, config.get('validate'))

graphql-api-generator/utils/utils.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -247,20 +247,21 @@ def add_input_to_create(schema: GraphQLSchema):
247247
for _type in schema.type_map.values():
248248
if not is_db_schema_defined_type(_type) or is_interface_type(_type):
249249
continue
250-
make += f'\nextend input _InputToCreate{_type.name} {{'
250+
make += f'\nextend input _InputToCreate{_type.name} {{\n'
251251
for field_name, field in _type.fields.items():
252252
if field_name == 'id' or field_name[0] == '_':
253253
continue
254+
254255
inner_field_type = get_named_type(field.type)
255256

256257
if is_enum_or_scalar(inner_field_type):
257-
make += f' {field_name}: {field.type} '
258+
make += f' {field_name}: {field.type} \n'
258259
else:
259260
schema = extend_connect(schema, _type, inner_field_type, field_name)
260261
connect_name = f'_InputToConnect{capitalize(field_name)}Of{_type.name}'
261262
connect = copy_wrapper_structure(schema.type_map[connect_name], field.type)
262-
make += f' {field_name}: {connect} '
263-
make += '}'
263+
make += f' {field_name}: {connect} \n'
264+
make += '}\n'
264265
schema = add_to_schema(schema, make)
265266
return schema
266267

@@ -387,12 +388,12 @@ def add_input_update(schema: GraphQLSchema):
387388
inner_field_type = get_named_type(f_type)
388389

389390
if is_enum_or_scalar(inner_field_type):
390-
make += f'extend input {update_name} {{ {field_name}: {f_type} }} '
391+
make += f'extend input {update_name} {{ {field_name}: {f_type} }} \n'
391392
else:
392393
# add create or connect field
393394
connect_name = f'_InputToConnect{capitalize(field_name)}Of{_type.name}'
394395
connect = copy_wrapper_structure(schema.get_type(connect_name), f_type)
395-
make += f'extend input {update_name} {{ {field_name}: {connect} }} '
396+
make += f'extend input {update_name} {{ {field_name}: {connect} }} \n'
396397
schema = add_to_schema(schema, make)
397398
return schema
398399

@@ -814,8 +815,8 @@ def directive_from_interface(directive, interface_name):
814815
# The only two cases who needs special attention is @requiredForTarget and @uniqueForTarget
815816
if directive_string == 'requiredForTarget':
816817
directive_string = '_requiredForTarget_AccordingToInterface(interface: "' + interface_name + '")'
817-
elif directive_string == 'uniqueForTarget':
818-
directive_string = '_uniqueForTarget_AccordingToInterface(interface: "' + interface_name + '")'
818+
#elif directive_string == 'uniqueForTarget':
819+
# directive_string = '_uniqueForTarget_AccordingToInterface(interface: "' + interface_name + '")'
819820
else:
820821
directive_string += get_directive_arguments(directive)
821822

@@ -859,9 +860,12 @@ def get_directive_arguments(directive):
859860
return output
860861

861862

862-
def get_field_directives(field, field_name, _type):
863+
def get_field_directives(field, field_name, _type, schema):
863864
"""
864865
Get the directives of given field, and return them as string
866+
:param field:
867+
:param field_name:
868+
:param _type:
865869
:param schema:
866870
:return string:
867871
"""
@@ -871,6 +875,20 @@ def get_field_directives(field, field_name, _type):
871875
# Used to make sure we don't add the same directive multiple times to the same field
872876
directives_set = set()
873877

878+
if is_input_type(_type):
879+
# Get the target type instead (unless it is a filter or delete input, then we dont care)
880+
# We also ignore @required directives for inputs
881+
if _type.name[:14] == '_InputToUpdate':
882+
directives_set.add('required')
883+
_type = schema.get_type(_type.name[14:])
884+
885+
elif _type.name[:14] == '_InputToCreate':
886+
_type = schema.get_type(_type.name[14:])
887+
directives_set.add('required')
888+
889+
else:
890+
return ''
891+
874892
# Get all directives directly on field
875893
for directive in field.ast_node.directives:
876894
if not directive.name.value in directives_set:
@@ -892,19 +910,57 @@ def get_field_directives(field, field_name, _type):
892910
return output
893911

894912

895-
def printSchemaWithDirectives(schema):
913+
def get_type_directives(_type, schema):
914+
"""
915+
Get the directives of given type, or target type if create- or update-input
916+
:param type:
917+
:return string:
918+
"""
919+
920+
output = ''
921+
922+
if is_input_type(_type):
923+
# Get the target type instead (unless it is a filter or delete input, then we dont care)
924+
if _type.name[:14] == '_InputToUpdate':
925+
_type = schema.get_type(_type.name[14:])
926+
927+
elif _type.name[:14] == '_InputToCreate':
928+
_type = schema.get_type(_type.name[14:])
929+
else:
930+
return ''
931+
932+
if hasattr(_type, 'ast_node') and _type.ast_node is not None:
933+
# Get directives on type
934+
for directive in _type.ast_node.directives:
935+
output+= ' @' + directive.name.value
936+
output += get_directive_arguments(directive)
937+
938+
return output
939+
940+
941+
def print_schema_with_directives(schema):
896942
"""
897943
Ouputs the given schema as string, in the format we want it.
898944
Types and fields will all contain directives
899945
:param schema:
900946
:return string:
901947
"""
902948

949+
950+
manual_directives = {'key':'directive @key(fields: [String!]!) on OBJECT | INPUT_OBJECT',\
951+
'distinct':'directive @distinct on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION',\
952+
'noloops':'directive @noloops on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION',\
953+
'requiredForTarget':'directive @requiredForTarget on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION',\
954+
'uniqueForTarget':'directive @uniqueForTarget on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION',\
955+
'_requiredForTarget_AccordingToInterface':'directive @_requiredForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION',\
956+
'_uniqueForTarget_AccordingToInterface':'directive @_uniqueForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION'\
957+
}
958+
903959
output = ''
904960

905-
# Start by adding directives
961+
# Add directives
906962
for _dir in schema.directives:
907-
if _dir.ast_node is not None:
963+
if _dir.ast_node is not None and _dir.name not in manual_directives.keys():
908964
# If the directive does not have a proper ast_node
909965
# Then it is an non-user defined directive, and can hence, be skipped
910966
output+= 'directive @' + _dir.name
@@ -920,11 +976,10 @@ def printSchemaWithDirectives(schema):
920976
output+= _location._name_ + ' | '
921977

922978
output = output[:-3] + '\n\n'
923-
print(_dir.name)
924-
925-
# Two special directives that should not exists in the db schema
926-
output += 'directive @_requiredForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION\n\n'
927-
output += 'directive @_uniqueForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION\n\n'
979+
980+
# Manualy handled directives
981+
for _dir in manual_directives.values():
982+
output+= _dir + '\n\n'
928983

929984
# For each type, and output the types sortad after name
930985
for _type in sorted(schema.type_map.values(), key=lambda x : x.name):
@@ -962,11 +1017,8 @@ def printSchemaWithDirectives(schema):
9621017
elif not is_enum_or_scalar(_type):
9631018
# This should be a type, or an interface
9641019

965-
if _type.ast_node is not None:
966-
# Get directives on type
967-
for directive in _type.ast_node.directives:
968-
output+= ' @' + directive.name.value
969-
output += get_directive_arguments(directive)
1020+
# Get directives on type
1021+
output += get_type_directives(_type, schema)
9701022

9711023
output += ' {\n'
9721024

@@ -984,7 +1036,7 @@ def printSchemaWithDirectives(schema):
9841036
output += ': ' + str(field.type)
9851037

9861038
# Add directives
987-
output += get_field_directives(field, field_name, _type)
1039+
output += get_field_directives(field, field_name, _type, schema)
9881040

9891041
output += '\n'
9901042

@@ -993,4 +1045,4 @@ def printSchemaWithDirectives(schema):
9931045
if _type.ast_node is not None:
9941046
output += '\n\n'
9951047

996-
return output
1048+
return output

graphql-server/tests/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
To run the client tests, run:
2+
```bash
3+
$ npm install
4+
$ node client-tests.js
5+
```
6+
7+
The test framework will execute a subset of all possible operations on the server, using randomly generated data.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* This class is meant as basic functionality/integration test to find obvious errors in the generated
3+
* resolvers, schema, or backend code. Basically, it's originally meant as something of a functionality + sanity check.
4+
*
5+
* What it does:
6+
* For each GraphQL type, a random object input object is created. Nested input objects are created to a configurable
7+
* depth. After level 3, only required fields are included in the generated inputs. For every created object, the
8+
* object is retrieved with all possible subfields down to a configurable level based on its ID. Then the all fields
9+
* are updated in the same way as an object is created. Finally, for all types, a list objects is retrieved with all
10+
* possible subfields down to a configurable level. A simple equality filter is added for the ID of the type.
11+
*
12+
*
13+
* What id does NOT do:
14+
* - Does not verify that the returned object matches the expected result. (TODO)
15+
* - Executes no queries over annotated edges (TODO)
16+
* - Executes no mutations to annotate edges (TODO).
17+
* - ... a lot of other things probably.
18+
*/
19+
20+
const { InMemoryCache } = require('apollo-cache-inmemory')
21+
const { ApolloClient } = require('apollo-client');
22+
const { HttpLink } = require('apollo-link-http');
23+
const gql = require('graphql-tag');
24+
const { introspectSchema } = require('graphql-tools');
25+
const fetch = require('node-fetch');
26+
const { isObjectType } = require('graphql');
27+
const tools = require('./generation-tools.js');
28+
29+
/**
30+
* The transaction tests requires the database to be empty prior to testing.
31+
*/
32+
async function transactionTest() {
33+
// connect
34+
let uri = 'http://localhost:4000';
35+
let {client, schema} = await connect(uri);
36+
37+
// attempt to create objects but trigger an exception
38+
39+
// verify that not
40+
}
41+
42+
async function run() {
43+
// connect client to server
44+
let uri = 'http://localhost:4000';
45+
let {client, schema} = await connect(uri);
46+
47+
// iterate type schema
48+
for(let i in schema._typeMap){
49+
let t = schema._typeMap[i];
50+
if(!isObjectType(t) || i == 'Query' || i == 'Mutation' || i.startsWith('_') || i.includes('EdgeFrom')) continue;
51+
52+
// mutations to create
53+
let inputToCreate = tools.makeInputToCreate(t, schema, 6, true);
54+
let createArg = tools.jsonToGraphQL(inputToCreate);
55+
let create = `
56+
mutation {
57+
create${t.name}(data:${createArg}) {
58+
id
59+
}
60+
}
61+
`;
62+
console.log(`Mutations:\tcreate${t.name}`);
63+
const m1 = await client.mutate({ mutation: gql`${create}` });
64+
if(m1.errors){
65+
console.error(m1.errors);
66+
}
67+
68+
// query to get by ID
69+
let subfields = tools.getSubFields(t, 3);
70+
let id = m1.data[`create${t.name}`].id;
71+
let n = `${t.name[0].toLowerCase() + t.name.substr(1)}`;
72+
let get = `
73+
query {
74+
${n}(id: "${id}")
75+
${subfields}
76+
}
77+
`;
78+
console.log(`Query:\t\t${t.name[0].toLowerCase() + t.name.substr(1)} (ID=${id})`);
79+
const q1 = await client.query({ query: gql`${get}` });
80+
if(q1.errors){
81+
console.error(q1.errors);
82+
}
83+
84+
// update fields, create a new object and write over the values
85+
let inputToUpdate = tools.makeInputToCreate(t, schema, 7, true);
86+
let updateArg = tools.jsonToGraphQL(inputToUpdate);
87+
let mutation2 = `
88+
mutation {
89+
update${t.name}(id: "${id}", data:${updateArg}) {
90+
id
91+
}
92+
}
93+
`;
94+
console.log(`Mutation:\tupdate${t.name} (id=${id})`);
95+
const m2 = await client.mutate({mutation: gql`${mutation2}`});
96+
if (m2.errors) {
97+
console.error(m2.errors);
98+
}
99+
100+
// query list of type
101+
subfields = tools.getSubFields(t, 3);
102+
let getList = `
103+
query {
104+
listOf${t.name}s(first: 7, after: "", filter: { id: { _neq: "" } }) {
105+
isEndOfWholeList
106+
totalCount
107+
content
108+
${subfields}
109+
}
110+
}
111+
`;
112+
const q2 = await client.query({ query: gql`${getList}` });
113+
let totalCount = q2.data[`listOf${t.name}s`].totalCount;
114+
console.log(`Query:\t\tlistOf${t.name}s ${totalCount}`);
115+
if(q2.errors){
116+
console.error(q2.errors);
117+
}
118+
}
119+
}
120+
121+
async function connect(uri){
122+
const httpLink = new HttpLink({ uri: uri, fetch });
123+
const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache() });
124+
const schema = await introspectSchema(httpLink); // parse remote schema
125+
return { client: client, schema: schema };
126+
}
127+
128+
run().then(() => {
129+
console.log("Client tests passed.");
130+
let exitAfterClientTests = process.env.EXIT_AFTER_CLIENT_TESTS === 'true';
131+
if(exitAfterClientTests) process.exit(0);
132+
}).catch(reason => {
133+
let exitAfterClientTests = process.env.EXIT_AFTER_CLIENT_TESTS === 'true';
134+
// Not the nicest way to exit, but it works for testing.
135+
console.error(reason);
136+
console.error("Client tests did NOT pass.");
137+
if(exitAfterClientTests) process.exit(1);
138+
});

0 commit comments

Comments
 (0)