UNPKG

@neo4j/graphql

Version:

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

414 lines 18.9 kB
"use strict"; /* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const merge_1 = require("@graphql-tools/merge"); const resolvers_composition_1 = require("@graphql-tools/resolvers-composition"); const schema_1 = require("@graphql-tools/schema"); const utils_1 = require("@graphql-tools/utils"); const debug_1 = __importDefault(require("debug")); const constants_1 = require("../constants"); const schema_2 = require("../schema"); const generate_model_1 = require("../schema-model/generate-model"); const definition_collection_1 = require("../schema-model/parser/definition-collection"); const make_document_to_augment_1 = require("../schema/make-document-to-augment"); const wrap_query_and_mutation_1 = require("../schema/resolvers/composition/wrap-query-and-mutation"); const wrap_subscription_1 = require("../schema/resolvers/composition/wrap-subscription"); const defaultField_1 = require("../schema/resolvers/field/defaultField"); const validation_1 = require("../schema/validation"); const schema_validation_1 = require("../schema/validation/schema-validation"); const utils_2 = require("../utils/utils"); const ComplexityEstimatorHelper_1 = require("./ComplexityEstimatorHelper"); const Executor_1 = require("./Executor"); const Neo4jDatabaseInfo_1 = require("./Neo4jDatabaseInfo"); const Neo4jGraphQLAuthorization_1 = require("./authorization/Neo4jGraphQLAuthorization"); const Neo4jGraphQLSubscriptionsCDCEngine_1 = require("./subscription/Neo4jGraphQLSubscriptionsCDCEngine"); const asserts_indexes_and_constraints_1 = require("./utils/asserts-indexes-and-constraints"); const generate_resolvers_composition_1 = require("./utils/generate-resolvers-composition"); const verify_database_1 = __importDefault(require("./utils/verify-database")); class Neo4jGraphQL { constructor(input) { const { driver, features, typeDefs, resolvers, debug, validate = true } = input; this.driver = driver; this.features = this.parseNeo4jFeatures(features); this.typeDefs = typeDefs; this.resolvers = resolvers; this.debug = debug; this.validate = validate; this.checkEnableDebug(); if (this.features?.authorization) { const authorizationSettings = this.features?.authorization; this.authorization = new Neo4jGraphQLAuthorization_1.Neo4jGraphQLAuthorization(authorizationSettings); } this.complexityEstimatorHelper = new ComplexityEstimatorHelper_1.ComplexityEstimatorHelper(!!this.features.complexityEstimators); } async getSchema() { return this.getExecutableSchema(); } async getExecutableSchema() { if (!this.executableSchema) { this.executableSchema = this.generateExecutableSchema(); await this.subscriptionMechanismSetup(); } return this.executableSchema; } async getSubgraphSchema() { if (!this.subgraphSchema) { this.subgraphSchema = this.generateSubgraphSchema().catch((err) => { console.error("Error", err); return Promise.reject(err); }); await this.subscriptionMechanismSetup(); } return this.subgraphSchema; } async checkNeo4jCompat({ driver, sessionConfig, } = {}) { const neo4jDriver = driver || this.driver; if (!neo4jDriver) { throw new Error("neo4j-driver Driver missing"); } if (!this.dbInfo) { this.dbInfo = await this.getNeo4jDatabaseInfo(neo4jDriver, sessionConfig); } return (0, verify_database_1.default)({ driver: neo4jDriver, sessionConfig, dbInfo: this.dbInfo, }); } async assertIndexesAndConstraints({ driver, sessionConfig, } = {}) { if (!(this.executableSchema || this.subgraphSchema)) { throw new Error("You must await `.getSchema()` before `.assertIndexesAndConstraints()`"); } await (this.executableSchema || this.subgraphSchema); const neo4jDriver = driver || this.driver; if (!neo4jDriver) { throw new Error("neo4j-driver Driver missing"); } if (!this.dbInfo) { this.dbInfo = await this.getNeo4jDatabaseInfo(neo4jDriver, sessionConfig); } if (!this.schemaModel) { throw new Error("Schema Model is not defined"); } await (0, asserts_indexes_and_constraints_1.assertIndexesAndConstraints)({ driver: neo4jDriver, sessionConfig, schemaModel: this.schemaModel, }); } get nodes() { if (!this._nodes) { throw new Error("You must await `.getSchema()` before accessing `nodes`"); } return this._nodes; } get relationships() { if (!this._relationships) { throw new Error("You must await `.getSchema()` before accessing `relationships`"); } return this._relationships; } /** * Currently just merges all type definitions into a document. Eventual intention described below: * * Normalizes the user's type definitions using the method with the lowest risk of side effects: * - Type definitions of type `string` are parsed using the `parse` function from the reference GraphQL implementation. * - Type definitions of type `DocumentNode` are returned as they are. * - Type definitions in arrays are merged using `mergeTypeDefs` from `@graphql-tools/merge`. * - Callbacks are resolved to a type which can be parsed into a document. * * This method maps to the Type Definition Normalization stage of the Schema Generation lifecycle. * * @param {TypeDefinitions} typeDefinitions - The unnormalized type definitions. * @returns {DocumentNode} The normalized type definitons as a document. */ normalizeTypeDefinitions(typeDefinitions) { // TODO: The dream: minimal modification of the type definitions. However, this does not merge extensions, which we can't currently deal with in translation. // if (typeof typeDefinitions === "function") { // return this.normalizeTypeDefinitions(typeDefinitions()); // } // if (typeof typeDefinitions === "string") { // return parse(typeDefinitions); // } // if (Array.isArray(typeDefinitions)) { // return mergeTypeDefs(typeDefinitions); // } // return typeDefinitions; return (0, merge_1.mergeTypeDefs)(typeDefinitions); } addDefaultFieldResolvers(schema) { (0, utils_1.forEachField)(schema, (field) => { if (!field.resolve) { field.resolve = defaultField_1.defaultFieldResolver; } }); return schema; } checkEnableDebug() { if (this.debug === true || this.debug === false) { if (this.debug) { debug_1.default.enable(constants_1.DEBUG_ALL); } else { debug_1.default.disable(); } } } async getNeo4jDatabaseInfo(driver, sessionConfig) { const executorConstructorParam = { executionContext: driver, sessionConfig, }; return (0, Neo4jDatabaseInfo_1.getNeo4jDatabaseInfo)(new Executor_1.Executor(executorConstructorParam)); } wrapResolvers(resolvers) { if (!this.schemaModel) { throw new Error("Schema Model is not defined"); } const wrapResolverArgs = { driver: this.driver, nodes: this.nodes, relationships: this.relationships, schemaModel: this.schemaModel, features: this.features, authorization: this.authorization, jwtPayloadFieldsMap: this.jwtFieldsMap, }; const queryAndMutationWrappers = [(0, wrap_query_and_mutation_1.wrapQueryAndMutation)(wrapResolverArgs)]; const isSubscriptionEnabled = !!this.features.subscriptions; const wrapSubscriptionResolverArgs = { subscriptionsEngine: this.features.subscriptionsEngine, schemaModel: this.schemaModel, authorization: this.authorization, jwtPayloadFieldsMap: this.jwtFieldsMap, }; const subscriptionWrappers = isSubscriptionEnabled ? [(0, wrap_subscription_1.wrapSubscription)(wrapSubscriptionResolverArgs)] : []; const resolversComposition = (0, generate_resolvers_composition_1.generateResolverComposition)({ schemaModel: this.schemaModel, isSubscriptionEnabled, queryAndMutationWrappers, subscriptionWrappers, }); // Merge generated and custom resolvers // Merging must be done before composing because wrapper won't run otherwise const mergedResolvers = (0, merge_1.mergeResolvers)([...(0, utils_2.asArray)(resolvers), ...(0, utils_2.asArray)(this.resolvers)]); return (0, resolvers_composition_1.composeResolvers)(mergedResolvers, resolversComposition); } composeSchema(schema) { // TODO: Keeping this in our back pocket - if we want to add native support for middleware to the library // if (this.middlewares) { // schema = applyMiddleware(schema, ...this.middlewares); // } // Get resolvers from schema - this will include generated _entities and _service for Federation const resolvers = (0, utils_1.getResolversFromSchema)(schema); // Wrap the resolvers using resolvers composition const wrappedResolvers = this.wrapResolvers(resolvers); // Add the wrapped resolvers back to the schema, context will now be populated (0, schema_1.addResolversToSchema)({ schema, resolvers: wrappedResolvers, updateResolversInPlace: true }); return this.addDefaultFieldResolvers(schema); } parseNeo4jFeatures(features) { let subscriptionPlugin; if (features?.subscriptions === true) { if (!this.driver) { throw new Error("Driver required for CDC subscriptions"); } subscriptionPlugin = new Neo4jGraphQLSubscriptionsCDCEngine_1.Neo4jGraphQLSubscriptionsCDCEngine({ driver: this.driver, }); } else { subscriptionPlugin = features?.subscriptions || undefined; } return { ...features, subscriptionsEngine: subscriptionPlugin, }; } generateSchemaModel(document) { if (!this.schemaModel) { return (0, generate_model_1.generateModel)(document); } return this.schemaModel; } generateExecutableSchema() { return new Promise((resolve) => { const initialDocument = this.normalizeTypeDefinitions(this.typeDefs); if (this.validate) { const { enumTypes: enums, interfaceTypes: interfaces, unionTypes: unions, objectTypes: objects, } = (0, definition_collection_1.getDefinitionCollection)(initialDocument); (0, validation_1.validateDocument)({ document: initialDocument, features: this.features, additionalDefinitions: { enums: [...enums.values()], interfaces: [...interfaces.values()], unions: [...unions.values()], objects: [...objects.values()], }, userCustomResolvers: this.resolvers, }); } const { document, typesExcludedFromGeneration } = (0, make_document_to_augment_1.makeDocumentToAugment)(initialDocument); const { jwt } = typesExcludedFromGeneration; if (jwt) { this.jwtFieldsMap = jwt.jwtFieldsMap; } this.schemaModel = this.generateSchemaModel(document); const { nodes, relationships, typeDefs, resolvers } = (0, schema_2.makeAugmentedSchema)({ document, features: this.features, userCustomResolvers: this.resolvers, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, }); if (this.validate) { (0, schema_validation_1.validateUserDefinition)({ userDocument: document, augmentedDocument: typeDefs, jwt: jwt?.type, features: this.features, }); } this._nodes = nodes; this._relationships = relationships; const schema = (0, schema_1.makeExecutableSchema)({ typeDefs, resolvers, }); this.complexityEstimatorHelper.hydrateSchemaFromSDLWithASTNodeExtensions(schema); resolve(this.composeSchema(schema)); }); } async generateSubgraphSchema() { // Import only when needed to avoid issues if GraphQL 15 being used const { Subgraph } = await Promise.resolve().then(() => __importStar(require("./Subgraph"))); const initialDocument = this.normalizeTypeDefinitions(this.typeDefs); const subgraph = new Subgraph(this.typeDefs); const { directives, types } = subgraph.getValidationDefinitions(); if (this.validate) { const { enumTypes: enums, interfaceTypes: interfaces, unionTypes: unions, objectTypes: objects, } = (0, definition_collection_1.getDefinitionCollection)(initialDocument); (0, validation_1.validateDocument)({ document: initialDocument, features: this.features, additionalDefinitions: { additionalDirectives: directives, additionalTypes: types, enums: [...enums.values()], interfaces: [...interfaces.values()], unions: [...unions.values()], objects: [...objects.values()], }, userCustomResolvers: this.resolvers, }); } const { document, typesExcludedFromGeneration } = (0, make_document_to_augment_1.makeDocumentToAugment)(initialDocument); const { jwt } = typesExcludedFromGeneration; if (jwt) { this.jwtFieldsMap = jwt.jwtFieldsMap; } this.schemaModel = this.generateSchemaModel(document); const { nodes, relationships, typeDefs, resolvers } = (0, schema_2.makeAugmentedSchema)({ document, features: this.features, userCustomResolvers: this.resolvers, subgraph, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, }); if (this.validate) { (0, schema_validation_1.validateUserDefinition)({ userDocument: document, augmentedDocument: typeDefs, additionalDirectives: directives, additionalTypes: types, jwt: jwt?.type, features: this.features, }); } this._nodes = nodes; this._relationships = relationships; // TODO: Move into makeAugmentedSchema, add resolvers alongside other resolvers const referenceResolvers = subgraph.getReferenceResolvers(this.schemaModel); const schema = subgraph.buildSchema({ typeDefs, resolvers: (0, merge_1.mergeResolvers)([resolvers, referenceResolvers]), }); return this.composeSchema(schema); } subscriptionMechanismSetup() { if (this.subscriptionInit) { return this.subscriptionInit; } const setup = async () => { const subscriptionsEngine = this.features?.subscriptionsEngine; const schema = await this.executableSchema; if (subscriptionsEngine && schema?.getSubscriptionType()) { subscriptionsEngine.events.setMaxListeners(0); // Removes warning regarding leak. >10 listeners are expected if (subscriptionsEngine.init) { if (!this.schemaModel) throw new Error("SchemaModel not available on subscription mechanism"); await subscriptionsEngine.init({ schemaModel: this.schemaModel }); } } }; this.subscriptionInit = setup(); return this.subscriptionInit; } } exports.default = Neo4jGraphQL; //# sourceMappingURL=Neo4jGraphQL.js.map