Skip to content
This repository was archived by the owner on Sep 3, 2021. It is now read-only.

Interface handling: generate queries for interface types + FRAGMENT_TYPE fix #336

Merged
merged 13 commits into from
Nov 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions example/apollo-server/movies-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,33 @@ type Book {
genre: BookGenre
}

interface Camera {
id: ID!
type: String
make: String
}

type OldCamera implements Camera {
id: ID!
type: String
make: String
weight: Int
}

type NewCamera implements Camera {
id: ID!
type: String
make: String
features: [String]
}

type CameraMan implements Person {
userId: ID!
name: String
favoriteCamera: Camera @relation(name: "favoriteCamera", direction: "OUT")
cameras: [Camera] @relation(name: "cameras", direction: "OUT")
}

type Query {
Movie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float): [Movie] MoviesByYear(year: Int, first: Int = 10, offset: Int = 0): [Movie]
AllMovies: [Movie]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "babel src --presets @babel/preset-env --out-dir dist",
"build-with-sourcemaps": "babel src --presets @babel/preset-env --out-dir dist --source-maps",
"precommit": "lint-staged",
"prepublish": "npm run build",
"prepare": "npm run build",
"test": "nyc --reporter=lcov ava test/unit/**.test.js --verbose",
"parse-tck": "babel-node test/helpers/tck/parseTck.js",
"test-tck": "nyc ava --fail-fast test/unit/filterTests.test.js",
Expand Down
3 changes: 2 additions & 1 deletion src/augment/types/node/mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../../directives';
import { getPrimaryKey } from '../../../utils';
import { shouldAugmentType } from '../../augment';
import { OperationType } from '../../types/types';
import { OperationType, isInterfaceTypeDefinition } from '../../types/types';
import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields';

