diff --git a/graphql-api-generator/generator.py b/graphql-api-generator/generator.py index 9480fd3..9625fe9 100644 --- a/graphql-api-generator/generator.py +++ b/graphql-api-generator/generator.py @@ -36,19 +36,20 @@ def cmd(args): with open(file, 'r') as f: schema_string += f.read() + '\n' schema = build_schema(schema_string) - + # run schema = run(schema, config) # write to file or stdout if args.output: with open(args.output, 'w') as out: - out.write(print_schema(schema)) + out.write(print_schema_with_directives(schema)) else: - print(print_schema(schema)) + print(print_schema_with_directives(schema)) def run(schema: GraphQLSchema, config: dict): + # validate if config.get('validate'): validate_names(schema, config.get('validate')) @@ -268,7 +269,8 @@ def datetime_control(schema): if not is_scalar_type(schema.type_map['DateTime']): raise Exception('DateTime exists but is not scalar type: ' + schema.type_map['DateTime']) else: - schema.type_map['DateTime'] = GraphQLScalarType('DateTime') + # ast_node definition ensures that DateTime appears as a user-defined scalar + schema.type_map['DateTime'] = GraphQLScalarType('DateTime', ast_node=ScalarTypeDefinitionNode()) if not is_scalar_type(schema.type_map['DateTime']): raise Exception('DateTime could not be added as scalar!') diff --git a/graphql-api-generator/utils/utils.py b/graphql-api-generator/utils/utils.py index 3b75de0..0de4721 100644 --- a/graphql-api-generator/utils/utils.py +++ b/graphql-api-generator/utils/utils.py @@ -202,14 +202,27 @@ def add_reverse_edges(schema: GraphQLSchema): # Reverse edge edge_from = get_named_type(field_type.type) edge_name = f'_{field_name}From{_type.name}' - edge_to = GraphQLList(_type) + + directives = {} + directive_to_add = '' + + if hasattr(field_type, 'ast_node') and field_type.ast_node is not None: + directives = {directive.name.value: directive for directive in field_type.ast_node.directives} + + if 'requiredForTarget' in directives: + directive_to_add = '@required' + + if 'uniqueForTarget' in directives: + edge_to = _type + else: + edge_to = GraphQLList(_type) if is_interface_type(edge_from): - make += 'extend interface {0} {{ {1}: {2} }}\n'.format(edge_from, edge_name, edge_to) + make += 'extend interface {0} {{ {1}: {2} {3} }}\n'.format(edge_from, edge_name, edge_to, directive_to_add) for implementing_type in schema.get_possible_types(edge_from): make += 'extend type {0} {{ {1}: {2} }}\n'.format(implementing_type, edge_name, edge_to) else: - make += 'extend type {0} {{ {1}: {2} }}\n'.format(edge_from, edge_name, edge_to) + make += 'extend type {0} {{ {1}: {2} {3} }}\n'.format(edge_from, edge_name, edge_to, directive_to_add) schema = add_to_schema(schema, make) return schema @@ -234,19 +247,21 @@ def add_input_to_create(schema: GraphQLSchema): for _type in schema.type_map.values(): if not is_db_schema_defined_type(_type) or is_interface_type(_type): continue - make += f'\nextend input _InputToCreate{_type.name} {{ ' + make += f'\nextend input _InputToCreate{_type.name} {{\n' for field_name, field in _type.fields.items(): if field_name == 'id' or field_name[0] == '_': continue + inner_field_type = get_named_type(field.type) + if is_enum_or_scalar(inner_field_type): - make += f'{field_name}: {field.type} ' + make += f' {field_name}: {field.type} \n' else: schema = extend_connect(schema, _type, inner_field_type, field_name) connect_name = f'_InputToConnect{capitalize(field_name)}Of{_type.name}' connect = copy_wrapper_structure(schema.type_map[connect_name], field.type) - make += f' {field_name}: {connect} ' - make += '} ' + make += f' {field_name}: {connect} \n' + make += '}\n' schema = add_to_schema(schema, make) return schema @@ -373,12 +388,12 @@ def add_input_update(schema: GraphQLSchema): inner_field_type = get_named_type(f_type) if is_enum_or_scalar(inner_field_type): - make += f'extend input {update_name} {{ {field_name}: {f_type} }} ' + make += f'extend input {update_name} {{ {field_name}: {f_type} }} \n' else: # add create or connect field connect_name = f'_InputToConnect{capitalize(field_name)}Of{_type.name}' connect = copy_wrapper_structure(schema.get_type(connect_name), f_type) - make += f'extend input {update_name} {{ {field_name}: {connect} }} ' + make += f'extend input {update_name} {{ {field_name}: {connect} }} \n' schema = add_to_schema(schema, make) return schema @@ -751,3 +766,274 @@ def add_delete_mutations(schema: GraphQLSchema): make += f'extend type Mutation {{ {delete}(id: ID!): {_type.name} }} ' schema = add_to_schema(schema, make) return schema + + +def ast_type_to_string(_type: GraphQLType): + """ + Print the ast_type properly + :param _type: + :return: + """ + + # ast_nodes types behavies differently than other types (as they are NodeTypes) + # So we can't use the normal functions + + + _post_str = '' + _pre_str = '' + # A, A!, [A!], [A]!, [A!]! + wrappers = [] + if isinstance(_type, NonNullTypeNode): + _post_str = '!' + _type = _type.type + if isinstance(_type, ListTypeNode): + _post_str = ']' + _post_str + _pre_str = '[' + _type = _type.type + if isinstance(_type, NonNullTypeNode): + _post_str = '!' + _post_str + _type = _type.type + + # Dig down to find the actual named node, should be the first one actually + name = _type + while not isinstance(name, NamedTypeNode): + name = name.type + name = name.name.value + + return _pre_str + name + _post_str + + +def directive_from_interface(directive, interface_name): + """ + Return the correct directive string from directives inhertied from interfaces + :param directive: + :param interface_name: + :return string: + """ + directive_string = directive.name.value + + # The only two cases who needs special attention is @requiredForTarget and @uniqueForTarget + if directive_string == 'requiredForTarget': + directive_string = '_requiredForTarget_AccordingToInterface(interface: "' + interface_name + '")' + elif directive_string == 'uniqueForTarget': + directive_string = '_uniqueForTarget_AccordingToInterface(interface: "' + interface_name + '")' + else: + directive_string += get_directive_arguments(directive) + + return directive_string + + +def get_directive_arguments(directive): + """ + Get the arguments of the given directive as string + :param directive: + :return string: + """ + + output = '' + if directive.arguments: + output+= '(' + for arg in directive.arguments: + output+= arg.name.value + ':' + if isinstance(arg.value, ListValueNode): + # List + output+= '[' + for V in arg.value.values: + if isinstance(V, StringValueNode): + output+='"' + V.value + '", ' + else: + output+= V.value + ', ' + + output = output[:-2] + ']' + + else: + # Non-list + if isinstance(arg.value, StringValueNode): + output+='"' + arg.value.value + '", ' + else: + output+= arg.value.value + ', ' + + output += ', ' + + output = output[:-2] + ')' + + return output + + +def get_field_directives(field_name, _type, schema): + """ + Get the directives of given field, and return them as string + :param field: + :param field_name: + :param _type: + :param schema: + :return string: + """ + + output = '' + + # Used to make sure we don't add the same directive multiple times to the same field + directives_set = set() + + if is_input_type(_type): + # Get the target type instead (unless it is a filter or delete input, then we dont care) + # We also ignore @required directives for inputs + if _type.name[:14] == '_InputToUpdate': + directives_set.add('required') + _type = schema.get_type(_type.name[14:]) + + elif _type.name[:14] == '_InputToCreate': + _type = schema.get_type(_type.name[14:]) + directives_set.add('required') + + else: + return '' + + # We got type without fields, just return empty + if not hasattr(_type, 'fields'): + return '' + + # Get the field from the correct type + field = _type.fields[field_name] + + # Get all directives directly on field + for directive in field.ast_node.directives: + if not directive.name.value in directives_set: + output+= ' @' + directive.name.value + directives_set.add(directive.name.value) + output += get_directive_arguments(directive) + + + if hasattr(_type, 'interfaces'): + # Get all inherited directives + for interface in _type.interfaces: + if field_name in interface.fields: + for directive in interface.fields[field_name].ast_node.directives: + directive_str = directive_from_interface(directive, interface.name) + if not directive_str in directives_set: + output+= ' @' + directive_str + directives_set.add(directive_str) + + return output + + +def get_type_directives(_type, schema): + """ + Get the directives of given type, or target type if create- or update-input + :param type: + :return string: + """ + + output = '' + + if is_input_type(_type): + # Get the target type instead (unless it is a filter or delete input, then we dont care) + if _type.name[:14] == '_InputToUpdate': + _type = schema.get_type(_type.name[14:]) + + elif _type.name[:14] == '_InputToCreate': + _type = schema.get_type(_type.name[14:]) + else: + return '' + + if hasattr(_type, 'ast_node') and _type.ast_node is not None: + # Get directives on type + for directive in _type.ast_node.directives: + output+= ' @' + directive.name.value + output += get_directive_arguments(directive) + + return output + + +def print_schema_with_directives(schema): + """ + Outputs the given schema as string, in the format we want it. + Types and fields will all contain directives + :param schema: + :return string: + """ + manual_directives = { + 'required': 'directive @required on FIELD_DEFINITION', + 'key': 'directive @key(fields: [String!]!) on OBJECT | INPUT_OBJECT', + 'distinct': 'directive @distinct on FIELD_DEFINITION | INPUT_FIELD_DEFINITION', + 'noloops': 'directive @noloops on FIELD_DEFINITION | INPUT_FIELD_DEFINITION', + 'requiredForTarget': 'directive @requiredForTarget on FIELD_DEFINITION | INPUT_FIELD_DEFINITION', + 'uniqueForTarget': 'directive @uniqueForTarget on FIELD_DEFINITION | INPUT_FIELD_DEFINITION', + '_requiredForTarget_AccordingToInterface': 'directive @_requiredForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION', + '_uniqueForTarget_AccordingToInterface': 'directive @_uniqueForTarget_AccordingToInterface(interface: String!) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION' + } + output = '' + # Add directives + for _dir in schema.directives: + # Skip non-user defined directives + if _dir.ast_node is None or _dir.name in manual_directives.keys(): + continue + + output += f'directive @{_dir.name}' + if _dir.ast_node.arguments: + args = ', '.join([f'{arg.name.value}: {ast_type_to_string(arg.type)}' for arg in _dir.ast_node.arguments]) + output += f'({args})' + + output += ' on ' + ' | '.join([loc.name for loc in _dir.locations]) + output += '\n\n' + + # Manually handled directives + for _dir in manual_directives.values(): + output += _dir + '\n\n' + + # For each type, and output the types sorted by name + for _type in sorted(schema.type_map.values(), key=lambda x: x.name): + # Internal type + if _type.name.startswith('__'): + continue + + if is_interface_type(_type): + output += 'interface ' + _type.name + elif is_enum_type(_type): + output += 'enum ' + _type.name + elif is_scalar_type(_type): + # Skip non-user defined directives + if _type.ast_node is not None: + output += 'scalar ' + _type.name + elif is_input_type(_type): + output += 'input ' + _type.name + else: + output += 'type ' + _type.name + if hasattr(_type, 'interfaces') and _type.interfaces: + output += ' implements ' + output += ' & '.join([interface.name for interface in _type.interfaces]) + + if is_enum_type(_type): + # For enums we can get the values directly and add them + output += ' {\n' + for value in _type.values: + output += ' ' + value + '\n' + output += '}' + + elif not is_enum_or_scalar(_type): + # This should be a type, or an interface + # Get directives on type + output += get_type_directives(_type, schema) + output += ' {\n' + + # Get fields + for field_name, field in _type.fields.items(): + output += ' ' + field_name + + # Get arguments for field + if hasattr(field, 'args') and field.args: + args = ', '.join([f'{arg_name}: {arg.type}' for arg_name, arg in field.args.items()]) + output += f'({args})' + + output += ': ' + str(field.type) + + # Add directives + output += get_field_directives(field_name, _type, schema) + output += '\n' + + output += '}' + + if _type.ast_node is not None: + output += '\n\n' + + return output diff --git a/graphql-server/drivers/arangodb/driver.js b/graphql-server/drivers/arangodb/driver.js index 7bec2cb..2c59ae9 100644 --- a/graphql-server/drivers/arangodb/driver.js +++ b/graphql-server/drivers/arangodb/driver.js @@ -7,6 +7,7 @@ const waitOn = require('wait-on'); let db; let disableEdgeValidation; +let disableDirectivesChecking; module.exports = { init: async function(args){ @@ -14,15 +15,15 @@ module.exports = { let db_name = args.db_name || 'dev-db'; let url = args.url || 'http://localhost:8529'; let drop = args.drop || false; + disableDirectivesChecking = args.disableDirectivesChecking || true; disableEdgeValidation = args.disableEdgeValidation || false; - db = new arangojs.Database({ url: url }); // wait for ArangoDB console.log(`Waiting for ArangoDB to become available at ${url}`); let urlGet = url.replace(/^http(s?)(.+$)/,'http$1-get$2'); const opts = { - resources: [ urlGet ], + resources:[urlGet], delay: 1000, // initial delay in ms interval: 1000, // poll interval in ms followRedirect: true @@ -33,7 +34,6 @@ module.exports = { // if drop is set if(drop) { await db.dropDatabase(db_name).then( - (msg) => console.info(`Database ${db_name} deleted: ${! msg['error']}`), () => console.log() ); } @@ -74,10 +74,13 @@ module.exports = { isEndOfList: async function (parent, args, info) { return await isEndOfList(parent, args, info); }, - addPossibleEdgeTypes: function(query, schema, type_name, field_name){ + addPossibleTypes: function (query, schema, type_name) { + return addPossibleTypes(query, schema, type_name); + }, + addPossibleEdgeTypes: function (query, schema, type_name, field_name) { return addPossibleEdgeTypes(query, schema, type_name, field_name); }, - getEdgeCollectionName: function(type, field){ + getEdgeCollectionName: function (type, field) { return getEdgeCollectionName(type, field); } }; @@ -382,6 +385,9 @@ async function create(isRoot, ctxt, data, returnType, info){ } } + // directives handling + addFinalDirectiveChecksForType(ctxt, returnType, aql`${asAQLVar(resVar)}._id`, info.schema); + // overwrite the current action if(isRoot) { ctxt.trans.code.push(`result['${info.path.key}'] = ${resVar};`); // add root result @@ -430,6 +436,8 @@ async function createEdge(isRoot, ctxt, source, sourceType, sourceField, target, ctxt.trans.params[docVar] = doc; ctxt.trans.code.push(`let ${resVar} = db._query(aql\`INSERT ${aqlDocVar} IN ${collection} RETURN NEW\`).next();`); + addFinalDirectiveChecksForType(ctxt, sourceType, source, info.schema); + // overwrite the current action if(isRoot) { ctxt.trans.code.push(`result['${info.path.key}'] = ${resVar};`); // add root result @@ -491,11 +499,17 @@ function initTransaction(){ 'const {aql} = require("@arangodb");', 'let result = Object.create(null);' ], + finalConstraintChecks: [], error: false }; } async function executeTransaction(ctxt){ + // add all finalConstraintChecks to code before executing + //ctxt.trans.code.concat(ctxt.trans.finalConstraintChecks); // concat is not working? + for (const row of ctxt.trans.finalConstraintChecks) { + ctxt.trans.code.push(row); + } try { let action = `function(params){\n\t${ctxt.trans.code.join('\n\t')}\n\treturn result;\n}`; console.log(action); @@ -660,6 +674,9 @@ async function update(isRoot, ctxt, id, data, returnType, info){ } } + // directives handling + addFinalDirectiveChecksForType(ctxt, returnType, aql`${asAQLVar(resVar)}._id`, info.schema); + // overwrite the current action if(isRoot) { ctxt.trans.code.push(`result['${info.path.key}'] = ${resVar}.new;`); // add root result @@ -686,16 +703,19 @@ function asAqlArray(array){ /** * Converts all values of enum or scalar type in inputDoc to mach format used for storage in the database, * and return result in output map - * @param map to be changed and returned, map to be used as input, type - * @returns map given as output + * @param {map} outputDoc + * @param {map} inputDoc + * @param type + * @returns {map} outputDoc */ function formatFixInput(outputDoc, inputDoc, type) { // Adds scalar/enum values to outputDoc - for (let i in type.getFields()) { - let field = type.getFields()[i]; + for (let f in type.getFields()) { + let field = type.getFields()[f]; + //let field = type.getFields()[i]; let t = graphql.getNamedType(field.type); - if(graphql.isEnumType(t) || graphql.isScalarType(t)){ - if(inputDoc[field.name] !== undefined) { + if (graphql.isEnumType(t) || graphql.isScalarType(t)) { + if (inputDoc[field.name] !== undefined) { outputDoc[field.name] = formatFixVariable(t, inputDoc[field.name]); } } @@ -705,17 +725,18 @@ function formatFixInput(outputDoc, inputDoc, type) { /** * Convert input data (value) to match format used for storage in the database - * @param type (of field), value + * @param type (of field) + * @param value * @returns value (in database ready format) */ function formatFixVariable(_type, v) { // DateTime has to be handled separately, which is currently the only reason for this function to exist if (_type == 'DateTime') // Arrays of DateTime needs special, special care. - if(Array.isArray(v)){ + if (Array.isArray(v)) { let newV = [] - for(let i in v) - newV.push(aql`DATE_TIMESTAMP(${v})`); + for (date of v) + newV.push(aql`DATE_TIMESTAMP(${date})`); return newV; } else @@ -731,7 +752,7 @@ function formatFixVariable(_type, v) { */ function formatFixVariableWrapper(field, info, v) { // no need to even try when we have _id as field - if(field == '_id') + if (field == '_id') return v; let namedReturnType = graphql.getNamedType(info.returnType.getFields()['content'].type) @@ -880,19 +901,7 @@ async function getList(args, info){ let first = args.first; let after = args.after; let query = [aql`FOR x IN FLATTEN(FOR i IN [`]; - if(graphql.isInterfaceType(type)){ - let possible_types = info.schema.getPossibleTypes(type); - for(let i in possible_types) { - if(i != 0){ - query.push(aql`,`); - } - let collection = db.collection(possible_types[i].name); - query.push(aql`${collection}`); - } - } else { - let collection = db.collection(type.name); - query.push(aql`${collection}`); - } + addPossibleTypes(query, info.schema, type); query.push(aql`] RETURN i)`); // add filters @@ -928,19 +937,7 @@ async function getList(args, info){ async function isEndOfList(parent, args, info){ let type = graphql.getNamedType(info.parentType.getFields()['content'].type); let query = [aql`FOR x IN FLATTEN(FOR i IN [`]; - if(graphql.isInterfaceType(type)){ - let possible_types = info.schema.getPossibleTypes(type); - for(let i in possible_types) { - if(i != 0){ - query.push(aql`,`); - } - let collection = db.collection(possible_types[i].name); - query.push(aql`${collection}`); - } - } else { - let collection = db.collection(type.name); - query.push(aql`${collection}`); - } + addPossibleTypes(query, info.schema, type); query.push(aql`] RETURN i)`); // add filters @@ -967,19 +964,7 @@ async function isEndOfList(parent, args, info){ async function getTotalCount(parent, args, info){ let type = graphql.getNamedType(info.parentType.getFields()['content'].type); let query = [aql`FOR x IN FLATTEN(FOR i IN [`]; - if(graphql.isInterfaceType(type)){ - let possible_types = info.schema.getPossibleTypes(type); - for(let i in possible_types) { - if(i != 0){ - query.push(aql`,`); - } - let collection = db.collection(possible_types[i].name); - query.push(aql`${collection}`); - } - } else { - let collection = db.collection(type.name); - query.push(aql`${collection}`); - } + addPossibleTypes(query, info.schema, type); query.push(aql`] RETURN i)`); // add filters @@ -1080,7 +1065,7 @@ function getTypeNameFromId(id) { function isOfType(id, type, schema) { let idType = getTypeNameFromId(id); if(type.name == idType || isImplementingType(idType, type, schema)){ - return true; + return true; } return false; } @@ -1102,23 +1087,172 @@ function conditionalThrow(msg){ } } +/** + * Add all possible collections for the given type to query + * @param query (modifies) + * @param schema + * @param type + * @param {bool} use_aql = true (optional) + */ +function addPossibleTypes(query, schema, type, use_aql = true) { + if (graphql.isInterfaceType(type)) { + let possible_types = schema.getPossibleTypes(type); + for (let i in possible_types) { + if (i != 0) { + if (use_aql) query.push(aql`,`); + else query[query.length - 1] += `,`; + } + if (use_aql) query.push(aql`${db.collection(possible_types[i].name)}`); + else { + let collection = asAQLVar(`db.${possible_types[i].name}`) + query.push(`${collection}`); + } + } + } else { + if (use_aql) query.push(aql`${db.collection(type.name)}`); + else { + let collection = asAQLVar(`db.${type.name}`); + query.push(`${collection}`); + } + } +} + /** * Add all possible edge-collections for the given type and field to query - * @param query, schema, type_name, field_name, directionString (Optional) + * @param query (modifies) + * @param schema + * @param type_name + * @param field_name + * @param {bool} use_aql = true (optional) + * @param directionString (optional) */ -function addPossibleEdgeTypes(query, schema, type_name, field_name, directionString = ""){ +function addPossibleEdgeTypes(query, schema, type_name, field_name, use_aql = true, directionString = "") { let type = schema._typeMap[type_name]; - if(graphql.isInterfaceType(type)){ + if (graphql.isInterfaceType(type)) { let possible_types = schema.getPossibleTypes(type); for (let i in possible_types) { if (i != 0) { - query.push(aql`,`); + if (use_aql) query.push(aql`,`); + else query[query.length - 1] += `,`; + } + let collectionName = getEdgeCollectionName(possible_types[i].name, field_name); + + if (use_aql) query.push(aql`${db.collection(collectionName)}`); + else { + let collection = asAQLVar(`db.${collectionName}`); + query.push(`${collection}`); } - let collection = db.collection(getEdgeCollectionName(possible_types[i].name, field_name)); - query.push(aql`${collection}`); } } else { - let collection = db.collection(getEdgeCollectionName(type.name, field_name)); - query.push(aql`${collection}`); + let collectionName = getEdgeCollectionName(type.name, field_name); + + if (use_aql) query.push(aql`${db.collection(collectionName)}`); + else { + let collection = asAQLVar(`db.${type.name}`); + query.push(`${collection}`); + } } } + +/** + * Append finalConstraintChecks to ctxt for all directives of all fields in input type + * @param ctxt (modifies) + * @param type + * @param resVar + * @param schema + */ +function addFinalDirectiveChecksForType(ctxt, type, id, schema) { + if (!disableDirectivesChecking) { + for (let f in type.getFields()) { + let field = type.getFields()[f]; + for (dir of field.astNode.directives) { + if (dir.name.value == 'noloops') { + let collection = asAQLVar(`db.${getEdgeCollectionName(type.name, field.name)}`); + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR v IN 1..1 OUTBOUND ${id} ${collection} FILTER ${id} == v._id RETURN v\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "Field ${f} in ${type.name} is breaking a @noloops directive!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == 'distinct') { + let collection = asAQLVar(`db.${getEdgeCollectionName(type.name, field.name)}`); + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR v, e IN 1..1 OUTBOUND ${id} ${collection} FOR v2, e2 IN 1..1 OUTBOUND ${id} ${collection} FILTER v._id == v2._id AND e._id != e2._id RETURN v\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "Field ${f} in ${type.name} is breaking a @distinct directive!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == 'uniqueForTarget') { + // The direct variant of @uniqueForTarget + // edge is named after current type etc. + let collection = asAQLVar(`db.${getEdgeCollectionName(type.name, field.name)}`); + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR v, e IN 1..1 OUTBOUND ${id} ${collection} FOR v2, e2 IN 1..1 INBOUND v._id ${collection} FILTER e._id != e2._id RETURN v\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "Field ${f} in ${type.name} is breaking a @uniqueForTarget directive!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == '_uniqueForTarget_AccordingToInterface') { + // The inherited/implemented variant of @uniqueForTarget + // The target does not only require at most one edge of this type, but at most one of any type implementing the interface + // Thankfully we got the name of the interface as a mandatory argument and can hence use this to get all types implementing it + + let interfaceName = dir.arguments[0].value.value; // If we add more arguments to the directive this will fail horrible. + // But that should not happen (and it is quite easy to fix) + + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR v, e IN 1..1 OUTBOUND ${id}`); + addPossibleEdgeTypes(ctxt.trans.finalConstraintChecks, schema, interfaceName, field.name, false); + ctxt.trans.finalConstraintChecks.push(`FOR v2, e2 IN 1..1 INBOUND v._id`); + addPossibleEdgeTypes(ctxt.trans.finalConstraintChecks, schema, interfaceName, field.name, false); + ctxt.trans.finalConstraintChecks.push(`FILTER e._id != e2._id RETURN v\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "Field ${f} in ${type.name} is breaking a @_uniqueForTarget_AccordingToInterface directive!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == 'requiredForTarget') { + // The direct variant of @requiredForTarget + // edge is named after current type etc. + let edgeCollection = asAQLVar(`db.${getEdgeCollectionName(type.name, field.name)}`); + + // The target type might be an interface, giving us slightly more to keep track of + // First, find the right collections to check + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR x IN FLATTEN(FOR i IN [`); + addPossibleTypes(ctxt.trans.finalConstraintChecks, schema, graphql.getNamedType(field.type), false); + // Second, count all edges ending at objects in these collections + ctxt.trans.finalConstraintChecks.push(`] RETURN i) LET endpoints = ( FOR v IN 1..1 INBOUND x ${edgeCollection} RETURN v)`); + // If the count returns 0, we have an object breaking the directive + ctxt.trans.finalConstraintChecks.push(`FILTER LENGTH(endpoints) == 0 RETURN x\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "There are object(s) breaking the @requiredForTarget directive of Field ${f} in ${type.name}!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == '_requiredForTarget_AccordingToInterface') { + // The inherited/implemented variant of @requiredForTarget + // The target does not directly require an edge of this type, but at least one of any type implementing the interface + // Thankfully we got the name of the interface as a mandatory argument and can hence use this to get all types implementing it + + let interfaceName = dir.arguments[0].value.value; // If we add more arguments to the directive this will fail horrible. + // But that should not happen (and it is quite easy to fix) + + // The target type might be an interface, giving us slightly more to keep track of + // First, find the right collections to check + ctxt.trans.finalConstraintChecks.push(`if(db._query(aql\`FOR x IN FLATTEN(FOR i IN [`); + addPossibleTypes(ctxt.trans.finalConstraintChecks, schema, graphql.getNamedType(field.type), false); + // Second, count all edges ending at objects in these collections + ctxt.trans.finalConstraintChecks.push(`] RETURN i) LET endpoints = ( FOR v IN 1..1 INBOUND x `); + addPossibleEdgeTypes(ctxt.trans.finalConstraintChecks, schema, interfaceName, field.name, false); + ctxt.trans.finalConstraintChecks.push(` RETURN v)`); + // If the count returns 0, we have an object breaking the directive + ctxt.trans.finalConstraintChecks.push(`FILTER LENGTH(endpoints) == 0 RETURN x\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "There are object(s) breaking the inherited @_requiredForTarget_AccordingToInterface directive of Field ${f} in ${type.name}!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + else if (dir.name.value == 'required' && field.name[0] == '_') { + // This is actually the reverse edge of a @requiredForTarget directive + + let pattern_string = `^_(.+?)From${type.name}$`; // get the non-reversed edge name + let re = new RegExp(pattern_string); + let field_name = re.exec(field.name)[1]; + + ctxt.trans.finalConstraintChecks.push(`if(!db._query(aql\`FOR v IN 1..1 INBOUND ${id}`); + addPossibleEdgeTypes(ctxt.trans.finalConstraintChecks, schema, graphql.getNamedType(field.type), field_name, false); + ctxt.trans.finalConstraintChecks.push(`RETURN x\`).next()){`); + ctxt.trans.finalConstraintChecks.push(` throw "Field ${f} in ${type.name} is breaking a @requiredForTarget directive (in reverse)!";`); + ctxt.trans.finalConstraintChecks.push(`}`); + } + } + } + } +} \ No newline at end of file diff --git a/graphql-server/server.js b/graphql-server/server.js index c2f19bd..8a772ab 100644 --- a/graphql-server/server.js +++ b/graphql-server/server.js @@ -22,6 +22,7 @@ async function run(){ 'db_name': process.env.DB_NAME || 'spirit-db', 'url': process.env.URL || 'http://localhost:8529', 'drop': process.env.DROP === 'true', + //'disableDirectivesChecking': process.env.DISABLE_DIRECTIVES_CHECKING === 'false', 'disableEdgeValidation': process.env.DISABLE_EDGE_VALIDATION === 'true' }; await driver.init(args); diff --git a/graphql-server/tests/client-tests.js b/graphql-server/tests/client-tests.js index c7a8fea..c180293 100644 --- a/graphql-server/tests/client-tests.js +++ b/graphql-server/tests/client-tests.js @@ -10,10 +10,11 @@ * possible subfields down to a configurable level. A simple equality filter is added for the ID of the type. * * - * What id does NOT do: + * What it does NOT do: * - Does not verify that the returned object matches the expected result. (TODO) * - Executes no queries over annotated edges (TODO) * - Executes no mutations to annotate edges (TODO). + * - Care about / check directives (Requires process.env.DISABLE_DIRECTIVES_CHECKING to be set to true if directives are used) (TODO). * - ... a lot of other things probably. */