UNPKG

@neo4j/graphql

Version:

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations

372 lines 19.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateModel = generateModel; const classes_1 = require("../classes"); const directives_1 = require("../graphql/directives"); const get_field_type_meta_1 = __importDefault(require("../schema/get-field-type-meta")); const utils_1 = require("../schema/validation/custom-rules/utils/utils"); const validate_schema_model_1 = require("../schema/validation/validate-schema-model"); const is_in_array_1 = require("../utils/is-in-array"); const utils_2 = require("../utils/utils"); const Neo4jGraphQLSchemaModel_1 = require("./Neo4jGraphQLSchemaModel"); const Operation_1 = require("./Operation"); const AttributeType_1 = require("./attribute/AttributeType"); const ConcreteEntity_1 = require("./entity/ConcreteEntity"); const InterfaceEntity_1 = require("./entity/InterfaceEntity"); const UnionEntity_1 = require("./entity/UnionEntity"); const library_directives_1 = require("./library-directives"); const definition_collection_1 = require("./parser/definition-collection"); const parse_annotation_1 = require("./parser/parse-annotation"); const parse_arguments_1 = require("./parser/parse-arguments"); const parse_attribute_1 = require("./parser/parse-attribute"); const utils_3 = require("./parser/utils"); const Relationship_1 = require("./relationship/Relationship"); const RelationshipDeclaration_1 = require("./relationship/RelationshipDeclaration"); function generateModel(document) { const definitionCollection = (0, definition_collection_1.getDefinitionCollection)(document); const operations = definitionCollection.operations.reduce((acc, definition) => { acc[definition.name.value] = generateOperation(definition, definitionCollection); return acc; }, {}); // hydrate interface to typeNames map hydrateInterfacesToTypeNamesMap(definitionCollection); const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => generateConcreteEntity(node, definitionCollection)); const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { acc.set(entity.name, entity); return acc; }, new Map()); const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map(([name, concreteEntities]) => { const interfaceNode = definitionCollection.interfaceTypes.get(name); if (!interfaceNode) { throw new Error(`Cannot find interface ${name}`); } return generateInterfaceEntity(name, interfaceNode, concreteEntities, concreteEntitiesMap, definitionCollection); }); const unionEntities = Array.from(definitionCollection.unionTypes).map(([unionName, unionDefinition]) => { return generateUnionEntity(unionName, unionDefinition, unionDefinition.types?.map((t) => t.name.value) || [], concreteEntitiesMap); }); const annotations = (0, parse_annotation_1.parseAnnotations)(definitionCollection.schemaDirectives); const schema = new Neo4jGraphQLSchemaModel_1.Neo4jGraphQLSchemaModel({ compositeEntities: [...unionEntities, ...interfaceEntities], concreteEntities, operations, annotations, }); (0, validate_schema_model_1.validateSchemaModel)(schema); definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); hydrateCypherAnnotations(schema, concreteEntities); definitionCollection.interfaceTypes.forEach((def) => hydrateRelationshipDeclarations(def, schema, definitionCollection)); addCompositeEntitiesToConcreteEntity(interfaceEntities); addCompositeEntitiesToConcreteEntity(unionEntities); return schema; } function addCompositeEntitiesToConcreteEntity(compositeEntities) { compositeEntities.forEach((compositeEntity) => { compositeEntity.concreteEntities.forEach((concreteEntity) => concreteEntity.addCompositeEntities(compositeEntity)); }); } function getCypherTarget(schema, attributeType, attributeName) { if (attributeType instanceof AttributeType_1.ListType) { return getCypherTarget(schema, attributeType.ofType, attributeName); } if (attributeType instanceof AttributeType_1.ObjectType) { const foundConcreteEntity = schema.getConcreteEntity(attributeType.name); if (!foundConcreteEntity) { return undefined; } return schema.getConcreteEntity(attributeType.name); } if (attributeType instanceof AttributeType_1.InterfaceType) { const compositeEntity = schema.compositeEntities.find((e) => e.name === attributeType.name); if (!compositeEntity || compositeEntity.concreteEntities.length === 0) { throw new classes_1.Neo4jGraphQLSchemaValidationError(`@cypher field ${attributeName} must target interface (${attributeType.name}) implemented by types annotated with the @node directive`); } } } // TODO: currently the below is used only for Filtering purposes, and therefore the target is set only for ObjectTypes but in the future we might want to use it for other types as well function hydrateCypherAnnotations(schema, concreteEntities) { for (const concreteEntity of concreteEntities) { for (const attributeField of concreteEntity.attributes.values()) { if (attributeField.annotations.cypher) { const target = getCypherTarget(schema, attributeField.type, attributeField.name); attributeField.annotations.cypher.targetEntity = target; } } } } function hydrateInterfacesToTypeNamesMap(definitionCollection) { return definitionCollection.nodes.forEach((node) => { if (!node.interfaces) { return; } const objectTypeName = node.name.value; node.interfaces?.forEach((i) => { const interfaceTypeName = i.name.value; const concreteEntities = definitionCollection.interfaceToImplementingTypeNamesMap.get(interfaceTypeName); if (!concreteEntities) { throw new classes_1.Neo4jGraphQLSchemaValidationError(`Could not find composite entity with name ${interfaceTypeName}`); } // TODO: modify the existing array instead of creating a new one definitionCollection.interfaceToImplementingTypeNamesMap.set(interfaceTypeName, concreteEntities.concat(objectTypeName)); }); }); } function generateUnionEntity(entityDefinitionName, unionDefinition, entityImplementingTypeNames, concreteEntities) { const unionEntity = generateCompositeEntity(entityDefinitionName, entityImplementingTypeNames, concreteEntities); const annotations = (0, parse_annotation_1.parseAnnotations)(unionDefinition.directives || []); return new UnionEntity_1.UnionEntity({ ...unionEntity, annotations }); } function generateInterfaceEntity(entityDefinitionName, definition, entityImplementingTypeNames, concreteEntities, definitionCollection) { const interfaceEntity = generateCompositeEntity(entityDefinitionName, entityImplementingTypeNames, concreteEntities); const fields = (definition.fields || []).map((fieldDefinition) => { const isRelationshipAttribute = (0, utils_3.findDirective)(fieldDefinition.directives, directives_1.declareRelationshipDirective.name); if (isRelationshipAttribute) { return; } return (0, parse_attribute_1.parseAttribute)(fieldDefinition, definitionCollection, definition.fields); }); const annotations = (0, parse_annotation_1.parseAnnotations)(definition.directives || []); return new InterfaceEntity_1.InterfaceEntity({ ...interfaceEntity, description: definition.description?.value, attributes: (0, utils_2.filterTruthy)(fields), annotations, }); } function generateCompositeEntity(entityDefinitionName, entityImplementingTypeNames, concreteEntities) { const compositeFields = (0, utils_2.filterTruthy)(entityImplementingTypeNames.map((type) => { return concreteEntities.get(type); })); return { name: entityDefinitionName, concreteEntities: compositeFields, }; } function hydrateRelationships(definition, schema, definitionCollection) { const name = definition.name.value; const entity = schema.getEntity(name); if (!entity) { throw new Error(`Cannot find entity ${name}`); } if (!(entity instanceof ConcreteEntity_1.ConcreteEntity)) { throw new Error(`Can only add relationship to concrete entity and ${name} is not concrete.`); } if (!definition.fields?.length) { return; } for (const fieldDefinition of definition.fields) { const { firstDeclaredInTypeName, originalTarget } = getFirstDeclaration(definition, fieldDefinition.name.value, definitionCollection, schema); const relationshipField = generateRelationshipField(fieldDefinition, schema, entity, definitionCollection, firstDeclaredInTypeName, originalTarget); if (relationshipField) { entity.addRelationship(relationshipField); } } } function hydrateRelationshipDeclarations(definition, schema, definitionCollection) { const name = definition.name.value; const entity = schema.getEntity(name); if (!entity) { throw new Error(`Cannot find entity ${name}`); } if (!(entity instanceof InterfaceEntity_1.InterfaceEntity)) { throw new Error(`Can only declare relationships on Interface entities and ${name} is not an Interface.`); } if (!definition.fields?.length) { return; } for (const fieldDefinition of definition.fields) { const { firstDeclaredInTypeName } = getFirstDeclaration(definition, fieldDefinition.name.value, definitionCollection, schema); const relationshipField = generateRelationshipDeclaration(fieldDefinition, schema, entity, definitionCollection, firstDeclaredInTypeName); if (relationshipField) { entity.addRelationshipDeclaration(relationshipField); const allImplementationsPropertiesTypeNames = (0, utils_2.filterTruthy)(relationshipField.relationshipImplementations.map((impl) => impl.propertiesTypeName)); for (const impl of relationshipField.relationshipImplementations) { impl.setSiblings(allImplementationsPropertiesTypeNames); } } } } function getFieldDeclaredAsRelationship(interfaceDef, fieldName) { const fields = interfaceDef?.fields || []; return fields.find((field) => field.name.value === fieldName && field.directives?.some((d) => d.name.value === "declareRelationship")); } function getDefinitionNodeFromNamedNode(interfaceNamedNode, definitionCollection) { const interfaceName = interfaceNamedNode.name.value; return definitionCollection.interfaceTypes.get(interfaceName); } /** * Goes up the inheritance chain checking for the field to have the @relationshipDeclaration directive * Finds the first interface that declares the field as a relationship * Returns the name of the first interface and the target of the relationship declaration in that first interface * * @param definition Entity with relationship field (Starting point) * @param fieldName Relationship field name (The one we look for first declaration for) * @param definitionCollection * @param schema * @returns Info about the interface at the top of the chain, nullable because there might not be any */ function getFirstDeclaration(definition, fieldName, definitionCollection, schema) { if (!definition || !definition.interfaces) { return {}; } let inheritedInterfaceWithDeclaredField; let declaredFieldTypeName; for (const interfaceNamedNode of definition.interfaces) { const interfaceDef = getDefinitionNodeFromNamedNode(interfaceNamedNode, definitionCollection); const declaredRelationshipField = getFieldDeclaredAsRelationship(interfaceDef, fieldName); if (declaredRelationshipField) { inheritedInterfaceWithDeclaredField = interfaceNamedNode; declaredFieldTypeName = (0, utils_1.getInnerTypeName)(declaredRelationshipField.type); } } if (!inheritedInterfaceWithDeclaredField) { // definition declares it first return {}; } // found implemented interface that declares it const currentInChain = { originalTarget: schema.getEntity(declaredFieldTypeName || ""), firstDeclaredInTypeName: inheritedInterfaceWithDeclaredField.name.value, }; // attempt to go up in chain const interfaceDef = getDefinitionNodeFromNamedNode(inheritedInterfaceWithDeclaredField, definitionCollection); const prevInChain = getFirstDeclaration(interfaceDef, fieldName, definitionCollection, schema); if (prevInChain.firstDeclaredInTypeName) { // found interface that declares it up in chain return prevInChain; } // this interface declares it first return currentInChain; } function generateRelationshipField(field, schema, source, definitionCollection, firstDeclaredInTypeName, originalTarget) { // TODO: remove reference to getFieldTypeMeta const fieldTypeMeta = (0, get_field_type_meta_1.default)(field.type); const relationshipUsage = (0, utils_3.findDirective)(field.directives, "relationship"); if (!relationshipUsage) return undefined; const fieldName = field.name.value; const relatedEntityName = fieldTypeMeta.name; const relatedToEntity = schema.getEntity(relatedEntityName); if (!relatedToEntity) throw new Error(`Entity ${relatedEntityName} Not Found`); const { type, direction, properties, queryDirection, nestedOperations, aggregate } = (0, parse_arguments_1.parseArguments)(directives_1.relationshipDirective, relationshipUsage); let attributes = []; let propertiesTypeName = undefined; if (properties && typeof properties === "string") { const propertyInterface = definitionCollection.relationshipProperties.get(properties); if (!propertyInterface) { throw new Error(`The \`@relationshipProperties\` directive could not be found on the \`${properties}\` interface`); } propertiesTypeName = properties; const fields = (propertyInterface.fields || []).map((fieldDefinition) => { return (0, parse_attribute_1.parseAttribute)(fieldDefinition, definitionCollection, propertyInterface.fields); }); attributes = (0, utils_2.filterTruthy)(fields); } const annotations = (0, parse_annotation_1.parseAnnotations)(field.directives || []); const args = (0, parse_attribute_1.parseAttributeArguments)(field.arguments || [], definitionCollection); return new Relationship_1.Relationship({ name: fieldName, type, args, attributes, source, target: relatedToEntity, direction, isList: Boolean(fieldTypeMeta.array), queryDirection, nestedOperations, aggregate, isNullable: !fieldTypeMeta.required, description: field.description?.value, annotations: annotations, propertiesTypeName, firstDeclaredInTypeName, originalTarget, }); } function generateRelationshipDeclaration(field, schema, source, definitionCollection, firstDeclaredInTypeName) { // TODO: remove reference to getFieldTypeMeta const fieldTypeMeta = (0, get_field_type_meta_1.default)(field.type); const declareRelationshipUsage = (0, utils_3.findDirective)(field.directives, "declareRelationship"); if (!declareRelationshipUsage) { return; } const fieldName = field.name.value; const relatedEntityName = fieldTypeMeta.name; const relatedToEntity = schema.getEntity(relatedEntityName); if (!relatedToEntity) { throw new Error(`Entity ${relatedEntityName} Not Found`); } const { nestedOperations, aggregate } = (0, parse_arguments_1.parseArguments)(directives_1.declareRelationshipDirective, declareRelationshipUsage); const annotations = (0, parse_annotation_1.parseAnnotations)(field.directives || []); const relationshipImplementations = source.concreteEntities .map((concreteEntity) => concreteEntity.findRelationship(fieldName)) .filter((x) => x); return new RelationshipDeclaration_1.RelationshipDeclaration({ name: fieldName, source, target: relatedToEntity, isList: Boolean(fieldTypeMeta.array), nestedOperations, aggregate, isNullable: !fieldTypeMeta.required, description: field.description?.value, args: (0, parse_attribute_1.parseAttributeArguments)(field.arguments || [], definitionCollection), annotations, relationshipImplementations, firstDeclaredInTypeName, }); } function generateConcreteEntity(definition, definitionCollection) { const fields = (definition.fields || []).map((fieldDefinition) => { const isRelationshipAttribute = (0, utils_3.findDirective)(fieldDefinition.directives, directives_1.relationshipDirective.name); if (isRelationshipAttribute) { return; } return (0, parse_attribute_1.parseAttribute)(fieldDefinition, definitionCollection, definition.fields); }); // schema configuration directives are propagated onto concrete entities const schemaDirectives = definitionCollection.schemaExtensions?.directives?.filter((x) => (0, is_in_array_1.isInArray)(library_directives_1.SCHEMA_CONFIGURATION_OBJECT_DIRECTIVES, x.name.value)); const annotations = (0, parse_annotation_1.parseAnnotations)((definition.directives || []).concat(schemaDirectives || [])); return new ConcreteEntity_1.ConcreteEntity({ name: definition.name.value, description: definition.description?.value, labels: getLabels(definition), attributes: (0, utils_2.filterTruthy)(fields), annotations, }); } function getLabels(entityDefinition) { const nodeDirectiveUsage = (0, utils_3.findDirective)(entityDefinition.directives, directives_1.nodeDirective.name); if (nodeDirectiveUsage) { const nodeArguments = (0, parse_arguments_1.parseArguments)(directives_1.nodeDirective, nodeDirectiveUsage); if (nodeArguments.labels?.length) { return nodeArguments.labels; } } return [entityDefinition.name.value]; } function generateOperation(definition, definitionCollection) { const { attributes, userResolvedAttributes } = (definition.fields || []) .map((fieldDefinition) => (0, parse_attribute_1.parseAttribute)(fieldDefinition, definitionCollection)) .reduce((acc, attribute) => { if (attribute.annotations.cypher) { acc.attributes.push(attribute); } else { acc.userResolvedAttributes.push(attribute); } return acc; }, { attributes: [], userResolvedAttributes: [] }); return new Operation_1.Operation({ name: definition.name.value, attributes, userResolvedAttributes, annotations: (0, parse_annotation_1.parseAnnotations)(definition.directives || []), }); } //# sourceMappingURL=generate-model.js.map