/**
Expand Down Expand Up @@ -45,6 +45,7 @@ export const augmentNodeMutationAPI = ({
const mutationTypeNameLower = mutationTypeName.toLowerCase();
if (
mutationType &&
!isInterfaceTypeDefinition({ definition }) &&
shouldAugmentType(config, mutationTypeNameLower, typeName)
) {
Object.values(NodeMutation).forEach(mutationAction => {
Expand Down
11 changes: 9 additions & 2 deletions src/augment/types/node/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '../../ast';
import {
OperationType,
isInterfaceTypeDefinition,
isNodeType,
isRelationshipType,
isObjectTypeDefinition,
Expand All @@ -53,7 +54,10 @@ export const augmentNodeType = ({
operationTypeMap,
config
}) => {
if (isObjectTypeDefinition({ definition })) {
if (
isObjectTypeDefinition({ definition }) ||
isInterfaceTypeDefinition({ definition })
) {
let [
nodeInputTypeMap,
propertyOutputFields,
Expand All @@ -69,7 +73,10 @@ export const augmentNodeType = ({
});
// A type is ignored when all its fields use @neo4j_ignore
if (!isIgnoredType) {
if (!isOperationTypeDefinition({ definition, operationTypeMap })) {
if (
!isOperationTypeDefinition({ definition, operationTypeMap }) &&
!isInterfaceTypeDefinition({ definition })
) {
[propertyOutputFields, nodeInputTypeMap] = buildNeo4jSystemIDField({
definition,
typeName,
Expand Down
3 changes: 2 additions & 1 deletion src/augment/types/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export const Neo4jDataType = {
[SpatialType.POINT]: 'Spatial'
},
STRUCTURAL: {
[Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType
[Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType,
[Kind.INTERFACE_TYPE_DEFINITION]: Neo4jStructuralType
}
};

Expand Down
82 changes: 65 additions & 17 deletions src/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
relationDirective,
typeIdentifiers,
decideNeo4jTypeConstructor,
getAdditionalLabels
getAdditionalLabels,
getDerivedTypeNames
} from './utils';
import {
getNamedType,
Expand All @@ -53,6 +54,9 @@ import { buildCypherSelection } from './selections';
import _ from 'lodash';
import { v1 as neo4j } from 'neo4j-driver';

const fragmentType = varName =>
`FRAGMENT_TYPE: head( [ label IN labels(${varName}) WHERE label IN $derivedTypes ] )`;

export const customCypherField = ({
customCypher,
cypherParams,
Expand Down Expand Up @@ -86,6 +90,15 @@ export const customCypherField = ({
// increments paramIndex. So here we need to decrement it in order to map
// appropriately to the indexed keys produced in getFilterParams()
const cypherFieldParamsIndex = paramIndex - 1;
const fragmentTypeParams = fieldIsInterfaceType
? {
derivedTypes: getDerivedTypeNames(
resolveInfo.schema,
fieldType.ofType.astNode.name
)
}
: {};

return {
initial: `${initial}${fieldName}: ${
fieldIsList ? '' : 'head('
Expand All @@ -97,9 +110,10 @@ export const customCypherField = ({
resolveInfo,
cypherFieldParamsIndex
)}}, true) | ${nestedVariable} {${
fieldIsInterfaceType ? `FRAGMENT_TYPE: labels(${nestedVariable})[0],` : ''
fieldIsInterfaceType ? `${fragmentType(nestedVariable)},` : ''
}${subSelection[0]}}]${fieldIsList ? '' : ')'}${skipLimit} ${commaIfTail}`,
...tailParams
...tailParams,
...fragmentTypeParams
};
};

Expand Down Expand Up @@ -172,6 +186,17 @@ export const relationFieldOnNodeType = ({
schemaType,
filterParams
);
const fragmentTypeParams = isInlineFragment
? {
derivedTypes: getDerivedTypeNames(
resolveInfo.schema,
innerSchemaType.name
)
}
: {};

subSelection[1] = { ...subSelection[1], ...fragmentTypeParams };

return {
selection: {
initial: `${initial}${fieldName}: ${
Expand Down Expand Up @@ -200,7 +225,7 @@ export const relationFieldOnNodeType = ({
whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : ''
} | ${nestedVariable} {${
isInlineFragment
? `FRAGMENT_TYPE: labels(${nestedVariable})[0]${
? `${fragmentType(nestedVariable)}${
subSelection[0] ? `, ${subSelection[0]}` : ''
}`
: subSelection[0]
Expand Down Expand Up @@ -413,6 +438,15 @@ const directedNodeTypeFieldOnRelationType = ({
const toTypeName = schemaTypeRelation.to;
const isFromField = fieldName === fromTypeName || fieldName === 'from';
const isToField = fieldName === toTypeName || fieldName === 'to';
const fragmentTypeParams = isInlineFragment
? {
derivedTypes: getDerivedTypeNames(
resolveInfo.schema,
innerSchemaType.name
)
}
: {};
subSelection[1] = { ...subSelection[1], ...fragmentTypeParams };
// Since the translations are significantly different,
// we first check whether the relationship is reflexive
if (fromTypeName === toTypeName) {
Expand Down Expand Up @@ -469,7 +503,7 @@ const directedNodeTypeFieldOnRelationType = ({
: ''
}| ${relationshipVariableName} {${
isInlineFragment
? `FRAGMENT_TYPE: labels(${nestedVariable})[0]${
? `${fragmentType(nestedVariable)}${
subSelection[0] ? `, ${subSelection[0]}` : ''
}`
: subSelection[0]
Expand Down Expand Up @@ -527,7 +561,7 @@ const directedNodeTypeFieldOnRelationType = ({
: ''
}${queryParams}) | ${nestedVariable} {${
isInlineFragment
? `FRAGMENT_TYPE: labels(${nestedVariable})[0]${
? `${fragmentType(nestedVariable)}${
subSelection[0] ? `, ${subSelection[0]}` : ''
}`
: subSelection[0]
Expand Down Expand Up @@ -786,13 +820,16 @@ const customQuery = ({
// FIXME: fix subselection translation for temporal type payload
!isNeo4jTypeOutput && !isScalarType
? `{${
isInterfaceType
? `FRAGMENT_TYPE: labels(${safeVariableName})[0],`
: ''
isInterfaceType ? `${fragmentType(safeVariableName)},` : ''
}${subQuery}} AS ${safeVariableName}${orderByClause}`
: ''
}${outerSkipLimit}`;
return [query, params];

const fragmentTypeParams = isInterfaceType
? { derivedTypes: getDerivedTypeNames(resolveInfo.schema, schemaType.name) }
: {};

return [query, { ...params, ...fragmentTypeParams }];
};

// Generated API
Expand Down Expand Up @@ -872,16 +909,22 @@ const nodeQuery = ({
const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : '';

const { optimization, cypherPart: orderByClause } = orderByValue;
const fragmentTypeValue = isGraphqlInterfaceType(schemaType)
? `${fragmentType(safeVariableName)},`
: '';
const fragmentTypeParams = isGraphqlInterfaceType(schemaType)
? { derivedTypes: getDerivedTypeNames(resolveInfo.schema, schemaType.name) }
: {};

let query = `MATCH (${safeVariableName}:${safeLabelName}${
argString ? ` ${argString}` : ''
}) ${predicate}${
optimization.earlyOrderBy ? `WITH ${safeVariableName}${orderByClause}` : ''
}RETURN ${safeVariableName} {${subQuery}} AS ${safeVariableName}${
}RETURN ${safeVariableName} {${fragmentTypeValue}${subQuery}} AS ${safeVariableName}${
optimization.earlyOrderBy ? '' : orderByClause
}${outerSkipLimit}`;

return [query, params];
return [query, { ...params, ...fragmentTypeParams }];
};

// Mutation API root operation branch
Expand All @@ -902,6 +945,10 @@ export const translateMutation = ({
schemaType,
getCypherParams(context)
);
const interfaceLabels =
typeof schemaType.getInterfaces === 'function'
? schemaType.getInterfaces().map(i => i.name)
: [];
const mutationTypeCypherDirective = getMutationCypherDirective(resolveInfo);
const params = initializeMutationParams({
resolveInfo,
Expand Down Expand Up @@ -930,7 +977,7 @@ export const translateMutation = ({
...mutationInfo,
variableName,
typeName,
additionalLabels: additionalNodeLabels
additionalLabels: additionalNodeLabels.concat(interfaceLabels)
});
} else if (isUpdateMutation(resolveInfo)) {
return nodeUpdate({
Expand Down Expand Up @@ -1014,13 +1061,14 @@ const customMutation = ({
RETURN ${safeVariableName} ${
!isNeo4jTypeOutput && !isScalarType
? `{${
isInterfaceType
? `FRAGMENT_TYPE: labels(${safeVariableName})[0],`
: ''
isInterfaceType ? `${fragmentType(safeVariableName)},` : ''
}${subQuery}} AS ${safeVariableName}${orderByClause}${outerSkipLimit}`
: ''
}`;
return [query, params];
const fragmentTypeParams = isInterfaceType
? { derivedTypes: getDerivedTypeNames(resolveInfo.schema, schemaType.name) }
: {};
return [query, { ...params, ...fragmentTypeParams }];
};

// Generated API
Expand Down
9 changes: 8 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parse } from 'graphql';
import { isObjectType, parse } from 'graphql';
import { v1 as neo4j } from 'neo4j-driver';
import _ from 'lodash';
import filter from 'lodash/filter';
Expand Down Expand Up @@ -981,3 +981,10 @@ const _getNamedType = type => {
}
return type;
};

export const getDerivedTypeNames = (schema, interfaceName) => {
return Object.values(schema.getTypeMap())
.filter(t => isObjectType(t))
.filter(t => t.getInterfaces().some(i => i.name === interfaceName))
.map(t => t.name);
};
Loading