Skip to content

Commit 49ddd90

Browse files
authored
Get Edge by ID and Update Edges #35 (#93)
* Working on filtering filtering for edge types, detected a bug I will push to master separatly * Working on the filters * Added aliasing for getFilters, and fixed the last bits for filtering * Reverted a local change that made it in by mistake * Implemented edge updates * Annotations should now be working proberly * Corrected getEdge out-inbound * Removed some left over debug code * Support for getting single edges by id added * edges by id not working yet * Implemented get_edge_by_id
1 parent bcb8b79 commit 49ddd90

File tree

7 files changed

+179
-12
lines changed

7 files changed

+179
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* Bug fix: Checks for existence of the database before attempting to drop it. See: https://github.com/LiUGraphQL/woo.sh/issues/89
33

44
**v0.1.1, released on June 23, 2020**
5-
* New feature: Edge annotations can now be set when creating and/or updating objects. See: https://github.com/LiUGraphQL/woo.sh/issues/24
6-
* New feature: Incoming and outgoing edges (with annotations) can now be queried (not just the sources and targets). See: https://github.com/LiUGraphQL/woo.sh/issues/31
5+
* New feature: Full support for edge annotations is now implemented, including querying (see https://github.com/LiUGraphQL/woo.sh/issues/31), updating and creation on edge/object creation.
6+
* New feature: Edges can now be deleted
77
* New feature: client-tests now handles edge annotations.
88
* Bug fix: Default values missing from field arguments is now fixed.
99
* Bug fix: Filters occuring in subfields nolonger causes errors.

graphql-api-generator/generator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def run(schema: GraphQLSchema, config: dict):
113113
schema = add_key_input_types(schema)
114114
schema = add_key_queries(schema)
115115

116+
if config.get('generation').get('query_edge_by_id'):
117+
schema = add_get_edge_queries(schema)
118+
116119
# add input types
117120
if config.get('generation').get('input_to_create_objects'):
118121
schema = add_input_to_create(schema)
@@ -123,7 +126,7 @@ def run(schema: GraphQLSchema, config: dict):
123126
if config.get('generation').get('input_to_create_edge_objects'):
124127
schema = add_input_to_create_edge_objects(schema)
125128
if config.get('generation').get('input_to_update_edge_objects'):
126-
raise UnsupportedOperation('{0} is currently not supported'.format('input_to_update_edge_objects'))
129+
schema = add_input_to_update_edge_objects(schema)
127130

128131
# add mutations
129132
if config.get('generation').get('create_objects'):
@@ -137,7 +140,7 @@ def run(schema: GraphQLSchema, config: dict):
137140
if config.get('generation').get('create_edge_objects'):
138141
schema = add_mutation_create_edge_objects(schema)
139142
if config.get('generation').get('update_edge_objects'):
140-
raise UnsupportedOperation('{0} is currently not supported'.format('update_edge_objects'))
143+
schema = add_mutation_update_edge_objects(schema)
141144
if config.get('generation').get('delete_edge_objects'):
142145
schema = add_mutation_delete_edge_objects(schema)
143146

graphql-api-generator/resources/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ generation:
2626
query_type_filter: true
2727
query_list_of: true
2828
query_by_key: true
29+
query_edge_by_id: true
2930
# add input types
3031
input_to_create_objects: true
3132
input_to_update_objects: true

graphql-api-generator/utils/utils.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,24 @@ def add_get_queries(schema: GraphQLSchema):
438438
return schema
439439

440440

441+
def add_get_edge_queries(schema: GraphQLSchema):
442+
"""
443+
Add query to get edge based on ID.
444+
:param schema:
445+
:return:
446+
"""
447+
# Create queries for object types
448+
make = ''
449+
for _type in schema.type_map.values():
450+
if is_db_schema_defined_type(_type) or is_input_type(_type):
451+
continue
452+
if not _type.name.startswith('_') or not 'EdgeFrom' in _type.name:
453+
continue
454+
make += f'extend type Query {{ {decapitalize(_type.name)}(id:ID!): {_type.name} }} '
455+
schema = add_to_schema(schema, make)
456+
return schema
457+
458+
441459
def add_list_of_types(schema: GraphQLSchema):
442460
"""
443461
Add list type to represent lists of all types and support paging.
@@ -635,6 +653,11 @@ def add_object_type_filters(schema: GraphQLSchema):
635653

636654

637655
def get_field_annotations(field: GraphQLField):
656+
"""
657+
Get the annotations of a given field and return them as string
658+
:param field:
659+
:return string:
660+
"""
638661
annotation_fields = []
639662
is_non_null_string = ''
640663
for arg, arg_type in field.args.items():
@@ -784,18 +807,40 @@ def add_input_to_create_edge_objects(schema: GraphQLSchema):
784807
annotations, is_non_null_string = get_field_annotations(field)
785808

786809
if len(annotations) > 0:
787-
make += f'input {edge_input} {{sourceID: ID! targetID: ID! annotations: {annotate_input}{is_non_null_string} }}\n'
788-
789810
# Make sure the annotation type actually exists first, and prevent us from defining it multiple times
790811
if annotate_input not in schema.type_map:
791812
make += f'input {annotate_input}{{{annotations}}}\n'
813+
814+
make += f'input {edge_input} {{sourceID: ID! targetID: ID! annotations: {annotate_input}{is_non_null_string} }}\n'
815+
792816
else:
793817
make += f'input {edge_input} {{sourceID: ID! targetID: ID!}}\n'
794818

795819
schema = add_to_schema(schema, make)
796820
return schema
797821

798822

823+
def add_input_to_update_edge_objects(schema: GraphQLSchema):
824+
make = ''
825+
for _type in schema.type_map.values():
826+
if not is_db_schema_defined_type(_type) or is_interface_type(_type):
827+
continue
828+
connected_types = schema.get_possible_types(_type) if is_interface_type(_type) else [_type]
829+
for field_name, field in _type.fields.items():
830+
inner_field_type = get_named_type(field.type)
831+
if field_name.startswith('_') or is_enum_or_scalar(inner_field_type):
832+
continue
833+
for t in connected_types:
834+
annotations, _ = get_field_annotations(field)
835+
if len(annotations) > 0:
836+
edge_from = f'{capitalize(field_name)}EdgeFrom{t.name}'
837+
edge_input = f'_InputToUpdate{edge_from}'
838+
make += f'input {edge_input} {{{annotations}}}\n'
839+
840+
schema = add_to_schema(schema, make)
841+
return schema
842+
843+
799844
def add_mutation_create_edge_objects(schema: GraphQLSchema):
800845
make = ''
801846
for _type in schema.type_map.values():
@@ -816,6 +861,29 @@ def add_mutation_create_edge_objects(schema: GraphQLSchema):
816861
return schema
817862

818863

864+
def add_mutation_update_edge_objects(schema: GraphQLSchema):
865+
make = ''
866+
867+
for _type in schema.type_map.values():
868+
if not is_db_schema_defined_type(_type) or is_interface_type(_type):
869+
continue
870+
connected_types = schema.get_possible_types(_type) if is_interface_type(_type) else [_type]
871+
for field_name, field in _type.fields.items():
872+
inner_field_type = get_named_type(field.type)
873+
if field_name.startswith('_') or is_enum_or_scalar(inner_field_type):
874+
continue
875+
for t in connected_types:
876+
annotations = get_field_annotations(field)
877+
if len(annotations) > 0:
878+
edge_from = f'{capitalize(field_name)}EdgeFrom{t.name}'
879+
update = f'update{edge_from}'
880+
881+
make += f'extend type Mutation {{{update}(id: ID!, data: _InputToUpdate{edge_from}!): _{edge_from} }} '
882+
883+
schema = add_to_schema(schema, make)
884+
return schema
885+
886+
819887
def add_mutation_delete_edge_objects(schema: GraphQLSchema):
820888
make = ''
821889

graphql-resolver-generator/generator.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
import yaml
33

44
import yaml
5-
from graphql import build_schema, is_object_type, get_named_type, is_interface_type, assert_valid_schema
5+
from graphql import build_schema, is_object_type, get_named_type, is_interface_type, assert_valid_schema, is_input_type
66
from mako.template import Template
77

8+
import sys
9+
sys.path.insert(1, '../graphql-api-generator')
10+
from utils.utils import is_enum_or_scalar
11+
812

913
def is_schema_defined_object_type(_type):
1014
return is_object_type(_type) and _type.name[0] != '_' and _type.name not in ['Mutation', 'Query']
1115

1216

1317
def is_edge_type(_type):
14-
return 'EdgeFrom' in _type.name
18+
return not is_input_type(_type) and 'EdgeFrom' in _type.name
1519

1620

1721
def camelCase(s):
@@ -28,14 +32,16 @@ def generate(input_file, output_dir, config: dict):
2832
schema_string = f.read()
2933
schema = build_schema(schema_string)
3034

31-
data = {'types': [], 'types_by_key': [], 'interfaces': [], 'typeDelete': [], 'edge_types_to_delete': []}
35+
data = {'types': [], 'types_by_key': [], 'interfaces': [], 'typeDelete': [], 'edge_types_to_delete': [], 'edge_types_to_update': [], 'edge_objects': []}
36+
3237

3338
# get list of types
3439
for type_name, _type in schema.type_map.items():
3540
if is_interface_type(_type):
3641
data['interfaces'].append(type_name)
3742
if is_edge_type(_type):
38-
continue
43+
if config.get('generation').get('query_edge_by_id'):
44+
data['edge_objects'].append(type_name)
3945
if is_schema_defined_object_type(_type):
4046
t = {
4147
'Name': type_name,
@@ -63,6 +69,11 @@ def generate(input_file, output_dir, config: dict):
6369
if config.get('generation').get('delete_edge_objects'):
6470
data['edge_types_to_delete'].append((f'{pascalCase(field_name)}EdgeFrom{type_name}', type_name))
6571

72+
if config.get('generation').get('update_edge_objects'):
73+
if is_there_field_annotations(schema.type_map[f'_{pascalCase(field_name)}EdgeFrom{type_name}']):
74+
data['edge_types_to_update'].append((f'{pascalCase(field_name)}EdgeFrom{type_name}', type_name))
75+
76+
6677
sort_before_rendering(t)
6778
data['types'].append(t)
6879

@@ -74,6 +85,8 @@ def generate(input_file, output_dir, config: dict):
7485
data['interfaces'].sort()
7586
data['typeDelete'].sort()
7687
data['edge_types_to_delete'].sort()
88+
data['edge_types_to_update'].sort()
89+
data['edge_objects'].sort()
7790

7891
# apply template
7992
template = Template(filename=f'resources/resolver.template')
@@ -87,6 +100,21 @@ def generate(input_file, output_dir, config: dict):
87100
f.write(updated_schema_string)
88101

89102

103+
def is_there_field_annotations(_type):
104+
"""
105+
Check if a given type contains fields that does not start with '_' and are of enum or scalar type
106+
These fields should be annotations
107+
:param type:
108+
:return boolean:
109+
"""
110+
for field_name, field_type in _type.fields.items():
111+
if field_name.startswith('_') or field_name == 'id':
112+
continue
113+
if is_enum_or_scalar(get_named_type(field_type.type)):
114+
return True
115+
return False
116+
117+
90118
def sort_before_rendering(d: dict):
91119
"""
92120
Sort all list values in the dictionary to allow consistent generation of resolver files.

graphql-resolver-generator/resources/resolver.template

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const resolvers = {
1313
${type['name']}: async (parent, args, context, info) =>
1414
await driver.get(args.id, info.returnType, info.schema),
1515
% endfor
16+
1617
% for type in data['types']:
1718
% if type['hasKeyDirective']:
1819
${type['name']}ByKey: async (parent, args, context, info) =>
@@ -27,6 +28,11 @@ const resolvers = {
2728
% endif
2829
% endfor
2930

31+
% for edge_name in data['edge_objects']:
32+
${edge_name}: async (parent, args, context, info) =>
33+
await driver.get(args.id, info.returnType, info.schema),
34+
% endfor
35+
3036
% for interface_name in data['interfaces']:
3137
% if interface_name[0] != '_':
3238
listOf${interface_name}s: async (parent, args, context, info) => await driver.getList(args, info),
@@ -71,6 +77,18 @@ const resolvers = {
7177
% endif
7278
% endfor
7379

80+
% for edgeName, sourceType in data['edge_types_to_update']:
81+
update${edgeName}: async (parent, args, context, info) =>
82+
await driver.updateEdge(
83+
true,
84+
context,
85+
args.id,
86+
args.data,
87+
'${edgeName}',
88+
info.schema.getType('_InputToUpdate${edgeName}'),
89+
info),
90+
% endfor
91+
7492
% for type_name in data['typeDelete']:
7593
delete${type_name}: (parent, args, context, info) =>
7694
driver.deleteObject(true, context, args.id, info.schema.getType('${type_name}'), info),

graphql-server/drivers/arangodb/driver.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ module.exports = {
2929
update: function (isRoot, ctxt, id, data, returnType, info) {
3030
return update(isRoot, ctxt, id, data, returnType, info);
3131
},
32+
updateEdge: function (isRoot, ctxt, id, data, edgeName, inputToUpdateType, info) {
33+
return updateEdge(isRoot, ctxt, id, data, edgeName, inputToUpdateType, info);
34+
},
3235
deleteObject: function (isRoot, context, id, typeToDelete, info) {
3336
return deleteObject (isRoot, context, id, typeToDelete, info);
3437
},
@@ -439,6 +442,45 @@ function create(isRoot, ctxt, data, returnType, info, resVar = null) {
439442
return isRoot ? getResult(ctxt, info, resVar) : null;
440443
}
441444

445+
446+
/**
447+
* Update an edge.
448+
*
449+
* @param isRoot
450+
* @param ctxt
451+
* @param id
452+
* @param data
453+
* @param edgeName
454+
* @param inputToUpdateType
455+
* @param info
456+
* @param resVar
457+
* @returns {null|Promise<any>}
458+
*/
459+
function updateEdge(isRoot, ctxt, id, data, edgeName, inputToUpdateType, info, resVar = null) {
460+
// init transaction (if not already defined)
461+
initTransaction(ctxt);
462+
463+
// create a new variable if resVar was not defined by the calling function
464+
resVar = resVar !== null ? resVar : createVar(ctxt);
465+
466+
let collectionVar = getCollectionVar(edgeName, ctxt, true);
467+
ctxt.trans.code.push(`\n\t/* update edge ${edgeName} */`);
468+
469+
// define doc
470+
let doc = getScalarsAndEnums(data, info.schema.getType(inputToUpdateType));;
471+
doc['_lastUpdateDate'] = new Date().valueOf();
472+
let docVar = addParameterVar(ctxt, createParamVar(ctxt), doc);
473+
let idVar = addParameterVar(ctxt, createParamVar(ctxt), id);
474+
475+
ctxt.trans.code.push(`let ${resVar} = db._query(aql\`UPDATE PARSE_IDENTIFIER(${asAQLVar(idVar)}).key WITH ${asAQLVar(docVar)} IN ${asAQLVar(collectionVar)} RETURN NEW\`).next();`);
476+
477+
//directives handling is not needed for edge updates as they can not have directives as of current
478+
479+
// return promises for roots and null for nested result
480+
return isRoot ? getResult(ctxt, info, resVar) : null;
481+
}
482+
483+
442484
/**
443485
* Update an object including, and replace existing edges.
444486
*
@@ -649,14 +691,20 @@ async function get(id, returnType, schema) {
649691
if (i != 0) {
650692
query.push(aql`,`);
651693
}
652-
let collection = db.collection(possible_types[i].name);
694+
let typeName = possible_types[i].name;
695+
if (typeName.startsWith('_'))
696+
typeName = typeName.substr(1)
697+
let collection = db.collection(typeName);
653698
query.push(aql`(FOR x IN ${collection} FILTER(x._id == ${id}) RETURN x)`);
654699
}
655700
if (possible_types.length > 1) {
656701
query.push(aql`)`);
657702
}
658703
} else {
659-
let collection = db.collection(type.name);
704+
let typeName = type.name;
705+
if (typeName.startsWith('_'))
706+
typeName = typeName.substr(1)
707+
let collection = db.collection(typeName);
660708
query.push(aql`${collection} FILTER(i._id == ${id})`);
661709
}
662710

@@ -783,6 +831,7 @@ async function getEdge(parent, args, info) {
783831
query_filters = query_filters.concat(filters[i]);
784832
}
785833
}
834+
786835
query = query.concat(query_filters);
787836
query.push(aql`RETURN e`);
788837

0 commit comments

Comments
 (0)