From 23729036ac6f81365cc7648a9654fea444d2b604 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 14:32:43 -0700 Subject: [PATCH 01/37] Update for augmentation refactor --- test/helpers/cypherTestHelpers.js | 79 +++++++++++-------------------- 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index c02145ae..232e44f7 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -1,11 +1,12 @@ import { cypherQuery, cypherMutation, - augmentSchema, - augmentTypeDefs + augmentTypeDefs, + makeAugmentedSchema } from '../../src/index'; -import { printTypeMap, extractTypeMapFromTypeDefs } from '../../src/utils'; -import { augmentTypeMap } from '../../src/augment'; +import { + printSchemaDocument +} from '../../src/augment/augment'; import { graphql } from 'graphql'; import { makeExecutableSchema } from 'graphql-tools'; import { testSchema } from './testSchema'; @@ -81,9 +82,9 @@ type Mutation { customWithArguments: checkCypherMutation } }; - + let augmentedTypeDefs = augmentTypeDefs(testMovieSchema, { auth: true }); const schema = makeExecutableSchema({ - typeDefs: augmentTypeDefs(testMovieSchema, { auth: true }), + typeDefs: augmentedTypeDefs, resolvers, resolverValidationOptions: { requireResolversForResolveType: false @@ -105,28 +106,28 @@ type Mutation { } // Optimization to prevent schema augmentation from running for every test -const typeMap = extractTypeMapFromTypeDefs(testSchema); -const augmentedTypeMap = augmentTypeMap( - typeMap, - { - // These custom field resolvers exist only for generating - // @neo4j_ignore directives used in a few tests - Movie: { - customField(object, params, ctx, resolveInfo) { - return ''; +const cypherTestTypeDefs = printSchemaDocument({ + schema: makeAugmentedSchema({ + typeDefs: testSchema, + resolvers: { + // These custom field resolvers exist only for generating + // @neo4j_ignore directives used in a few tests + Movie: { + customField(object, params, ctx, resolveInfo) { + return ''; + } + }, + State: { + customField(object, params, ctx, resolveInfo) { + return ''; + } } }, - State: { - customField(object, params, ctx, resolveInfo) { - return ''; - } + config: { + auth: true } - }, - { - auth: true - } -); -const augmentedSchemaCypherTestRunnerTypeDefs = printTypeMap(augmentedTypeMap); + }) +}); export function augmentedSchemaCypherTestRunner( t, @@ -200,7 +201,7 @@ export function augmentedSchemaCypherTestRunner( }; const augmentedSchema = makeExecutableSchema({ - typeDefs: augmentedSchemaCypherTestRunnerTypeDefs, + typeDefs: cypherTestTypeDefs, resolvers, resolverValidationOptions: { requireResolversForResolveType: false @@ -219,29 +220,3 @@ export function augmentedSchemaCypherTestRunner( graphqlParams ); } - -const augmentedSchemaTypeDefs = augmentTypeDefs(testSchema, { - auth: true -}); - -export function augmentedSchema() { - const schema = makeExecutableSchema({ - typeDefs: augmentedSchemaTypeDefs, - resolvers: { - Movie: { - customField(object, params, ctx, resolveInfo) { - return ''; - } - }, - State: { - customField(object, params, ctx, resolveInfo) { - return ''; - } - } - }, - resolverValidationOptions: { - requireResolversForResolveType: false - } - }); - return augmentSchema(schema); -} From 8f3e41fb7e2bcbb22248dfc3845135c5876e7d4b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 14:34:41 -0700 Subject: [PATCH 02/37] Update for augmentation refactor --- test/helpers/testSchema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 900436c0..ebbb42ed 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -105,7 +105,7 @@ export const testSchema = ` favorites: [Movie] @relation(name: "FAVORITED", direction: "OUT") } - type FriendOf { + type FriendOf @relation { from: User currentUserId: String @cypher( @@ -121,7 +121,7 @@ export const testSchema = ` to: User } - type Rated { + type Rated @relation { from: User currentUserId(strArg: String): String @cypher( From 3834f4954a297826d6d58ed628985e5a44d799d8 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 14:43:12 -0700 Subject: [PATCH 03/37] formatting fix --- test/helpers/cypherTestHelpers.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 232e44f7..527650ba 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -4,9 +4,7 @@ import { augmentTypeDefs, makeAugmentedSchema } from '../../src/index'; -import { - printSchemaDocument -} from '../../src/augment/augment'; +import { printSchemaDocument } from '../../src/augment/augment'; import { graphql } from 'graphql'; import { makeExecutableSchema } from 'graphql-tools'; import { testSchema } from './testSchema'; @@ -106,9 +104,9 @@ type Mutation { } // Optimization to prevent schema augmentation from running for every test -const cypherTestTypeDefs = printSchemaDocument({ +const cypherTestTypeDefs = printSchemaDocument({ schema: makeAugmentedSchema({ - typeDefs: testSchema, + typeDefs: testSchema, resolvers: { // These custom field resolvers exist only for generating // @neo4j_ignore directives used in a few tests From c8c904214f3995ab8d94de4204d0d2a44bd8a294 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 14:45:16 -0700 Subject: [PATCH 04/37] Update for augmentation refactor --- test/unit/augmentSchemaTest.test.js | 2340 ++++++++++++++------------- 1 file changed, 1206 insertions(+), 1134 deletions(-) diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 5624f1a1..7a218245 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -1,1139 +1,1211 @@ import test from 'ava'; -import { augmentedSchema } from '../helpers/cypherTestHelpers'; -import { printSchema } from 'graphql'; +import { printSchema, parse, print } from 'graphql'; +import { makeExecutableSchema } from 'graphql-tools'; +import { + augmentedSchema, + mapDefinitions, + mergeDefinitionMaps +} from '../../src/augment/augment'; +import { buildDocument } from '../../src/augment/ast'; +import { testSchema } from '../helpers/testSchema'; +import { augmentDirectiveDefinitions } from '../../src/augment/directives'; test.cb('Test augmented schema', t => { - let schema = augmentedSchema(); - let expectedSchema = `directive @cypher(statement: String) on FIELD_DEFINITION - -directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT - -directive @additionalLabels(labels: [String]) on OBJECT - -directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION - -directive @neo4j_ignore on FIELD_DEFINITION - -directive @isAuthenticated on OBJECT | FIELD_DEFINITION - -directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION - -directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION - -input _ActorFilter { - AND: [_ActorFilter!] - OR: [_ActorFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter -} - -input _ActorInput { - userId: ID! -} - -enum _ActorOrdering { - userId_asc - userId_desc - name_asc - name_desc - _id_asc - _id_desc -} - -type _AddActorMoviesPayload { - from: Actor - to: Movie -} - -type _AddGenreMoviesPayload { - from: Movie - to: Genre -} - -type _AddMovieActorsPayload { - from: Actor - to: Movie -} - -type _AddMovieFilmedInPayload { - from: Movie - to: State -} - -type _AddMovieGenresPayload { - from: Movie - to: Genre -} - -type _AddMovieRatingsPayload { - from: User - to: Movie - currentUserId: String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] -} - -type _AddTemporalNodeTemporalNodesPayload { - from: TemporalNode - to: TemporalNode -} - -type _AddUserFavoritesPayload { - from: User - to: Movie -} - -type _AddUserFriendsPayload { - from: User - to: User - currentUserId: String - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime -} - -type _AddUserRatedPayload { - from: User - to: Movie - currentUserId: String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] -} - -input _BookFilter { - AND: [_BookFilter!] - OR: [_BookFilter!] - genre: BookGenre - genre_not: BookGenre - genre_in: [BookGenre!] - genre_not_in: [BookGenre!] -} - -input _BookInput { - genre: BookGenre! -} - -enum _BookOrdering { - genre_asc - genre_desc - _id_asc - _id_desc -} - -input _currentUserIdFilter { - AND: [_currentUserIdFilter!] - OR: [_currentUserIdFilter!] - userId: String - userId_not: String - userId_in: [String!] - userId_not_in: [String!] - userId_contains: String - userId_not_contains: String - userId_starts_with: String - userId_not_starts_with: String - userId_ends_with: String - userId_not_ends_with: String -} - -input _currentUserIdInput { - userId: String! -} - -enum _currentUserIdOrdering { - userId_asc - userId_desc - _id_asc - _id_desc -} - -input _FriendOfDirectionsFilter { - from: _FriendOfFilter - to: _FriendOfFilter -} - -input _FriendOfFilter { - AND: [_FriendOfFilter!] - OR: [_FriendOfFilter!] - since: Int - since_not: Int - since_in: [Int!] - since_not_in: [Int!] - since_lt: Int - since_lte: Int - since_gt: Int - since_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter -} - -input _FriendOfInput { - since: Int - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetimes: [_Neo4jDateTimeInput] - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput -} - -input _GenreFilter { - AND: [_GenreFilter!] - OR: [_GenreFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter -} - -input _GenreInput { - name: String! -} - -enum _GenreOrdering { - name_desc - name_asc -} - -input _MovieFilter { - AND: [_MovieFilter!] - OR: [_MovieFilter!] - movieId: ID - movieId_not: ID - movieId_in: [ID!] - movieId_not_in: [ID!] - movieId_contains: ID - movieId_not_contains: ID - movieId_starts_with: ID - movieId_not_starts_with: ID - movieId_ends_with: ID - movieId_not_ends_with: ID - title: String - title_not: String - title_in: [String!] - title_not_in: [String!] - title_contains: String - title_not_contains: String - title_starts_with: String - title_not_starts_with: String - title_ends_with: String - title_not_ends_with: String - someprefix_title_with_underscores: String - someprefix_title_with_underscores_not: String - someprefix_title_with_underscores_in: [String!] - someprefix_title_with_underscores_not_in: [String!] - someprefix_title_with_underscores_contains: String - someprefix_title_with_underscores_not_contains: String - someprefix_title_with_underscores_starts_with: String - someprefix_title_with_underscores_not_starts_with: String - someprefix_title_with_underscores_ends_with: String - someprefix_title_with_underscores_not_ends_with: String - year: Int - year_not: Int - year_in: [Int!] - year_not_in: [Int!] - year_lt: Int - year_lte: Int - year_gt: Int - year_gte: Int - released: _Neo4jDateTimeInput - released_not: _Neo4jDateTimeInput - released_in: [_Neo4jDateTimeInput!] - released_not_in: [_Neo4jDateTimeInput!] - released_lt: _Neo4jDateTimeInput - released_lte: _Neo4jDateTimeInput - released_gt: _Neo4jDateTimeInput - released_gte: _Neo4jDateTimeInput - plot: String - plot_not: String - plot_in: [String!] - plot_not_in: [String!] - plot_contains: String - plot_not_contains: String - plot_starts_with: String - plot_not_starts_with: String - plot_ends_with: String - plot_not_ends_with: String - poster: String - poster_not: String - poster_in: [String!] - poster_not_in: [String!] - poster_contains: String - poster_not_contains: String - poster_starts_with: String - poster_not_starts_with: String - poster_ends_with: String - poster_not_ends_with: String - imdbRating: Float - imdbRating_not: Float - imdbRating_in: [Float!] - imdbRating_not_in: [Float!] - imdbRating_lt: Float - imdbRating_lte: Float - imdbRating_gt: Float - imdbRating_gte: Float - genres: _GenreFilter - genres_not: _GenreFilter - genres_in: [_GenreFilter!] - genres_not_in: [_GenreFilter!] - genres_some: _GenreFilter - genres_none: _GenreFilter - genres_single: _GenreFilter - genres_every: _GenreFilter - actors: _ActorFilter - actors_not: _ActorFilter - actors_in: [_ActorFilter!] - actors_not_in: [_ActorFilter!] - actors_some: _ActorFilter - actors_none: _ActorFilter - actors_single: _ActorFilter - actors_every: _ActorFilter - avgStars: Float - avgStars_not: Float - avgStars_in: [Float!] - avgStars_not_in: [Float!] - avgStars_lt: Float - avgStars_lte: Float - avgStars_gt: Float - avgStars_gte: Float - filmedIn: _StateFilter - filmedIn_not: _StateFilter - filmedIn_in: [_StateFilter!] - filmedIn_not_in: [_StateFilter!] - ratings: _MovieRatedFilter - ratings_not: _MovieRatedFilter - ratings_in: [_MovieRatedFilter!] - ratings_not_in: [_MovieRatedFilter!] - ratings_some: _MovieRatedFilter - ratings_none: _MovieRatedFilter - ratings_single: _MovieRatedFilter - ratings_every: _MovieRatedFilter -} - -input _MovieInput { - movieId: ID! -} - -enum _MovieOrdering { - title_desc - title_asc -} - -input _MovieRatedFilter { - AND: [_MovieRatedFilter!] - OR: [_MovieRatedFilter!] - rating: Int - rating_not: Int - rating_in: [Int!] - rating_not_in: [Int!] - rating_lt: Int - rating_lte: Int - rating_gt: Int - rating_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter -} - -type _MovieRatings { - currentUserId(strArg: String): String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - User: User -} - -type _Neo4jDate { - year: Int - month: Int - day: Int - formatted: String -} - -input _Neo4jDateInput { - year: Int - month: Int - day: Int - formatted: String -} - -type _Neo4jDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String -} - -input _Neo4jDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String -} - -type _Neo4jLocalDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String -} - -input _Neo4jLocalDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String -} - -type _Neo4jLocalTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String -} - -input _Neo4jLocalTimeInput { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String -} - -type _Neo4jTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String -} - -input _Neo4jTimeInput { - hour: Int - minute: Int - second: Int - nanosecond: Int - millisecond: Int - microsecond: Int - timezone: String - formatted: String -} - -input _RatedInput { - rating: Int - ratings: [Int] - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - datetimes: [_Neo4jDateTimeInput] -} - -enum _RelationDirections { - IN - OUT -} - -type _RemoveActorMoviesPayload { - from: Actor - to: Movie -} - -type _RemoveGenreMoviesPayload { - from: Movie - to: Genre -} - -type _RemoveMovieActorsPayload { - from: Actor - to: Movie -} - -type _RemoveMovieFilmedInPayload { - from: Movie - to: State -} - -type _RemoveMovieGenresPayload { - from: Movie - to: Genre -} - -type _RemoveMovieRatingsPayload { - from: User - to: Movie -} - -type _RemoveTemporalNodeTemporalNodesPayload { - from: TemporalNode - to: TemporalNode -} - -type _RemoveUserFavoritesPayload { - from: User - to: Movie -} - -type _RemoveUserFriendsPayload { - from: User - to: User -} - -type _RemoveUserRatedPayload { - from: User - to: Movie -} - -input _StateFilter { - AND: [_StateFilter!] - OR: [_StateFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String -} - -input _StateInput { - name: String! -} - -enum _StateOrdering { - name_asc - name_desc - _id_asc - _id_desc -} - -input _TemporalNodeFilter { - AND: [_TemporalNodeFilter!] - OR: [_TemporalNodeFilter!] - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - temporalNodes: _TemporalNodeFilter - temporalNodes_not: _TemporalNodeFilter - temporalNodes_in: [_TemporalNodeFilter!] - temporalNodes_not_in: [_TemporalNodeFilter!] - temporalNodes_some: _TemporalNodeFilter - temporalNodes_none: _TemporalNodeFilter - temporalNodes_single: _TemporalNodeFilter - temporalNodes_every: _TemporalNodeFilter -} - -input _TemporalNodeInput { - datetime: _Neo4jDateTimeInput! -} - -enum _TemporalNodeOrdering { - datetime_asc - datetime_desc - name_asc - name_desc - time_asc - time_desc - date_asc - date_desc - localtime_asc - localtime_desc - localdatetime_asc - localdatetime_desc - computedTimestamp_asc - computedTimestamp_desc - _id_asc - _id_desc -} - -input _UserFilter { - AND: [_UserFilter!] - OR: [_UserFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - rated: _UserRatedFilter - rated_not: _UserRatedFilter - rated_in: [_UserRatedFilter!] - rated_not_in: [_UserRatedFilter!] - rated_some: _UserRatedFilter - rated_none: _UserRatedFilter - rated_single: _UserRatedFilter - rated_every: _UserRatedFilter - friends: _FriendOfDirectionsFilter - friends_not: _FriendOfDirectionsFilter - friends_in: [_FriendOfDirectionsFilter!] - friends_not_in: [_FriendOfDirectionsFilter!] - friends_some: _FriendOfDirectionsFilter - friends_none: _FriendOfDirectionsFilter - friends_single: _FriendOfDirectionsFilter - friends_every: _FriendOfDirectionsFilter - favorites: _MovieFilter - favorites_not: _MovieFilter - favorites_in: [_MovieFilter!] - favorites_not_in: [_MovieFilter!] - favorites_some: _MovieFilter - favorites_none: _MovieFilter - favorites_single: _MovieFilter - favorites_every: _MovieFilter -} - -type _UserFriends { - currentUserId: String - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - User: User -} - -type _UserFriendsDirections { - from(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] - to(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] -} - -input _UserInput { - userId: ID! -} - -enum _UserOrdering { - userId_asc - userId_desc - name_asc - name_desc - currentUserId_asc - currentUserId_desc - _id_asc - _id_desc -} - -type _UserRated { - currentUserId(strArg: String): String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - Movie: Movie -} - -input _UserRatedFilter { - AND: [_UserRatedFilter!] - OR: [_UserRatedFilter!] - rating: Int - rating_not: Int - rating_in: [Int!] - rating_not_in: [Int!] - rating_lt: Int - rating_lte: Int - rating_gt: Int - rating_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - Movie: _MovieFilter -} - -type Actor implements Person { - userId: ID! - name: String - movies(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - _id: String -} - -type Book { - genre: BookGenre - _id: String -} - -enum BookGenre { - Mystery - Science - Math -} - -type currentUserId { - userId: String - _id: String -} - -scalar Date - -scalar DateTime - -type FriendOf { - from: User - currentUserId: String - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - to: User -} - -type Genre { - _id: String - name: String - movies(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - highestRatedMovie: Movie -} - -type ignoredType { - ignoredField: String -} - -scalar LocalDateTime - -scalar LocalTime - -type Movie { - _id: String - movieId: ID! - title: String - someprefix_title_with_underscores: String - year: Int - released: _Neo4jDateTime! - plot: String - poster: String - imdbRating: Float - genres(first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] - similar(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering]): [Movie] - mostSimilar: Movie - degree: Int - actors(first: Int = 3, offset: Int = 0, name: String, names: [String], orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] - avgStars: Float - filmedIn(filter: _StateFilter): State - scaleRating(scale: Int = 3): Float - scaleRatingFloat(scale: Float = 1.5): Float - actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] - ratings(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _MovieRatedFilter): [_MovieRatings] - years: [Int] - titles: [String] - imdbRatings: [Float] - releases: [_Neo4jDateTime] - customField: String - currentUserId(strArg: String): String -} - -type Mutation { - currentUserId: String - computedObjectWithCypherParams: currentUserId - computedTemporal: _Neo4jDateTime - computedStringList: [String] - customWithArguments(strArg: String, strInputArg: strInput): String - CreateMovie(movieId: ID, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput!, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie - UpdateMovie(movieId: ID!, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie - DeleteMovie(movieId: ID!): Movie - AddMovieGenres(from: _MovieInput!, to: _GenreInput!): _AddMovieGenresPayload - RemoveMovieGenres(from: _MovieInput!, to: _GenreInput!): _RemoveMovieGenresPayload - AddMovieActors(from: _ActorInput!, to: _MovieInput!): _AddMovieActorsPayload - RemoveMovieActors(from: _ActorInput!, to: _MovieInput!): _RemoveMovieActorsPayload - AddMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _AddMovieFilmedInPayload - RemoveMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _RemoveMovieFilmedInPayload - AddMovieRatings(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddMovieRatingsPayload - RemoveMovieRatings(from: _UserInput!, to: _MovieInput!): _RemoveMovieRatingsPayload - CreateGenre(name: String): Genre - DeleteGenre(name: String!): Genre - AddGenreMovies(from: _MovieInput!, to: _GenreInput!): _AddGenreMoviesPayload - RemoveGenreMovies(from: _MovieInput!, to: _GenreInput!): _RemoveGenreMoviesPayload - CreateActor(userId: ID, name: String): Actor - UpdateActor(userId: ID!, name: String): Actor - DeleteActor(userId: ID!): Actor - AddActorMovies(from: _ActorInput!, to: _MovieInput!): _AddActorMoviesPayload - RemoveActorMovies(from: _ActorInput!, to: _MovieInput!): _RemoveActorMoviesPayload - CreateState(name: String!): State - DeleteState(name: String!): State - CreateUser(userId: ID, name: String): User - UpdateUser(userId: ID!, name: String): User - DeleteUser(userId: ID!): User - AddUserRated(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddUserRatedPayload - RemoveUserRated(from: _UserInput!, to: _MovieInput!): _RemoveUserRatedPayload - AddUserFriends(from: _UserInput!, to: _UserInput!, data: _FriendOfInput!): _AddUserFriendsPayload - RemoveUserFriends(from: _UserInput!, to: _UserInput!): _RemoveUserFriendsPayload - AddUserFavorites(from: _UserInput!, to: _MovieInput!): _AddUserFavoritesPayload - RemoveUserFavorites(from: _UserInput!, to: _MovieInput!): _RemoveUserFavoritesPayload - CreateBook(genre: BookGenre): Book - DeleteBook(genre: BookGenre!): Book - CreatecurrentUserId(userId: String): currentUserId - DeletecurrentUserId(userId: String!): currentUserId - CreateTemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode - UpdateTemporalNode(datetime: _Neo4jDateTimeInput!, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode - DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode - AddTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _AddTemporalNodeTemporalNodesPayload - RemoveTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _RemoveTemporalNodeTemporalNodesPayload -} - -interface Person { - userId: ID! - name: String -} - -type Query { - Movie(_id: String, movieId: ID, title: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MoviesByYear(year: Int, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MoviesByYears(year: [Int], first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MovieById(movieId: ID!, filter: _MovieFilter): Movie - MovieBy_Id(_id: String!, filter: _MovieFilter): Movie - GenresBySubstring(substring: String, first: Int, offset: Int, orderBy: [_GenreOrdering]): [Genre] - State(first: Int, offset: Int, orderBy: [_StateOrdering], filter: _StateFilter): [State] - User(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_UserOrdering], filter: _UserFilter): [User] - Books(first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] - currentUserId: String - computedBoolean: Boolean - computedFloat: Float - computedInt: Int - computedIntList: [Int] - computedStringList: [String] - computedTemporal: _Neo4jDateTime - computedObjectWithCypherParams: currentUserId - customWithArguments(strArg: String, strInputArg: strInput): String - Genre(_id: String, name: String, first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] - Actor(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] - Book(genre: BookGenre, _id: String, first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] - TemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: _Neo4jLocalDateTimeInput, computedTimestamp: String, _id: String, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] -} - -type Rated { - from: User - currentUserId(strArg: String): String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - to: Movie -} - -enum Role { - reader - user - admin -} - -type State { - customField: String - name: String! - _id: String -} - -input strInput { - strArg: String -} - -type TemporalNode { - datetime: _Neo4jDateTime - name: String - time: _Neo4jTime - date: _Neo4jDate - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - localdatetimes: [_Neo4jLocalDateTime] - computedTimestamp: String - temporalNodes(time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] - _id: String -} - -scalar Time - -type User implements Person { - userId: ID! - name: String - currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String - rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _UserRatedFilter): [_UserRated] - friends: _UserFriendsDirections - favorites(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - _id: String -} -`; - - t.is(printSchema(schema), expectedSchema); + let [ + typeDefinitionMap, + typeExtensionDefinitionMap, + directiveDefinitionMap, + operationTypeMap + ] = mapDefinitions({ + definitions: parse(testSchema).definitions + }); + [typeDefinitionMap, directiveDefinitionMap] = augmentDirectiveDefinitions({ + typeDefinitionMap, + directiveDefinitionMap, + config: { + auth: true + } + }); + const mergedDefinitions = mergeDefinitionMaps({ + generatedTypeMap: typeDefinitionMap, + typeExtensionDefinitionMap, + operationTypeMap, + directiveDefinitionMap + }); + const documentAST = buildDocument({ + definitions: mergedDefinitions + }); + const schema = makeExecutableSchema({ + typeDefs: print(documentAST), + resolvers: { + Movie: { + customField(object, params, ctx, resolveInfo) { + return ''; + } + }, + State: { + customField(object, params, ctx, resolveInfo) { + return ''; + } + } + }, + resolverValidationOptions: { + requireResolversForResolveType: false + } + }); + const augmentedTestSchema = augmentedSchema(schema, { + auth: true + }); + + const expectedSchema = `directive @cypher(statement: String) on FIELD_DEFINITION + + directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT + + directive @additionalLabels(labels: [String]) on OBJECT + + directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION + + directive @neo4j_ignore on FIELD_DEFINITION + + directive @isAuthenticated on OBJECT | FIELD_DEFINITION + + directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION + + directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION + + input _ActorFilter { + AND: [_ActorFilter!] + OR: [_ActorFilter!] + userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter + } + + input _ActorInput { + userId: ID! + } + + enum _ActorOrdering { + userId_asc + userId_desc + name_asc + name_desc + _id_asc + _id_desc + } + + type _AddActorMoviesPayload { + from: Actor + to: Movie + } + + type _AddGenreMoviesPayload { + from: Movie + to: Genre + } + + type _AddMovieActorsPayload { + from: Actor + to: Movie + } + + type _AddMovieFilmedInPayload { + from: Movie + to: State + } + + type _AddMovieGenresPayload { + from: Movie + to: Genre + } + + type _AddMovieRatingsPayload { + from: User + to: Movie + currentUserId: String + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + } + + type _AddTemporalNodeTemporalNodesPayload { + from: TemporalNode + to: TemporalNode + } + + type _AddUserFavoritesPayload { + from: User + to: Movie + } + + type _AddUserFriendsPayload { + from: User + to: User + currentUserId: String + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + } + + type _AddUserRatedPayload { + from: User + to: Movie + currentUserId: String + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + } + + input _BookFilter { + AND: [_BookFilter!] + OR: [_BookFilter!] + genre: BookGenre + genre_not: BookGenre + genre_in: [BookGenre!] + genre_not_in: [BookGenre!] + } + + input _BookInput { + genre: BookGenre! + } + + enum _BookOrdering { + genre_asc + genre_desc + _id_asc + _id_desc + } + + input _currentUserIdFilter { + AND: [_currentUserIdFilter!] + OR: [_currentUserIdFilter!] + userId: String + userId_not: String + userId_in: [String!] + userId_not_in: [String!] + userId_contains: String + userId_not_contains: String + userId_starts_with: String + userId_not_starts_with: String + userId_ends_with: String + userId_not_ends_with: String + } + + input _currentUserIdInput { + userId: String! + } + + enum _currentUserIdOrdering { + userId_asc + userId_desc + _id_asc + _id_desc + } + + input _FriendOfDirectionsFilter { + from: _FriendOfFilter + to: _FriendOfFilter + } + + input _FriendOfFilter { + AND: [_FriendOfFilter!] + OR: [_FriendOfFilter!] + since: Int + since_not: Int + since_in: [Int!] + since_not_in: [Int!] + since_lt: Int + since_lte: Int + since_gt: Int + since_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter + } + + input _FriendOfInput { + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetimes: [_Neo4jDateTimeInput] + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + } + + input _GenreFilter { + AND: [_GenreFilter!] + OR: [_GenreFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter + } + + input _GenreInput { + name: String! + } + + enum _GenreOrdering { + name_desc + name_asc + } + + input _MovieFilter { + AND: [_MovieFilter!] + OR: [_MovieFilter!] + movieId: ID + movieId_not: ID + movieId_in: [ID!] + movieId_not_in: [ID!] + movieId_contains: ID + movieId_not_contains: ID + movieId_starts_with: ID + movieId_not_starts_with: ID + movieId_ends_with: ID + movieId_not_ends_with: ID + title: String + title_not: String + title_in: [String!] + title_not_in: [String!] + title_contains: String + title_not_contains: String + title_starts_with: String + title_not_starts_with: String + title_ends_with: String + title_not_ends_with: String + someprefix_title_with_underscores: String + someprefix_title_with_underscores_not: String + someprefix_title_with_underscores_in: [String!] + someprefix_title_with_underscores_not_in: [String!] + someprefix_title_with_underscores_contains: String + someprefix_title_with_underscores_not_contains: String + someprefix_title_with_underscores_starts_with: String + someprefix_title_with_underscores_not_starts_with: String + someprefix_title_with_underscores_ends_with: String + someprefix_title_with_underscores_not_ends_with: String + year: Int + year_not: Int + year_in: [Int!] + year_not_in: [Int!] + year_lt: Int + year_lte: Int + year_gt: Int + year_gte: Int + released: _Neo4jDateTimeInput + released_not: _Neo4jDateTimeInput + released_in: [_Neo4jDateTimeInput!] + released_not_in: [_Neo4jDateTimeInput!] + released_lt: _Neo4jDateTimeInput + released_lte: _Neo4jDateTimeInput + released_gt: _Neo4jDateTimeInput + released_gte: _Neo4jDateTimeInput + plot: String + plot_not: String + plot_in: [String!] + plot_not_in: [String!] + plot_contains: String + plot_not_contains: String + plot_starts_with: String + plot_not_starts_with: String + plot_ends_with: String + plot_not_ends_with: String + poster: String + poster_not: String + poster_in: [String!] + poster_not_in: [String!] + poster_contains: String + poster_not_contains: String + poster_starts_with: String + poster_not_starts_with: String + poster_ends_with: String + poster_not_ends_with: String + imdbRating: Float + imdbRating_not: Float + imdbRating_in: [Float!] + imdbRating_not_in: [Float!] + imdbRating_lt: Float + imdbRating_lte: Float + imdbRating_gt: Float + imdbRating_gte: Float + genres: _GenreFilter + genres_not: _GenreFilter + genres_in: [_GenreFilter!] + genres_not_in: [_GenreFilter!] + genres_some: _GenreFilter + genres_none: _GenreFilter + genres_single: _GenreFilter + genres_every: _GenreFilter + actors: _ActorFilter + actors_not: _ActorFilter + actors_in: [_ActorFilter!] + actors_not_in: [_ActorFilter!] + actors_some: _ActorFilter + actors_none: _ActorFilter + actors_single: _ActorFilter + actors_every: _ActorFilter + avgStars: Float + avgStars_not: Float + avgStars_in: [Float!] + avgStars_not_in: [Float!] + avgStars_lt: Float + avgStars_lte: Float + avgStars_gt: Float + avgStars_gte: Float + filmedIn: _StateFilter + filmedIn_not: _StateFilter + filmedIn_in: [_StateFilter!] + filmedIn_not_in: [_StateFilter!] + ratings: _MovieRatedFilter + ratings_not: _MovieRatedFilter + ratings_in: [_MovieRatedFilter!] + ratings_not_in: [_MovieRatedFilter!] + ratings_some: _MovieRatedFilter + ratings_none: _MovieRatedFilter + ratings_single: _MovieRatedFilter + ratings_every: _MovieRatedFilter + } + + input _MovieInput { + movieId: ID! + } + + enum _MovieOrdering { + title_desc + title_asc + } + + input _MovieRatedFilter { + AND: [_MovieRatedFilter!] + OR: [_MovieRatedFilter!] + rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter + } + + type _MovieRatings { + currentUserId(strArg: String): String + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + User: User + } + + type _Neo4jDate { + year: Int + month: Int + day: Int + formatted: String + } + + input _Neo4jDateInput { + year: Int + month: Int + day: Int + formatted: String + } + + type _Neo4jDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + input _Neo4jDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + type _Neo4jLocalDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _Neo4jLocalDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + type _Neo4jLocalTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _Neo4jLocalTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + type _Neo4jTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + input _Neo4jTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + input _RatedInput { + rating: Int + ratings: [Int] + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + datetimes: [_Neo4jDateTimeInput] + } + + enum _RelationDirections { + IN + OUT + } + + type _RemoveActorMoviesPayload { + from: Actor + to: Movie + } + + type _RemoveGenreMoviesPayload { + from: Movie + to: Genre + } + + type _RemoveMovieActorsPayload { + from: Actor + to: Movie + } + + type _RemoveMovieFilmedInPayload { + from: Movie + to: State + } + + type _RemoveMovieGenresPayload { + from: Movie + to: Genre + } + + type _RemoveMovieRatingsPayload { + from: User + to: Movie + } + + type _RemoveTemporalNodeTemporalNodesPayload { + from: TemporalNode + to: TemporalNode + } + + type _RemoveUserFavoritesPayload { + from: User + to: Movie + } + + type _RemoveUserFriendsPayload { + from: User + to: User + } + + type _RemoveUserRatedPayload { + from: User + to: Movie + } + + input _StateFilter { + AND: [_StateFilter!] + OR: [_StateFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + } + + input _StateInput { + name: String! + } + + enum _StateOrdering { + name_asc + name_desc + _id_asc + _id_desc + } + + input _TemporalNodeFilter { + AND: [_TemporalNodeFilter!] + OR: [_TemporalNodeFilter!] + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + temporalNodes: _TemporalNodeFilter + temporalNodes_not: _TemporalNodeFilter + temporalNodes_in: [_TemporalNodeFilter!] + temporalNodes_not_in: [_TemporalNodeFilter!] + temporalNodes_some: _TemporalNodeFilter + temporalNodes_none: _TemporalNodeFilter + temporalNodes_single: _TemporalNodeFilter + temporalNodes_every: _TemporalNodeFilter + } + + input _TemporalNodeInput { + datetime: _Neo4jDateTimeInput! + } + + enum _TemporalNodeOrdering { + datetime_asc + datetime_desc + name_asc + name_desc + time_asc + time_desc + date_asc + date_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + computedTimestamp_asc + computedTimestamp_desc + _id_asc + _id_desc + } + + input _UserFilter { + AND: [_UserFilter!] + OR: [_UserFilter!] + userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + rated: _UserRatedFilter + rated_not: _UserRatedFilter + rated_in: [_UserRatedFilter!] + rated_not_in: [_UserRatedFilter!] + rated_some: _UserRatedFilter + rated_none: _UserRatedFilter + rated_single: _UserRatedFilter + rated_every: _UserRatedFilter + friends: _FriendOfDirectionsFilter + friends_not: _FriendOfDirectionsFilter + friends_in: [_FriendOfDirectionsFilter!] + friends_not_in: [_FriendOfDirectionsFilter!] + friends_some: _FriendOfDirectionsFilter + friends_none: _FriendOfDirectionsFilter + friends_single: _FriendOfDirectionsFilter + friends_every: _FriendOfDirectionsFilter + favorites: _MovieFilter + favorites_not: _MovieFilter + favorites_in: [_MovieFilter!] + favorites_not_in: [_MovieFilter!] + favorites_some: _MovieFilter + favorites_none: _MovieFilter + favorites_single: _MovieFilter + favorites_every: _MovieFilter + } + + type _UserFriends { + currentUserId: String + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + User: User + } + + type _UserFriendsDirections { + from(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] + to(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] + } + + input _UserInput { + userId: ID! + } + + enum _UserOrdering { + userId_asc + userId_desc + name_asc + name_desc + currentUserId_asc + currentUserId_desc + _id_asc + _id_desc + } + + type _UserRated { + currentUserId(strArg: String): String + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + Movie: Movie + } + + input _UserRatedFilter { + AND: [_UserRatedFilter!] + OR: [_UserRatedFilter!] + rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + Movie: _MovieFilter + } + + type Actor implements Person { + userId: ID! + name: String + movies(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + _id: String + } + + type Book { + genre: BookGenre + _id: String + } + + enum BookGenre { + Mystery + Science + Math + } + + type currentUserId { + userId: String + _id: String + } + + scalar Date + + scalar DateTime + + type FriendOf { + from: User + currentUserId: String + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + to: User + } + + type Genre { + _id: String + name: String + movies(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + highestRatedMovie: Movie + } + + type ignoredType { + ignoredField: String + } + + scalar LocalDateTime + + scalar LocalTime + + type Movie { + _id: String + movieId: ID! + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTime! + plot: String + poster: String + imdbRating: Float + genres(first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] + similar(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering]): [Movie] + mostSimilar: Movie + degree: Int + actors(first: Int = 3, offset: Int = 0, name: String, names: [String], orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] + avgStars: Float + filmedIn(filter: _StateFilter): State + scaleRating(scale: Int = 3): Float + scaleRatingFloat(scale: Float = 1.5): Float + actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] + ratings(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _MovieRatedFilter): [_MovieRatings] + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTime] + customField: String + currentUserId(strArg: String): String + } + + type Mutation { + currentUserId: String + computedObjectWithCypherParams: currentUserId + computedTemporal: _Neo4jDateTime + computedStringList: [String] + customWithArguments(strArg: String, strInputArg: strInput): String + AddMovieGenres(from: _MovieInput!, to: _GenreInput!): _AddMovieGenresPayload + RemoveMovieGenres(from: _MovieInput!, to: _GenreInput!): _RemoveMovieGenresPayload + AddMovieActors(from: _ActorInput!, to: _MovieInput!): _AddMovieActorsPayload + RemoveMovieActors(from: _ActorInput!, to: _MovieInput!): _RemoveMovieActorsPayload + AddMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _AddMovieFilmedInPayload + RemoveMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _RemoveMovieFilmedInPayload + AddMovieRatings(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddMovieRatingsPayload + RemoveMovieRatings(from: _UserInput!, to: _MovieInput!): _RemoveMovieRatingsPayload + CreateMovie(movieId: ID, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput!, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie + UpdateMovie(movieId: ID!, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie + DeleteMovie(movieId: ID!): Movie + AddGenreMovies(from: _MovieInput!, to: _GenreInput!): _AddGenreMoviesPayload + RemoveGenreMovies(from: _MovieInput!, to: _GenreInput!): _RemoveGenreMoviesPayload + CreateGenre(name: String): Genre + DeleteGenre(name: String!): Genre + AddActorMovies(from: _ActorInput!, to: _MovieInput!): _AddActorMoviesPayload + RemoveActorMovies(from: _ActorInput!, to: _MovieInput!): _RemoveActorMoviesPayload + CreateActor(userId: ID, name: String): Actor + UpdateActor(userId: ID!, name: String): Actor + DeleteActor(userId: ID!): Actor + CreateState(name: String!): State + DeleteState(name: String!): State + AddUserRated(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddUserRatedPayload + RemoveUserRated(from: _UserInput!, to: _MovieInput!): _RemoveUserRatedPayload + AddUserFriends(from: _UserInput!, to: _UserInput!, data: _FriendOfInput!): _AddUserFriendsPayload + RemoveUserFriends(from: _UserInput!, to: _UserInput!): _RemoveUserFriendsPayload + AddUserFavorites(from: _UserInput!, to: _MovieInput!): _AddUserFavoritesPayload + RemoveUserFavorites(from: _UserInput!, to: _MovieInput!): _RemoveUserFavoritesPayload + CreateUser(userId: ID, name: String): User + UpdateUser(userId: ID!, name: String): User + DeleteUser(userId: ID!): User + CreateBook(genre: BookGenre): Book + DeleteBook(genre: BookGenre!): Book + CreatecurrentUserId(userId: String): currentUserId + DeletecurrentUserId(userId: String!): currentUserId + AddTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _AddTemporalNodeTemporalNodesPayload + RemoveTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _RemoveTemporalNodeTemporalNodesPayload + CreateTemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode + UpdateTemporalNode(datetime: _Neo4jDateTimeInput!, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode + DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode + } + + interface Person { + userId: ID! + name: String + } + + type Query { + Movie(_id: String, movieId: ID, title: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + MoviesByYear(year: Int, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + MoviesByYears(year: [Int], first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + MovieById(movieId: ID!, filter: _MovieFilter): Movie + MovieBy_Id(_id: String!, filter: _MovieFilter): Movie + GenresBySubstring(substring: String, first: Int, offset: Int, orderBy: [_GenreOrdering]): [Genre] + State(first: Int, offset: Int, orderBy: [_StateOrdering], filter: _StateFilter): [State] + User(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_UserOrdering], filter: _UserFilter): [User] + Books(first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] + currentUserId: String + computedBoolean: Boolean + computedFloat: Float + computedInt: Int + computedIntList: [Int] + computedStringList: [String] + computedTemporal: _Neo4jDateTime + computedObjectWithCypherParams: currentUserId + customWithArguments(strArg: String, strInputArg: strInput): String + Genre(_id: String, name: String, first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] + Actor(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] + Book(genre: BookGenre, _id: String, first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] + TemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: _Neo4jLocalDateTimeInput, computedTimestamp: String, _id: String, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] + } + + type Rated { + from: User + currentUserId(strArg: String): String + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + to: Movie + } + + enum Role { + reader + user + admin + } + + type State { + customField: String + name: String! + _id: String + } + + input strInput { + strArg: String + } + + type TemporalNode { + datetime: _Neo4jDateTime + name: String + time: _Neo4jTime + date: _Neo4jDate + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + localdatetimes: [_Neo4jLocalDateTime] + computedTimestamp: String + temporalNodes(time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] + _id: String + } + + scalar Time + + type User implements Person { + userId: ID! + name: String + currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String + rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _UserRatedFilter): [_UserRated] + friends: _UserFriendsDirections + favorites(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] + _id: String + } + `; + + compareSchemas({ + test: t, + targetSchema: expectedSchema, + sourceSchema: augmentedTestSchema + }); t.end(); }); + +const compareSchemas = ({ test, sourceSchema = {}, targetSchema = {} }) => { + const definitions = parse(targetSchema).definitions; + const augmentedDefinitions = parse(printSchema(sourceSchema)).definitions; + definitions.forEach(definition => { + const name = definition.name.value; + const augmented = augmentedDefinitions.find( + augmented => name === augmented.name.value + ); + if (!augmented) { + throw new Error(`${name} is missing from the augmented schema`); + } + test.is(print(augmented), print(definition)); + }); +}; From e351c48e1db3fcd8a9f10695bf2eb42282478539 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 19:53:41 -0700 Subject: [PATCH 05/37] Update for augmentation refactor --- src/utils.js | 250 ++------------------------------------------------- 1 file changed, 8 insertions(+), 242 deletions(-) diff --git a/src/utils.js b/src/utils.js index 8400e57f..25f04cc3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,4 @@ -import { print, parse } from 'graphql'; -import { possiblyAddDirectiveDeclarations } from './auth'; +import { parse } from 'graphql'; import { v1 as neo4j } from 'neo4j-driver'; import _ from 'lodash'; import filter from 'lodash/filter'; @@ -42,28 +41,6 @@ export function parseArgs(args, variableValues) { }, {}); } -export const parseFieldSdl = sdl => { - return sdl ? parse(`type Type { ${sdl} }`).definitions[0].fields[0] : {}; -}; - -export const parseInputFieldSdl = sdl => { - return sdl ? parse(`input Type { ${sdl} }`).definitions[0].fields : {}; -}; - -export const buildInputValueDefinitions = fields => { - let arr = []; - if (Array.isArray(fields)) { - fields = fields.join('\n'); - arr = fields ? parse(`type Type { ${fields} }`).definitions[0].fields : []; - arr = arr.map(e => ({ - kind: 'InputValueDefinition', - name: e.name, - type: e.type - })); - } - return arr; -}; - export const parseDirectiveSdl = sdl => { return sdl ? parse(`type Type { field: String ${sdl} }`).definitions[0].fields[0] @@ -71,23 +48,6 @@ export const parseDirectiveSdl = sdl => { : {}; }; -export const printTypeMap = typeMap => { - return print({ - kind: 'Document', - definitions: Object.values(typeMap) - }); -}; - -export const extractTypeMapFromTypeDefs = typeDefs => { - // TODO accept alternative typeDefs formats (arr of strings, ast, etc.) - // into a single string for parse, add validatation - const astNodes = parse(typeDefs).definitions; - return astNodes.reduce((acc, t) => { - if (t.name) acc[t.name.value] = t; - return acc; - }, {}); -}; - export function extractSelections(selections, fragments) { // extract any fragment selection sets into a single array of selections return selections.reduce((acc, cur) => { @@ -226,36 +186,6 @@ export const isRelationTypeDirectedField = fieldName => { return fieldName === 'from' || fieldName === 'to'; }; -export const isKind = (type, kind) => type && type.kind && type.kind === kind; - -export const _isListType = (type, isList = false) => { - if (!isKind(type, 'NamedType')) { - if (isKind(type, 'ListType')) isList = true; - return _isListType(type.type, isList); - } - return isList; -}; - -export const isNonNullType = (type, isRequired = false, parent = {}) => { - if (!isKind(type, 'NamedType')) { - return isNonNullType(type.type, isRequired, type); - } - if (isKind(parent, 'NonNullType')) { - isRequired = true; - } - return isRequired; -}; - -export const isBasicScalar = name => { - return ( - name === 'ID' || - name === 'String' || - name === 'Float' || - name === 'Int' || - name === 'Boolean' - ); -}; - export const isNodeType = astNode => { return ( astNode && @@ -618,55 +548,6 @@ export const getFieldDirective = (field, directive) => { ); }; -export const getRelationDirection = relationDirective => { - let direction = {}; - try { - direction = relationDirective.arguments.filter( - a => a.name.value === 'direction' - )[0]; - return direction.value.value; - } catch (e) { - // FIXME: should we ignore this error to define default behavior? - throw new Error('No direction argument specified on @relation directive'); - } -}; - -export const getRelationName = relationDirective => { - let name = {}; - try { - name = relationDirective.arguments.filter(a => a.name.value === 'name')[0]; - return name.value.value; - } catch (e) { - // FIXME: should we ignore this error to define default behavior? - throw new Error('No name argument specified on @relation directive'); - } -}; - -export const addDirectiveDeclarations = (typeMap, config) => { - // overwrites any provided directive declarations for system directive names - typeMap['cypher'] = parse( - `directive @cypher(statement: String) on FIELD_DEFINITION` - ).definitions[0]; - typeMap['relation'] = parse( - `directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT` - ).definitions[0]; - typeMap['additionalLabels'] = parse( - `directive @additionalLabels(labels: [String]) on OBJECT` - ).definitions[0]; - // TODO should we change these system directives to having a '_Neo4j' prefix - typeMap['MutationMeta'] = parse( - `directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION` - ).definitions[0]; - typeMap['neo4j_ignore'] = parse( - `directive @neo4j_ignore on FIELD_DEFINITION` - ).definitions[0]; - typeMap['_RelationDirections'] = parse( - `enum _RelationDirections { IN OUT }` - ).definitions[0]; - typeMap = possiblyAddDirectiveDeclarations(typeMap, config); - return typeMap; -}; - export const getQueryCypherDirective = resolveInfo => { if (resolveInfo.fieldName === '_entities') return; return resolveInfo.schema @@ -711,33 +592,6 @@ export const getRelationTypeDirective = relationshipType => { : undefined; }; -export const getRelationMutationPayloadFieldsFromAst = relatedAstNode => { - let isList = false; - let fieldName = ''; - return relatedAstNode.fields - .reduce((acc, t) => { - fieldName = t.name.value; - if (fieldName !== 'to' && fieldName !== 'from') { - isList = _isListType(t); - // Use name directly in order to prevent requiring required fields on the payload type - acc.push( - `${fieldName}: ${isList ? '[' : ''}${_getNamedType(t).name.value}${ - isList ? `]` : '' - }${print(t.directives)}` - ); - } - return acc; - }, []) - .join('\n'); -}; - -export const _getNamedType = type => { - if (type.kind !== 'NamedType') { - return _getNamedType(type.type); - } - return type; -}; - const firstNonNullAndIdField = fields => { let valueTypeName = ''; return fields.find(e => { @@ -791,14 +645,6 @@ export const getPrimaryKey = astNode => { return pk; }; -export const createOperationMap = type => { - const fields = type ? type.fields : []; - return fields.reduce((acc, t) => { - acc[t.name.value] = t; - return acc; - }, {}); -}; - /** * Render safe a variable name according to cypher rules * @param {String} i input variable name @@ -1079,93 +925,6 @@ export const temporalPredicateClauses = ( }, []); }; -// An ignored type is a type without at least 1 non-ignored field -export const excludeIgnoredTypes = (typeMap, config = {}) => { - const queryExclusionMap = {}; - const mutationExclusionMap = {}; - // If .query is an object and .exclude is provided, use it, else use new arr - let excludedQueries = getExcludedTypes(config, 'query'); - let excludedMutations = getExcludedTypes(config, 'mutation'); - // Add any ignored types to exclusion arrays - Object.keys(typeMap).forEach(name => { - if ( - typeMap[name].fields && - !typeMap[name].fields.find( - field => !getFieldDirective(field, 'neo4j_ignore') - ) - ) { - // All fields are ignored, so exclude the type - excludedQueries.push(name); - excludedMutations.push(name); - } - }); - // As long as the API is still allowed, convert the exclusion arrays - // to a boolean map for quicker reference later - if (config.query !== false) { - excludedQueries.forEach(e => { - queryExclusionMap[e] = true; - }); - config.query = { exclude: queryExclusionMap }; - } - if (config.mutation !== false) { - excludedMutations.forEach(e => { - mutationExclusionMap[e] = true; - }); - config.mutation = { exclude: mutationExclusionMap }; - } - return config; -}; - -export const getExcludedTypes = (config, rootType) => { - return config && - rootType && - config[rootType] && - typeof config[rootType] === 'object' && - config[rootType].exclude - ? config[rootType].exclude - : []; -}; - -export const possiblyAddIgnoreDirective = ( - astNode, - typeMap, - resolvers, - config -) => { - const fields = astNode && astNode.fields ? astNode.fields : []; - let valueTypeName = ''; - return fields.map(field => { - // for any field of any type, if a custom resolver is provided - // but there is no @ignore directive - valueTypeName = _getNamedType(field).name.value; - if ( - // has a custom resolver but not a directive - getCustomFieldResolver(astNode, field, resolvers) && - !getFieldDirective(field, 'neo4j_ignore') && - // fields that behave in ways specific to the neo4j mapping do not recieve ignore - // directives and can instead have their data post-processed by a custom field resolver - !getFieldDirective(field, 'relation') && - !getFieldDirective(field, 'cypher') && - !getTypeDirective(typeMap[valueTypeName], 'relation') && - !isTemporalType(valueTypeName) - ) { - // possibly initialize directives - if (!field.directives) field.directives = []; - // add the ignore directive for use in runtime translation - field.directives.push(parseDirectiveSdl(`@neo4j_ignore`)); - } - return field; - }); -}; - -export const getCustomFieldResolver = (astNode, field, resolvers) => { - const typeResolver = - astNode && astNode.name && astNode.name.value - ? resolvers[astNode.name.value] - : undefined; - return typeResolver ? typeResolver[field.name.value] : undefined; -}; - export const removeIgnoredFields = (schemaType, selections) => { if (!isGraphqlScalarType(schemaType) && selections && selections.length) { let schemaTypeField = ''; @@ -1185,3 +944,10 @@ export const removeIgnoredFields = (schemaType, selections) => { } return selections; }; + +const _getNamedType = type => { + if (type.kind !== 'NamedType') { + return _getNamedType(type.type); + } + return type; +}; From 555b86ec6d9c74801accf58b20bbe3cd9762d8ab Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 19:56:38 -0700 Subject: [PATCH 06/37] Update for augmentation refactor --- src/auth.js | 54 ++--------------------------------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/src/auth.js b/src/auth.js index 74b2a808..b3ad9b0e 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,11 +1,9 @@ // Initial support for checking auth -import { parse } from 'graphql'; import { IsAuthenticatedDirective, HasRoleDirective, HasScopeDirective } from 'graphql-auth-directives'; -import { parseDirectiveSdl } from './utils'; /* * Check is context.req.error or context.error * have been defined. @@ -20,7 +18,7 @@ export const checkRequestError = context => { } }; -export const shouldAddAuthDirective = (config, authDirective) => { +const shouldAddAuthDirective = (config, authDirective) => { if (config && typeof config === 'object') { return ( config.auth === true || @@ -32,27 +30,7 @@ export const shouldAddAuthDirective = (config, authDirective) => { return false; }; -export const possiblyAddDirectiveDeclarations = (typeMap, config) => { - if (shouldAddAuthDirective(config, 'isAuthenticated')) { - typeMap['isAuthenticated'] = parse( - `directive @isAuthenticated on OBJECT | FIELD_DEFINITION` - ).definitions[0]; - } - if (shouldAddAuthDirective(config, 'hasRole')) { - getRoleType(typeMap); // ensure Role enum is specified in typedefs - typeMap['hasRole'] = parse( - `directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION` - ).definitions[0]; - } - if (shouldAddAuthDirective(config, 'hasScope')) { - typeMap['hasScope'] = parse( - `directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION` - ).definitions[0]; - } - return typeMap; -}; - -export const possiblyAddDirectiveImplementations = ( +export const addAuthDirectiveImplementations = ( schemaDirectives, typeMap, config @@ -80,31 +58,3 @@ const getRoleType = typeMap => { return roleType; }; -export const possiblyAddScopeDirective = ({ - typeName, - relatedTypeName, - operationType, - entityType, - config -}) => { - if (shouldAddAuthDirective(config, 'hasScope')) { - if (entityType === 'node') { - if ( - operationType === 'Create' || - operationType === 'Read' || - operationType === 'Update' || - operationType === 'Delete' - ) { - return parseDirectiveSdl( - `@hasScope(scopes: ["${typeName}: ${operationType}"])` - ); - } - } - if (entityType === 'relation') { - if (operationType === 'Add') operationType = 'Create'; - else if (operationType === 'Remove') operationType = 'Delete'; - return `@hasScope(scopes: ["${typeName}: ${operationType}", "${relatedTypeName}: ${operationType}"])`; - } - } - return undefined; -}; From 2fcc59b687044b5825d702da9a8c01c26825f847 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 19:57:45 -0700 Subject: [PATCH 07/37] Update for augmentation refactor --- src/index.js | 87 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 216c8832..f2772635 100644 --- a/src/index.js +++ b/src/index.js @@ -1,24 +1,25 @@ +import { parse, print } from 'graphql'; +import Neo4jSchemaTree from './neo4j-schema/Neo4jSchemaTree'; +import graphQLMapper from './neo4j-schema/graphQLMapper'; +import { checkRequestError } from './auth'; +import { translateMutation, translateQuery } from './translate'; +import Debug from 'debug'; import { extractQueryResult, isMutation, typeIdentifiers, - extractTypeMapFromTypeDefs, - addDirectiveDeclarations, - printTypeMap, getPayloadSelections } from './utils'; import { - extractTypeMapFromSchema, - extractResolversFromSchema, augmentedSchema, makeAugmentedExecutableSchema, - addTemporalTypes -} from './augment'; -import { checkRequestError } from './auth'; -import { translateMutation, translateQuery } from './translate'; -import Neo4jSchemaTree from './neo4j-schema/Neo4jSchemaTree'; -import graphQLMapper from './neo4j-schema/graphQLMapper'; -import Debug from 'debug'; + augmentTypes, + mapDefinitions, + mergeDefinitionMaps +} from './augment/augment'; +import { transformNeo4jTypes } from './augment/types/types'; +import { buildDocument } from './augment/ast'; +import { augmentDirectiveDefinitions } from './augment/directives'; const debug = Debug('neo4j-graphql-js'); @@ -120,6 +121,53 @@ export function cypherMutation( }); } +export const augmentTypeDefs = (typeDefs, config = {}) => { + config.query = false; + config.mutation = false; + const definitions = parse(typeDefs).definitions; + let generatedTypeMap = {}; + let [ + typeDefinitionMap, + typeExtensionDefinitionMap, + directiveDefinitionMap, + operationTypeMap + ] = mapDefinitions({ + definitions, + config + }); + [ + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentTypes({ + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + [typeDefinitionMap, directiveDefinitionMap] = augmentDirectiveDefinitions({ + typeDefinitionMap: generatedTypeMap, + directiveDefinitionMap, + config + }); + const mergedDefinitions = mergeDefinitionMaps({ + generatedTypeMap, + typeExtensionDefinitionMap, + operationTypeMap, + directiveDefinitionMap + }); + const transformedDefinitions = transformNeo4jTypes({ + definitions: mergedDefinitions, + config + }); + const documentAST = buildDocument({ + definitions: transformedDefinitions + }); + typeDefs = print(documentAST); + return typeDefs; +}; + export const augmentSchema = ( schema, config = { @@ -128,9 +176,7 @@ export const augmentSchema = ( temporal: true } ) => { - const typeMap = extractTypeMapFromSchema(schema); - const resolvers = extractResolversFromSchema(schema); - return augmentedSchema(typeMap, resolvers, config); + return augmentedSchema(schema, config); }; export const makeAugmentedSchema = ({ @@ -151,7 +197,7 @@ export const makeAugmentedSchema = ({ } }) => { if (schema) { - return augmentSchema(schema, config); + return augmentedSchema(schema, config); } if (!typeDefs) throw new Error('Must provide typeDefs'); return makeAugmentedExecutableSchema({ @@ -168,15 +214,6 @@ export const makeAugmentedSchema = ({ }); }; -export const augmentTypeDefs = (typeDefs, config) => { - let typeMap = extractTypeMapFromTypeDefs(typeDefs); - // overwrites any provided declarations of system directives - typeMap = addDirectiveDeclarations(typeMap, config); - // adds managed types; tepmoral, spatial, etc. - typeMap = addTemporalTypes(typeMap, config); - return printTypeMap(typeMap); -}; - /** * Infer a GraphQL schema by inspecting the contents of a Neo4j instance. * @param {} driver From 917fa1b5ac72f9e726da1ed699ac7307bc644e40 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 19:59:35 -0700 Subject: [PATCH 08/37] Augmentation refactor --- src/augment/ast.js | 136 +++++ src/augment/augment.js | 304 +++++++++++ src/augment/directives.js | 388 ++++++++++++++ src/augment/fields.js | 123 +++++ src/augment/input-values.js | 323 ++++++++++++ src/augment/resolvers.js | 111 ++++ src/augment/types/node/mutation.js | 209 ++++++++ src/augment/types/node/node.js | 380 ++++++++++++++ src/augment/types/node/query.js | 241 +++++++++ src/augment/types/relationship/mutation.js | 366 ++++++++++++++ src/augment/types/relationship/query.js | 472 ++++++++++++++++++ .../types/relationship/relationship.js | 204 ++++++++ src/augment/types/temporal.js | 138 +++++ src/augment/types/types.js | 266 ++++++++++ 14 files changed, 3661 insertions(+) create mode 100644 src/augment/ast.js create mode 100644 src/augment/augment.js create mode 100644 src/augment/directives.js create mode 100644 src/augment/fields.js create mode 100644 src/augment/input-values.js create mode 100644 src/augment/resolvers.js create mode 100644 src/augment/types/node/mutation.js create mode 100644 src/augment/types/node/node.js create mode 100644 src/augment/types/node/query.js create mode 100644 src/augment/types/relationship/mutation.js create mode 100644 src/augment/types/relationship/query.js create mode 100644 src/augment/types/relationship/relationship.js create mode 100644 src/augment/types/temporal.js create mode 100644 src/augment/types/types.js diff --git a/src/augment/ast.js b/src/augment/ast.js new file mode 100644 index 00000000..be506307 --- /dev/null +++ b/src/augment/ast.js @@ -0,0 +1,136 @@ +import { Kind } from 'graphql'; +import { TypeWrappers } from './fields'; + +export const buildName = ({ name = '' }) => ({ + kind: Kind.NAME, + value: name +}); + +export const buildDocument = ({ definitions = [] }) => ({ + kind: Kind.DOCUMENT, + definitions +}); + +export const buildDirectiveArgument = ({ name = '', value }) => ({ + kind: Kind.ARGUMENT, + name, + value +}); + +export const buildDirective = ({ name = '', args = [] }) => ({ + kind: Kind.DIRECTIVE, + name, + arguments: args +}); + +export const buildNamedType = ({ name = '', wrappers = {} }) => { + let type = { + kind: Kind.NAMED_TYPE, + name: buildName({ name }) + }; + if (wrappers[TypeWrappers.NON_NULL_NAMED_TYPE]) { + type = { + kind: Kind.NON_NULL_TYPE, + type: type + }; + } + if (wrappers[TypeWrappers.LIST_TYPE]) { + type = { + kind: Kind.LIST_TYPE, + type: type + }; + } + if (wrappers[TypeWrappers.NON_NULL_LIST_TYPE]) { + type = { + kind: Kind.NON_NULL_TYPE, + type: type + }; + } + return type; +}; + +export const buildObjectType = ({ + name = '', + fields = [], + directives = [], + description +}) => ({ + kind: Kind.OBJECT_TYPE_DEFINITION, + name, + fields, + directives, + description +}); + +export const buildField = ({ + name = '', + type = {}, + args = [], + directives = [], + description +}) => ({ + kind: Kind.FIELD_DEFINITION, + name, + type, + arguments: args, + directives, + description +}); + +export const buildInputValue = ({ + name = '', + type = {}, + directives = [], + defaultValue, + description +}) => { + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name, + type, + directives, + defaultValue, + description + }; +}; + +export const buildEnumType = ({ name = '', values = [], description }) => ({ + kind: Kind.ENUM_TYPE_DEFINITION, + name, + values, + description +}); + +export const buildEnumValue = ({ name = '', description }) => ({ + kind: Kind.ENUM_VALUE_DEFINITION, + name, + description +}); + +export const buildInputObjectType = ({ + name = '', + fields = [], + directives = [], + description +}) => ({ + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + name, + fields, + directives, + description +}); + +export const buildDirectiveDefinition = ({ + name = '', + args = [], + locations = [], + description +}) => { + return { + kind: Kind.DIRECTIVE_DEFINITION, + name, + arguments: args, + locations, + description + }; +}; diff --git a/src/augment/augment.js b/src/augment/augment.js new file mode 100644 index 00000000..54de73b6 --- /dev/null +++ b/src/augment/augment.js @@ -0,0 +1,304 @@ +import { + Kind, + parse, + print, + isTypeDefinitionNode, + isTypeExtensionNode +} from 'graphql'; +import { makeExecutableSchema } from 'graphql-tools'; +import { buildDocument } from './ast'; +import { + isNodeType, + buildNeo4jTypes, + transformNeo4jTypes, + initializeOperationTypes +} from './types/types'; +import { augmentNodeType } from './types/node/node'; +import { augmentDirectiveDefinitions } from './directives'; +import { extractResolversFromSchema, augmentResolvers } from './resolvers'; +import { addAuthDirectiveImplementations } from '../auth'; + +export const makeAugmentedExecutableSchema = ({ + typeDefs, + resolvers, + logger, + allowUndefinedInResolve, + resolverValidationOptions, + directiveResolvers, + schemaDirectives = {}, + parseOptions, + inheritResolversFromInterfaces, + config +}) => { + const definitions = parse(typeDefs).definitions; + let generatedTypeMap = {}; + let [ + typeDefinitionMap, + typeExtensionDefinitionMap, + directiveDefinitionMap, + operationTypeMap + ] = mapDefinitions({ + definitions, + config + }); + [ + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentTypes({ + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + [generatedTypeMap, directiveDefinitionMap] = augmentDirectiveDefinitions({ + typeDefinitionMap: generatedTypeMap, + directiveDefinitionMap, + config + }); + schemaDirectives = addAuthDirectiveImplementations( + schemaDirectives, + generatedTypeMap, + config + ); + const mergedDefinitions = mergeDefinitionMaps({ + generatedTypeMap, + typeExtensionDefinitionMap, + operationTypeMap, + directiveDefinitionMap + }); + const transformedDefinitions = transformNeo4jTypes({ + definitions: mergedDefinitions, + config + }); + const documentAST = buildDocument({ + definitions: transformedDefinitions + }); + const augmentedResolvers = augmentResolvers( + generatedTypeMap, + resolvers, + config + ); + resolverValidationOptions.requireResolversForResolveType = false; + return makeExecutableSchema({ + typeDefs: print(documentAST), + resolvers: augmentedResolvers, + logger, + allowUndefinedInResolve, + resolverValidationOptions, + directiveResolvers, + schemaDirectives, + parseOptions, + inheritResolversFromInterfaces + }); +}; + +export const augmentedSchema = (schema, config) => { + const definitions = extractSchemaDefinitions({ schema }); + let [ + typeDefinitionMap, + typeExtensionDefinitionMap, + directiveDefinitionMap, + operationTypeMap + ] = mapDefinitions({ + definitions, + config + }); + let generatedTypeMap = {}; + [ + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentTypes({ + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + [generatedTypeMap, directiveDefinitionMap] = augmentDirectiveDefinitions({ + typeDefinitionMap: generatedTypeMap, + directiveDefinitionMap, + config + }); + let schemaDirectives = {}; + schemaDirectives = addAuthDirectiveImplementations( + schemaDirectives, + generatedTypeMap, + config + ); + const mergedDefinitions = mergeDefinitionMaps({ + generatedTypeMap, + typeExtensionDefinitionMap, + operationTypeMap, + directiveDefinitionMap + }); + const transformedDefinitions = transformNeo4jTypes({ + definitions: mergedDefinitions, + config + }); + const documentAST = buildDocument({ + definitions: transformedDefinitions + }); + const resolvers = extractResolversFromSchema(schema); + const augmentedResolvers = augmentResolvers( + generatedTypeMap, + resolvers, + config + ); + return makeExecutableSchema({ + typeDefs: print(documentAST), + resolvers: augmentedResolvers, + resolverValidationOptions: { + requireResolversForResolveType: false + }, + schemaDirectives + }); +}; + +export const augmentTypes = ({ + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap = {}, + config = {} +}) => { + Object.entries({ + ...typeDefinitionMap, + ...operationTypeMap + }).forEach(([typeName, definition]) => { + if (isNodeType({ definition })) { + [definition, generatedTypeMap, operationTypeMap] = augmentNodeType({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + generatedTypeMap[typeName] = definition; + } else { + generatedTypeMap[typeName] = definition; + } + return definition; + }); + generatedTypeMap = buildNeo4jTypes({ + generatedTypeMap, + config + }); + return [typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap]; +}; + +export const mapDefinitions = ({ definitions = [], config = {} }) => { + const typeExtensionDefinitionMap = {}; + const directiveDefinitionMap = {}; + let typeDefinitionMap = {}; + let operationTypeMap = {}; + // TODO Use to get operation type names + // let schemaDefinitionNode = {}; + definitions.forEach(def => { + const name = def.name.value; + if (def.kind === Kind.SCHEMA_DEFINITION) { + // schemaDefinitionNode = def; + typeDefinitionMap[name] = def; + } else if (isTypeDefinitionNode(def)) { + typeDefinitionMap[name] = def; + } else if (isTypeExtensionNode(def)) { + if (!typeExtensionDefinitionMap[name]) { + typeExtensionDefinitionMap[name] = []; + } + typeExtensionDefinitionMap[name].push(def); + } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { + directiveDefinitionMap[name] = def; + } + }); + [typeDefinitionMap, operationTypeMap] = initializeOperationTypes({ + typeDefinitionMap, + config + }); + return [ + typeDefinitionMap, + typeExtensionDefinitionMap, + directiveDefinitionMap, + operationTypeMap + ]; +}; + +export const mergeDefinitionMaps = ({ + generatedTypeMap = {}, + typeExtensionDefinitionMap = {}, + operationTypeMap = {}, + directiveDefinitionMap = {} +}) => { + const typeExtensions = Object.values(typeExtensionDefinitionMap); + if (typeExtensions) { + typeExtensionDefinitionMap = typeExtensions.reduce( + (typeExtensions, extensions) => { + typeExtensions.push(...extensions); + return typeExtensions; + }, + [] + ); + } + return Object.values({ + ...generatedTypeMap, + ...typeExtensionDefinitionMap, + ...operationTypeMap, + ...directiveDefinitionMap + }); +}; + +export const extractSchemaDefinitions = ({ schema = {} }) => + Object.values({ + ...schema.getDirectives(), + ...schema.getTypeMap() + }).reduce((astNodes, definition) => { + const astNode = definition.astNode; + if (astNode) { + astNodes.push(astNode); + const extensionASTNodes = definition.extensionASTNodes; + if (extensionASTNodes) { + astNodes.push(...extensionASTNodes); + } + } + return astNodes; + }, []); + +export const printSchemaDocument = ({ schema }) => + print( + buildDocument({ + definitions: extractSchemaDefinitions({ schema }) + }) + ); + +export const shouldAugmentType = (config, operationTypeName, typeName) => { + return typeof config[operationTypeName] === 'boolean' + ? config[operationTypeName] + : // here .exclude should be an object, + // set at the end of excludeIgnoredTypes + typeName + ? !getExcludedTypes(config, operationTypeName).some( + excludedType => excludedType === typeName + ) + : false; +}; + +export const shouldAugmentRelationshipField = ( + config, + operationTypeName, + fromName, + toName +) => + shouldAugmentType(config, operationTypeName, fromName) && + shouldAugmentType(config, operationTypeName, toName); + +const getExcludedTypes = (config, operationTypeName) => { + return config && + operationTypeName && + config[operationTypeName] && + typeof config[operationTypeName] === 'object' && + config[operationTypeName].exclude + ? config[operationTypeName].exclude + : []; +}; diff --git a/src/augment/directives.js b/src/augment/directives.js new file mode 100644 index 00000000..2949da5d --- /dev/null +++ b/src/augment/directives.js @@ -0,0 +1,388 @@ +import { Kind, DirectiveLocation, GraphQLString } from 'graphql'; +import { TypeWrappers } from './fields'; +import { + buildDirectiveDefinition, + buildInputValue, + buildNamedType, + buildEnumType, + buildEnumValue, + buildDirectiveArgument, + buildDirective, + buildName +} from './ast'; + +export const DirectiveDefinition = { + CYPHER: 'cypher', + RELATION: 'relation', + MUTATION_META: 'MutationMeta', + NEO4J_IGNORE: 'neo4j_ignore', + IS_AUTHENTICATED: 'isAuthenticated', + HAS_ROLE: 'hasRole', + HAS_SCOPE: 'hasScope', + ADDITIONAL_LABELS: 'additionalLabels' +}; + +const ROLE_TYPE = 'Role'; + +export const isCypherField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.CYPHER + }); + +export const isIgnoredField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.NEO4J_IGNORE + }); + +export const augmentDirectiveDefinitions = ({ + typeDefinitionMap = {}, + directiveDefinitionMap = {}, + config = {} +}) => { + // For each directive definition used by the integration + Object.entries({ + ...directiveDefinitionBuilderMap, + ...AuthDirectiveDefinitionMap + }).forEach(([name, buildDefinition]) => { + // If directive definition not provided + if (!directiveDefinitionMap[name]) { + // Try to build a config object for building the definition + // AST node for this directive + const astNodeConfig = buildDefinition({ typeDefinitionMap, config }); + if (astNodeConfig) { + if (astNodeConfig.args) { + astNodeConfig.args = astNodeConfig.args.map(arg => + buildInputValue({ + name: buildName({ name: arg.name }), + type: buildNamedType(arg.type) + }) + ); + } + // Build and map a new AST node for this directive + directiveDefinitionMap[name] = buildDirectiveDefinition({ + name: buildName({ name }), + args: astNodeConfig.args, + locations: astNodeConfig.locations.map(name => buildName({ name })) + }); + } + } + }); + const relationshipDirectionEnumName = '_RelationDirections'; + typeDefinitionMap[relationshipDirectionEnumName] = buildEnumType({ + name: buildName({ name: relationshipDirectionEnumName }), + values: [ + buildEnumValue({ + name: buildName({ name: 'IN' }) + }), + buildEnumValue({ + name: buildName({ name: 'OUT' }) + }) + ] + }); + return [typeDefinitionMap, directiveDefinitionMap]; +}; + +const RelationshipDirectionField = { + FROM: 'from', + TO: 'to' +}; + +export const buildRelationDirective = ({ + relationshipName, + fromType, + toType +}) => + buildDirective({ + name: buildName({ name: DirectiveDefinition.RELATION }), + args: [ + buildDirectiveArgument({ + name: buildName({ name: 'name' }), + value: { + kind: Kind.STRING, + value: relationshipName + } + }), + buildDirectiveArgument({ + name: buildName({ name: RelationshipDirectionField.FROM }), + value: { + kind: Kind.STRING, + value: fromType + } + }), + buildDirectiveArgument({ + name: buildName({ name: RelationshipDirectionField.TO }), + value: { + kind: Kind.STRING, + value: toType + } + }) + ] + }); + +export const buildMutationMetaDirective = ({ + relationshipName, + fromType, + toType +}) => + buildDirective({ + name: buildName({ name: DirectiveDefinition.MUTATION_META }), + args: [ + buildDirectiveArgument({ + name: buildName({ name: 'relationship' }), + value: { + kind: Kind.STRING, + value: relationshipName + } + }), + buildDirectiveArgument({ + name: buildName({ name: RelationshipDirectionField.FROM }), + value: { + kind: Kind.STRING, + value: fromType + } + }), + buildDirectiveArgument({ + name: buildName({ name: RelationshipDirectionField.TO }), + value: { + kind: Kind.STRING, + value: toType + } + }) + ] + }); + +export const buildAuthScopeDirective = ({ scopes = [] }) => + buildDirective({ + name: buildName({ name: DirectiveDefinition.HAS_SCOPE }), + args: [ + buildDirectiveArgument({ + name: buildName({ name: 'scopes' }), + value: { + kind: Kind.LIST, + values: scopes.map(scope => ({ + kind: Kind.STRING, + value: `${scope.typeName}: ${scope.mutation}` + })) + } + }) + ] + }); + +const AuthDirectiveDefinitionMap = { + [DirectiveDefinition.IS_AUTHENTICATED]: ({ config }) => { + if (useAuthDirective(config, DirectiveDefinition.IS_AUTHENTICATED)) { + return { + name: DirectiveDefinition.IS_AUTHENTICATED, + locations: [ + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION + ] + }; + } + }, + [DirectiveDefinition.HAS_ROLE]: ({ typeDefinitionMap, config }) => { + if (useAuthDirective(config, DirectiveDefinition.HAS_ROLE)) { + const roleEnumType = typeDefinitionMap[ROLE_TYPE]; + if (!roleEnumType) + throw new Error( + `A Role enum type is required for the @hasRole auth directive.` + ); + if (roleEnumType && roleEnumType.kind !== Kind.ENUM_TYPE_DEFINITION) + throw new Error(`The Role type must be an Enum type`); + return { + name: DirectiveDefinition.HAS_ROLE, + args: [ + { + name: 'roles', + type: { + name: ROLE_TYPE, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + } + } + ], + locations: [ + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION + ] + }; + } + }, + [DirectiveDefinition.HAS_SCOPE]: ({ config }) => { + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + return { + name: DirectiveDefinition.HAS_SCOPE, + args: [ + { + name: 'scopes', + type: { + name: GraphQLString, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + } + } + ], + locations: [ + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION + ] + }; + } + } +}; + +// Map of AST configs for ASTNodeBuilder +const directiveDefinitionBuilderMap = { + [DirectiveDefinition.CYPHER]: ({ config }) => { + return { + name: DirectiveDefinition.CYPHER, + args: [ + { + name: 'statement', + type: { + name: GraphQLString + } + } + ], + locations: [DirectiveLocation.FIELD_DEFINITION] + }; + }, + [DirectiveDefinition.RELATION]: ({ config }) => { + return { + name: DirectiveDefinition.RELATION, + args: [ + { + name: 'name', + type: { + name: GraphQLString + } + }, + { + name: 'direction', + type: { + name: '_RelationDirections' + } + }, + { + name: RelationshipDirectionField.FROM, + type: { + name: GraphQLString + } + }, + { + name: RelationshipDirectionField.TO, + type: { + name: GraphQLString + } + } + ], + locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT] + }; + }, + [DirectiveDefinition.ADDITIONAL_LABELS]: ({ config }) => { + if (useAuthDirective(config, DirectiveDefinition.ADDITIONAL_LABELS)) { + return { + name: DirectiveDefinition.ADDITIONAL_LABELS, + args: [ + { + name: 'labels', + type: { + name: GraphQLString, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + } + } + ], + locations: [DirectiveLocation.OBJECT] + }; + } + }, + [DirectiveDefinition.MUTATION_META]: ({ config }) => { + return { + name: DirectiveDefinition.MUTATION_META, + args: [ + { + name: 'relationship', + type: { + name: GraphQLString + } + }, + { + name: RelationshipDirectionField.FROM, + type: { + name: GraphQLString + } + }, + { + name: RelationshipDirectionField.TO, + type: { + name: GraphQLString + } + } + ], + locations: [DirectiveLocation.FIELD_DEFINITION] + }; + }, + [DirectiveDefinition.NEO4J_IGNORE]: ({ config }) => { + return { + name: DirectiveDefinition.NEO4J_IGNORE, + locations: [DirectiveLocation.FIELD_DEFINITION] + }; + } +}; + +export const useAuthDirective = (config, authDirective) => { + if (config && typeof config === 'object') { + return ( + config.auth === true || + (config && + typeof config.auth === 'object' && + config.auth[authDirective] === true) + ); + } + return false; +}; + +export const getRelationDirection = relationDirective => { + let direction = {}; + try { + direction = relationDirective.arguments.filter( + a => a.name.value === 'direction' + )[0]; + return direction.value.value; + } catch (e) { + // FIXME: should we ignore this error to define default behavior? + throw new Error('No direction argument specified on @relation directive'); + } +}; + +export const getRelationName = relationDirective => { + let name = {}; + try { + name = relationDirective.arguments.filter(a => a.name.value === 'name')[0]; + return name.value.value; + } catch (e) { + // FIXME: should we ignore this error to define default behavior? + throw new Error('No name argument specified on @relation directive'); + } +}; + +export const getDirective = ({ directives, name }) => { + return directives.find(directive => directive.name.value === name); +}; + +export const getDirectiveArgument = ({ directive, name }) => { + let value = ''; + const arg = directive.arguments.find( + arg => arg.name && arg.name.value === name + ); + if (arg) { + value = arg.value.value; + } + return value; +}; diff --git a/src/augment/fields.js b/src/augment/fields.js new file mode 100644 index 00000000..8c5cf565 --- /dev/null +++ b/src/augment/fields.js @@ -0,0 +1,123 @@ +import { Kind } from 'graphql'; +import { Neo4jDataType, isNeo4jPropertyType } from './types/types'; + +const Neo4jSystemIDField = `_id`; + +export const isIntegerField = ({ type }) => + Neo4jDataType.PROPERTY[type] === 'Integer'; + +export const isFloatField = ({ type }) => + Neo4jDataType.PROPERTY[type] === 'Float'; + +export const isStringField = ({ kind, type }) => + Neo4jDataType.PROPERTY[kind] === 'String' || + Neo4jDataType.PROPERTY[type] === 'String'; + +export const isBooleanField = ({ type }) => + Neo4jDataType.PROPERTY[type] === 'Boolean'; + +export const isTemporalField = ({ type }) => + Neo4jDataType.PROPERTY[type] === 'Temporal'; + +export const isNeo4jIDField = ({ name }) => name === Neo4jSystemIDField; + +export const isCustomScalarField = ({ kind }) => + kind === Kind.SCALAR_TYPE_DEFINITION; + +export const isPropertyTypeField = ({ kind, type }) => + isIntegerField({ type }) || + isFloatField({ type }) || + isStringField({ kind, type }) || + isBooleanField({ type }) || + isCustomScalarField({ kind }) || + isTemporalField({ type }) || + isNeo4jPropertyType({ type }); + +export const TypeWrappers = { + NAME: 'name', + NON_NULL_NAMED_TYPE: 'isNonNullNamedType', + LIST_TYPE: 'isListType', + NON_NULL_LIST_TYPE: 'isNonNullListType' +}; + +export const isNonNullNamedTypeField = ({ wrappers = {} }) => + wrappers[TypeWrappers.NON_NULL_NAMED_TYPE]; + +export const isListTypeField = ({ wrappers = {} }) => + wrappers[TypeWrappers.LIST_TYPE]; + +export const isNonNullListTypeField = ({ wrappers = {} }) => + wrappers[TypeWrappers.NON_NULL_LIST_TYPE]; + +export const unwrapNamedType = ({ type = {}, unwrappedType = {} }) => { + // Initialize wrappers for this type + unwrappedType.wrappers = { + [TypeWrappers.LIST_TYPE]: false, + [TypeWrappers.NON_NULL_NAMED_TYPE]: false, + [TypeWrappers.NON_NULL_LIST_TYPE]: false + }; + // Get wrapped type + const wrappedType = type.type; + // Recursing down through all type wrappers: + // See: https://graphql.github.io/graphql-spec/June2018/#sec-Type-References + if (wrappedType) { + unwrappedType = unwrapNamedType({ + type: wrappedType, + unwrappedType + }); + } + // Making decisions on the way back up: + // Cases: (1) Name, (2) [Name], (3) [Name!], (4) Name!, (5) [Name]!, (6) [Name!]! + // See: https://graphql.github.io/graphql-spec/June2018/#sec-Wrapping-Types + if (type.kind === Kind.NAMED_TYPE && type.name) { + if (type.name.kind === Kind.NAME) { + // (1) Name - name of unwrapped type + unwrappedType[TypeWrappers.NAME] = type.name.value; + } + } else if (type.kind === Kind.LIST_TYPE) { + // (2) [Name], (3) [Name!] + unwrappedType.wrappers[TypeWrappers.LIST_TYPE] = true; + } else if (type.kind === Kind.NON_NULL_TYPE) { + // Check the wrapped type; a name or a list + if (wrappedType) { + if (wrappedType.kind === Kind.NAMED_TYPE) { + // (4) Name! + unwrappedType.wrappers[TypeWrappers.NON_NULL_NAMED_TYPE] = true; + } else if (wrappedType.kind === Kind.LIST_TYPE) { + // (5) [Name]!, (6) [Name!]! + unwrappedType.wrappers[TypeWrappers.NON_NULL_LIST_TYPE] = true; + } + } + } + return unwrappedType; +}; + +export const getFieldDefinition = ({ fields = [], name = '' }) => + fields.find(field => field.name && field.name.value === name); + +export const getFieldType = ({ fields = [], name = '' }) => { + let typeName = ''; + const field = getFieldDefinition({ + fields, + name + }); + if (field) { + typeName = unwrapNamedType({ type: field.type }).name; + } + return typeName; +}; + +export const toSnakeCase = name => { + return Object.keys(name) + .reduce((acc, t) => { + const char = name.charAt(t); + const uppercased = char.toUpperCase(); + if (char === uppercased && t > 0) { + acc.push(`_${uppercased}`); + } else { + acc.push(uppercased); + } + return acc; + }, []) + .join(''); +}; diff --git a/src/augment/input-values.js b/src/augment/input-values.js new file mode 100644 index 00000000..72678228 --- /dev/null +++ b/src/augment/input-values.js @@ -0,0 +1,323 @@ +import { Kind, GraphQLInt } from 'graphql'; +import { + buildName, + buildNamedType, + buildInputValue, + buildInputObjectType, + buildEnumType, + buildEnumValue +} from './ast'; +import { isNeo4jPropertyType } from './types/types'; +import { isCypherField } from './directives'; +import { + TypeWrappers, + isListTypeField, + isNeo4jIDField, + isIntegerField, + isFloatField, + isStringField, + isBooleanField, + isTemporalField +} from './fields'; + +export const PagingArgument = { + FIRST: 'first', + OFFSET: 'offset' +}; + +export const OrderingArgument = { + ORDER_BY: 'orderBy' +}; + +export const FilteringArgument = { + FILTER: 'filter' +}; + +export const augmentInputTypePropertyFields = ({ + inputTypeMap = {}, + fieldName, + fieldDirectives, + outputType, + outputKind, + outputTypeWrappers +}) => { + const filteringType = inputTypeMap[FilteringArgument.FILTER]; + const orderingType = inputTypeMap[OrderingArgument.ORDER_BY]; + if (!isListTypeField({ wrappers: outputTypeWrappers })) { + if ( + !isCypherField({ directives: fieldDirectives }) && + !isNeo4jIDField({ name: fieldName }) + ) { + if (filteringType) { + filteringType.fields.push( + ...buildPropertyFilters({ + fieldName, + outputType, + outputKind + }) + ); + } + } + if (orderingType) { + orderingType.values.push(...buildPropertyOrderingValues({ fieldName })); + } + } + return inputTypeMap; +}; + +export const buildQueryFieldArguments = ({ + augmentationMap = {}, + fieldArguments, + fieldDirectives, + outputType, + outputTypeWrappers +}) => { + Object.values(augmentationMap).forEach(name => { + if (isListTypeField({ wrappers: outputTypeWrappers })) { + if (name === PagingArgument.FIRST) { + // Result Arguments + if ( + !fieldArguments.some(arg => arg.name.value === PagingArgument.FIRST) + ) { + fieldArguments.push( + buildQueryPagingArgument({ + name: PagingArgument.FIRST + }) + ); + } + } else if (name === PagingArgument.OFFSET) { + // Result Arguments + if ( + !fieldArguments.some(arg => arg.name.value === PagingArgument.OFFSET) + ) { + fieldArguments.push( + buildQueryPagingArgument({ + name: PagingArgument.OFFSET + }) + ); + } + } else if (name === OrderingArgument.ORDER_BY) { + // Overwrite + fieldArguments.push( + buildQueryOrderingArgument({ + typeName: outputType + }) + ); + } + } + // Overwrite + if (name === FilteringArgument.FILTER) { + if (!isCypherField({ directives: fieldDirectives })) { + fieldArguments.push( + buildQueryFilteringArgument({ + typeName: outputType + }) + ); + } + } + }); + return fieldArguments; +}; + +const buildQueryPagingArgument = ({ name = '' }) => { + let arg = {}; + // Prevent overwrite + if (name === PagingArgument.FIRST) { + arg = buildInputValue({ + name: buildName({ name: PagingArgument.FIRST }), + type: buildNamedType({ + name: GraphQLInt.name + }) + }); + } + if (name === PagingArgument.OFFSET) { + arg = buildInputValue({ + name: buildName({ name: PagingArgument.OFFSET }), + type: buildNamedType({ + name: GraphQLInt.name + }) + }); + } + return arg; +}; + +const buildQueryOrderingArgument = ({ typeName }) => + buildInputValue({ + name: buildName({ name: OrderingArgument.ORDER_BY }), + type: buildNamedType({ + name: `_${typeName}Ordering`, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + }) + }); + +export const buildQueryOrderingEnumType = ({ + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap +}) => { + const inputType = nodeInputTypeMap[OrderingArgument.ORDER_BY]; + if (inputType) { + const orderingTypeName = inputType.name; + const type = typeDefinitionMap[inputType.name]; + // Prevent overwrite + if (!type) { + inputType.name = buildName({ name: orderingTypeName }); + generatedTypeMap[orderingTypeName] = buildEnumType(inputType); + } + } + return generatedTypeMap; +}; + +export const buildPropertyOrderingValues = ({ fieldName }) => [ + buildEnumValue({ + name: buildName({ name: `${fieldName}_asc` }) + }), + buildEnumValue({ + name: buildName({ name: `${fieldName}_desc` }) + }) +]; + +const buildQueryFilteringArgument = ({ typeName }) => + buildInputValue({ + name: buildName({ name: FilteringArgument.FILTER }), + type: buildNamedType({ + name: `_${typeName}Filter` + }) + }); + +export const buildQueryFilteringInputType = ({ + typeName, + inputTypeMap, + typeDefinitionMap, + generatedTypeMap +}) => { + const inputType = inputTypeMap[FilteringArgument.FILTER]; + if (inputType) { + const inputTypeName = inputType.name; + inputType.name = buildName({ name: inputTypeName }); + inputType.fields.unshift( + ...[ + buildInputValue({ + name: buildName({ name: 'AND' }), + type: buildNamedType({ + name: typeName, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true, + [TypeWrappers.LIST_TYPE]: true + } + }) + }), + buildInputValue({ + name: buildName({ name: 'OR' }), + type: buildNamedType({ + name: typeName, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true, + [TypeWrappers.LIST_TYPE]: true + } + }) + }) + ] + ); + if (!typeDefinitionMap[inputTypeName]) { + generatedTypeMap[inputTypeName] = buildInputObjectType(inputType); + } + } + return generatedTypeMap; +}; + +const buildPropertyFilters = ({ + fieldName = '', + outputType = '', + outputKind = '' +}) => { + let filters = []; + if ( + isIntegerField({ type: outputType }) || + isFloatField({ type: outputType }) || + isTemporalField({ type: outputType }) || + isNeo4jPropertyType({ type: outputType }) + ) { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: ['_not', '_in', '_not_in', '_lt', '_lte', '_gt', '_gte'] + }); + } else if (isBooleanField({ type: outputType })) { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: ['_not'] + }); + } else if (isStringField({ kind: outputKind, type: outputType })) { + if (outputKind === Kind.ENUM_TYPE_DEFINITION) { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: ['_not', '_in', '_not_in'] + }); + } else { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: [ + '_not', + '_in', + '_not_in', + '_contains', + '_not_contains', + '_starts_with', + '_not_starts_with', + '_ends_with', + '_not_ends_with' + ] + }); + } + } + return filters; +}; + +export const buildFilters = ({ fieldName, fieldConfig, filterTypes = [] }) => [ + buildInputValue({ + name: buildName({ name: fieldConfig.name }), + type: buildNamedType(fieldConfig.type) + }), + ...filterTypes.map(filter => { + let wrappers = {}; + if (filter === '_in' || filter === '_not_in') { + wrappers = { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true, + [TypeWrappers.LIST_TYPE]: true + }; + } + return buildInputValue({ + name: buildName({ name: `${fieldName}${filter}` }), + type: buildNamedType({ + name: fieldConfig.type.name, + wrappers + }) + }); + }) +]; diff --git a/src/augment/resolvers.js b/src/augment/resolvers.js new file mode 100644 index 00000000..0daae0e3 --- /dev/null +++ b/src/augment/resolvers.js @@ -0,0 +1,111 @@ +import { neo4jgraphql } from '../index'; +import { OperationType } from '../augment/types/types'; + +export const augmentResolvers = (augmentedTypeMap, resolvers, config) => { + let queryResolvers = + resolvers && resolvers[OperationType.QUERY] + ? resolvers[OperationType.QUERY] + : {}; + const queryType = augmentedTypeMap[OperationType.QUERY]; + if (queryType) { + queryResolvers = possiblyAddResolvers(queryType, queryResolvers, config); + if (Object.keys(queryResolvers).length > 0) { + resolvers[OperationType.QUERY] = queryResolvers; + } + } + let mutationResolvers = + resolvers && resolvers[OperationType.MUTATION] + ? resolvers[OperationType.MUTATION] + : {}; + const mutationType = augmentedTypeMap[OperationType.MUTATION]; + if (mutationType) { + mutationResolvers = possiblyAddResolvers( + mutationType, + mutationResolvers, + config + ); + if (Object.keys(mutationResolvers).length > 0) { + resolvers[OperationType.MUTATION] = mutationResolvers; + } + } + // must implement __resolveInfo for every Interface type + // we use "FRAGMENT_TYPE" key to identify the Interface implementation + // type at runtime, so grab this value + const interfaceTypes = Object.keys(augmentedTypeMap).filter( + e => augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' + ); + interfaceTypes.map(e => { + resolvers[e] = {}; + + resolvers[e]['__resolveType'] = (obj, context, info) => { + return obj['FRAGMENT_TYPE']; + }; + }); + return resolvers; +}; + +const possiblyAddResolvers = (operationType, resolvers, config) => { + let operationName = ''; + const fields = operationType ? operationType.fields : []; + const operationTypeMap = fields.reduce((acc, t) => { + acc[t.name.value] = t; + return acc; + }, {}); + return Object.keys(operationTypeMap).reduce((acc, t) => { + // if no resolver provided for this operation type field + operationName = operationTypeMap[t].name.value; + if (acc[operationName] === undefined) { + acc[operationName] = function(...args) { + return neo4jgraphql(...args, config.debug); + }; + } + return acc; + }, resolvers); +}; + +export const extractResolversFromSchema = schema => { + const _typeMap = schema && schema._typeMap ? schema._typeMap : {}; + const types = Object.keys(_typeMap); + let type = {}; + let schemaTypeResolvers = {}; + return types.reduce((acc, t) => { + // prevent extraction from schema introspection system keys + if ( + t !== '__Schema' && + t !== '__Type' && + t !== '__TypeKind' && + t !== '__Field' && + t !== '__InputValue' && + t !== '__EnumValue' && + t !== '__Directive' + ) { + type = _typeMap[t]; + // resolvers are stored on the field level at a .resolve key + schemaTypeResolvers = extractFieldResolversFromSchemaType(type); + // do not add unless there exists at least one field resolver for type + if (schemaTypeResolvers) { + acc[t] = schemaTypeResolvers; + } + } + return acc; + }, {}); +}; + +const extractFieldResolversFromSchemaType = type => { + const fields = type._fields; + const fieldKeys = fields ? Object.keys(fields) : []; + const fieldResolvers = + fieldKeys.length > 0 + ? fieldKeys.reduce((acc, t) => { + // do not add entry for this field unless it has resolver + if (fields[t].resolve !== undefined) { + acc[t] = fields[t].resolve; + } + return acc; + }, {}) + : undefined; + // do not return value unless there exists at least 1 field resolver + return fieldResolvers && Object.keys(fieldResolvers).length > 0 + ? fieldResolvers + : undefined; +}; diff --git a/src/augment/types/node/mutation.js b/src/augment/types/node/mutation.js new file mode 100644 index 00000000..e19df092 --- /dev/null +++ b/src/augment/types/node/mutation.js @@ -0,0 +1,209 @@ +import { GraphQLID } from 'graphql'; +import { + buildField, + buildName, + buildNamedType, + buildInputValue +} from '../../ast'; +import { + DirectiveDefinition, + buildAuthScopeDirective, + useAuthDirective, + isCypherField +} from '../../directives'; +import { getPrimaryKey } from '../../../utils'; +import { shouldAugmentType } from '../../augment'; +import { OperationType } from '../../types/types'; +import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields'; + +export const NodeMutation = { + CREATE: 'Create', + UPDATE: 'Update', + DELETE: 'Delete', + MERGE: 'Merge' +}; + +export const augmentNodeMutationAPI = ({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + operationTypeMap, + config +}) => { + const primaryKey = getPrimaryKey(definition); + const mutationTypeName = OperationType.MUTATION; + const mutationType = operationTypeMap[mutationTypeName]; + const mutationTypeNameLower = mutationTypeName.toLowerCase(); + if ( + mutationType && + shouldAugmentType(config, mutationTypeNameLower, typeName) + ) { + Object.values(NodeMutation).forEach(mutationAction => { + operationTypeMap = buildNodeMutationField({ + mutationType, + mutationAction, + primaryKey, + typeName, + propertyInputValues, + operationTypeMap, + config + }); + }); + } + return [operationTypeMap, generatedTypeMap]; +}; + +const buildNodeMutationField = ({ + mutationType, + mutationAction, + primaryKey, + typeName, + propertyInputValues, + operationTypeMap, + config +}) => { + const mutationFields = mutationType.fields; + const mutationName = `${mutationAction}${typeName}`; + if ( + !getFieldDefinition({ + fields: mutationFields, + name: mutationName + }) + ) { + const mutationField = { + name: buildName({ name: mutationName }), + args: buildNodeMutationArguments({ + operationName: mutationAction, + primaryKey, + args: propertyInputValues + }), + type: buildNamedType({ + name: typeName + }), + directives: buildNodeMutationDirectives({ + mutationAction, + typeName, + config + }) + }; + if (mutationAction === NodeMutation.CREATE) { + mutationFields.push(buildField(mutationField)); + } else if (mutationAction === NodeMutation.UPDATE) { + if (primaryKey && mutationField.args.length > 1) { + mutationFields.push(buildField(mutationField)); + } + } else if (mutationAction === NodeMutation.DELETE) { + if (primaryKey) { + mutationFields.push(buildField(mutationField)); + } + } + operationTypeMap[OperationType.MUTATION].fields = mutationFields; + } + return operationTypeMap; +}; + +const buildNodeMutationDirectives = ({ mutationAction, typeName, config }) => { + const directives = []; + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + directives.push( + buildAuthScopeDirective({ + scopes: [ + { + typeName, + mutation: mutationAction + } + ] + }) + ); + } + return directives; +}; + +const buildNodeMutationArguments = ({ + operationName = '', + primaryKey, + args = [] +}) => { + const primaryKeyName = primaryKey ? primaryKey.name.value : ''; + args = args.reduce((args, field) => { + const name = field.name; + const directives = field.directives; + if (!isNeo4jIDField({ name }) && !isCypherField({ directives })) { + const type = field.type; + if (operationName === NodeMutation.CREATE) { + // Uses primary key and any other property field + if (primaryKeyName === field.name) { + if (field.type.name === GraphQLID.name) { + // Create auto-generates ID primary keys + args.push({ + name, + type: { + name: type.name + } + }); + } else { + args.push({ + name, + type: { + name: type.name, + wrappers: type.wrappers + } + }); + } + } else { + args.push({ + name, + type + }); + } + } else if (operationName === NodeMutation.UPDATE) { + // Uses primary key and any other property field + if (primaryKeyName === name) { + // Require primary key otherwise + args.push({ + name, + type: { + name: type.name, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + } + }); + } else { + // Persist list type wrapper + args.push({ + name, + type: { + name: type.name, + wrappers: { + [TypeWrappers.LIST_TYPE]: type.wrappers[TypeWrappers.LIST_TYPE] + } + } + }); + } + } else if (operationName === NodeMutation.DELETE) { + // Only uses primary key + if (primaryKeyName === name) { + // Require primary key otherwise + args.push({ + name, + type: { + name: type.name, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + } + }); + } + } + } + return args; + }, []); + return args.map(arg => + buildInputValue({ + name: buildName({ name: arg.name }), + type: buildNamedType(arg.type) + }) + ); +}; diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js new file mode 100644 index 00000000..fea92c3a --- /dev/null +++ b/src/augment/types/node/node.js @@ -0,0 +1,380 @@ +import { GraphQLString } from 'graphql'; +import { buildNodeQueryAPI, augmentNodeTypeFieldInput } from './query'; +import { augmentNodeMutationAPI } from './mutation'; +import { augmentRelationshipTypeField } from '../relationship/relationship'; +import { augmentRelationshipMutationAPI } from '../relationship/mutation'; +import { shouldAugmentType } from '../../augment'; +import { + TypeWrappers, + unwrapNamedType, + isPropertyTypeField +} from '../../fields'; +import { + FilteringArgument, + OrderingArgument, + augmentInputTypePropertyFields, + buildPropertyOrderingValues +} from '../../input-values'; +import { + getRelationDirection, + getRelationName, + getDirective, + isIgnoredField, + DirectiveDefinition +} from '../../directives'; +import { + buildName, + buildNamedType, + buildField, + buildInputObjectType, + buildInputValue +} from '../../ast'; +import { + OperationType, + isNodeType, + isRelationshipType, + isObjectTypeDefinition, + isOperationTypeDefinition, + isQueryTypeDefinition +} from '../../types/types'; +import { getPrimaryKey } from '../../../utils'; + +export const augmentNodeType = ({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + if (isObjectTypeDefinition({ definition })) { + let [ + nodeInputTypeMap, + propertyOutputFields, + propertyInputValues, + isIgnoredType + ] = augmentNodeTypeFields({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + if (!isOperationTypeDefinition({ definition }) && !isIgnoredType) { + [ + propertyOutputFields, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentNodeTypeAPI({ + definition, + typeName, + propertyOutputFields, + propertyInputValues, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } + definition.fields = propertyOutputFields; + } + return [definition, generatedTypeMap, operationTypeMap]; +}; + +const augmentNodeTypeFields = ({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + const fields = definition.fields; + let isIgnoredType = true; + const propertyInputValues = []; + let nodeInputTypeMap = { + [FilteringArgument.FILTER]: { + name: `_${typeName}Filter`, + fields: [] + }, + [OrderingArgument.ORDER_BY]: { + name: `_${typeName}Ordering`, + values: [] + } + }; + let propertyOutputFields = fields.reduce((outputFields, field) => { + let fieldType = field.type; + let fieldArguments = field.arguments; + const fieldDirectives = field.directives; + if (!isIgnoredField({ directives: fieldDirectives })) { + isIgnoredType = false; + const fieldName = field.name.value; + const unwrappedType = unwrapNamedType({ type: fieldType }); + const outputType = unwrappedType.name; + const outputDefinition = typeDefinitionMap[outputType]; + const outputKind = outputDefinition ? outputDefinition.kind : ''; + const outputTypeWrappers = unwrappedType.wrappers; + const relationshipDirective = getDirective({ + directives: fieldDirectives, + name: DirectiveDefinition.RELATION + }); + if ( + isPropertyTypeField({ + kind: outputKind, + type: outputType + }) + ) { + nodeInputTypeMap = augmentInputTypePropertyFields({ + inputTypeMap: nodeInputTypeMap, + fieldName, + fieldDirectives, + outputType, + outputKind, + outputTypeWrappers + }); + propertyInputValues.push({ + name: fieldName, + type: unwrappedType, + directives: fieldDirectives + }); + } else if (isNodeType({ definition: outputDefinition })) { + [ + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentNodeTypeField({ + typeName, + definition, + fieldArguments, + fieldDirectives, + fieldName, + outputType, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config, + relationshipDirective, + outputTypeWrappers + }); + } else if (isRelationshipType({ definition: outputDefinition })) { + [ + fieldType, + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentRelationshipTypeField({ + typeName, + definition, + fieldType, + fieldArguments, + fieldDirectives, + fieldName, + outputTypeWrappers, + outputType, + outputDefinition, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } + } + outputFields.push({ + ...field, + type: fieldType, + arguments: fieldArguments + }); + return outputFields; + }, []); + if (!isOperationTypeDefinition({ definition }) && !isIgnoredType) { + const queryTypeName = OperationType.QUERY; + const queryTypeNameLower = queryTypeName.toLowerCase(); + if (shouldAugmentType(config, queryTypeNameLower, typeName)) { + const neo4jInternalIDConfig = { + name: '_id', + type: { + name: GraphQLString.name + } + }; + const systemIDIndex = propertyOutputFields.findIndex( + e => e.name.value === '_id' + ); + const systemIDField = buildField({ + name: buildName({ name: neo4jInternalIDConfig.name }), + type: buildNamedType({ + name: GraphQLString.name + }) + }); + if (systemIDIndex >= 0) { + propertyOutputFields.splice(systemIDIndex, 1, systemIDField); + } else { + propertyOutputFields.push(systemIDField); + } + nodeInputTypeMap[OrderingArgument.ORDER_BY].values.push( + ...buildPropertyOrderingValues({ + fieldName: neo4jInternalIDConfig.name + }) + ); + } + } + return [ + nodeInputTypeMap, + propertyOutputFields, + propertyInputValues, + isIgnoredType + ]; +}; + +const augmentNodeTypeField = ({ + typeName, + definition, + fieldArguments, + fieldDirectives, + fieldName, + outputType, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config, + relationshipDirective, + outputTypeWrappers +}) => { + [fieldArguments, nodeInputTypeMap] = augmentNodeTypeFieldInput({ + typeName, + definition, + fieldName, + fieldArguments, + fieldDirectives, + outputType, + config, + relationshipDirective, + outputTypeWrappers, + nodeInputTypeMap + }); + if (relationshipDirective && !isQueryTypeDefinition({ definition })) { + const relationshipName = getRelationName(relationshipDirective); + const relationshipDirection = getRelationDirection(relationshipDirective); + // Assume direction OUT + let fromType = typeName; + let toType = outputType; + if (relationshipDirection === 'IN') { + let temp = fromType; + fromType = outputType; + toType = temp; + } + [ + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentRelationshipMutationAPI({ + typeName, + fieldName, + outputType, + fromType, + toType, + relationshipName, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } + return [ + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ]; +}; + +const augmentNodeTypeAPI = ({ + definition, + typeName, + propertyOutputFields, + propertyInputValues, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + [operationTypeMap, generatedTypeMap] = augmentNodeMutationAPI({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + operationTypeMap, + config + }); + [operationTypeMap, generatedTypeMap] = buildNodeQueryAPI({ + typeName, + propertyInputValues, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + generatedTypeMap = buildNodeSelectionInputType({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + config + }); + return [ + propertyOutputFields, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ]; +}; + +const buildNodeSelectionInputType = ({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + config +}) => { + const mutationTypeName = OperationType.QUERY; + const mutationTypeNameLower = mutationTypeName.toLowerCase(); + if (shouldAugmentType(config, mutationTypeNameLower, typeName)) { + const primaryKey = getPrimaryKey(definition); + const propertyInputName = `_${typeName}Input`; + if (primaryKey) { + const primaryKeyName = primaryKey.name.value; + const primaryKeyInputConfig = propertyInputValues.find( + field => field.name === primaryKeyName + ); + if (primaryKeyInputConfig) { + generatedTypeMap[propertyInputName] = buildInputObjectType({ + name: buildName({ name: propertyInputName }), + fields: [ + buildInputValue({ + name: buildName({ name: primaryKeyName }), + type: buildNamedType({ + name: primaryKeyInputConfig.type.name, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + }) + }) + ] + }); + } + } + } + return generatedTypeMap; +}; diff --git a/src/augment/types/node/query.js b/src/augment/types/node/query.js new file mode 100644 index 00000000..ca2567a9 --- /dev/null +++ b/src/augment/types/node/query.js @@ -0,0 +1,241 @@ +import { GraphQLString } from 'graphql'; +import { buildRelationshipFilters } from '../relationship/query'; +import { + buildField, + buildInputValue, + buildName, + buildNamedType +} from '../../ast'; +import { + DirectiveDefinition, + buildAuthScopeDirective, + useAuthDirective +} from '../../directives'; +import { shouldAugmentType } from '../../augment'; +import { + OperationType, + isQueryTypeDefinition, + isMutationTypeDefinition, + isSubscriptionTypeDefinition +} from '../../types/types'; +import { TypeWrappers, getFieldDefinition } from '../../fields'; +import { + FilteringArgument, + PagingArgument, + OrderingArgument, + buildQueryFieldArguments, + buildQueryFilteringInputType, + buildQueryOrderingEnumType +} from '../../input-values'; + +const NodeQueryArgument = { + ...PagingArgument, + ...OrderingArgument, + ...FilteringArgument +}; + +export const augmentNodeTypeFieldInput = ({ + typeName, + definition, + fieldName, + fieldArguments, + fieldDirectives, + outputType, + config, + relationshipDirective, + outputTypeWrappers, + nodeInputTypeMap +}) => { + fieldArguments = augmentNodeQueryArguments({ + definition, + fieldArguments, + fieldDirectives, + outputType, + outputTypeWrappers, + config + }); + nodeInputTypeMap = augmentNodeQueryArgumentTypes({ + typeName, + definition, + fieldName, + outputType, + outputTypeWrappers, + relationshipDirective, + nodeInputTypeMap, + config + }); + return [fieldArguments, nodeInputTypeMap]; +}; + +const augmentNodeQueryArguments = ({ + definition, + fieldArguments, + fieldDirectives, + outputType, + outputTypeWrappers, + config +}) => { + const queryTypeNameLower = OperationType.QUERY.toLowerCase(); + if ( + !isMutationTypeDefinition({ definition }) && + !isSubscriptionTypeDefinition({ definition }) && + shouldAugmentType(config, queryTypeNameLower, outputType) + ) { + fieldArguments = buildQueryFieldArguments({ + augmentationMap: NodeQueryArgument, + fieldArguments, + fieldDirectives, + outputType, + outputTypeWrappers + }); + } + return fieldArguments; +}; + +const augmentNodeQueryArgumentTypes = ({ + typeName, + definition, + fieldName, + outputType, + outputTypeWrappers, + relationshipDirective, + nodeInputTypeMap, + config +}) => { + if (relationshipDirective && !isQueryTypeDefinition({ definition })) { + nodeInputTypeMap[FilteringArgument.FILTER].fields.push( + ...buildRelationshipFilters({ + typeName, + fieldName, + outputType: `_${outputType}Filter`, + relatedType: outputType, + outputTypeWrappers, + config + }) + ); + } + return nodeInputTypeMap; +}; + +export const buildNodeQueryAPI = ({ + typeName, + propertyInputValues, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + const queryTypeName = OperationType.QUERY; + const queryTypeNameLower = queryTypeName.toLowerCase(); + const queryType = operationTypeMap[queryTypeName]; + if (shouldAugmentType(config, queryTypeNameLower, typeName)) { + if (queryType) { + operationTypeMap = buildNodeQueryField({ + typeName, + queryType, + propertyInputValues, + operationTypeMap, + config + }); + } + generatedTypeMap = buildQueryOrderingEnumType({ + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap + }); + generatedTypeMap = buildQueryFilteringInputType({ + typeName: `_${typeName}Filter`, + typeDefinitionMap, + generatedTypeMap, + inputTypeMap: nodeInputTypeMap + }); + } + return [operationTypeMap, generatedTypeMap]; +}; + +const buildNodeQueryField = ({ + typeName, + queryType, + propertyInputValues, + operationTypeMap, + config +}) => { + const queryFields = queryType.fields; + if ( + !getFieldDefinition({ + fields: queryFields, + name: typeName + }) + ) { + queryFields.push( + buildField({ + name: buildName({ name: typeName }), + type: buildNamedType({ + name: typeName, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + }), + args: buildNodeQueryArguments({ + typeName, + propertyInputValues + }), + directives: buildNodeQueryDirectives({ + typeName, + config + }) + }) + ); + } + operationTypeMap[OperationType.QUERY].fields = queryFields; + return operationTypeMap; +}; + +const buildNodeQueryArguments = ({ typeName, propertyInputValues }) => { + // Do not persist type wrappers + propertyInputValues = propertyInputValues.map(arg => + buildInputValue({ + name: buildName({ name: arg.name }), + type: buildNamedType({ + name: arg.type.name + }) + }) + ); + if (!propertyInputValues.some(field => field.name.value === '_id')) { + propertyInputValues.push( + buildInputValue({ + name: buildName({ name: '_id' }), + type: buildNamedType({ + name: GraphQLString.name + }) + }) + ); + } + propertyInputValues = buildQueryFieldArguments({ + augmentationMap: NodeQueryArgument, + fieldArguments: propertyInputValues, + outputType: typeName, + outputTypeWrappers: { + [TypeWrappers.LIST_TYPE]: true + } + }); + return propertyInputValues; +}; + +const buildNodeQueryDirectives = ({ typeName, config }) => { + const directives = []; + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + directives.push( + buildAuthScopeDirective({ + scopes: [ + { + typeName, + mutation: `Read` + } + ] + }) + ); + } + return directives; +}; diff --git a/src/augment/types/relationship/mutation.js b/src/augment/types/relationship/mutation.js new file mode 100644 index 00000000..dfa71c14 --- /dev/null +++ b/src/augment/types/relationship/mutation.js @@ -0,0 +1,366 @@ +import _ from 'lodash'; +import { RelationshipDirectionField } from './relationship'; +import { buildNodeOutputFields } from './query'; +import { shouldAugmentRelationshipField } from '../../augment'; +import { OperationType } from '../../types/types'; +import { TypeWrappers, getFieldDefinition } from '../../fields'; +import { + DirectiveDefinition, + buildAuthScopeDirective, + buildMutationMetaDirective, + buildRelationDirective, + useAuthDirective, + getDirective, + isCypherField +} from '../../directives'; +import { + buildInputValue, + buildName, + buildNamedType, + buildField, + buildObjectType, + buildInputObjectType +} from '../../ast'; + +export const RelationshipMutation = { + CREATE: 'Add', + DELETE: 'Remove' +}; + +export const augmentRelationshipMutationAPI = ({ + typeName, + fieldName, + outputType, + fromType, + toType, + relationshipName, + propertyInputValues = [], + propertyOutputFields = [], + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + const mutationTypeName = OperationType.MUTATION; + const mutationType = operationTypeMap[mutationTypeName]; + const mutationTypeNameLower = mutationTypeName.toLowerCase(); + if ( + mutationType && + shouldAugmentRelationshipField( + config, + mutationTypeNameLower, + fromType, + toType + ) + ) { + Object.values(RelationshipMutation).forEach(mutationAction => { + const mutationName = buildRelationshipMutationName({ + mutationAction, + typeName, + fieldName + }); + if ( + !getFieldDefinition({ + fields: mutationType.fields, + name: mutationName + }) + ) { + [operationTypeMap, generatedTypeMap] = buildRelationshipMutationAPI({ + mutationAction, + mutationName, + relationshipName, + fromType, + toType, + propertyInputValues, + propertyOutputFields, + outputType, + generatedTypeMap, + operationTypeMap, + config + }); + } + }); + } + return [typeDefinitionMap, generatedTypeMap, operationTypeMap]; +}; + +const buildNodeSelectionArguments = ({ fromType, toType }) => { + return [ + buildInputValue({ + name: buildName({ + name: RelationshipDirectionField.FROM + }), + type: buildNamedType({ + name: `_${fromType}Input`, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + }) + }), + buildInputValue({ + name: buildName({ + name: RelationshipDirectionField.TO + }), + type: buildNamedType({ + name: `_${toType}Input`, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + }) + }) + ]; +}; + +const buildRelationshipMutationAPI = ({ + mutationAction, + mutationName, + relationshipName, + fromType, + toType, + propertyInputValues, + propertyOutputFields, + outputType, + generatedTypeMap, + operationTypeMap, + config +}) => { + const mutationOutputType = `_${mutationName}Payload`; + operationTypeMap = buildRelationshipMutationField({ + mutationAction, + mutationName, + relationshipName, + fromType, + toType, + propertyOutputFields, + mutationOutputType, + outputType, + operationTypeMap, + config + }); + generatedTypeMap = buildRelationshipMutationPropertyInputType({ + mutationAction, + outputType, + propertyInputValues, + generatedTypeMap + }); + generatedTypeMap = buildRelationshipMutationOutputType({ + mutationAction, + mutationOutputType, + propertyOutputFields, + relationshipName, + fromType, + toType, + generatedTypeMap + }); + return [operationTypeMap, generatedTypeMap]; +}; + +const buildRelationshipMutationField = ({ + mutationAction, + mutationName, + relationshipName, + fromType, + toType, + propertyOutputFields, + mutationOutputType, + outputType, + operationTypeMap, + config +}) => { + if ( + mutationAction === RelationshipMutation.CREATE || + mutationAction === RelationshipMutation.DELETE + ) { + operationTypeMap[OperationType.MUTATION].fields.push( + buildField({ + name: buildName({ + name: mutationName + }), + type: buildNamedType({ + name: mutationOutputType + }), + args: buildRelationshipMutationArguments({ + mutationAction, + fromType, + toType, + propertyOutputFields, + outputType + }), + directives: buildRelationshipMutationDirectives({ + mutationAction, + relationshipName, + fromType, + toType, + propertyOutputFields, + config + }) + }) + ); + } + return operationTypeMap; +}; + +const buildRelationshipPropertyInputArgument = ({ outputType }) => { + return buildInputValue({ + name: buildName({ name: 'data' }), + type: buildNamedType({ + name: `_${outputType}Input`, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true + } + }) + }); +}; + +const buildRelationshipMutationPropertyInputType = ({ + mutationAction, + outputType, + propertyInputValues, + generatedTypeMap +}) => { + if ( + mutationAction === RelationshipMutation.CREATE && + propertyInputValues.length + ) { + let nonComputedPropertyInputFields = propertyInputValues.filter(field => { + const cypherDirective = getDirective({ + directives: field.directives, + name: DirectiveDefinition.CYPHER + }); + return !cypherDirective; + }); + const inputTypeName = `_${outputType}Input`; + generatedTypeMap[inputTypeName] = buildInputObjectType({ + name: buildName({ name: inputTypeName }), + fields: nonComputedPropertyInputFields.map(inputValue => + buildInputValue({ + name: buildName({ name: inputValue.name }), + type: buildNamedType(inputValue.type) + }) + ) + }); + } + return generatedTypeMap; +}; + +const buildRelationshipMutationArguments = ({ + mutationAction, + fromType, + toType, + propertyOutputFields, + outputType +}) => { + const fieldArguments = buildNodeSelectionArguments({ fromType, toType }); + if ( + mutationAction === RelationshipMutation.CREATE && + propertyOutputFields.length + ) { + fieldArguments.push( + buildRelationshipPropertyInputArgument({ + outputType + }) + ); + } + return fieldArguments; +}; + +const buildRelationshipMutationDirectives = ({ + mutationAction, + relationshipName, + fromType, + toType, + propertyOutputFields, + config +}) => { + const mutationMetaDirective = buildMutationMetaDirective({ + relationshipName, + fromType, + toType + }); + const directives = [mutationMetaDirective]; + if (propertyOutputFields.length) { + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + directives.push( + buildAuthScopeDirective({ + scopes: [ + { + typeName: fromType, + mutation: `Create` + }, + { + typeName: toType, + mutation: `Create` + } + ] + }) + ); + } + } else if (mutationAction === RelationshipMutation.DELETE) { + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + directives.push( + buildAuthScopeDirective({ + scopes: [ + { + typeName: fromType, + mutation: `Delete` + }, + { + typeName: toType, + mutation: `Delete` + } + ] + }) + ); + } + } + return directives; +}; + +const buildRelationshipMutationOutputType = ({ + mutationAction, + mutationOutputType, + propertyOutputFields, + relationshipName, + fromType, + toType, + generatedTypeMap +}) => { + if ( + mutationAction === RelationshipMutation.CREATE || + mutationAction === RelationshipMutation.DELETE + ) { + const relationTypeDirective = buildRelationDirective({ + relationshipName, + fromType, + toType + }); + let fields = buildNodeOutputFields({ fromType, toType }); + if (mutationAction === RelationshipMutation.CREATE) { + // console.log("mutationOutputType: ", mutationOutputType); + // TODO temporary block on cypher field arguments + const mutationOutputFields = propertyOutputFields.map(field => { + if (isCypherField({ directives: field.directives })) { + return { + ...field, + arguments: [] + }; + } else return field; + }); + fields.push(...mutationOutputFields); + } + generatedTypeMap[mutationOutputType] = buildObjectType({ + name: buildName({ name: mutationOutputType }), + fields, + directives: [relationTypeDirective] + }); + } + return generatedTypeMap; +}; + +const buildRelationshipMutationName = ({ + mutationAction, + typeName, + fieldName +}) => + `${mutationAction}${typeName}${fieldName[0].toUpperCase() + + fieldName.substr(1)}`; diff --git a/src/augment/types/relationship/query.js b/src/augment/types/relationship/query.js new file mode 100644 index 00000000..f50bfee2 --- /dev/null +++ b/src/augment/types/relationship/query.js @@ -0,0 +1,472 @@ +import { RelationshipDirectionField } from './relationship'; +import { shouldAugmentRelationshipField } from '../../augment'; +import { + OperationType, + isMutationTypeDefinition, + isSubscriptionTypeDefinition +} from '../../types/types'; +import { TypeWrappers, isListTypeField, unwrapNamedType } from '../../fields'; +import { + FilteringArgument, + buildFilters, + buildQueryFieldArguments, + buildQueryFilteringInputType +} from '../../input-values'; +import { buildRelationDirective } from '../../directives'; +import { + buildInputObjectType, + buildField, + buildName, + buildNamedType, + buildObjectType, + buildInputValue +} from '../../ast'; + +const RelationshipQueryArgument = { + // ...PagingArgument, + // ...OrderingArgument, + ...FilteringArgument +}; + +export const augmentRelationshipQueryAPI = ({ + typeName, + definition, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap, + relationshipInputTypeMap, + outputTypeWrappers, + config, + relationshipName, + fieldType, + propertyOutputFields +}) => { + const queryTypeNameLower = OperationType.QUERY.toLowerCase(); + if ( + shouldAugmentRelationshipField(config, queryTypeNameLower, fromType, toType) + ) { + const relatedType = decideRelatedType({ + typeName, + fromType, + toType + }); + if ( + validateRelationTypeDirectedFields( + typeName, + fieldName, + fromType, + toType, + outputType + ) + ) { + [fieldType, generatedTypeMap] = augmentRelationshipTypeFieldOutput({ + typeName, + relatedType, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + generatedTypeMap, + outputTypeWrappers, + config, + relationshipName, + fieldType, + propertyOutputFields + }); + [ + fieldArguments, + generatedTypeMap, + nodeInputTypeMap + ] = augmentRelationshipTypeFieldInput({ + typeName, + definition, + relatedType, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap, + relationshipInputTypeMap, + outputTypeWrappers, + config + }); + } + } + + return [ + fieldType, + fieldArguments, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap + ]; +}; + +const augmentRelationshipTypeFieldInput = ({ + typeName, + definition, + relatedType, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap, + relationshipInputTypeMap, + outputTypeWrappers, + config +}) => { + const nodeFilteringFields = nodeInputTypeMap[FilteringArgument.FILTER].fields; + let relationshipFilterTypeName = `_${typeName}${outputType[0].toUpperCase() + + outputType.substr(1)}`; + // Assume outgoing relationship + if (fromType === toType) { + relationshipFilterTypeName = `_${outputType}Directions`; + } + nodeFilteringFields.push( + ...buildRelationshipFilters({ + typeName, + fieldName, + outputType: `${relationshipFilterTypeName}Filter`, + relatedType: outputType, + outputTypeWrappers, + config + }) + ); + [fieldArguments, generatedTypeMap] = augmentRelationshipTypeFieldArguments({ + fieldArguments, + typeName, + definition, + fromType, + toType, + outputType, + relatedType, + relationshipFilterTypeName, + outputTypeWrappers, + typeDefinitionMap, + generatedTypeMap, + relationshipInputTypeMap + }); + return [fieldArguments, generatedTypeMap, nodeInputTypeMap]; +}; + +const augmentRelationshipTypeFieldOutput = ({ + typeName, + relatedType, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + generatedTypeMap, + outputTypeWrappers, + relationshipName, + fieldType, + propertyOutputFields +}) => { + const relationshipOutputName = `_${typeName}${fieldName[0].toUpperCase() + + fieldName.substr(1)}`; + const unwrappedType = unwrapNamedType({ type: fieldType }); + if (fromType === toType) { + // Clear arguments on this field, given their distribution + fieldType = buildNamedType({ + name: `${relationshipOutputName}Directions` + }); + } else { + // Output transform + unwrappedType.name = relationshipOutputName; + fieldType = buildNamedType(unwrappedType); + } + generatedTypeMap = buildRelationshipFieldOutputTypes({ + outputType, + fromType, + toType, + outputTypeWrappers, + fieldArguments, + relationshipOutputName, + relationshipName, + relatedType, + propertyOutputFields, + generatedTypeMap + }); + return [fieldType, generatedTypeMap]; +}; + +const augmentRelationshipTypeFieldArguments = ({ + fieldArguments, + typeName, + definition, + fromType, + toType, + outputType, + relatedType, + relationshipFilterTypeName, + outputTypeWrappers, + typeDefinitionMap, + generatedTypeMap, + relationshipInputTypeMap +}) => { + if ( + !isMutationTypeDefinition({ definition }) && + !isSubscriptionTypeDefinition({ definition }) + ) { + if (fromType !== toType) { + fieldArguments = buildQueryFieldArguments({ + augmentationMap: RelationshipQueryArgument, + fieldArguments, + outputType: `${typeName}${outputType}`, + outputTypeWrappers + }); + } else { + fieldArguments = []; + } + } + generatedTypeMap = buildRelationshipSelectionArgumentInputTypes({ + fromType, + toType, + relatedType, + relationshipFilterTypeName, + generatedTypeMap, + relationshipInputTypeMap, + typeDefinitionMap + }); + return [fieldArguments, generatedTypeMap]; +}; + +export const buildRelationshipFilters = ({ + typeName, + fieldName, + outputType, + relatedType, + outputTypeWrappers, + config +}) => { + let filters = []; + const queryTypeNameLower = OperationType.QUERY.toLowerCase(); + if ( + shouldAugmentRelationshipField( + config, + queryTypeNameLower, + typeName, + relatedType + ) + ) { + if (isListTypeField({ wrappers: outputTypeWrappers })) { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: [ + '_not', + '_in', + '_not_in', + '_some', + '_none', + '_single', + '_every' + ] + }); + } else { + filters = buildFilters({ + fieldName, + fieldConfig: { + name: fieldName, + type: { + name: outputType + } + }, + filterTypes: ['_not', '_in', '_not_in'] + }); + } + } + return filters; +}; + +export const buildNodeOutputFields = ({ + fromType, + toType, + args = [], + wrappers = {} +}) => { + return [ + buildField({ + name: buildName({ + name: RelationshipDirectionField.FROM + }), + args, + type: buildNamedType({ + name: fromType, + wrappers + }) + }), + buildField({ + name: buildName({ + name: RelationshipDirectionField.TO + }), + args, + type: buildNamedType({ + name: toType, + wrappers + }) + }) + ]; +}; + +const buildRelationshipFieldOutputTypes = ({ + outputType, + fromType, + toType, + outputTypeWrappers, + fieldArguments, + relationshipOutputName, + relationshipName, + relatedType, + propertyOutputFields, + generatedTypeMap +}) => { + const relationTypeDirective = buildRelationDirective({ + relationshipName, + fromType, + toType + }); + if (fromType === toType) { + fieldArguments = buildQueryFieldArguments({ + augmentationMap: RelationshipQueryArgument, + fieldArguments, + outputType, + outputTypeWrappers + }); + const reflexiveOutputName = `${relationshipOutputName}Directions`; + generatedTypeMap[reflexiveOutputName] = buildObjectType({ + name: buildName({ name: reflexiveOutputName }), + fields: buildNodeOutputFields({ + fromType: relationshipOutputName, + toType: relationshipOutputName, + args: fieldArguments, + wrappers: { + [TypeWrappers.LIST_TYPE]: true + } + }), + directives: [relationTypeDirective] + }); + } + generatedTypeMap[relationshipOutputName] = buildObjectType({ + name: buildName({ name: relationshipOutputName }), + fields: [ + ...propertyOutputFields, + buildField({ + name: buildName({ name: relatedType }), + type: buildNamedType({ + name: relatedType + }) + }) + ], + directives: [relationTypeDirective] + }); + return generatedTypeMap; +}; + +const buildRelationshipSelectionArgumentInputTypes = ({ + fromType, + toType, + relatedType, + relationshipFilterTypeName, + generatedTypeMap, + relationshipInputTypeMap, + typeDefinitionMap +}) => { + const relationshipFilteringFields = + relationshipInputTypeMap[FilteringArgument.FILTER].fields; + const relatedTypeFilterName = + relationshipInputTypeMap[FilteringArgument.FILTER].name; + if (fromType === toType) { + const reflexiveFilteringTypeName = `${relationshipFilterTypeName}Filter`; + generatedTypeMap[reflexiveFilteringTypeName] = buildInputObjectType({ + name: buildName({ + name: reflexiveFilteringTypeName + }), + fields: buildNodeInputFields({ + fromType: relatedTypeFilterName, + toType: relatedTypeFilterName + }) + }); + } + const relatedTypeFilteringField = buildInputValue({ + name: buildName({ name: relatedType }), + type: buildNamedType({ + name: `_${relatedType}Filter` + }) + }); + relationshipFilteringFields.push(relatedTypeFilteringField); + generatedTypeMap = buildQueryFilteringInputType({ + typeName: relatedTypeFilterName, + typeDefinitionMap, + generatedTypeMap, + inputTypeMap: relationshipInputTypeMap + }); + return generatedTypeMap; +}; + +const buildNodeInputFields = ({ fromType, toType }) => { + return [ + buildInputValue({ + name: buildName({ + name: RelationshipDirectionField.FROM + }), + type: buildNamedType({ + name: fromType + }) + }), + buildInputValue({ + name: buildName({ + name: RelationshipDirectionField.TO + }), + type: buildNamedType({ + name: toType + }) + }) + ]; +}; + +const decideRelatedType = ({ typeName, fromType, toType }) => { + let relatedType = toType; + if (fromType !== toType) { + // Interpret relationship direction + if (typeName === toType) { + // Is incoming relationship + relatedType = fromType; + } + } + return relatedType; +}; + +const validateRelationTypeDirectedFields = ( + typeName, + fieldName, + fromName, + toName, + outputType +) => { + // directive to and from are not the same and neither are equal to this + if (fromName !== toName && toName !== typeName && fromName !== typeName) { + throw new Error( + `The ${fieldName} field on the ${typeName} node type uses the ${outputType} relationship type but ${outputType} comes from ${fromName} and goes to ${toName}` + ); + } + return true; +}; diff --git a/src/augment/types/relationship/relationship.js b/src/augment/types/relationship/relationship.js new file mode 100644 index 00000000..2a55165a --- /dev/null +++ b/src/augment/types/relationship/relationship.js @@ -0,0 +1,204 @@ +import { augmentRelationshipQueryAPI } from './query'; +import { augmentRelationshipMutationAPI } from './mutation'; +import { + unwrapNamedType, + isPropertyTypeField, + getFieldType, + toSnakeCase +} from '../../fields'; +import { + FilteringArgument, + augmentInputTypePropertyFields +} from '../../input-values'; +import { + DirectiveDefinition, + getDirective, + isIgnoredField, + isCypherField, + getDirectiveArgument +} from '../../directives'; +import { isOperationTypeDefinition } from '../../types/types'; + +export const RelationshipDirectionField = { + FROM: 'from', + TO: 'to' +}; + +export const augmentRelationshipTypeField = ({ + typeName, + definition, + fieldType, + fieldArguments, + fieldDirectives, + fieldName, + outputDefinition, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + outputType, + config, + outputTypeWrappers +}) => { + if (!isOperationTypeDefinition({ definition })) { + if (!isCypherField({ directives: fieldDirectives })) { + const relationshipTypeDirective = getDirective({ + directives: outputDefinition.directives, + name: DirectiveDefinition.RELATION + }); + let relationshipName = getDirectiveArgument({ + directive: relationshipTypeDirective, + name: 'name' + }); + relationshipName = decideDefaultRelationshipName({ + relationshipTypeDirective, + outputType, + relationshipName + }); + let [ + fromType, + toType, + propertyInputValues, + propertyOutputFields, + relationshipInputTypeMap + ] = augmentRelationshipTypeFields({ + typeName, + outputType, + outputDefinition, + typeDefinitionMap, + config + }); + [ + fieldType, + fieldArguments, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap + ] = augmentRelationshipQueryAPI({ + typeName, + definition, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + typeDefinitionMap, + generatedTypeMap, + nodeInputTypeMap, + relationshipInputTypeMap, + outputTypeWrappers, + config, + relationshipName, + fieldType, + propertyOutputFields + }); + [ + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentRelationshipMutationAPI({ + typeName, + fieldName, + outputType, + fromType, + toType, + relationshipName, + propertyInputValues, + propertyOutputFields, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } + } + return [ + fieldType, + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ]; +}; + +const augmentRelationshipTypeFields = ({ + typeName, + outputType, + outputDefinition, + typeDefinitionMap, + config +}) => { + const fields = outputDefinition.fields; + const fromTypeName = getFieldType({ + fields, + name: RelationshipDirectionField.FROM + }); + const toTypeName = getFieldType({ + fields, + name: RelationshipDirectionField.TO + }); + let relatedTypeFilterName = `_${typeName}${outputType}Filter`; + if (fromTypeName === toTypeName) { + relatedTypeFilterName = `_${outputType}Filter`; + } + let relationshipInputTypeMap = { + [FilteringArgument.FILTER]: { + name: relatedTypeFilterName, + fields: [] + } + }; + const propertyInputValues = []; + const propertyOutputFields = fields.reduce((outputFields, field) => { + const fieldName = field.name.value; + const fieldDirectives = field.directives; + if (!isIgnoredField({ directives: fieldDirectives })) { + const unwrappedType = unwrapNamedType({ type: field.type }); + const outputType = unwrappedType.name; + const outputTypeWrappers = unwrappedType.wrappers; + const fieldDefinition = typeDefinitionMap[outputType]; + const outputKind = fieldDefinition ? fieldDefinition.kind : ''; + if ( + isPropertyTypeField({ + kind: outputKind, + type: outputType + }) + ) { + relationshipInputTypeMap = augmentInputTypePropertyFields({ + inputTypeMap: relationshipInputTypeMap, + fieldName, + fieldDirectives, + outputType, + outputKind, + outputTypeWrappers, + config + }); + propertyInputValues.push({ + name: fieldName, + type: unwrappedType, + directives: fieldDirectives + }); + outputFields.push(field); + } + } + return outputFields; + }, []); + return [ + fromTypeName, + toTypeName, + propertyInputValues, + propertyOutputFields, + relationshipInputTypeMap + ]; +}; + +const decideDefaultRelationshipName = ({ + relationshipTypeDirective, + outputType, + relationshipName +}) => { + if (relationshipTypeDirective && !relationshipName) { + relationshipName = toSnakeCase(outputType); + } + return relationshipName; +}; diff --git a/src/augment/types/temporal.js b/src/augment/types/temporal.js new file mode 100644 index 00000000..debcbe77 --- /dev/null +++ b/src/augment/types/temporal.js @@ -0,0 +1,138 @@ +import { GraphQLInt, GraphQLString } from 'graphql'; +import { Neo4jTypeName, buildNeo4jType } from '../types/types'; +import { buildName, buildField, buildNamedType, buildInputValue } from '../ast'; + +export const TemporalType = { + TIME: 'Time', + DATE: 'Date', + DATETIME: 'DateTime', + LOCALTIME: 'LocalTime', + LOCALDATETIME: 'LocalDateTime' +}; + +const Neo4jTimeField = { + HOUR: 'hour', + MINUTE: 'minute', + SECOND: 'second', + MILLISECOND: 'millisecond', + MICROSECOND: 'microsecond', + NANOSECOND: 'nanosecond', + TIMEZONE: 'timezone' +}; + +const Neo4jDateField = { + YEAR: 'year', + MONTH: 'month', + DAY: 'day' +}; + +const Neo4jTypeFormatted = { + FORMATTED: 'formatted' +}; + +const Neo4jTime = { + [Neo4jTimeField.HOUR]: GraphQLInt.name, + [Neo4jTimeField.MINUTE]: GraphQLInt.name, + [Neo4jTimeField.SECOND]: GraphQLInt.name, + [Neo4jTimeField.MILLISECOND]: GraphQLInt.name, + [Neo4jTimeField.MICROSECOND]: GraphQLInt.name, + [Neo4jTimeField.NANOSECOND]: GraphQLInt.name, + [Neo4jTimeField.TIMEZONE]: GraphQLString.name +}; + +const Neo4jDate = { + [Neo4jDateField.YEAR]: GraphQLInt.name, + [Neo4jDateField.MONTH]: GraphQLInt.name, + [Neo4jDateField.DAY]: GraphQLInt.name +}; + +export const buildTemporalTypes = ({ typeMap, config = {} }) => { + config.temporal = decideTemporalConfig(config); + const temporalConfig = config.temporal; + Object.values(TemporalType).forEach(typeName => { + const typeNameLower = typeName.toLowerCase(); + if (temporalConfig[typeNameLower] === true) { + const objectTypeName = `${Neo4jTypeName}${typeName}`; + const inputTypeName = `${objectTypeName}Input`; + let fields = []; + if (typeName === TemporalType.DATE) { + fields = Object.entries(Neo4jDate); + } else if (typeName === TemporalType.TIME) { + fields = Object.entries(Neo4jTime); + } else if (typeName === TemporalType.LOCALTIME) { + fields = Object.entries({ + ...Neo4jTime + }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); + } else if (typeName === TemporalType.DATETIME) { + fields = Object.entries({ + ...Neo4jDate, + ...Neo4jTime + }); + } else if (typeName === TemporalType.LOCALDATETIME) { + fields = Object.entries({ + ...Neo4jDate, + ...Neo4jTime + }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); + } + let inputFields = []; + let outputFields = []; + fields.forEach(([fieldName, fieldType]) => { + const fieldNameLower = fieldName.toLowerCase(); + const fieldConfig = { + name: buildName({ name: fieldNameLower }), + type: buildNamedType({ + name: fieldType + }) + }; + inputFields.push(buildInputValue(fieldConfig)); + outputFields.push(buildField(fieldConfig)); + }); + const formattedFieldConfig = { + name: buildName({ + name: Neo4jTypeFormatted.FORMATTED + }), + type: buildNamedType({ + name: GraphQLString.name + }) + }; + inputFields.push(buildInputValue(formattedFieldConfig)); + outputFields.push(buildField(formattedFieldConfig)); + typeMap = buildNeo4jType({ + inputTypeName, + inputFields, + objectTypeName, + outputFields, + typeMap + }); + } + }); + return typeMap; +}; + +const decideTemporalConfig = config => { + let defaultConfig = { + time: true, + date: true, + datetime: true, + localtime: true, + localdatetime: true + }; + const providedConfig = config ? config.temporal : defaultConfig; + if (typeof providedConfig === 'boolean') { + if (providedConfig === false) { + defaultConfig.time = false; + defaultConfig.date = false; + defaultConfig.datetime = false; + defaultConfig.localtime = false; + defaultConfig.localdatetime = false; + } + } else if (typeof providedConfig === 'object') { + Object.keys(defaultConfig).forEach(e => { + if (providedConfig[e] === undefined) { + providedConfig[e] = defaultConfig[e]; + } + }); + defaultConfig = providedConfig; + } + return defaultConfig; +}; diff --git a/src/augment/types/types.js b/src/augment/types/types.js new file mode 100644 index 00000000..10733c14 --- /dev/null +++ b/src/augment/types/types.js @@ -0,0 +1,266 @@ +import { + visit, + Kind, + GraphQLID, + GraphQLString, + GraphQLInt, + GraphQLFloat, + GraphQLBoolean +} from 'graphql'; +import { + isIgnoredField, + DirectiveDefinition, + getDirective +} from '../directives'; +import { shouldAugmentType } from '../augment'; +import { + buildName, + buildNamedType, + buildObjectType, + buildInputObjectType +} from '../ast'; +import { TemporalType, buildTemporalTypes } from './temporal'; +import { + isTemporalField, + unwrapNamedType, + getFieldDefinition +} from '../fields'; +import { RelationshipDirectionField } from '../types/relationship/relationship'; + +export const Neo4jTypeName = `_Neo4j`; + +export const Neo4jStructuralType = { + NODE: 'Node', + RELATIONSHIP: 'Relationship' +}; + +export const OperationType = { + QUERY: 'Query', + MUTATION: 'Mutation', + SUBSCRIPTION: 'Subscription' +}; + +// https://neo4j.com/docs/cypher-manual/current/syntax/values/#cypher-values +export const Neo4jDataType = { + PROPERTY: { + [GraphQLInt.name]: 'Integer', + [GraphQLFloat.name]: 'Float', + [GraphQLString.name]: 'String', + [GraphQLID.name]: 'String', + [Kind.ENUM_TYPE_DEFINITION]: 'String', + [GraphQLBoolean.name]: 'Boolean', + [TemporalType.TIME]: 'Temporal', + [TemporalType.DATE]: 'Temporal', + [TemporalType.DATETIME]: 'Temporal', + [TemporalType.LOCALTIME]: 'Temporal', + [TemporalType.LOCALDATETIME]: 'Temporal' + }, + STRUCTURAL: { + [Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType + } +}; + +export const isNodeType = ({ definition }) => + interpretType({ definition }) === Neo4jStructuralType.NODE; + +export const isRelationshipType = ({ definition }) => + interpretType({ definition }) === Neo4jStructuralType.RELATIONSHIP; + +export const isObjectTypeDefinition = ({ definition = {} }) => + definition.kind === Kind.OBJECT_TYPE_DEFINITION; + +export const isInputObjectTypeDefinition = ({ definition = {} }) => + definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION; + +export const isInterfaceTypeDefinition = ({ definition = {} }) => + definition.kind === Kind.INTERFACE_TYPE_DEFINITION; + +export const isUnionTypeDefinition = ({ definition = {} }) => + definition.kind === Kind.UNION_TYPE_DEFINITION; + +export const isOperationTypeDefinition = ({ definition = {} }) => + isQueryTypeDefinition({ definition }) || + isMutationTypeDefinition({ definition }) || + isSubscriptionTypeDefinition({ definition }); + +export const isQueryTypeDefinition = ({ definition }) => + definition.name && definition.name.value === OperationType.QUERY; + +export const isMutationTypeDefinition = ({ definition }) => + definition.name && definition.name.value === OperationType.MUTATION; + +export const isSubscriptionTypeDefinition = ({ definition }) => + definition.name && definition.name.value === OperationType.SUBSCRIPTION; + +export const isNeo4jTemporalType = ({ type }) => + Object.values(TemporalType).some(name => type === `${Neo4jTypeName}${name}`); + +export const isNeo4jPropertyType = ({ type }) => isNeo4jTemporalType({ type }); + +export const interpretType = ({ definition = {} }) => { + const kind = definition.kind; + // Get the structural types allows for this definition kind + const neo4jStructuralTypes = Neo4jDataType.STRUCTURAL[kind]; + let neo4jType = ''; + if (neo4jStructuralTypes) { + const name = definition.name.value; + if (!isNeo4jPropertyType({ type: name })) { + const fields = definition.fields; + const typeDirectives = definition.directives; + if ( + neo4jStructuralTypes.RELATIONSHIP && + getDirective({ + directives: typeDirectives, + name: DirectiveDefinition.RELATION + }) && + getFieldDefinition({ + fields, + name: RelationshipDirectionField.FROM + }) && + getFieldDefinition({ + fields, + name: RelationshipDirectionField.TO + }) + ) { + neo4jType = neo4jStructuralTypes.RELATIONSHIP; + } else if (neo4jStructuralTypes.NODE) { + // If not a relationship, assume node + neo4jType = neo4jStructuralTypes.NODE; + } + } + } + return neo4jType; +}; + +export const buildNeo4jTypes = ({ generatedTypeMap, config }) => { + generatedTypeMap = buildTemporalTypes({ + typeMap: generatedTypeMap, + config + }); + return generatedTypeMap; +}; + +export const buildNeo4jType = ({ + inputTypeName, + inputFields, + objectTypeName, + outputFields, + typeMap +}) => { + typeMap[objectTypeName] = buildObjectType({ + name: buildName({ name: objectTypeName }), + fields: outputFields + }); + typeMap[inputTypeName] = buildInputObjectType({ + name: buildName({ name: inputTypeName }), + fields: inputFields + }); + return typeMap; +}; + +export const transformNeo4jTypes = ({ definitions = [], config }) => { + const inputTypeSuffix = `Input`; + return visit(definitions, { + [Kind.INPUT_VALUE_DEFINITION]: field => { + const directives = field.directives; + if (!isIgnoredField({ directives })) { + const type = field.type; + const unwrappedType = unwrapNamedType({ type }); + const typeName = unwrappedType.name; + if (isTemporalField({ type: typeName })) { + const typeNameLower = typeName.toLowerCase(); + if (config.temporal[typeNameLower]) { + unwrappedType.name = `${Neo4jTypeName}${typeName}${inputTypeSuffix}`; + } + } else if (isNeo4jPropertyType({ type: typeName })) { + unwrappedType.name = `${typeName}${inputTypeSuffix}`; + } + field.type = buildNamedType(unwrappedType); + } + return field; + }, + [Kind.FIELD_DEFINITION]: field => { + const directives = field.directives; + if (!isIgnoredField({ directives })) { + const type = field.type; + const unwrappedType = unwrapNamedType({ type }); + const typeName = unwrappedType.name; + if (isTemporalField({ type: typeName })) { + const typeNameLower = typeName.toLowerCase(); + if (config.temporal[typeNameLower]) { + unwrappedType.name = `${Neo4jTypeName}${typeName}`; + } + } + field.type = buildNamedType(unwrappedType); + } + return field; + } + }); +}; + +export const initializeOperationTypes = ({ + typeDefinitionMap, + config = {} +}) => { + const types = Object.keys(typeDefinitionMap); + const queryTypeName = OperationType.QUERY; + const queryTypeNameLower = queryTypeName.toLowerCase(); + const mutationTypeName = OperationType.MUTATION; + const mutationTypeNameLower = mutationTypeName.toLowerCase(); + const subscriptionTypeName = OperationType.SUBSCRIPTION; + let queryType = typeDefinitionMap[queryTypeName]; + let mutationType = typeDefinitionMap[mutationTypeName]; + let subscriptionType = typeDefinitionMap[subscriptionTypeName]; + if ( + hasNonExcludedNodeType( + types, + typeDefinitionMap, + queryTypeNameLower, + config + ) && + !queryType && + config.query + ) { + queryType = buildObjectType({ + name: buildName({ name: queryTypeName }) + }); + } + if ( + hasNonExcludedNodeType( + types, + typeDefinitionMap, + mutationTypeNameLower, + config + ) && + !mutationType && + config.mutation + ) { + mutationType = buildObjectType({ + name: buildName({ name: mutationTypeName }) + }); + } + const operationTypeMap = {}; + if (queryType) { + operationTypeMap[OperationType.QUERY] = queryType; + } + if (mutationType) { + operationTypeMap[OperationType.MUTATION] = mutationType; + } + if (subscriptionType) { + operationTypeMap[OperationType.SUBSCRIPTION] = subscriptionType; + } + return [typeDefinitionMap, operationTypeMap]; +}; + +const hasNonExcludedNodeType = (types, typeMap, rootType, config) => { + return types.find(e => { + const type = typeMap[e]; + const typeName = type.name ? type.name.value : ''; + if (typeName) { + return ( + isNodeType({ definition: type }) && + shouldAugmentType(config, rootType, typeName) + ); + } + }); +}; From a5c8b214ec75fe9cd712a0119bad77dd16a1ca58 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 20:00:15 -0700 Subject: [PATCH 09/37] Replaced with folder for refactor --- src/augment.js | 1997 ------------------------------------------------ 1 file changed, 1997 deletions(-) delete mode 100644 src/augment.js diff --git a/src/augment.js b/src/augment.js deleted file mode 100644 index e3d8d6fb..00000000 --- a/src/augment.js +++ /dev/null @@ -1,1997 +0,0 @@ -import { makeExecutableSchema } from 'graphql-tools'; -import { parse, print } from 'graphql'; -import { neo4jgraphql } from './index'; -import { - printTypeMap, - extractTypeMapFromTypeDefs, - createOperationMap, - addDirectiveDeclarations, - _getNamedType, - getPrimaryKey, - getFieldDirective, - getRelationTypeDirective, - getRelationMutationPayloadFieldsFromAst, - getRelationDirection, - getRelationName, - getTypeDirective, - isBasicScalar, - _isListType, - isKind, - isNonNullType, - isNodeType, - parseFieldSdl, - parseDirectiveSdl, - isTemporalType, - excludeIgnoredTypes, - getCustomFieldResolver, - possiblyAddIgnoreDirective, - getExcludedTypes, - buildInputValueDefinitions -} from './utils'; -import { - possiblyAddDirectiveImplementations, - possiblyAddScopeDirective -} from './auth'; - -export const augmentedSchema = (typeMap, resolvers, config) => { - const augmentedTypeMap = augmentTypeMap(typeMap, resolvers, config); - const augmentedResolvers = augmentResolvers( - augmentedTypeMap, - resolvers, - config - ); - const schemaDirectives = possiblyAddDirectiveImplementations( - schemaDirectives, - typeMap, - config - ); - return makeExecutableSchema({ - typeDefs: printTypeMap(augmentedTypeMap), - resolvers: augmentedResolvers, - resolverValidationOptions: { - requireResolversForResolveType: false - }, - schemaDirectives - }); -}; - -export const makeAugmentedExecutableSchema = ({ - typeDefs, - resolvers, - logger, - allowUndefinedInResolve, - resolverValidationOptions, - directiveResolvers, - schemaDirectives, - parseOptions, - inheritResolversFromInterfaces, - config -}) => { - const typeMap = extractTypeMapFromTypeDefs(typeDefs); - const augmentedTypeMap = augmentTypeMap(typeMap, resolvers, config); - const augmentedResolvers = augmentResolvers( - augmentedTypeMap, - resolvers, - config - ); - resolverValidationOptions.requireResolversForResolveType = false; - schemaDirectives = possiblyAddDirectiveImplementations( - schemaDirectives, - typeMap, - config - ); - return makeExecutableSchema({ - typeDefs: printTypeMap(augmentedTypeMap), - resolvers: augmentedResolvers, - logger: logger, - allowUndefinedInResolve: allowUndefinedInResolve, - resolverValidationOptions: resolverValidationOptions, - directiveResolvers: directiveResolvers, - schemaDirectives: schemaDirectives, - parseOptions: parseOptions, - inheritResolversFromInterfaces: inheritResolversFromInterfaces - }); -}; - -export const extractTypeMapFromSchema = schema => { - const typeMap = schema.getTypeMap(); - const directives = schema.getDirectives(); - const types = { ...typeMap, ...directives }; - let astNode = {}; - return Object.keys(types).reduce((acc, t) => { - astNode = types[t].astNode; - if (astNode !== undefined) { - acc[astNode.name.value] = astNode; - } - return acc; - }, {}); -}; - -export const extractResolversFromSchema = schema => { - const _typeMap = schema && schema._typeMap ? schema._typeMap : {}; - const types = Object.keys(_typeMap); - let type = {}; - let schemaTypeResolvers = {}; - return types.reduce((acc, t) => { - // prevent extraction from schema introspection system keys - if ( - t !== '__Schema' && - t !== '__Type' && - t !== '__TypeKind' && - t !== '__Field' && - t !== '__InputValue' && - t !== '__EnumValue' && - t !== '__Directive' - ) { - type = _typeMap[t]; - // resolvers are stored on the field level at a .resolve key - schemaTypeResolvers = extractFieldResolversFromSchemaType(type); - // do not add unless there exists at least one field resolver for type - if (schemaTypeResolvers) { - acc[t] = schemaTypeResolvers; - } - } - return acc; - }, {}); -}; - -const extractFieldResolversFromSchemaType = type => { - const fields = type._fields; - const fieldKeys = fields ? Object.keys(fields) : []; - const fieldResolvers = - fieldKeys.length > 0 - ? fieldKeys.reduce((acc, t) => { - // do not add entry for this field unless it has resolver - if (fields[t].resolve !== undefined) { - acc[t] = fields[t].resolve; - } - return acc; - }, {}) - : undefined; - // do not return value unless there exists at least 1 field resolver - return fieldResolvers && Object.keys(fieldResolvers).length > 0 - ? fieldResolvers - : undefined; -}; - -export const augmentTypeMap = (typeMap, resolvers, config) => { - // IDEA: elevate into config as config.rootTypes? - const rootTypes = { - query: 'Query', - mutation: 'Mutation' - }; - config = excludeIgnoredTypes(typeMap, config); - typeMap = initializeOperationTypes(typeMap, rootTypes, config); - typeMap = addRelationTypeDirectives(typeMap); - typeMap = addTemporalTypes(typeMap, config); - Object.entries(typeMap).forEach(([name, type]) => { - if (!isTemporalType(name)) { - typeMap[name] = augmentType(type, typeMap, resolvers, rootTypes, config); - typeMap = possiblyAddQuery(type, typeMap, resolvers, rootTypes, config); - typeMap = possiblyAddOrderingEnum(type, typeMap, resolvers, config); - typeMap = possiblyAddTypeInput(type, typeMap, resolvers, config); - typeMap = possiblyAddFilterInput(type, typeMap, resolvers, config); - typeMap = possiblyAddTypeMutations(type, typeMap, resolvers, config); - typeMap = handleRelationFields(type, typeMap, resolvers, config); - } - }); - typeMap = augmentQueryArguments(typeMap, config, rootTypes); - typeMap = addDirectiveDeclarations(typeMap, config); - return typeMap; -}; - -const augmentResolvers = (augmentedTypeMap, resolvers, config) => { - let queryResolvers = resolvers && resolvers.Query ? resolvers.Query : {}; - const generatedQueryMap = createOperationMap(augmentedTypeMap.Query); - queryResolvers = possiblyAddResolvers( - generatedQueryMap, - queryResolvers, - config - ); - if (Object.keys(queryResolvers).length > 0) { - resolvers.Query = queryResolvers; - } - let mutationResolvers = - resolvers && resolvers.Mutation ? resolvers.Mutation : {}; - const generatedMutationMap = createOperationMap(augmentedTypeMap.Mutation); - mutationResolvers = possiblyAddResolvers( - generatedMutationMap, - mutationResolvers, - config - ); - if (Object.keys(mutationResolvers).length > 0) { - resolvers.Mutation = mutationResolvers; - } - // must implement __resolveInfo for every Interface type - // we use "FRAGMENT_TYPE" key to identify the Interface implementation - // type at runtime, so grab this value - const interfaceTypes = Object.keys(augmentedTypeMap).filter( - e => augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' - ); - interfaceTypes.map(e => { - resolvers[e] = {}; - - resolvers[e]['__resolveType'] = (obj, context, info) => { - return obj['FRAGMENT_TYPE']; - }; - }); - - return resolvers; -}; - -const possiblyAddOrderingArgument = (args, fieldName) => { - const orderingType = `_${fieldName}Ordering`; - if (args.findIndex(e => e.name.value === fieldName) === -1) { - args.push({ - kind: 'InputValueDefinition', - name: { - kind: 'Name', - value: 'orderBy' - }, - type: { - kind: 'ListType', - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: orderingType - } - } - } - }); - } - return args; -}; - -export const possiblyAddArgument = (args, fieldName, fieldType) => { - if (args.findIndex(e => e.name.value === fieldName) === -1) { - args.push({ - kind: 'InputValueDefinition', - name: { - kind: 'Name', - value: fieldName - }, - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: fieldType - } - } - }); - } - return args; -}; - -const augmentType = (astNode, typeMap, resolvers, rootTypes, config) => { - const queryType = rootTypes.query; - if (isNodeType(astNode)) { - if (shouldAugmentType(config, 'query', astNode.name.value)) { - // Only add _id field to type if query API is generated for type - astNode.fields = addOrReplaceNodeIdField(astNode, resolvers); - } - astNode.fields = possiblyAddTypeFieldArguments( - astNode, - typeMap, - resolvers, - config, - queryType - ); - } - // FIXME: inferring where to add @neo4j_ignore directive improperly causes - // fields to be ignored when logger is added, so remove functionality - // until we refactor how to infer when @neo4j_ignore directive is needed - // see https://github.com/neo4j-graphql/neo4j-graphql-js/issues/189 - // astNode.fields = possiblyAddIgnoreDirective( - // astNode, - // typeMap, - // resolvers, - // config - // ); - return astNode; -}; - -const augmentQueryArguments = (typeMap, config, rootTypes) => { - const queryType = rootTypes.query; - // adds first / offset / orderBy to queries returning node type lists - const queryMap = createOperationMap(typeMap.Query); - let args = []; - let valueTypeName = ''; - let valueType = {}; - let field = {}; - let queryNames = Object.keys(queryMap); - if (queryNames.length > 0) { - queryNames.forEach(t => { - field = queryMap[t]; - valueTypeName = _getNamedType(field).name.value; - valueType = typeMap[valueTypeName]; - if ( - isNodeType(valueType) && - shouldAugmentType(config, 'query', valueTypeName) - ) { - // does not add arguments if the field value type is excluded - args = field.arguments; - if (_isListType(field)) { - queryMap[t].arguments = possiblyAddArgument(args, 'first', 'Int'); - queryMap[t].arguments = possiblyAddArgument(args, 'offset', 'Int'); - queryMap[t].arguments = possiblyAddOrderingArgument( - args, - valueTypeName - ); - } - if (!getFieldDirective(field, 'cypher')) { - queryMap[t].arguments = possiblyAddArgument( - args, - 'filter', - `_${valueTypeName}Filter` - ); - } - } - }); - typeMap[queryType].fields = Object.values(queryMap); - } - return typeMap; -}; - -const possiblyAddResolvers = (operationTypeMap, resolvers, config) => { - let operationName = ''; - return Object.keys(operationTypeMap).reduce((acc, t) => { - // if no resolver provided for this operation type field - operationName = operationTypeMap[t].name.value; - if (acc[operationName] === undefined) { - acc[operationName] = function(...args) { - return neo4jgraphql(...args, config.debug); - }; - } - return acc; - }, resolvers); -}; - -const possiblyAddTypeInput = (astNode, typeMap, resolvers, config) => { - const typeName = astNode.name.value; - if (shouldAugmentType(config, 'mutation', typeName)) { - const inputName = `_${astNode.name.value}Input`; - if (isNodeType(astNode)) { - if (typeMap[inputName] === undefined) { - const pk = getPrimaryKey(astNode); - if (pk) { - const nodeInputType = ` - input ${inputName} { ${pk.name.value}: ${ - // Always exactly require the pk of a node type - decideFieldType(_getNamedType(pk).name.value) - }! }`; - typeMap[inputName] = parse(nodeInputType); - } - } - } else if (getTypeDirective(astNode, 'relation')) { - // Only used for the .data argument in generated relation creation mutations - if (typeMap[inputName] === undefined) { - const fields = astNode.fields; - // The .data arg on add relation mutations, - // which is the only arg in the API that uses - // relation input types, is only generate if there - // is at least one non-directed field (property field) - const hasSomePropertyField = fields.find( - e => e.name.value !== 'from' && e.name.value !== 'to' - ); - const fromField = fields.find(e => e.name.value === 'from'); - const fromName = _getNamedType(fromField).name.value; - const toField = fields.find(e => e.name.value === 'to'); - const toName = _getNamedType(toField).name.value; - // only generate an input type for the relationship if we know that both - // the from and to nodes are not excluded, since thus we know that - // relation mutations are generated for this relation, which would - // make use of the relation input type - if ( - hasSomePropertyField && - shouldAugmentRelationField(config, 'mutation', fromName, toName) - ) { - const relationInputFields = buildRelationTypeInputFields( - astNode, - fields, - typeMap, - resolvers - ); - typeMap[inputName] = parse( - `input ${inputName} {${relationInputFields}}` - ); - } - } - } - } - return typeMap; -}; - -const possiblyAddQuery = (astNode, typeMap, resolvers, rootTypes, config) => { - const typeName = astNode.name.value; - const queryType = rootTypes.query; - const queryMap = createOperationMap(typeMap.Query); - if (isNodeType(astNode) && shouldAugmentType(config, 'query', typeName)) { - const authDirectives = possiblyAddScopeDirective({ - entityType: 'node', - operationType: 'Read', - typeName, - config - }); - const name = astNode.name.value; - if (queryMap[name] === undefined) { - typeMap[queryType].fields.push({ - kind: 'FieldDefinition', - name: { - kind: 'Name', - value: name - }, - arguments: createQueryArguments(astNode, resolvers, typeMap), - type: { - kind: 'ListType', - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: name - } - } - }, - directives: [authDirectives] - }); - } - } - return typeMap; -}; - -const possiblyAddFilterInput = (astNode, typeMap, resolvers, config) => { - const typeName = astNode.name.value; - let filterType = `_${typeName}Filter`; - const filterFields = []; - if (shouldAugmentType(config, 'query', typeName)) { - if (isNodeType(astNode)) { - astNode.fields.forEach(t => { - const fieldName = t.name.value; - const isList = _isListType(t); - let valueTypeName = _getNamedType(t).name.value; - const valueType = typeMap[valueTypeName]; - if ( - fieldIsNotIgnored(astNode, t, resolvers) && - isNotSystemField(fieldName) && - !getFieldDirective(t, 'cypher') - ) { - const relatedType = typeMap[valueTypeName]; - const relationTypeDirective = getRelationTypeDirective(relatedType); - let isRelationType = false; - let isReflexiveRelationType = false; - let relationFilterName = `_${typeName}${valueTypeName}Filter`; - const reflexiveFilterName = `_${valueTypeName}DirectionsFilter`; - if (relationTypeDirective) { - isRelationType = true; - let fromType = ''; - let toType = ''; - fromType = relationTypeDirective.from; - toType = relationTypeDirective.to; - if (fromType === toType) { - isReflexiveRelationType = true; - if (typeMap[reflexiveFilterName] === undefined) { - relationFilterName = `_${valueTypeName}Filter`; - typeMap[reflexiveFilterName] = parse(` - input ${reflexiveFilterName} { - from: ${relationFilterName} - to: ${relationFilterName} - } - `); - const relationTypeFilters = buildFilterFields({ - filterType: relationFilterName, - astNode: relatedType, - typeMap, - resolvers, - config - }); - relationTypeFilters.push(` - ${toType}: _${toType}Filter - `); - typeMap[relationFilterName] = parse( - `input ${relationFilterName} {${relationTypeFilters.join( - '' - )}}` - ); - } - } else { - if (typeMap[relationFilterName] === undefined) { - const relationTypeFilters = buildFilterFields({ - filterType: relationFilterName, - astNode: relatedType, - typeMap, - resolvers, - config - }); - let relatedTypeName = toType; - if (typeName === toType) { - relatedTypeName = fromType; - } - relationTypeFilters.push(` - ${relatedTypeName}: _${relatedTypeName}Filter - `); - typeMap[relationFilterName] = parse( - `input ${relationFilterName} {${relationTypeFilters.join( - '' - )}}` - ); - } - } - } - if (!isList) { - if (valueTypeName === 'ID' || valueTypeName == 'String') { - filterFields.push(`${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - ${fieldName}_contains: ${valueTypeName} - ${fieldName}_not_contains: ${valueTypeName} - ${fieldName}_starts_with: ${valueTypeName} - ${fieldName}_not_starts_with: ${valueTypeName} - ${fieldName}_ends_with: ${valueTypeName} - ${fieldName}_not_ends_with: ${valueTypeName} - `); - } else if ( - valueTypeName === 'Int' || - valueTypeName === 'Float' || - isTemporalType(valueTypeName) - ) { - if (isTemporalType(valueTypeName)) { - valueTypeName = `${valueTypeName}Input`; - } - filterFields.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - ${fieldName}_lt: ${valueTypeName} - ${fieldName}_lte: ${valueTypeName} - ${fieldName}_gt: ${valueTypeName} - ${fieldName}_gte: ${valueTypeName} - `); - } else if (valueTypeName === 'Boolean') { - filterFields.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - `); - } else if (isKind(valueType, 'EnumTypeDefinition')) { - filterFields.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - `); - } else if ( - isKind(valueType, 'ObjectTypeDefinition') && - shouldAugmentType(config, 'query', valueTypeName) - ) { - let relationFilterType = ''; - if (getFieldDirective(t, 'relation')) { - relationFilterType = `_${valueTypeName}Filter`; - } else if (isReflexiveRelationType) { - relationFilterType = reflexiveFilterName; - } else if (isRelationType) { - relationFilterType = relationFilterName; - } - if (relationFilterType) { - filterFields.push(` - ${fieldName}: ${relationFilterType} - ${fieldName}_not: ${relationFilterType} - ${fieldName}_in: [${relationFilterType}!] - ${fieldName}_not_in: [${relationFilterType}!] - `); - } - } - } else if ( - isKind(valueType, 'ObjectTypeDefinition') && - shouldAugmentType(config, 'query', valueTypeName) - ) { - let relationFilterType = ''; - if (getFieldDirective(t, 'relation')) { - relationFilterType = `_${valueTypeName}Filter`; - } else if (isReflexiveRelationType) { - relationFilterType = reflexiveFilterName; - } else if (isRelationType) { - relationFilterType = relationFilterName; - } - if (relationFilterType) { - filterFields.push(` - ${fieldName}: ${relationFilterType} - ${fieldName}_not: ${relationFilterType} - ${fieldName}_in: [${relationFilterType}!] - ${fieldName}_not_in: [${relationFilterType}!] - ${fieldName}_some: ${relationFilterType} - ${fieldName}_none: ${relationFilterType} - ${fieldName}_single: ${relationFilterType} - ${fieldName}_every: ${relationFilterType} - `); - } - } - } - }); - } - } - if (filterFields.length) { - filterFields.unshift(` - AND: [${filterType}!] - OR: [${filterType}!] - `); - } - if (typeMap[filterType] === undefined && filterFields.length) { - typeMap[filterType] = parse( - `input ${filterType} {${filterFields.join('')}}` - ); - } - return typeMap; -}; - -const buildFilterFields = ({ - filterType, - astNode, - typeMap, - resolvers, - config -}) => { - const filterFields = astNode.fields.reduce((filters, t) => { - const fieldName = t.name.value; - const isList = _isListType(t); - const valueType = typeMap[valueTypeName]; - let valueTypeName = _getNamedType(t).name.value; - if ( - fieldIsNotIgnored(astNode, t, resolvers) && - isNotSystemField(fieldName) && - !getFieldDirective(t, 'cypher') - ) { - if (!isList) { - if (valueTypeName === 'ID' || valueTypeName == 'String') { - filters.push(`${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - ${fieldName}_contains: ${valueTypeName} - ${fieldName}_not_contains: ${valueTypeName} - ${fieldName}_starts_with: ${valueTypeName} - ${fieldName}_not_starts_with: ${valueTypeName} - ${fieldName}_ends_with: ${valueTypeName} - ${fieldName}_not_ends_with: ${valueTypeName} - `); - } else if ( - valueTypeName === 'Int' || - valueTypeName === 'Float' || - isTemporalType(valueTypeName) - ) { - if (isTemporalType(valueTypeName)) { - valueTypeName = `${valueTypeName}Input`; - } - filters.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - ${fieldName}_lt: ${valueTypeName} - ${fieldName}_lte: ${valueTypeName} - ${fieldName}_gt: ${valueTypeName} - ${fieldName}_gte: ${valueTypeName} - `); - } else if (valueTypeName === 'Boolean') { - filters.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - `); - } else if (isKind(valueType, 'EnumTypeDefinition')) { - filters.push(` - ${fieldName}: ${valueTypeName} - ${fieldName}_not: ${valueTypeName} - ${fieldName}_in: [${valueTypeName}!] - ${fieldName}_not_in: [${valueTypeName}!] - `); - } else if ( - isKind(valueType, 'ObjectTypeDefinition') && - shouldAugmentType(config, 'query', valueTypeName) - ) { - if (getFieldDirective(t, 'relation')) { - // one-to-one @relation field - filters.push(` - ${fieldName}: _${valueTypeName}Filter - ${fieldName}_not: _${valueTypeName}Filter - ${fieldName}_in: [_${valueTypeName}Filter!] - ${fieldName}_not_in: [_${valueTypeName}Filter!] - `); - } - } - } else if ( - isKind(valueType, 'ObjectTypeDefinition') && - shouldAugmentType(config, 'query', valueTypeName) - ) { - if (getFieldDirective(t, 'relation')) { - filters.push(` - ${fieldName}: _${valueTypeName}Filter - ${fieldName}_not: _${valueTypeName}Filter - ${fieldName}_in: [_${valueTypeName}Filter!] - ${fieldName}_not_in: [_${valueTypeName}Filter!] - ${fieldName}_some: _${valueTypeName}Filter - ${fieldName}_none: _${valueTypeName}Filter - ${fieldName}_single: _${valueTypeName}Filter - ${fieldName}_every: _${valueTypeName}Filter - `); - } - } - } - return filters; - }, []); - if (filterFields.length) { - filterFields.unshift(` - AND: [${filterType}!] - OR: [${filterType}!] - `); - } - return filterFields; -}; - -const possiblyAddOrderingEnum = (astNode, typeMap, resolvers, config) => { - const typeName = astNode.name.value; - if (isNodeType(astNode) && shouldAugmentType(config, 'query', typeName)) { - const name = `_${astNode.name.value}Ordering`; - const values = createOrderingFields(astNode, typeMap, resolvers); - // Add ordering enum if it does not exist already and if - // there is at least one basic scalar field on this type - if (typeMap[name] === undefined && values.length > 0) { - typeMap[name] = { - kind: 'EnumTypeDefinition', - name: { - kind: 'Name', - value: name - }, - directives: [], - values: values - }; - } - } - return typeMap; -}; - -const possiblyAddTypeMutations = (astNode, typeMap, resolvers, config) => { - const typeName = astNode.name.value; - if (shouldAugmentType(config, 'mutation', typeName)) { - const mutationMap = createOperationMap(typeMap.Mutation); - if ( - isNodeType(astNode) && - shouldAugmentType(config, 'mutation', typeName) - ) { - typeMap = possiblyAddTypeMutation( - `Create`, - astNode, - resolvers, - typeMap, - mutationMap, - config - ); - typeMap = possiblyAddTypeMutation( - `Update`, - astNode, - resolvers, - typeMap, - mutationMap, - config - ); - typeMap = possiblyAddTypeMutation( - `Delete`, - astNode, - resolvers, - typeMap, - mutationMap, - config - ); - } - } - return typeMap; -}; - -const possiblyAddTypeFieldArguments = ( - astNode, - typeMap, - resolvers, - config, - queryType -) => { - const fields = astNode.fields; - fields.forEach(field => { - let fieldTypeName = _getNamedType(field).name.value; - let fieldType = typeMap[fieldTypeName]; - let args = field.arguments; - if ( - fieldType && - fieldIsNotIgnored(astNode, field, resolvers) && - shouldAugmentType(config, 'query', fieldTypeName) - ) { - const relationTypeDirective = getRelationTypeDirective(fieldType); - if (isNodeType(fieldType)) { - if (getFieldDirective(field, 'cypher')) { - if (_isListType(field)) { - args = addPaginationArgs(args, fieldTypeName); - } - } else if (getFieldDirective(field, 'relation')) { - if (_isListType(field)) { - args = addPaginationArgs(args, fieldTypeName); - } - args = possiblyAddArgument(args, 'filter', `_${fieldTypeName}Filter`); - } - } else if (relationTypeDirective) { - const fromType = relationTypeDirective.from; - const toType = relationTypeDirective.to; - let filterTypeName = `_${astNode.name.value}${fieldTypeName}Filter`; - if (fromType === toType) { - filterTypeName = `_${fieldTypeName}Filter`; - } - args = possiblyAddArgument(args, 'filter', filterTypeName); - } - field.arguments = args; - } - }); - return fields; -}; - -const addPaginationArgs = (args, fieldTypeName) => { - args = possiblyAddArgument(args, 'first', 'Int'); - args = possiblyAddArgument(args, 'offset', 'Int'); - args = possiblyAddOrderingArgument(args, fieldTypeName); - return args; -}; - -const possiblyAddObjectType = (typeMap, name) => { - if (typeMap[name] === undefined) { - typeMap[name] = { - kind: 'ObjectTypeDefinition', - name: { - kind: 'Name', - value: name - }, - interfaces: [], - directives: [], - fields: [] - }; - } - return typeMap; -}; - -const possiblyAddTypeMutation = ( - mutationType, - astNode, - resolvers, - typeMap, - mutationMap, - config -) => { - const typeName = astNode.name.value; - const mutationName = mutationType + typeName; - // Only generate if the mutation named mutationName does not already exist - if (mutationMap[mutationName] === undefined) { - const args = buildMutationArguments( - mutationType, - astNode, - resolvers, - typeMap - ); - if (args.length > 0) { - const typeName = astNode.name.value; - const authDirectives = possiblyAddScopeDirective({ - entityType: 'node', - operationType: mutationType, - typeName, - config - }); - typeMap['Mutation'].fields.push({ - kind: 'FieldDefinition', - name: { - kind: 'Name', - value: mutationName - }, - arguments: args, - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: typeName - } - }, - directives: [authDirectives] - }); - } - } - return typeMap; -}; - -const possiblyAddRelationTypeFieldPayload = ( - relationAstNode, - capitalizedFieldName, - typeName, - typeMap, - field -) => { - const fieldTypeName = `_${typeName}${capitalizedFieldName}`; - if (!typeMap[fieldTypeName]) { - let fieldName = ''; - let fieldValueName = ''; - let fromField = {}; - let toField = {}; - let _fromField = {}; - let _toField = {}; - let fromValue = undefined; - let toValue = undefined; - let fields = relationAstNode.fields; - const relationTypeDirective = getRelationTypeDirective(relationAstNode); - if (relationTypeDirective) { - // TODO refactor - const relationTypePayloadFields = fields - .reduce((acc, t) => { - fieldValueName = _getNamedType(t).name.value; - fieldName = t.name.value; - if (fieldName === 'from') { - fromValue = fieldValueName; - fromField = t; - } else if (fieldName === 'to') { - toValue = fieldValueName; - toField = t; - } else { - // Exclude .to and .from, but gather them from along the way - // using previous branches above - acc.push(print(t)); - } - return acc; - }, []) - .join('\n'); - - if (fromValue && fromValue === toValue) { - // If field is a list type, then make .from and .to list types - const fieldIsList = _isListType(field); - const fieldArgs = getFieldArgumentsFromAst( - field, - typeName, - fieldIsList, - fieldTypeName - ); - typeMap[`${fieldTypeName}Directions`] = parse(` - type ${fieldTypeName}Directions ${print(relationAstNode.directives)} { - from${fieldArgs}: ${fieldIsList ? '[' : ''}${fieldTypeName}${ - fieldIsList ? ']' : '' - } - to${fieldArgs}: ${fieldIsList ? '[' : ''}${fieldTypeName}${ - fieldIsList ? ']' : '' - } - }`); - - typeMap[fieldTypeName] = parse(` - type ${fieldTypeName} ${print(relationAstNode.directives)} { - ${relationTypePayloadFields} - ${fromValue}: ${fromValue} - } - `); - - // remove arguments on field - field.arguments = []; - } else { - // Non-reflexive case, (User)-[RATED]->(Movie) - typeMap[fieldTypeName] = parse(` - type ${fieldTypeName} ${print(relationAstNode.directives)} { - ${relationTypePayloadFields} - ${ - typeName === toValue - ? // If this is the from, the allow selecting the to - `${fromValue}: ${fromValue}` - : // else this is the to, so allow selecting the from - typeName === fromValue - ? `${toValue}: ${toValue}` - : '' - } - } - `); - } - } - } - return typeMap; -}; - -const possiblyAddRelationMutationField = ( - typeName, - capitalizedFieldName, - fromName, - toName, - mutationMap, - typeMap, - relationName, - relatedAstNode, - relationHasProps, - config -) => { - const mutationTypes = ['Add', 'Remove']; - let mutationName = ''; - let payloadTypeName = ''; - let hasSomePropertyField = false; - mutationTypes.forEach(action => { - mutationName = `${action}${typeName}${capitalizedFieldName}`; - // Prevents overwriting - if (mutationMap[mutationName] === undefined) { - payloadTypeName = `_${mutationName}Payload`; - hasSomePropertyField = relatedAstNode.fields.find( - e => e.name.value !== 'from' && e.name.value !== 'to' - ); - // If we know we should expect data properties (from context: relationHasProps) - // and if there is at least 1 field that is not .to or .from (hasSomePropertyField) - // and if we are generating the add relation mutation, then add the .data argument - const shouldUseRelationDataArgument = - relationHasProps && hasSomePropertyField && action === 'Add'; - const authDirectives = possiblyAddScopeDirective({ - entityType: 'relation', - operationType: action, - typeName, - relatedTypeName: toName, - config - }); - // Relation mutation type - typeMap.Mutation.fields.push( - parseFieldSdl(` - ${mutationName}(from: _${fromName}Input!, to: _${toName}Input!${ - shouldUseRelationDataArgument - ? `, data: _${relatedAstNode.name.value}Input!` - : '' - }): ${payloadTypeName} @MutationMeta(relationship: "${relationName}", from: "${fromName}", to: "${toName}") ${ - authDirectives ? authDirectives : '' - } - `) - ); - // Prevents overwriting - if (typeMap[payloadTypeName] === undefined) { - typeMap[payloadTypeName] = parse(` - type ${payloadTypeName} @relation(name: \"${relationName}\", from: \"${fromName}\", to: \"${toName}\") { - from: ${fromName} - to: ${toName} - ${ - shouldUseRelationDataArgument - ? getRelationMutationPayloadFieldsFromAst(relatedAstNode) - : '' - } - } - `); - } - } - }); - return typeMap; -}; - -const decideFieldType = name => { - if (isTemporalType(name)) { - name = `${name}Input`; - } - return name; -}; - -const validateRelationTypeDirectedFields = (typeName, fromName, toName) => { - // directive to and from are not the same and neither are equal to this - if (fromName !== toName && toName !== typeName && fromName !== typeName) { - throw new Error(`The '${ - field.name.value - }' field on the '${typeName}' type uses the '${relatedAstNode.name.value}' - but '${ - relatedAstNode.name.value - }' comes from '${fromName}' and goes to '${toName}'`); - } - return true; -}; - -const handleRelationFields = (astNode, typeMap, resolvers, config) => { - const mutationMap = createOperationMap(typeMap.Mutation); - const typeName = astNode.name.value; - const fields = astNode.fields; - const fieldCount = fields ? fields.length : 0; - let relationFieldDirective = {}; - let fieldValueName = ''; - let relatedAstNode = {}; - let relationTypeDirective = {}; - let capitalizedFieldName = ''; - let field = {}; - let fieldIndex = 0; - if (isNodeType(astNode)) { - for (; fieldIndex < fieldCount; ++fieldIndex) { - field = fields[fieldIndex]; - if (fieldIsNotIgnored(astNode, field, resolvers)) { - fieldValueName = _getNamedType(field).name.value; - capitalizedFieldName = - field.name.value.charAt(0).toUpperCase() + field.name.value.substr(1); - relatedAstNode = typeMap[fieldValueName]; - if (relatedAstNode) { - relationTypeDirective = getTypeDirective(relatedAstNode, 'relation'); - relationFieldDirective = getFieldDirective(field, 'relation'); - // continue if typeName is allowed - // in either Query or Mutation - if (isNodeType(relatedAstNode)) { - // the field has a node type - if (relationFieldDirective) { - // Relation Mutation API - // relation directive exists on field - typeMap = handleRelationFieldDirective({ - relatedAstNode, - typeName, - capitalizedFieldName, - fieldValueName, - relationFieldDirective, - mutationMap, - typeMap, - config - }); - } - } else if (relationTypeDirective) { - // Query and Relation Mutation API - // the field value is a non-node type using a relation type directive - typeMap = handleRelationTypeDirective({ - relatedAstNode, - typeName, - fields, - field, - fieldIndex, - capitalizedFieldName, - relationTypeDirective, - config, - typeMap, - mutationMap - }); - } - } - } - } - } - return typeMap; -}; - -const handleRelationTypeDirective = ({ - relatedAstNode, - typeName, - fields, - field, - fieldIndex, - capitalizedFieldName, - relationTypeDirective, - config, - typeMap, - mutationMap -}) => { - const typeDirectiveArgs = relationTypeDirective - ? relationTypeDirective.arguments - : []; - const nameArgument = typeDirectiveArgs.find(e => e.name.value === 'name'); - const fromArgument = typeDirectiveArgs.find(e => e.name.value === 'from'); - const toArgument = typeDirectiveArgs.find(e => e.name.value === 'to'); - const relationName = nameArgument.value.value; - const fromName = fromArgument.value.value; - const toName = toArgument.value.value; - // Relation Mutation API, adds relation mutation to Mutation - if ( - shouldAugmentRelationField(config, 'mutation', fromName, toName) && - validateRelationTypeDirectedFields(typeName, fromName, toName) - ) { - typeMap = possiblyAddRelationMutationField( - typeName, - capitalizedFieldName, - fromName, - toName, - mutationMap, - typeMap, - relationName, - relatedAstNode, - true, - config - ); - } - // Relation type field payload transformation for selection sets - typeMap = possiblyAddRelationTypeFieldPayload( - relatedAstNode, - capitalizedFieldName, - typeName, - typeMap, - field - ); - // Replaces the field's value with the generated payload type - fields[fieldIndex] = replaceRelationTypeValue( - fromName, - toName, - field, - capitalizedFieldName, - typeName - ); - return typeMap; -}; - -const handleRelationFieldDirective = ({ - relatedAstNode, - typeName, - capitalizedFieldName, - fieldValueName, - relationFieldDirective, - mutationMap, - typeMap, - config -}) => { - let fromName = typeName; - let toName = fieldValueName; - // Mutation API, relation mutations for field directives - if (shouldAugmentRelationField(config, 'mutation', fromName, toName)) { - const relationName = getRelationName(relationFieldDirective); - const direction = getRelationDirection(relationFieldDirective); - // possibly swap directions to fit assertion of fromName = typeName - if (direction === 'IN' || direction === 'in') { - let temp = fromName; - fromName = toName; - toName = temp; - } - // (Mutation API) add relation mutation to Mutation - typeMap = possiblyAddRelationMutationField( - typeName, - capitalizedFieldName, - fromName, - toName, - mutationMap, - typeMap, - relationName, - relatedAstNode, - false, - config - ); - } - return typeMap; -}; - -const replaceRelationTypeValue = ( - fromName, - toName, - field, - capitalizedFieldName, - typeName -) => { - const isList = _isListType(field); - let type = { - kind: 'NamedType', - name: { - kind: 'Name', - value: `_${typeName}${capitalizedFieldName}${ - fromName === toName ? 'Directions' : '' - }` - } - }; - if (isList && fromName !== toName) { - type = { - kind: 'ListType', - type: type - }; - } - field.type = type; - return field; -}; - -const addOrReplaceNodeIdField = (astNode, resolvers) => { - const fields = astNode ? astNode.fields : []; - const index = fields.findIndex(e => e.name.value === '_id'); - const definition = { - kind: 'FieldDefinition', - name: { - kind: 'Name', - value: '_id' - }, - arguments: [], - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: 'String' - } - }, - directives: [] - }; - if (index >= 0) { - if (fieldIsNotIgnored(astNode, fields[index], resolvers)) { - fields.splice(index, 1, definition); - } - } else { - fields.push(definition); - } - return fields; -}; - -const addRelationTypeDirectives = typeMap => { - let astNode = {}; - let fields = []; - let name = ''; - let to = {}; - let from = {}; - let fromTypeName = ''; - let toTypeName = ''; - let typeDirective = {}; - let relationName = ''; - let typeDirectiveIndex = -1; - Object.keys(typeMap).forEach(typeName => { - astNode = typeMap[typeName]; - if (astNode.kind === 'ObjectTypeDefinition') { - name = astNode.name.value; - fields = astNode.fields; - to = fields ? fields.find(e => e.name.value === 'to') : undefined; - from = fields ? fields.find(e => e.name.value === 'from') : undefined; - if (to && !from) { - throw new Error( - `Relationship type ${name} has a 'to' field but no corresponding 'from' field` - ); - } - if (from && !to) { - throw new Error( - `Relationship type ${name} has a 'from' field but no corresponding 'to' field` - ); - } - if (from && to) { - // get values of .to and .from fields - fromTypeName = _getNamedType(from).name.value; - toTypeName = _getNamedType(to).name.value; - // assume the default relationship name - relationName = transformRelationName(astNode); - // get its relation type directive - typeDirectiveIndex = astNode.directives.findIndex( - e => e.name.value === 'relation' - ); - if (typeDirectiveIndex >= 0) { - typeDirective = astNode.directives[typeDirectiveIndex]; - // get the arguments of type directive - let args = typeDirective ? typeDirective.arguments : []; - if (args.length > 0) { - // get its name argument - let nameArg = args.find(e => e.name.value === 'name'); - if (nameArg) { - relationName = nameArg.value.value; - } - } - // replace it if it exists in order to force correct configuration - astNode.directives[typeDirectiveIndex] = parseDirectiveSdl(` - @relation( - name: \"${relationName}\", - from: \"${fromTypeName}\", - to: \"${toTypeName}\" - ) - `); - } else { - astNode.directives.push( - parseDirectiveSdl(` - @relation( - name: \"${relationName}\", - from: \"${fromTypeName}\", - to: \"${toTypeName}\" - ) - `) - ); - } - typeMap[typeName] = astNode; - } - } - }); - return typeMap; -}; - -const createOrderingFields = (astNode, typeMap, resolvers) => { - const fields = astNode ? astNode.fields : []; - let type = {}; - let valueType = {}; - let valueTypeName = ''; - let fieldName = ''; - return fields.reduce((acc, field) => { - type = _getNamedType(field); - valueTypeName = type.name.value; - valueType = typeMap[valueTypeName]; - if ( - !_isListType(field) && - fieldIsNotIgnored(astNode, field, resolvers) && - (isBasicScalar(type.name.value) || - isKind(valueType, 'EnumTypeDefinition') || - isTemporalType(valueTypeName)) - ) { - fieldName = field.name.value; - acc.push({ - kind: 'EnumValueDefinition', - name: { - kind: 'Name', - value: `${fieldName}_asc` - } - }); - acc.push({ - kind: 'EnumValueDefinition', - name: { - kind: 'Name', - value: `${fieldName}_desc` - } - }); - } - return acc; - }, []); -}; - -const createQueryArguments = (astNode, resolvers, typeMap) => { - let type = {}; - let valueTypeName = ''; - let valueKind = ''; - let queryArg = {}; - return astNode.fields.reduce((acc, t) => { - if (fieldIsNotIgnored(astNode, t, resolvers)) { - type = _getNamedType(t); - valueTypeName = type.name.value; - valueKind = typeMap[valueTypeName] - ? typeMap[valueTypeName].kind - : undefined; - queryArg = { - kind: 'InputValueDefinition', - name: { - kind: 'Name', - value: t.name.value - }, - type: type - }; - if ( - isBasicScalar(valueTypeName) || - valueKind === 'EnumTypeDefinition' || - valueKind === 'ScalarTypeDefinition' - ) { - acc.push(queryArg); - } else if (isTemporalType(valueTypeName)) { - queryArg.type = { - kind: 'NamedType', - name: { - kind: 'Name', - value: `${valueTypeName}Input` - } - }; - acc.push(queryArg); - } - } - return acc; - }, []); -}; - -const hasNonExcludedNodeType = (types, typeMap, rootType, config) => { - let type = ''; - return types.find(e => { - type = typeMap[e]; - return ( - isNodeType(type) && - type.name && - shouldAugmentType(config, rootType, type.name.value) - ); - }); -}; - -const initializeOperationTypes = (typeMap, rootTypes, config) => { - const queryType = rootTypes.query; - const mutationType = rootTypes.mutation; - const types = Object.keys(typeMap); - if (hasNonExcludedNodeType(types, typeMap, 'query', config)) { - typeMap = possiblyAddObjectType(typeMap, queryType); - } - if (hasNonExcludedNodeType(types, typeMap, 'mutation', config)) { - typeMap = possiblyAddObjectType(typeMap, mutationType); - } - return typeMap; -}; - -const transformRelationName = relatedAstNode => { - const name = relatedAstNode.name.value; - let char = ''; - let uppercased = ''; - return Object.keys(name) - .reduce((acc, t) => { - char = name.charAt(t); - uppercased = char.toUpperCase(); - if (char === uppercased && t > 0) { - // already uppercased - acc.push(`_${uppercased}`); - } else { - acc.push(uppercased); - } - return acc; - }, []) - .join(''); -}; - -const temporalTypes = (typeMap, types) => { - if (types.time === true) { - typeMap['_Neo4jTime'] = parse(` - type _Neo4jTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - `).definitions[0]; - typeMap['_Neo4jTimeInput'] = parse(` - input _Neo4jTimeInput { - hour: Int - minute: Int - second: Int - nanosecond: Int - millisecond: Int - microsecond: Int - timezone: String - formatted: String - } - `).definitions[0]; - } - if (types.date === true) { - typeMap['_Neo4jDate'] = parse(` - type _Neo4jDate { - year: Int - month: Int - day: Int - formatted: String - } - `).definitions[0]; - typeMap['_Neo4jDateInput'] = parse(` - input _Neo4jDateInput { - year: Int - month: Int - day: Int - formatted: String - } - `).definitions[0]; - } - if (types.datetime === true) { - typeMap['_Neo4jDateTime'] = parse(` - type _Neo4jDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - `).definitions[0]; - typeMap['_Neo4jDateTimeInput'] = parse(` - input _Neo4jDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - `).definitions[0]; - } - if (types.localtime === true) { - typeMap['_Neo4jLocalTime'] = parse(` - type _Neo4jLocalTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - `).definitions[0]; - typeMap['_Neo4jLocalTimeInput'] = parse(` - input _Neo4jLocalTimeInput { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - `).definitions[0]; - } - if (types.localdatetime === true) { - typeMap['_Neo4jLocalDateTime'] = parse(` - type _Neo4jLocalDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - `).definitions[0]; - typeMap['_Neo4jLocalDateTimeInput'] = parse(` - input _Neo4jLocalDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - `).definitions[0]; - } - return typeMap; -}; - -const transformTemporalFieldArgs = (field, config) => { - field.arguments.forEach(arg => { - arg.type = transformTemporalTypeName(arg.type, config, true); - }); - return field; -}; - -const transformTemporalFields = (typeMap, config) => { - Object.keys(typeMap).forEach(t => { - if (typeMap[t].kind === 'ObjectTypeDefinition') { - if (!isTemporalType(t)) { - typeMap[t].fields.forEach(field => { - // released: DateTime -> released: _Neo4jDateTime - field.type = transformTemporalTypeName(field.type, config); - field = transformTemporalFieldArgs(field, config); - }); - } - } - }); - return typeMap; -}; - -const transformTemporalTypeName = (type, config, isArgument) => { - if (type.kind !== 'NamedType') { - type.type = transformTemporalTypeName(type.type, config); - return type; - } - if (type.kind === 'NamedType') { - switch (type.name.value) { - case 'Time': { - if (config.time === true) { - type.name.value = `_Neo4jTime${isArgument ? `Input` : ''}`; - } - break; - } - case 'Date': { - if (config.date === true) { - type.name.value = `_Neo4jDate${isArgument ? `Input` : ''}`; - } - break; - } - case 'DateTime': { - if (config.datetime === true) { - type.name.value = `_Neo4jDateTime${isArgument ? `Input` : ''}`; - } - break; - } - case 'LocalTime': { - if (config.localtime === true) { - type.name.value = `_Neo4jLocalTime${isArgument ? `Input` : ''}`; - } - break; - } - case 'LocalDateTime': { - if (config.localdatetime === true) { - type.name.value = `_Neo4jLocalDateTime${isArgument ? `Input` : ''}`; - } - break; - } - default: - break; - } - } - return type; -}; - -const decideTemporalConfig = config => { - let defaultConfig = { - time: true, - date: true, - datetime: true, - localtime: true, - localdatetime: true - }; - const providedConfig = config ? config.temporal : defaultConfig; - if (typeof providedConfig === 'boolean') { - if (providedConfig === false) { - defaultConfig.time = false; - defaultConfig.date = false; - defaultConfig.datetime = false; - defaultConfig.localtime = false; - defaultConfig.localdatetime = false; - } - } else if (typeof providedConfig === 'object') { - Object.keys(defaultConfig).forEach(e => { - if (providedConfig[e] === undefined) { - providedConfig[e] = defaultConfig[e]; - } - }); - defaultConfig = providedConfig; - } - return defaultConfig; -}; - -const shouldAugmentType = (config, rootType, type) => { - return typeof config[rootType] === 'boolean' - ? config[rootType] - : // here .exclude should be an object, - // set at the end of excludeIgnoredTypes - type - ? !getExcludedTypes(config, rootType)[type] - : false; -}; - -const shouldAugmentRelationField = (config, rootType, fromName, toName) => - shouldAugmentType(config, rootType, fromName) && - shouldAugmentType(config, rootType, toName); - -const fieldIsNotIgnored = (astNode, field, resolvers) => { - return !getFieldDirective(field, 'neo4j_ignore'); - // FIXME: issue related to inferences on AST field .resolve - // See: possiblyAddIgnoreDirective - // !getCustomFieldResolver(astNode, field, resolvers) -}; - -const isNotSystemField = name => { - return name !== '_id' && name !== 'to' && name !== 'from'; -}; - -export const addTemporalTypes = (typeMap, config) => { - config = decideTemporalConfig(config); - typeMap = temporalTypes(typeMap, config); - return transformTemporalFields(typeMap, config); -}; - -const getFieldArgumentsFromAst = ( - field, - typeName, - fieldIsList, - fieldTypeName -) => { - let args = field.arguments ? field.arguments : []; - if (fieldIsList) { - // TODO https://github.com/neo4j-graphql/neo4j-graphql-js/issues/232 - // args = possiblyAddArgument(args, 'first', 'Int'); - // args = possiblyAddArgument(args, 'offset', 'Int'); - // args = possiblyAddArgument( - // args, - // 'orderBy', - // `${fieldTypeName}Ordering` - // ); - } - args = possiblyAddArgument(args, 'filter', `${fieldTypeName}Filter`); - args = args - .reduce((acc, t) => { - acc.push(print(t)); - return acc; - }, []) - .join('\n'); - return args.length > 0 ? `(${args})` : ''; -}; - -const buildMutationArguments = (mutationType, astNode, resolvers, typeMap) => { - const primaryKey = getPrimaryKey(astNode); - switch (mutationType) { - case 'Create': { - return buildCreateMutationArguments(astNode, typeMap, resolvers); - } - case 'Update': { - if (primaryKey) { - return buildUpdateMutationArguments( - primaryKey, - astNode, - typeMap, - resolvers - ); - } - } - case 'Delete': { - if (primaryKey) { - return buildDeleteMutationArguments(primaryKey); - } - } - } -}; - -const buildUpdateMutationArguments = ( - primaryKey, - astNode, - typeMap, - resolvers -) => { - const primaryKeyName = primaryKey.name.value; - const primaryKeyType = _getNamedType(primaryKey); - // Primary key field is first arg and required for node selection - const parsedPrimaryKeyField = `${primaryKeyName}: ${ - primaryKeyType.name.value - }!`; - let type = {}; - let valueTypeName = ''; - let valueType = {}; - let fieldName = ''; - let mutationArgs = []; - mutationArgs = astNode.fields.reduce((acc, t) => { - type = _getNamedType(t); - fieldName = t.name.value; - valueTypeName = type.name.value; - valueType = typeMap[valueTypeName]; - if (fieldIsNotIgnored(astNode, t, resolvers)) { - if ( - fieldName !== primaryKeyName && - isNotSystemField(fieldName) && - !getFieldDirective(t, 'cypher') && - (isBasicScalar(valueTypeName) || - isKind(valueType, 'EnumTypeDefinition') || - isKind(valueType, 'ScalarTypeDefinition') || - isTemporalType(valueTypeName)) - ) { - acc.push( - print({ - kind: 'InputValueDefinition', - name: t.name, - // Don't require update fields, that wouldn't be very flexible - type: isNonNullType(t) ? t.type.type : t.type - }) - ); - } - } - return acc; - }, []); - // Add pk as first arg is other update fields exist - if (mutationArgs.length > 0) { - mutationArgs.unshift(parsedPrimaryKeyField); - mutationArgs = transformManagedFieldTypes(mutationArgs); - mutationArgs = buildInputValueDefinitions(mutationArgs); - } - return mutationArgs; -}; - -const buildDeleteMutationArguments = primaryKey => { - let mutationArgs = []; - mutationArgs.push( - print({ - kind: 'InputValueDefinition', - name: { - kind: 'Name', - value: primaryKey.name.value - }, - type: { - kind: 'NonNullType', - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: _getNamedType(primaryKey).name.value - } - } - } - }) - ); - mutationArgs = transformManagedFieldTypes(mutationArgs); - return buildInputValueDefinitions(mutationArgs); -}; - -const buildCreateMutationArguments = (astNode, typeMap, resolvers) => { - let type = {}; - let valueTypeName = ''; - let valueType = {}; - let fieldName = ''; - let firstIdField = undefined; - let field = {}; - let mutationArgs = astNode.fields.reduce((acc, t) => { - type = _getNamedType(t); - fieldName = t.name.value; - valueTypeName = type.name.value; - valueType = typeMap[valueTypeName]; - if (fieldIsNotIgnored(astNode, t, resolvers)) { - if ( - isNotSystemField(fieldName) && - !getFieldDirective(t, 'cypher') && - (isBasicScalar(valueTypeName) || - isKind(valueType, 'EnumTypeDefinition') || - isKind(valueType, 'ScalarTypeDefinition') || - isTemporalType(valueTypeName)) - ) { - if ( - isNonNullType(t) && - !_isListType(t) && - valueTypeName === 'ID' && - !firstIdField - ) { - firstIdField = t; - field = { - kind: 'InputValueDefinition', - name: { - kind: 'Name', - value: fieldName - }, - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: valueTypeName - } - } - }; - } else { - field = t; - } - acc.push(print(field)); - } - } - return acc; - }, []); - // Transform managed field types: _Neo4jTime -> _Neo4jTimeInput - mutationArgs = transformManagedFieldTypes(mutationArgs); - // Use a helper to get the AST for all fields - mutationArgs = buildInputValueDefinitions(mutationArgs); - return mutationArgs; -}; - -const buildRelationTypeInputFields = (astNode, fields, typeMap, resolvers) => { - let fieldName = ''; - let valueTypeName = ''; - let valueType = {}; - let relationInputFields = fields.reduce((acc, t) => { - fieldName = t.name.value; - valueTypeName = _getNamedType(t).name.value; - valueType = typeMap[valueTypeName]; - if ( - fieldIsNotIgnored(astNode, t, resolvers) && - isNotSystemField(fieldName) && - !getFieldDirective(t, 'cypher') && - (isBasicScalar(valueTypeName) || - isKind(valueType, 'EnumTypeDefinition') || - isKind(valueType, 'ScalarTypeDefinition') || - isTemporalType(valueTypeName)) - ) { - acc.push( - print({ - kind: 'InputValueDefinition', - name: t.name, - type: t.type - }) - ); - } - return acc; - }, []); - relationInputFields = transformManagedFieldTypes(relationInputFields); - return relationInputFields.join('\n'); -}; - -const transformManagedFieldTypes = fields => { - return fields.reduce((acc, field) => { - if ( - field !== '_Neo4jDateTimeInput' && - field !== '_Neo4jDateInput' && - field !== '_Neo4jTimeInput' && - field !== '_Neo4jLocalTimeInput' && - field !== '_Neo4jLocalDateTimeInput' - ) { - if (field.includes('_Neo4jDateTime')) { - field = field.replace('_Neo4jDateTime', '_Neo4jDateTimeInput'); - } else if (field.includes('_Neo4jDate')) { - field = field.replace('_Neo4jDate', '_Neo4jDateInput'); - } else if (field.includes('_Neo4jTime')) { - field = field.replace('_Neo4jTime', '_Neo4jTimeInput'); - } else if (field.includes('_Neo4jLocalTime')) { - field = field.replace('_Neo4jLocalTime', '_Neo4jLocalTimeInput'); - } else if (field.includes('_Neo4jLocalDateTime')) { - field = field.replace( - '_Neo4jLocalDateTime', - '_Neo4jLocalDateTimeInput' - ); - } - } - acc.push(field); - return acc; - }, []); -}; From 8094500080a611e200840f927ca2df7b24c3bff6 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 20:18:19 -0700 Subject: [PATCH 10/37] avoid overwriting existent filtering test file --- test/helpers/tck/parseTck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/tck/parseTck.js b/test/helpers/tck/parseTck.js index 70526dae..b13d3bfd 100644 --- a/test/helpers/tck/parseTck.js +++ b/test/helpers/tck/parseTck.js @@ -1,6 +1,6 @@ import { generateTestFile } from './parser'; const TCK_FILE = './test/helpers/tck/filterTck.md'; -const TEST_FILE = './test/tck/filterTest.test.js'; +const TEST_FILE = './test/unit/filterTests.test.js'; generateTestFile(TCK_FILE, TEST_FILE); From f97548c5fb18cb80f68e53de26e74d166b274442 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 20:18:56 -0700 Subject: [PATCH 11/37] Updates resultant from augmentation refactor --- test/helpers/tck/parser.js | 47 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/test/helpers/tck/parser.js b/test/helpers/tck/parser.js index 384cb8c4..cb02c4ff 100644 --- a/test/helpers/tck/parser.js +++ b/test/helpers/tck/parser.js @@ -1,15 +1,11 @@ import { makeExecutableSchema } from 'graphql-tools'; import { parse, print, graphql } from 'graphql'; import { createReadStream, createWriteStream } from 'fs'; -import { - extractTypeMapFromTypeDefs, - createOperationMap -} from '../../../src/utils'; import { augmentTypeDefs, cypherQuery } from '../../../src/index'; -export const generateTestFile = async (tckFile, testFile) => { +export const generateTestFile = async (tckFile, testFile, extractionLimit) => { const tck = await extractTck(tckFile, testFile); - const testDeclarations = buildTestDeclarations(tck); + const testDeclarations = buildTestDeclarations(tck, extractionLimit); writeTestFile(testFile, tck, testDeclarations); }; @@ -19,11 +15,11 @@ const extractTck = fileName => { const rs = createReadStream(fileName, { encoding: 'utf8' }); rs.on('data', lines => { // split into array of lines - lines = lines.split('\n'); + lines = lines.split('\r\n'); // extract array elements for typeDefs const typeDefs = extractBlock('schema', lines); resolve({ - typeDefs: typeDefs.join('\n '), + typeDefs: typeDefs.join('\r\n '), tests: extractTestBlocks(lines) }); }); @@ -86,9 +82,10 @@ const getTestName = (lines, index) => { return name; }; -const buildTestDeclarations = tck => { +const buildTestDeclarations = (tck, extractionLimit) => { const schema = makeTestDataSchema(tck); const testData = buildTestData(schema, tck); + //! refactor to involve forEach and a counter that is compared against extractionLimit return testData .reduce((acc, test) => { // escape " so that we can wrap the cypher in "s @@ -101,19 +98,16 @@ const buildTestDeclarations = tck => { filterTestRunner(t, typeDefs, graphQLQuery, ${JSON.stringify( test.params )}, expectedCypherQuery, ${JSON.stringify(test.expectedCypherParams)}); -});\n`); +});\r\n`); return acc; }, []) - .join('\n'); + .join('\r\n'); }; const makeTestDataSchema = tck => { const typeDefs = tck.typeDefs; - const typeMap = extractTypeMapFromTypeDefs(typeDefs); - const resolvers = buildTestDataResolvers(typeMap); - // augmentation for directive and temporal support const augmentedTypeDefs = augmentTypeDefs(typeDefs); - // build a schema to be used in running resolvers that use cypherQuery + const resolvers = buildTestDataResolvers(augmentedTypeDefs); return makeExecutableSchema({ typeDefs: augmentedTypeDefs, resolvers, @@ -128,14 +122,14 @@ const buildTestData = (schema, tck) => { return extractedTckTestData.reduce((acc, testBlocks) => { const testName = testBlocks.test; // graphql - let testGraphql = testBlocks.graphql.join('\n'); + let testGraphql = testBlocks.graphql.join('\r\n'); // validation and formatting through parse -> print testGraphql = parse(testGraphql); testGraphql = print(testGraphql); // graphql variables let testParams = {}; if (testBlocks.params) { - testParams = testBlocks.params.join('\n'); + testParams = testBlocks.params.join('\r\n'); testParams = JSON.parse(testParams); } const testCypher = testBlocks.cypher.join(' '); @@ -168,16 +162,23 @@ message: ${message} }, []); }; -const buildTestDataResolvers = typeMap => { +const buildTestDataResolvers = augmentedTypeDefs => { + const definitions = parse(augmentedTypeDefs).definitions; const resolvers = {}; - const queryType = typeMap.Query; + const queryType = definitions.find(definition => definition.name && definition.name.value === 'Query'); if (queryType) { - const queryMap = createOperationMap(queryType); + const queryMap = queryType.fields.reduce((acc, t) => { + acc[t.name.value] = t; + return acc; + }, {}); resolvers['Query'] = buildResolvers(queryMap); } - const mutationType = typeMap.Mutation; + const mutationType = definitions.find(definition => definition.name && definition.name.value === 'Mutation'); if (mutationType) { - const mutationMap = createOperationMap(mutationType); + const mutationMap = mutationType.fields.reduce((acc, t) => { + acc[t.name.value] = t; + return acc; + }, {}); resolvers['Mutation'] = buildResolvers(mutationMap); } return resolvers; @@ -202,7 +203,7 @@ const buildResolvers = fieldMap => { const writeTestFile = (testFile, tck, testDeclarations) => { const typeDefs = tck.typeDefs; const writeStream = createWriteStream(testFile); - writeStream.write(`// Generated by test/tck/parseTck.js + writeStream.write(`// Generated by test/helpers/tck/parseTck.js import test from 'ava'; import { filterTestRunner } from '../helpers/filterTestHelpers'; From 797155a396be5aff4640ad69368e010c3e349531 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 8 Oct 2019 20:20:22 -0700 Subject: [PATCH 12/37] file name change for generated filtering tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12f06aa1..ef34cde5 100755 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "prepublish": "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/tck/*.test.js", + "test-tck": "nyc ava --fail-fast test/unit/filterTests.test.js", "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "test-all": "nyc ava --verbose", "test-isolated": "nyc ava test/**/*.test.js --verbose --match='!*not-isolated*'", From d2b52690475d5e71cdab0df46a79a74d7968ac19 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 9 Oct 2019 11:42:39 -0700 Subject: [PATCH 13/37] Removal of Windows \r line separators --- test/helpers/tck/parser.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/helpers/tck/parser.js b/test/helpers/tck/parser.js index cb02c4ff..9912621d 100644 --- a/test/helpers/tck/parser.js +++ b/test/helpers/tck/parser.js @@ -15,11 +15,11 @@ const extractTck = fileName => { const rs = createReadStream(fileName, { encoding: 'utf8' }); rs.on('data', lines => { // split into array of lines - lines = lines.split('\r\n'); + lines = lines.split('\n'); // extract array elements for typeDefs const typeDefs = extractBlock('schema', lines); resolve({ - typeDefs: typeDefs.join('\r\n '), + typeDefs: typeDefs.join('\n '), tests: extractTestBlocks(lines) }); }); @@ -98,10 +98,10 @@ const buildTestDeclarations = (tck, extractionLimit) => { filterTestRunner(t, typeDefs, graphQLQuery, ${JSON.stringify( test.params )}, expectedCypherQuery, ${JSON.stringify(test.expectedCypherParams)}); -});\r\n`); +});\n`); return acc; }, []) - .join('\r\n'); + .join('\n'); }; const makeTestDataSchema = tck => { @@ -122,14 +122,14 @@ const buildTestData = (schema, tck) => { return extractedTckTestData.reduce((acc, testBlocks) => { const testName = testBlocks.test; // graphql - let testGraphql = testBlocks.graphql.join('\r\n'); + let testGraphql = testBlocks.graphql.join('\n'); // validation and formatting through parse -> print testGraphql = parse(testGraphql); testGraphql = print(testGraphql); // graphql variables let testParams = {}; if (testBlocks.params) { - testParams = testBlocks.params.join('\r\n'); + testParams = testBlocks.params.join('\n'); testParams = JSON.parse(testParams); } const testCypher = testBlocks.cypher.join(' '); From df2e3ce26e8d1b24992de518dc75eee2aa5331cd Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:27:14 -0700 Subject: [PATCH 14/37] documentation comments --- src/augment/ast.js | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/augment/ast.js b/src/augment/ast.js index be506307..f35ac5d6 100644 --- a/src/augment/ast.js +++ b/src/augment/ast.js @@ -1,28 +1,43 @@ import { Kind } from 'graphql'; import { TypeWrappers } from './fields'; +/** + * Builds the AST definition for a Name + */ export const buildName = ({ name = '' }) => ({ kind: Kind.NAME, value: name }); +/** + * Builds the AST definition for a Document + */ export const buildDocument = ({ definitions = [] }) => ({ kind: Kind.DOCUMENT, definitions }); +/** + * Builds the AST definition for a Directive Argument + */ export const buildDirectiveArgument = ({ name = '', value }) => ({ kind: Kind.ARGUMENT, name, value }); +/** + * Builds the AST definition for a Directive instance + */ export const buildDirective = ({ name = '', args = [] }) => ({ kind: Kind.DIRECTIVE, name, arguments: args }); +/** + * Builds the AST definition for a type + */ export const buildNamedType = ({ name = '', wrappers = {} }) => { let type = { kind: Kind.NAMED_TYPE, @@ -49,6 +64,27 @@ export const buildNamedType = ({ name = '', wrappers = {} }) => { return type; }; +/** + * Builds the AST definition for a schema type + */ +export const buildSchemaDefinition = ({ operationTypes = [] }) => ({ + kind: Kind.SCHEMA_DEFINITION, + operationTypes +}); + +/** + * Builds the AST definition for an operation type on + * the schema type + */ +export const buildOperationType = ({ operation = '', type = {} }) => ({ + kind: Kind.OPERATION_TYPE_DEFINITION, + operation, + type +}); + +/** + * Builds the AST definition for an Object type + */ export const buildObjectType = ({ name = '', fields = [], @@ -62,6 +98,9 @@ export const buildObjectType = ({ description }); +/** + * Builds the AST definition for a Field + */ export const buildField = ({ name = '', type = {}, @@ -77,6 +116,10 @@ export const buildField = ({ description }); +/** + * Builds the AST definition for an Input Value, + * used for both field arguments and input object types + */ export const buildInputValue = ({ name = '', type = {}, @@ -94,6 +137,9 @@ export const buildInputValue = ({ }; }; +/** + * Builds the AST definition for an Enum type + */ export const buildEnumType = ({ name = '', values = [], description }) => ({ kind: Kind.ENUM_TYPE_DEFINITION, name, @@ -101,12 +147,18 @@ export const buildEnumType = ({ name = '', values = [], description }) => ({ description }); +/** + * Builds the AST definition for an Enum type value + */ export const buildEnumValue = ({ name = '', description }) => ({ kind: Kind.ENUM_VALUE_DEFINITION, name, description }); +/** + * Builds the AST definition for an Input Object type + */ export const buildInputObjectType = ({ name = '', fields = [], @@ -120,6 +172,9 @@ export const buildInputObjectType = ({ description }); +/** + * Builds the AST definition for a Directive definition + */ export const buildDirectiveDefinition = ({ name = '', args = [], From 534a967151ced209a1adc29cb525f664f22cf936 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:28:08 -0700 Subject: [PATCH 15/37] schema type support, documentation comments --- src/augment/augment.js | 307 +++++++++++++++++++++++++++++++---------- 1 file changed, 235 insertions(+), 72 deletions(-) diff --git a/src/augment/augment.js b/src/augment/augment.js index 54de73b6..8d62f2e0 100644 --- a/src/augment/augment.js +++ b/src/augment/augment.js @@ -6,18 +6,28 @@ import { isTypeExtensionNode } from 'graphql'; import { makeExecutableSchema } from 'graphql-tools'; -import { buildDocument } from './ast'; import { + buildDocument, + buildOperationType, + buildSchemaDefinition, + buildName, + buildNamedType, + buildObjectType +} from './ast'; +import { + OperationType, isNodeType, - buildNeo4jTypes, - transformNeo4jTypes, - initializeOperationTypes + augmentTypes, + transformNeo4jTypes } from './types/types'; -import { augmentNodeType } from './types/node/node'; +import { unwrapNamedType } from './fields'; import { augmentDirectiveDefinitions } from './directives'; import { extractResolversFromSchema, augmentResolvers } from './resolvers'; import { addAuthDirectiveImplementations } from '../auth'; +/** + * The main export for augmenting an SDL document + */ export const makeAugmentedExecutableSchema = ({ typeDefs, resolvers, @@ -77,6 +87,7 @@ export const makeAugmentedExecutableSchema = ({ }); const augmentedResolvers = augmentResolvers( generatedTypeMap, + operationTypeMap, resolvers, config ); @@ -94,6 +105,9 @@ export const makeAugmentedExecutableSchema = ({ }); }; +/** + * The main export for augmnetation a schema + */ export const augmentedSchema = (schema, config) => { const definitions = extractSchemaDefinitions({ schema }); let [ @@ -144,6 +158,7 @@ export const augmentedSchema = (schema, config) => { const resolvers = extractResolversFromSchema(schema); const augmentedResolvers = augmentResolvers( generatedTypeMap, + operationTypeMap, resolvers, config ); @@ -157,74 +172,48 @@ export const augmentedSchema = (schema, config) => { }); }; -export const augmentTypes = ({ - typeDefinitionMap, - typeExtensionDefinitionMap, - generatedTypeMap, - operationTypeMap = {}, - config = {} -}) => { - Object.entries({ - ...typeDefinitionMap, - ...operationTypeMap - }).forEach(([typeName, definition]) => { - if (isNodeType({ definition })) { - [definition, generatedTypeMap, operationTypeMap] = augmentNodeType({ - typeName, - definition, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap, - config - }); - generatedTypeMap[typeName] = definition; - } else { - generatedTypeMap[typeName] = definition; - } - return definition; - }); - generatedTypeMap = buildNeo4jTypes({ - generatedTypeMap, - config - }); - return [typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap]; -}; - +/** + * Builds separate type definition maps for use in augmentation + */ export const mapDefinitions = ({ definitions = [], config = {} }) => { const typeExtensionDefinitionMap = {}; const directiveDefinitionMap = {}; let typeDefinitionMap = {}; - let operationTypeMap = {}; - // TODO Use to get operation type names - // let schemaDefinitionNode = {}; + let schemaDefinitionNode = undefined; definitions.forEach(def => { - const name = def.name.value; if (def.kind === Kind.SCHEMA_DEFINITION) { - // schemaDefinitionNode = def; - typeDefinitionMap[name] = def; + schemaDefinitionNode = def; + typeDefinitionMap[`schema`] = def; } else if (isTypeDefinitionNode(def)) { + const name = def.name.value; typeDefinitionMap[name] = def; } else if (isTypeExtensionNode(def)) { + const name = def.name.value; if (!typeExtensionDefinitionMap[name]) { typeExtensionDefinitionMap[name] = []; } typeExtensionDefinitionMap[name].push(def); } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { + const name = def.name.value; directiveDefinitionMap[name] = def; } }); - [typeDefinitionMap, operationTypeMap] = initializeOperationTypes({ + const [typeMap, operationTypeMap] = initializeOperationTypes({ typeDefinitionMap, + schemaDefinitionNode, config }); return [ - typeDefinitionMap, + typeMap, typeExtensionDefinitionMap, directiveDefinitionMap, operationTypeMap ]; }; +/** + * Merges back together all type definition maps used in augmentation + */ export const mergeDefinitionMaps = ({ generatedTypeMap = {}, typeExtensionDefinitionMap = {}, @@ -249,29 +238,10 @@ export const mergeDefinitionMaps = ({ }); }; -export const extractSchemaDefinitions = ({ schema = {} }) => - Object.values({ - ...schema.getDirectives(), - ...schema.getTypeMap() - }).reduce((astNodes, definition) => { - const astNode = definition.astNode; - if (astNode) { - astNodes.push(astNode); - const extensionASTNodes = definition.extensionASTNodes; - if (extensionASTNodes) { - astNodes.push(...extensionASTNodes); - } - } - return astNodes; - }, []); - -export const printSchemaDocument = ({ schema }) => - print( - buildDocument({ - definitions: extractSchemaDefinitions({ schema }) - }) - ); - +/** + * Given a type name, checks whether it is excluded from + * the Query or Mutation API + */ export const shouldAugmentType = (config, operationTypeName, typeName) => { return typeof config[operationTypeName] === 'boolean' ? config[operationTypeName] @@ -284,15 +254,208 @@ export const shouldAugmentType = (config, operationTypeName, typeName) => { : false; }; +/** + * Given the type names of the nodes of a relationship, checks + * whether the relationship is excluded from the API by way of + * both related nodes being excluded + */ export const shouldAugmentRelationshipField = ( config, operationTypeName, fromName, toName -) => - shouldAugmentType(config, operationTypeName, fromName) && - shouldAugmentType(config, operationTypeName, toName); +) => { + return ( + shouldAugmentType(config, operationTypeName, fromName) && + shouldAugmentType(config, operationTypeName, toName) + ); +}; + +/** + * Prints the AST of a GraphQL SDL Document containing definitions + * extracted from a given schema, along with no loss of directives and a + * regenerated schema type + */ +export const printSchemaDocument = ({ schema }) => { + return print( + buildDocument({ + definitions: extractSchemaDefinitions({ schema }) + }) + ); +}; + +/** + * Builds any operation types that do not exist but should + */ +const initializeOperationTypes = ({ + typeDefinitionMap, + schemaDefinitionNode, + config = {} +}) => { + let queryTypeName = OperationType.QUERY; + let mutationTypeName = OperationType.MUTATION; + let subscriptionTypeName = OperationType.SUBSCRIPTION; + if (schemaDefinitionNode) { + const operationTypes = schemaDefinitionNode.operationTypes; + operationTypes.forEach(definition => { + const operation = definition.operation; + const unwrappedType = unwrapNamedType({ type: definition.type }); + if (operation === queryTypeName.toLowerCase()) { + queryTypeName = unwrappedType.name; + } else if (operation === mutationTypeName.toLowerCase()) { + mutationTypeName = unwrappedType.name; + } else if (operation === subscriptionTypeName.toLowerCase()) { + subscriptionTypeName = unwrappedType.name; + } + }); + } + typeDefinitionMap = initializeOperationType({ + typeName: queryTypeName, + typeDefinitionMap, + config + }); + typeDefinitionMap = initializeOperationType({ + typeName: mutationTypeName, + typeDefinitionMap, + config + }); + typeDefinitionMap = initializeOperationType({ + typeName: subscriptionTypeName, + typeDefinitionMap, + config + }); + return buildAugmentationTypeMaps({ + typeDefinitionMap, + queryTypeName, + mutationTypeName, + subscriptionTypeName + }); +}; + +/** + * Builds an operation type if it does not exist but should + */ +const initializeOperationType = ({ + typeName = '', + typeDefinitionMap = {}, + config = {} +}) => { + const typeNameLower = typeName.toLowerCase(); + const types = Object.keys(typeDefinitionMap); + let operationType = typeDefinitionMap[typeName]; + if ( + hasNonExcludedNodeType(types, typeDefinitionMap, typeNameLower, config) && + !operationType && + config[typeNameLower] + ) { + operationType = buildObjectType({ + name: buildName({ name: typeName }) + }); + } + if (operationType) typeDefinitionMap[typeName] = operationType; + return typeDefinitionMap; +}; + +/** + * Ensures that an operation type is only generated if an operation + * field would be generated - should be able to factor out + */ +const hasNonExcludedNodeType = (types, typeMap, rootType, config) => { + return types.find(e => { + const type = typeMap[e]; + const typeName = type.name ? type.name.value : ''; + if (typeName) { + return ( + isNodeType({ definition: type }) && + shouldAugmentType(config, rootType, typeName) + ); + } + }); +}; + +/** + * Builds a typeDefinitionMap that excludes operation types, instead placing them + * within an operationTypeMap + */ +const buildAugmentationTypeMaps = ({ + typeDefinitionMap = {}, + queryTypeName, + mutationTypeName, + subscriptionTypeName +}) => { + return Object.entries(typeDefinitionMap).reduce( + ([augmentationTypeMap, operationTypeMap], [typeName, definition]) => { + if (typeName === queryTypeName) { + operationTypeMap[OperationType.QUERY] = definition; + } else if (typeName === mutationTypeName) { + operationTypeMap[OperationType.MUTATION] = definition; + } else if (typeName === subscriptionTypeName) { + operationTypeMap[OperationType.SUBSCRIPTION] = definition; + } else { + augmentationTypeMap[typeName] = definition; + } + return [augmentationTypeMap, operationTypeMap]; + }, + [{}, {}] + ); +}; + +/** + * Extracts type definitions from a schema and regenerates the schema type + */ +const extractSchemaDefinitions = ({ schema = {} }) => { + let definitions = Object.values({ + ...schema.getDirectives(), + ...schema.getTypeMap() + }).reduce((astNodes, definition) => { + const astNode = definition.astNode; + if (astNode) { + astNodes.push(astNode); + const extensionASTNodes = definition.extensionASTNodes; + if (extensionASTNodes) { + astNodes.push(...extensionASTNodes); + } + } + return astNodes; + }, []); + definitions = regenerateSchemaType({ schema, definitions }); + return definitions; +}; + +/** + * Regenerates the schema type definition using any existing operation types + */ +const regenerateSchemaType = ({ schema = {}, definitions = [] }) => { + const operationTypes = []; + Object.values(OperationType).forEach(name => { + let operationType = undefined; + if (name === OperationType.QUERY) operationType = schema.getQueryType(); + else if (name === OperationType.MUTATION) + operationType = schema.getMutationType(); + else if (name === OperationType.SUBSCRIPTION) + operationType = schema.getSubscriptionType(); + if (operationType) { + operationTypes.push( + buildOperationType({ + operation: name.toLowerCase(), + type: buildNamedType({ name: operationType.name }) + }) + ); + } + }); + if (operationTypes.length) { + definitions.push( + buildSchemaDefinition({ + operationTypes + }) + ); + } + return definitions; +}; +/** + * Getter for an array of type names excludes from an operation type + */ const getExcludedTypes = (config, operationTypeName) => { return config && operationTypeName && From bc5d452068720d3ba1885a45a5583a8b41030360 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:28:51 -0700 Subject: [PATCH 16/37] documentation comments --- src/augment/directives.js | 87 +++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/src/augment/directives.js b/src/augment/directives.js index 2949da5d..a30cd503 100644 --- a/src/augment/directives.js +++ b/src/augment/directives.js @@ -11,6 +11,10 @@ import { buildName } from './ast'; +/** + * An enum describing the names of directive definitions and instances + * used by this integration + */ export const DirectiveDefinition = { CYPHER: 'cypher', RELATION: 'relation', @@ -22,20 +26,38 @@ export const DirectiveDefinition = { ADDITIONAL_LABELS: 'additionalLabels' }; +// The name of Role type used in authorization logic const ROLE_TYPE = 'Role'; +/** + * Enum for the names of directed fields on relationship types + */ +const RelationshipDirectionField = { + FROM: 'from', + TO: 'to' +}; + +/** + * A predicate function for cypher directive fields + */ export const isCypherField = ({ directives = [] }) => getDirective({ directives, name: DirectiveDefinition.CYPHER }); +/** + * A predicate function for neo4j_ignore directive fields + */ export const isIgnoredField = ({ directives = [] }) => getDirective({ directives, name: DirectiveDefinition.NEO4J_IGNORE }); +/** + * The main export for augmenting directive definitions + */ export const augmentDirectiveDefinitions = ({ typeDefinitionMap = {}, directiveDefinitionMap = {}, @@ -84,11 +106,9 @@ export const augmentDirectiveDefinitions = ({ return [typeDefinitionMap, directiveDefinitionMap]; }; -const RelationshipDirectionField = { - FROM: 'from', - TO: 'to' -}; - +/** + * Builds a relation directive for generated relationship output types + */ export const buildRelationDirective = ({ relationshipName, fromType, @@ -121,6 +141,9 @@ export const buildRelationDirective = ({ ] }); +/** + * Builds a MutationMeta directive for translating relationship mutations + */ export const buildMutationMetaDirective = ({ relationshipName, fromType, @@ -153,6 +176,9 @@ export const buildMutationMetaDirective = ({ ] }); +/** + * Builds the hasScope directive used in API authorization logic + */ export const buildAuthScopeDirective = ({ scopes = [] }) => buildDirective({ name: buildName({ name: DirectiveDefinition.HAS_SCOPE }), @@ -170,6 +196,10 @@ export const buildAuthScopeDirective = ({ scopes = [] }) => ] }); +/** + * A map of the AST configurations for directive definitions + * used in API authorization logic + */ const AuthDirectiveDefinitionMap = { [DirectiveDefinition.IS_AUTHENTICATED]: ({ config }) => { if (useAuthDirective(config, DirectiveDefinition.IS_AUTHENTICATED)) { @@ -235,7 +265,9 @@ const AuthDirectiveDefinitionMap = { } }; -// Map of AST configs for ASTNodeBuilder +/** + * Map of AST configs for ASTNodeBuilder + */ const directiveDefinitionBuilderMap = { [DirectiveDefinition.CYPHER]: ({ config }) => { return { @@ -284,23 +316,21 @@ const directiveDefinitionBuilderMap = { }; }, [DirectiveDefinition.ADDITIONAL_LABELS]: ({ config }) => { - if (useAuthDirective(config, DirectiveDefinition.ADDITIONAL_LABELS)) { - return { - name: DirectiveDefinition.ADDITIONAL_LABELS, - args: [ - { - name: 'labels', - type: { - name: GraphQLString, - wrappers: { - [TypeWrappers.LIST_TYPE]: true - } + return { + name: DirectiveDefinition.ADDITIONAL_LABELS, + args: [ + { + name: 'labels', + type: { + name: GraphQLString, + wrappers: { + [TypeWrappers.LIST_TYPE]: true } } - ], - locations: [DirectiveLocation.OBJECT] - }; - } + } + ], + locations: [DirectiveLocation.OBJECT] + }; }, [DirectiveDefinition.MUTATION_META]: ({ config }) => { return { @@ -336,6 +366,9 @@ const directiveDefinitionBuilderMap = { } }; +/** + * Predicate function for deciding whether to a given directive + */ export const useAuthDirective = (config, authDirective) => { if (config && typeof config === 'object') { return ( @@ -348,6 +381,9 @@ export const useAuthDirective = (config, authDirective) => { return false; }; +/** + * Gets the direction argument of the relation field directive + */ export const getRelationDirection = relationDirective => { let direction = {}; try { @@ -361,6 +397,9 @@ export const getRelationDirection = relationDirective => { } }; +/** + * Gets the name argument of a relation directive + */ export const getRelationName = relationDirective => { let name = {}; try { @@ -372,10 +411,16 @@ export const getRelationName = relationDirective => { } }; +/** + * Gets a directive instance of a given name + */ export const getDirective = ({ directives, name }) => { return directives.find(directive => directive.name.value === name); }; +/** + * Gets an argument of a directive + */ export const getDirectiveArgument = ({ directive, name }) => { let value = ''; const arg = directive.arguments.find( From 1f28271c3e2615b9307d2dafecacca895dfcfb8f Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:29:25 -0700 Subject: [PATCH 17/37] documentation comments --- src/augment/fields.js | 67 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/src/augment/fields.js b/src/augment/fields.js index 8c5cf565..ede6baab 100644 --- a/src/augment/fields.js +++ b/src/augment/fields.js @@ -1,7 +1,25 @@ import { Kind } from 'graphql'; import { Neo4jDataType, isNeo4jPropertyType } from './types/types'; -const Neo4jSystemIDField = `_id`; +/** + * The name of the Neo4j system ID field + * See: https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-id + */ +export const Neo4jSystemIDField = `_id`; + +/** + * Given the kind or type of a field, this predicate function identifies which + * Neo4j property type, if any, it represents + * See: https://neo4j.com/docs/cypher-manual/current/syntax/values/#property-types + */ +export const isPropertyTypeField = ({ kind, type }) => + isIntegerField({ type }) || + isFloatField({ type }) || + isStringField({ kind, type }) || + isBooleanField({ type }) || + isCustomScalarField({ kind }) || + isTemporalField({ type }) || + isNeo4jPropertyType({ type }); export const isIntegerField = ({ type }) => Neo4jDataType.PROPERTY[type] === 'Integer'; @@ -24,15 +42,9 @@ export const isNeo4jIDField = ({ name }) => name === Neo4jSystemIDField; export const isCustomScalarField = ({ kind }) => kind === Kind.SCALAR_TYPE_DEFINITION; -export const isPropertyTypeField = ({ kind, type }) => - isIntegerField({ type }) || - isFloatField({ type }) || - isStringField({ kind, type }) || - isBooleanField({ type }) || - isCustomScalarField({ kind }) || - isTemporalField({ type }) || - isNeo4jPropertyType({ type }); - +/** + * An Enum used for referring to the unique combinations of GraphQL type wrappers + */ export const TypeWrappers = { NAME: 'name', NON_NULL_NAMED_TYPE: 'isNonNullNamedType', @@ -40,15 +52,35 @@ export const TypeWrappers = { NON_NULL_LIST_TYPE: 'isNonNullListType' }; +/** + * Predicate function identifying whether a GraphQL NamedType + * contained in a type was wrapped with a NonNullType wrapper + */ export const isNonNullNamedTypeField = ({ wrappers = {} }) => wrappers[TypeWrappers.NON_NULL_NAMED_TYPE]; +/** + * Predicate function identifying whether a type was wrapped + * with a GraphQL ListType wrapper + */ export const isListTypeField = ({ wrappers = {} }) => wrappers[TypeWrappers.LIST_TYPE]; +/** + * Predicate function identifying whether a GraphQL ListType + * contained in a type was wrapped with a NonNullType wrapper + */ export const isNonNullListTypeField = ({ wrappers = {} }) => wrappers[TypeWrappers.NON_NULL_LIST_TYPE]; +/** + * A helper function that reduces the type wrappers of a given type + * to unique cases described by the TypeWrappers enum. This enables the use + * of simplified predicate functions for identifying type wrapper conditions, + * as well as enables a configurable generative process for wrapping types, + * using buildNamedType. + * See: https://graphql.github.io/graphql-spec/June2018/#sec-Wrapping-Types + */ export const unwrapNamedType = ({ type = {}, unwrappedType = {} }) => { // Initialize wrappers for this type unwrappedType.wrappers = { @@ -68,7 +100,6 @@ export const unwrapNamedType = ({ type = {}, unwrappedType = {} }) => { } // Making decisions on the way back up: // Cases: (1) Name, (2) [Name], (3) [Name!], (4) Name!, (5) [Name]!, (6) [Name!]! - // See: https://graphql.github.io/graphql-spec/June2018/#sec-Wrapping-Types if (type.kind === Kind.NAMED_TYPE && type.name) { if (type.name.kind === Kind.NAME) { // (1) Name - name of unwrapped type @@ -92,9 +123,18 @@ export const unwrapNamedType = ({ type = {}, unwrappedType = {} }) => { return unwrappedType; }; +/** + * A getter for a field definition of a given name, contained + * in a given array of field definitions + */ export const getFieldDefinition = ({ fields = [], name = '' }) => fields.find(field => field.name && field.name.value === name); +/** + * A getter for the type name of a field of a given name, + * finding the field, unwrapping its type, and returning + * the value of its NamedType + */ export const getFieldType = ({ fields = [], name = '' }) => { let typeName = ''; const field = getFieldDefinition({ @@ -107,6 +147,11 @@ export const getFieldType = ({ fields = [], name = '' }) => { return typeName; }; +/** + * Transformation helper for conversion of a given string to + * snake case, used in generating default relationship names + * ex: fooBar -> FOO_BAR + */ export const toSnakeCase = name => { return Object.keys(name) .reduce((acc, t) => { From 3468ce66f07e5e298460dc6cbda16294bf203bd2 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:30:05 -0700 Subject: [PATCH 18/37] documentation comments --- src/augment/input-values.js | 127 ++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 29 deletions(-) diff --git a/src/augment/input-values.js b/src/augment/input-values.js index 72678228..0638a2c2 100644 --- a/src/augment/input-values.js +++ b/src/augment/input-values.js @@ -17,22 +17,40 @@ import { isFloatField, isStringField, isBooleanField, - isTemporalField + isTemporalField, + getFieldDefinition } from './fields'; +/** + * An enum describing the names of the input value definitions + * used for the field argument AST for data result pagination + */ export const PagingArgument = { FIRST: 'first', OFFSET: 'offset' }; +/** + * An enum describing the names of the input value definitions + * used for the field argument AST for data result ordering + */ export const OrderingArgument = { ORDER_BY: 'orderBy' }; +/** + * An enum describing the names of the input value definitions + * used for the field argument AST for data selection filtering + */ export const FilteringArgument = { FILTER: 'filter' }; +/** + * Builds the AST definitions for input values that compose the + * input object types used by Query API field arguments, + * e.g., pagination, ordering, filtering, etc. + */ export const augmentInputTypePropertyFields = ({ inputTypeMap = {}, fieldName, @@ -65,19 +83,26 @@ export const augmentInputTypePropertyFields = ({ return inputTypeMap; }; +/** + * Given an argumentMap of expected Query API field arguments, + * builds their AST definitions + */ export const buildQueryFieldArguments = ({ - augmentationMap = {}, + argumentMap = {}, fieldArguments, fieldDirectives, outputType, outputTypeWrappers }) => { - Object.values(augmentationMap).forEach(name => { + Object.values(argumentMap).forEach(name => { if (isListTypeField({ wrappers: outputTypeWrappers })) { if (name === PagingArgument.FIRST) { // Result Arguments if ( - !fieldArguments.some(arg => arg.name.value === PagingArgument.FIRST) + !getFieldDefinition({ + fields: fieldArguments, + name: PagingArgument.FIRST + }) ) { fieldArguments.push( buildQueryPagingArgument({ @@ -88,7 +113,10 @@ export const buildQueryFieldArguments = ({ } else if (name === PagingArgument.OFFSET) { // Result Arguments if ( - !fieldArguments.some(arg => arg.name.value === PagingArgument.OFFSET) + !getFieldDefinition({ + fields: fieldArguments, + name: PagingArgument.OFFSET + }) ) { fieldArguments.push( buildQueryPagingArgument({ @@ -119,6 +147,10 @@ export const buildQueryFieldArguments = ({ return fieldArguments; }; +/** + * Builds the AST definition for pagination field arguments + * used in the Query API + */ const buildQueryPagingArgument = ({ name = '' }) => { let arg = {}; // Prevent overwrite @@ -141,6 +173,9 @@ const buildQueryPagingArgument = ({ name = '' }) => { return arg; }; +/** + * Builds the AST definition for ordering field arguments + */ const buildQueryOrderingArgument = ({ typeName }) => buildInputValue({ name: buildName({ name: OrderingArgument.ORDER_BY }), @@ -152,6 +187,10 @@ const buildQueryOrderingArgument = ({ typeName }) => }) }); +/** + * Builds the AST definition for an enum type used as the + * type of an ordering field argument + */ export const buildQueryOrderingEnumType = ({ nodeInputTypeMap, typeDefinitionMap, @@ -170,6 +209,10 @@ export const buildQueryOrderingEnumType = ({ return generatedTypeMap; }; +/** + * Builds the AST definitions for the values of an enum + * definitions used by an ordering field argument + */ export const buildPropertyOrderingValues = ({ fieldName }) => [ buildEnumValue({ name: buildName({ name: `${fieldName}_asc` }) @@ -179,6 +222,10 @@ export const buildPropertyOrderingValues = ({ fieldName }) => [ }) ]; +/** + * Builds the AST definition for the input value definition + * used for a filtering field argument + */ const buildQueryFilteringArgument = ({ typeName }) => buildInputValue({ name: buildName({ name: FilteringArgument.FILTER }), @@ -187,6 +234,10 @@ const buildQueryFilteringArgument = ({ typeName }) => }) }); +/** + * Builds the AST definition for an input object type used + * as the type of a filtering field argument + */ export const buildQueryFilteringInputType = ({ typeName, inputTypeMap, @@ -197,30 +248,7 @@ export const buildQueryFilteringInputType = ({ if (inputType) { const inputTypeName = inputType.name; inputType.name = buildName({ name: inputTypeName }); - inputType.fields.unshift( - ...[ - buildInputValue({ - name: buildName({ name: 'AND' }), - type: buildNamedType({ - name: typeName, - wrappers: { - [TypeWrappers.NON_NULL_NAMED_TYPE]: true, - [TypeWrappers.LIST_TYPE]: true - } - }) - }), - buildInputValue({ - name: buildName({ name: 'OR' }), - type: buildNamedType({ - name: typeName, - wrappers: { - [TypeWrappers.NON_NULL_NAMED_TYPE]: true, - [TypeWrappers.LIST_TYPE]: true - } - }) - }) - ] - ); + inputType.fields.unshift(...buildLogicalFilterInputValues({ typeName })); if (!typeDefinitionMap[inputTypeName]) { generatedTypeMap[inputTypeName] = buildInputObjectType(inputType); } @@ -228,6 +256,43 @@ export const buildQueryFilteringInputType = ({ return generatedTypeMap; }; +// An enum containing the semantics of logical filtering arguments +const LogicalFilteringArgument = { + AND: 'AND', + OR: 'OR' +}; + +/** + * Builds the AST definitions for logical filtering arguments + */ +const buildLogicalFilterInputValues = ({ typeName = '' }) => { + return [ + buildInputValue({ + name: buildName({ name: LogicalFilteringArgument.AND }), + type: buildNamedType({ + name: typeName, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true, + [TypeWrappers.LIST_TYPE]: true + } + }) + }), + buildInputValue({ + name: buildName({ name: LogicalFilteringArgument.OR }), + type: buildNamedType({ + name: typeName, + wrappers: { + [TypeWrappers.NON_NULL_NAMED_TYPE]: true, + [TypeWrappers.LIST_TYPE]: true + } + }) + }) + ]; +}; + +/** + * Builds the AST definitions for filtering Neo4j property type fields + */ const buildPropertyFilters = ({ fieldName = '', outputType = '', @@ -299,6 +364,10 @@ const buildPropertyFilters = ({ return filters; }; +/** + * Builds the input value definitions that compose input object types + * used by filtering arguments + */ export const buildFilters = ({ fieldName, fieldConfig, filterTypes = [] }) => [ buildInputValue({ name: buildName({ name: fieldConfig.name }), From 1a707a595b3ef4316f1a793b48c95db868811028 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:30:38 -0700 Subject: [PATCH 19/37] schema type support, documentation comments --- src/augment/resolvers.js | 64 ++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/augment/resolvers.js b/src/augment/resolvers.js index 0daae0e3..1827bd2c 100644 --- a/src/augment/resolvers.js +++ b/src/augment/resolvers.js @@ -1,31 +1,57 @@ import { neo4jgraphql } from '../index'; import { OperationType } from '../augment/types/types'; -export const augmentResolvers = (augmentedTypeMap, resolvers, config) => { - let queryResolvers = - resolvers && resolvers[OperationType.QUERY] - ? resolvers[OperationType.QUERY] - : {}; - const queryType = augmentedTypeMap[OperationType.QUERY]; +/** + * The main export for the generation of resolvers for the + * Query and Mutation API. Prevent overwriting. + */ +export const augmentResolvers = ( + augmentedTypeMap, + operationTypeMap, + resolvers, + config +) => { + // Persist and generate Query resolvers + let queryTypeName = OperationType.QUERY; + const queryType = operationTypeMap[queryTypeName]; if (queryType) { + queryTypeName = queryType.name.value; + let queryResolvers = + resolvers && resolvers[queryTypeName] ? resolvers[queryTypeName] : {}; queryResolvers = possiblyAddResolvers(queryType, queryResolvers, config); if (Object.keys(queryResolvers).length > 0) { - resolvers[OperationType.QUERY] = queryResolvers; + resolvers[queryTypeName] = queryResolvers; } } - let mutationResolvers = - resolvers && resolvers[OperationType.MUTATION] - ? resolvers[OperationType.MUTATION] - : {}; - const mutationType = augmentedTypeMap[OperationType.MUTATION]; + // Persist and generate Mutation resolvers + let mutationTypeName = OperationType.MUTATION; + const mutationType = operationTypeMap[mutationTypeName]; if (mutationType) { + mutationTypeName = mutationType.name.value; + let mutationResolvers = + resolvers && resolvers[mutationTypeName] + ? resolvers[mutationTypeName] + : {}; mutationResolvers = possiblyAddResolvers( mutationType, mutationResolvers, config ); if (Object.keys(mutationResolvers).length > 0) { - resolvers[OperationType.MUTATION] = mutationResolvers; + resolvers[mutationTypeName] = mutationResolvers; + } + } + // Persist Subscription resolvers + let subscriptionTypeName = OperationType.SUBSCRIPTION; + const subscriptionType = operationTypeMap[subscriptionTypeName]; + if (subscriptionType) { + subscriptionTypeName = subscriptionType.name.value; + let subscriptionResolvers = + resolvers && resolvers[subscriptionTypeName] + ? resolvers[subscriptionTypeName] + : {}; + if (Object.keys(subscriptionResolvers).length > 0) { + resolvers[subscriptionTypeName] = subscriptionResolvers; } } // must implement __resolveInfo for every Interface type @@ -44,6 +70,10 @@ export const augmentResolvers = (augmentedTypeMap, resolvers, config) => { return resolvers; }; +/** + * Generates resolvers for a given operation type, if + * any fields exist, for any resolver not provided + */ const possiblyAddResolvers = (operationType, resolvers, config) => { let operationName = ''; const fields = operationType ? operationType.fields : []; @@ -54,6 +84,7 @@ const possiblyAddResolvers = (operationType, resolvers, config) => { return Object.keys(operationTypeMap).reduce((acc, t) => { // if no resolver provided for this operation type field operationName = operationTypeMap[t].name.value; + // If not provided if (acc[operationName] === undefined) { acc[operationName] = function(...args) { return neo4jgraphql(...args, config.debug); @@ -63,6 +94,9 @@ const possiblyAddResolvers = (operationType, resolvers, config) => { }, resolvers); }; +/** + * Extracts resolvers from a schema + */ export const extractResolversFromSchema = schema => { const _typeMap = schema && schema._typeMap ? schema._typeMap : {}; const types = Object.keys(_typeMap); @@ -91,6 +125,10 @@ export const extractResolversFromSchema = schema => { }, {}); }; +/** + * Extracts field resolvers from a given type taken + * from a schema + */ const extractFieldResolversFromSchemaType = type => { const fields = type._fields; const fieldKeys = fields ? Object.keys(fields) : []; From 43f9f46c9c2e29e4cdc313a437d10875c0731a13 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:31:28 -0700 Subject: [PATCH 20/37] schema type support, documentation comments --- src/augment/types/node/node.js | 52 ++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js index fea92c3a..d26e048f 100644 --- a/src/augment/types/node/node.js +++ b/src/augment/types/node/node.js @@ -1,11 +1,12 @@ import { GraphQLString } from 'graphql'; -import { buildNodeQueryAPI, augmentNodeTypeFieldInput } from './query'; +import { augmentNodeQueryAPI, augmentNodeTypeFieldInput } from './query'; import { augmentNodeMutationAPI } from './mutation'; import { augmentRelationshipTypeField } from '../relationship/relationship'; import { augmentRelationshipMutationAPI } from '../relationship/mutation'; import { shouldAugmentType } from '../../augment'; import { TypeWrappers, + Neo4jSystemIDField, unwrapNamedType, isPropertyTypeField } from '../../fields'; @@ -39,6 +40,10 @@ import { } from '../../types/types'; import { getPrimaryKey } from '../../../utils'; +/** + * The main export for the augmentation process of a GraphQL + * type definition representing a Neo4j node entity + */ export const augmentNodeType = ({ typeName, definition, @@ -61,7 +66,10 @@ export const augmentNodeType = ({ operationTypeMap, config }); - if (!isOperationTypeDefinition({ definition }) && !isIgnoredType) { + if ( + !isOperationTypeDefinition({ definition, operationTypeMap }) && + !isIgnoredType + ) { [ propertyOutputFields, typeDefinitionMap, @@ -84,6 +92,11 @@ export const augmentNodeType = ({ return [definition, generatedTypeMap, operationTypeMap]; }; +/** + * Iterates through all field definitions of a node type, deciding whether + * to generate the corresponding field or input value definitions that compose + * the output and input types used in the Query and Mutation API + */ const augmentNodeTypeFields = ({ typeName, definition, @@ -195,18 +208,21 @@ const augmentNodeTypeFields = ({ }); return outputFields; }, []); - if (!isOperationTypeDefinition({ definition }) && !isIgnoredType) { + if ( + !isOperationTypeDefinition({ definition, operationTypeMap }) && + !isIgnoredType + ) { const queryTypeName = OperationType.QUERY; const queryTypeNameLower = queryTypeName.toLowerCase(); if (shouldAugmentType(config, queryTypeNameLower, typeName)) { const neo4jInternalIDConfig = { - name: '_id', + name: Neo4jSystemIDField, type: { name: GraphQLString.name } }; const systemIDIndex = propertyOutputFields.findIndex( - e => e.name.value === '_id' + e => e.name.value === Neo4jSystemIDField ); const systemIDField = buildField({ name: buildName({ name: neo4jInternalIDConfig.name }), @@ -234,6 +250,10 @@ const augmentNodeTypeFields = ({ ]; }; +/** + * Builds the Query API field arguments and relationship field mutation + * API for a node type field + */ const augmentNodeTypeField = ({ typeName, definition, @@ -259,9 +279,13 @@ const augmentNodeTypeField = ({ config, relationshipDirective, outputTypeWrappers, - nodeInputTypeMap + nodeInputTypeMap, + operationTypeMap }); - if (relationshipDirective && !isQueryTypeDefinition({ definition })) { + if ( + relationshipDirective && + !isQueryTypeDefinition({ definition, operationTypeMap }) + ) { const relationshipName = getRelationName(relationshipDirective); const relationshipDirection = getRelationDirection(relationshipDirective); // Assume direction OUT @@ -298,6 +322,10 @@ const augmentNodeTypeField = ({ ]; }; +/** + * Uses the results of augmentNodeTypeFields to build the AST definitions + * used to in supporting the Query and Mutation API of a node type + */ const augmentNodeTypeAPI = ({ definition, typeName, @@ -317,7 +345,7 @@ const augmentNodeTypeAPI = ({ operationTypeMap, config }); - [operationTypeMap, generatedTypeMap] = buildNodeQueryAPI({ + [operationTypeMap, generatedTypeMap] = augmentNodeQueryAPI({ typeName, propertyInputValues, nodeInputTypeMap, @@ -341,6 +369,12 @@ const augmentNodeTypeAPI = ({ ]; }; +/** + * Builds the AST definition of the node input object type used + * by relationship mutations for selecting the nodes of the + * relationship + */ + const buildNodeSelectionInputType = ({ definition, typeName, @@ -348,7 +382,7 @@ const buildNodeSelectionInputType = ({ generatedTypeMap, config }) => { - const mutationTypeName = OperationType.QUERY; + const mutationTypeName = OperationType.MUTATION; const mutationTypeNameLower = mutationTypeName.toLowerCase(); if (shouldAugmentType(config, mutationTypeNameLower, typeName)) { const primaryKey = getPrimaryKey(definition); From ba8567703e02b6c68796541196b222dd5b5ab130 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:32:03 -0700 Subject: [PATCH 21/37] schema type support, documentation comments --- src/augment/types/node/query.js | 133 +++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/src/augment/types/node/query.js b/src/augment/types/node/query.js index ca2567a9..9b58d481 100644 --- a/src/augment/types/node/query.js +++ b/src/augment/types/node/query.js @@ -28,12 +28,63 @@ import { buildQueryOrderingEnumType } from '../../input-values'; +/** + * An enum describing which arguments are implemented for + * node type fields in the Query API + */ const NodeQueryArgument = { ...PagingArgument, ...OrderingArgument, ...FilteringArgument }; +/** + * Given the results of augmentNodeTypeFields, builds or augments + * the AST definition of the Query operation field and any + * generated input or output types required for translation + */ +export const augmentNodeQueryAPI = ({ + typeName, + propertyInputValues, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config +}) => { + const queryType = operationTypeMap[OperationType.QUERY]; + const queryTypeNameLower = OperationType.QUERY.toLowerCase(); + if (shouldAugmentType(config, queryTypeNameLower, typeName)) { + if (queryType) { + operationTypeMap = buildNodeQueryField({ + typeName, + queryType, + propertyInputValues, + operationTypeMap, + config + }); + } + generatedTypeMap = buildQueryOrderingEnumType({ + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap + }); + generatedTypeMap = buildQueryFilteringInputType({ + typeName: `_${typeName}Filter`, + typeDefinitionMap, + generatedTypeMap, + inputTypeMap: nodeInputTypeMap + }); + } + return [operationTypeMap, generatedTypeMap]; +}; + +/** + * Given a node type field, builds the input value definitions + * for its Query arguments, along with those needed for input + * types generated to support the same Query API for the given + * field of the given node type + */ export const augmentNodeTypeFieldInput = ({ typeName, definition, @@ -44,14 +95,16 @@ export const augmentNodeTypeFieldInput = ({ config, relationshipDirective, outputTypeWrappers, - nodeInputTypeMap + nodeInputTypeMap, + operationTypeMap }) => { - fieldArguments = augmentNodeQueryArguments({ + fieldArguments = augmentNodeTypeFieldArguments({ definition, fieldArguments, fieldDirectives, outputType, outputTypeWrappers, + operationTypeMap, config }); nodeInputTypeMap = augmentNodeQueryArgumentTypes({ @@ -62,27 +115,33 @@ export const augmentNodeTypeFieldInput = ({ outputTypeWrappers, relationshipDirective, nodeInputTypeMap, + operationTypeMap, config }); return [fieldArguments, nodeInputTypeMap]; }; -const augmentNodeQueryArguments = ({ +/** + * Builds the AST for the input value definitions used for + * node type Query field arguments + */ +const augmentNodeTypeFieldArguments = ({ definition, fieldArguments, fieldDirectives, outputType, outputTypeWrappers, + operationTypeMap, config }) => { const queryTypeNameLower = OperationType.QUERY.toLowerCase(); if ( - !isMutationTypeDefinition({ definition }) && - !isSubscriptionTypeDefinition({ definition }) && + !isMutationTypeDefinition({ definition, operationTypeMap }) && + !isSubscriptionTypeDefinition({ definition, operationTypeMap }) && shouldAugmentType(config, queryTypeNameLower, outputType) ) { fieldArguments = buildQueryFieldArguments({ - augmentationMap: NodeQueryArgument, + argumentMap: NodeQueryArgument, fieldArguments, fieldDirectives, outputType, @@ -92,6 +151,11 @@ const augmentNodeQueryArguments = ({ return fieldArguments; }; +/** + * Given information about a field on a node type, builds the AST + * for associated input value definitions used by input types + * generated for the Query API + */ const augmentNodeQueryArgumentTypes = ({ typeName, definition, @@ -100,9 +164,13 @@ const augmentNodeQueryArgumentTypes = ({ outputTypeWrappers, relationshipDirective, nodeInputTypeMap, + operationTypeMap, config }) => { - if (relationshipDirective && !isQueryTypeDefinition({ definition })) { + if ( + relationshipDirective && + !isQueryTypeDefinition({ definition, operationTypeMap }) + ) { nodeInputTypeMap[FilteringArgument.FILTER].fields.push( ...buildRelationshipFilters({ typeName, @@ -117,43 +185,10 @@ const augmentNodeQueryArgumentTypes = ({ return nodeInputTypeMap; }; -export const buildNodeQueryAPI = ({ - typeName, - propertyInputValues, - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap, - config -}) => { - const queryTypeName = OperationType.QUERY; - const queryTypeNameLower = queryTypeName.toLowerCase(); - const queryType = operationTypeMap[queryTypeName]; - if (shouldAugmentType(config, queryTypeNameLower, typeName)) { - if (queryType) { - operationTypeMap = buildNodeQueryField({ - typeName, - queryType, - propertyInputValues, - operationTypeMap, - config - }); - } - generatedTypeMap = buildQueryOrderingEnumType({ - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap - }); - generatedTypeMap = buildQueryFilteringInputType({ - typeName: `_${typeName}Filter`, - typeDefinitionMap, - generatedTypeMap, - inputTypeMap: nodeInputTypeMap - }); - } - return [operationTypeMap, generatedTypeMap]; -}; - +/** + * Builds the AST for the Query type field definition for + * a given node type + */ const buildNodeQueryField = ({ typeName, queryType, @@ -192,6 +227,10 @@ const buildNodeQueryField = ({ return operationTypeMap; }; +/** + * Builds the AST for input value definitions used for the + * arguments of the Query type field for a given node type + */ const buildNodeQueryArguments = ({ typeName, propertyInputValues }) => { // Do not persist type wrappers propertyInputValues = propertyInputValues.map(arg => @@ -213,7 +252,7 @@ const buildNodeQueryArguments = ({ typeName, propertyInputValues }) => { ); } propertyInputValues = buildQueryFieldArguments({ - augmentationMap: NodeQueryArgument, + argumentMap: NodeQueryArgument, fieldArguments: propertyInputValues, outputType: typeName, outputTypeWrappers: { @@ -223,6 +262,10 @@ const buildNodeQueryArguments = ({ typeName, propertyInputValues }) => { return propertyInputValues; }; +/** + * Builds the AST for directive instances on the Query type + * field for a given node type + */ const buildNodeQueryDirectives = ({ typeName, config }) => { const directives = []; if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { From 39287e3a4764b0fbec2fab510658ff2d03f89d6a Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:32:54 -0700 Subject: [PATCH 22/37] documentation comments --- src/augment/types/node/mutation.js | 59 ++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/augment/types/node/mutation.js b/src/augment/types/node/mutation.js index e19df092..d7b4169f 100644 --- a/src/augment/types/node/mutation.js +++ b/src/augment/types/node/mutation.js @@ -16,13 +16,21 @@ import { shouldAugmentType } from '../../augment'; import { OperationType } from '../../types/types'; import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields'; +/** + * An enum describing the names of node type mutations + */ export const NodeMutation = { CREATE: 'Create', UPDATE: 'Update', - DELETE: 'Delete', - MERGE: 'Merge' + DELETE: 'Delete' + // MERGE: 'Merge' }; +/** + * Given the results of augmentNodeTypeFields, builds or augments + * the AST definitions of the Mutation operation fields and any + * generated input or output types required for translation + */ export const augmentNodeMutationAPI = ({ definition, typeName, @@ -54,6 +62,11 @@ export const augmentNodeMutationAPI = ({ return [operationTypeMap, generatedTypeMap]; }; +/** + * Given the results of augmentNodeTypeFields, builds the AST + * definition for a Mutation operation field of a given + * NodeMutation name + */ const buildNodeMutationField = ({ mutationType, mutationAction, @@ -103,23 +116,10 @@ const buildNodeMutationField = ({ return operationTypeMap; }; -const buildNodeMutationDirectives = ({ mutationAction, typeName, config }) => { - const directives = []; - if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { - directives.push( - buildAuthScopeDirective({ - scopes: [ - { - typeName, - mutation: mutationAction - } - ] - }) - ); - } - return directives; -}; - +/** + * Builds the AST for the input value definitions used as arguments + * on generated node Mutation fields of NodeMutation names + */ const buildNodeMutationArguments = ({ operationName = '', primaryKey, @@ -207,3 +207,24 @@ const buildNodeMutationArguments = ({ }) ); }; + +/** + * Builds the AST definitions for directive instances used by + * generated node Mutation fields of NodeMutation names + */ +const buildNodeMutationDirectives = ({ mutationAction, typeName, config }) => { + const directives = []; + if (useAuthDirective(config, DirectiveDefinition.HAS_SCOPE)) { + directives.push( + buildAuthScopeDirective({ + scopes: [ + { + typeName, + mutation: mutationAction + } + ] + }) + ); + } + return directives; +}; From 5df9d61bd5c02b2deef9403265ff29dfb8a9072d Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:33:26 -0700 Subject: [PATCH 23/37] schema type support, documentation comments --- .../types/relationship/relationship.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/augment/types/relationship/relationship.js b/src/augment/types/relationship/relationship.js index 2a55165a..c7ebc307 100644 --- a/src/augment/types/relationship/relationship.js +++ b/src/augment/types/relationship/relationship.js @@ -19,11 +19,16 @@ import { } from '../../directives'; import { isOperationTypeDefinition } from '../../types/types'; +// An enum for the semantics of the directed fields of a relationship type export const RelationshipDirectionField = { FROM: 'from', TO: 'to' }; +/** + * The main export for the augmentation process of a GraphQL + * type definition representing a Neo4j relationship entity + */ export const augmentRelationshipTypeField = ({ typeName, definition, @@ -40,7 +45,7 @@ export const augmentRelationshipTypeField = ({ config, outputTypeWrappers }) => { - if (!isOperationTypeDefinition({ definition })) { + if (!isOperationTypeDefinition({ definition, operationTypeMap })) { if (!isCypherField({ directives: fieldDirectives })) { const relationshipTypeDirective = getDirective({ directives: outputDefinition.directives, @@ -90,7 +95,8 @@ export const augmentRelationshipTypeField = ({ config, relationshipName, fieldType, - propertyOutputFields + propertyOutputFields, + operationTypeMap }); [ typeDefinitionMap, @@ -122,6 +128,11 @@ export const augmentRelationshipTypeField = ({ ]; }; +/** + * Iterates through all field definitions of a relationship type, deciding whether + * to generate the corresponding field or input value definitions that compose + * the output and input types used in the Query and Mutation API + */ const augmentRelationshipTypeFields = ({ typeName, outputType, @@ -192,6 +203,10 @@ const augmentRelationshipTypeFields = ({ ]; }; +/** + * Generates a default value for the name argument + * of the relation type directive, if none is provided + */ const decideDefaultRelationshipName = ({ relationshipTypeDirective, outputType, From accf5d2cfefdee2c89fd42dccca73e519988a66a Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:34:02 -0700 Subject: [PATCH 24/37] documentation comments --- src/augment/types/relationship/query.js | 153 +++++++++++++++++------- 1 file changed, 107 insertions(+), 46 deletions(-) diff --git a/src/augment/types/relationship/query.js b/src/augment/types/relationship/query.js index f50bfee2..48397c33 100644 --- a/src/augment/types/relationship/query.js +++ b/src/augment/types/relationship/query.js @@ -22,12 +22,21 @@ import { buildInputValue } from '../../ast'; +/** + * An enum describing which arguments are implemented for + * relationship type fields in the Query API + */ const RelationshipQueryArgument = { // ...PagingArgument, // ...OrderingArgument, ...FilteringArgument }; +/** + * Given the results of augmentRelationshipTypeFields, builds or + * augments the AST definition of the Query operation field and + * any generated input or output types required for translation + */ export const augmentRelationshipQueryAPI = ({ typeName, definition, @@ -44,7 +53,8 @@ export const augmentRelationshipQueryAPI = ({ config, relationshipName, fieldType, - propertyOutputFields + propertyOutputFields, + operationTypeMap }) => { const queryTypeNameLower = OperationType.QUERY.toLowerCase(); if ( @@ -64,7 +74,7 @@ export const augmentRelationshipQueryAPI = ({ outputType ) ) { - [fieldType, generatedTypeMap] = augmentRelationshipTypeFieldOutput({ + [fieldType, generatedTypeMap] = transformRelationshipTypeFieldOutput({ typeName, relatedType, fieldArguments, @@ -97,11 +107,11 @@ export const augmentRelationshipQueryAPI = ({ nodeInputTypeMap, relationshipInputTypeMap, outputTypeWrappers, + operationTypeMap, config }); } } - return [ fieldType, fieldArguments, @@ -111,6 +121,12 @@ export const augmentRelationshipQueryAPI = ({ ]; }; +/** + * Given a relationship type field, builds the input value + * definitions for its Query arguments, along with those needed + * for input types generated to support the same Query API + * for the given field of the given relationship type + */ const augmentRelationshipTypeFieldInput = ({ typeName, definition, @@ -125,6 +141,7 @@ const augmentRelationshipTypeFieldInput = ({ nodeInputTypeMap, relationshipInputTypeMap, outputTypeWrappers, + operationTypeMap, config }) => { const nodeFilteringFields = nodeInputTypeMap[FilteringArgument.FILTER].fields; @@ -156,12 +173,65 @@ const augmentRelationshipTypeFieldInput = ({ outputTypeWrappers, typeDefinitionMap, generatedTypeMap, + operationTypeMap, relationshipInputTypeMap }); return [fieldArguments, generatedTypeMap, nodeInputTypeMap]; }; -const augmentRelationshipTypeFieldOutput = ({ +/** + * Builds the AST for the input value definitions used for + * relationship type Query field arguments + */ +const augmentRelationshipTypeFieldArguments = ({ + fieldArguments, + typeName, + definition, + fromType, + toType, + outputType, + relatedType, + relationshipFilterTypeName, + outputTypeWrappers, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + relationshipInputTypeMap +}) => { + if ( + !isMutationTypeDefinition({ definition, operationTypeMap }) && + !isSubscriptionTypeDefinition({ definition, operationTypeMap }) + ) { + if (fromType !== toType) { + fieldArguments = buildQueryFieldArguments({ + argumentMap: RelationshipQueryArgument, + fieldArguments, + outputType: `${typeName}${outputType}`, + outputTypeWrappers + }); + } else { + fieldArguments = []; + } + } + generatedTypeMap = buildRelationshipSelectionArgumentInputTypes({ + fromType, + toType, + relatedType, + relationshipFilterTypeName, + generatedTypeMap, + relationshipInputTypeMap, + typeDefinitionMap + }); + return [fieldArguments, generatedTypeMap]; +}; + +/** + * Builds the AST for object type definitions used for transforming + * a relationship type field on a node type - will likely not be + * necessary once we allow for dynamically named fields for the + * 'from' and 'to' node type reference fields on relationship types + */ +const transformRelationshipTypeFieldOutput = ({ typeName, relatedType, fieldArguments, @@ -203,47 +273,10 @@ const augmentRelationshipTypeFieldOutput = ({ return [fieldType, generatedTypeMap]; }; -const augmentRelationshipTypeFieldArguments = ({ - fieldArguments, - typeName, - definition, - fromType, - toType, - outputType, - relatedType, - relationshipFilterTypeName, - outputTypeWrappers, - typeDefinitionMap, - generatedTypeMap, - relationshipInputTypeMap -}) => { - if ( - !isMutationTypeDefinition({ definition }) && - !isSubscriptionTypeDefinition({ definition }) - ) { - if (fromType !== toType) { - fieldArguments = buildQueryFieldArguments({ - augmentationMap: RelationshipQueryArgument, - fieldArguments, - outputType: `${typeName}${outputType}`, - outputTypeWrappers - }); - } else { - fieldArguments = []; - } - } - generatedTypeMap = buildRelationshipSelectionArgumentInputTypes({ - fromType, - toType, - relatedType, - relationshipFilterTypeName, - generatedTypeMap, - relationshipInputTypeMap, - typeDefinitionMap - }); - return [fieldArguments, generatedTypeMap]; -}; - +/** + * Builds the AST definitions that compose the Query filtering input type + * values for a given relationship field + */ export const buildRelationshipFilters = ({ typeName, fieldName, @@ -297,6 +330,11 @@ export const buildRelationshipFilters = ({ return filters; }; +/** + * Builds the AST definitions for the incoming and outgoing node type + * fields of the output object types generated for querying relationship + * type fields + */ export const buildNodeOutputFields = ({ fromType, toType, @@ -327,6 +365,10 @@ export const buildNodeOutputFields = ({ ]; }; +/** + * Builds the AST definitions for the object types generated + * for querying relationship type fields on node types + */ const buildRelationshipFieldOutputTypes = ({ outputType, fromType, @@ -346,7 +388,7 @@ const buildRelationshipFieldOutputTypes = ({ }); if (fromType === toType) { fieldArguments = buildQueryFieldArguments({ - augmentationMap: RelationshipQueryArgument, + argumentMap: RelationshipQueryArgument, fieldArguments, outputType, outputTypeWrappers @@ -381,6 +423,11 @@ const buildRelationshipFieldOutputTypes = ({ return generatedTypeMap; }; +/** + * Given information about a field on a relationship type, builds + * the AST for associated input value definitions used by input + * types generated for the Query API + */ const buildRelationshipSelectionArgumentInputTypes = ({ fromType, toType, @@ -422,6 +469,11 @@ const buildRelationshipSelectionArgumentInputTypes = ({ return generatedTypeMap; }; +/** + * Builds the AST definitions for the input values of the + * incoming and outgoing nodes, used as relationship mutation + * field arguments for selecting the related nodes + */ const buildNodeInputFields = ({ fromType, toType }) => { return [ buildInputValue({ @@ -443,6 +495,11 @@ const buildNodeInputFields = ({ fromType, toType }) => { ]; }; +/** + * Given the name of a type, and the names of the node types + * of a relationship type, decides which type it is related to + * (possibly itself) + */ const decideRelatedType = ({ typeName, fromType, toType }) => { let relatedType = toType; if (fromType !== toType) { @@ -455,6 +512,10 @@ const decideRelatedType = ({ typeName, fromType, toType }) => { return relatedType; }; +/** + * Validates that a given relationship type field on a node type + * has that node type as its 'from' or 'to' node type field + */ const validateRelationTypeDirectedFields = ( typeName, fieldName, From dc7146ce4dc1502950d089514f44a70b83f3e4cf Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:34:50 -0700 Subject: [PATCH 25/37] documentation comments --- src/augment/types/relationship/mutation.js | 54 ++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/augment/types/relationship/mutation.js b/src/augment/types/relationship/mutation.js index dfa71c14..100f3382 100644 --- a/src/augment/types/relationship/mutation.js +++ b/src/augment/types/relationship/mutation.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import { RelationshipDirectionField } from './relationship'; import { buildNodeOutputFields } from './query'; import { shouldAugmentRelationshipField } from '../../augment'; @@ -22,11 +21,21 @@ import { buildInputObjectType } from '../../ast'; +/** + * An enum describing the names of relationship mutations, + * for node and relationship type fields (field and type + * relation directive) + */ export const RelationshipMutation = { CREATE: 'Add', DELETE: 'Remove' }; +/** + * Given the results of augmentRelationshipTypeFields, builds or + * augments the AST definitions of the Mutation operation fields + * and any generated input or output types required for translation + */ export const augmentRelationshipMutationAPI = ({ typeName, fieldName, @@ -84,6 +93,11 @@ export const augmentRelationshipMutationAPI = ({ return [typeDefinitionMap, generatedTypeMap, operationTypeMap]; }; +/** + * Builds the AST for the input value definitions used as + * field arguments on relationship mutations for selecting + * the related nodes + */ const buildNodeSelectionArguments = ({ fromType, toType }) => { return [ buildInputValue({ @@ -111,6 +125,10 @@ const buildNodeSelectionArguments = ({ fromType, toType }) => { ]; }; +/** + * Builds the AST definitions decided and configured in + * augmentRelationshipMutationAPI + */ const buildRelationshipMutationAPI = ({ mutationAction, mutationName, @@ -155,6 +173,10 @@ const buildRelationshipMutationAPI = ({ return [operationTypeMap, generatedTypeMap]; }; +/** + * Builds the AST definition for a Mutation operation field + * of a given RelationshipMutation name + */ const buildRelationshipMutationField = ({ mutationAction, mutationName, @@ -200,6 +222,12 @@ const buildRelationshipMutationField = ({ return operationTypeMap; }; +/** + * Given the use of a relationship type field, builds the AST + * for the input value definition of the 'data' argument for its 'Add' + * relationship mutation field, which inputs a generated input object + * type for providing relationship properties + */ const buildRelationshipPropertyInputArgument = ({ outputType }) => { return buildInputValue({ name: buildName({ name: 'data' }), @@ -212,6 +240,11 @@ const buildRelationshipPropertyInputArgument = ({ outputType }) => { }); }; +/** + * Builds the AST for the relationship type property input + * object definition, used as the type of the 'data' input value + * definition built by buildRelationshipPropertyInputArgument + */ const buildRelationshipMutationPropertyInputType = ({ mutationAction, outputType, @@ -243,6 +276,10 @@ const buildRelationshipMutationPropertyInputType = ({ return generatedTypeMap; }; +/** + * Builds the AST for the input value definitions used as arguments on + * generated relationship Mutation fields of RelationshipMutation names + */ const buildRelationshipMutationArguments = ({ mutationAction, fromType, @@ -264,6 +301,11 @@ const buildRelationshipMutationArguments = ({ return fieldArguments; }; +/** + * Builds the AST definitions for directive instances used by + * generated relationship Mutation fields of RelationshipMutation + * names + */ const buildRelationshipMutationDirectives = ({ mutationAction, relationshipName, @@ -316,6 +358,10 @@ const buildRelationshipMutationDirectives = ({ return directives; }; +/** + * Builds the AST for the object type definition used for the + * output type of relationship type Mutation fields + */ const buildRelationshipMutationOutputType = ({ mutationAction, mutationOutputType, @@ -336,8 +382,7 @@ const buildRelationshipMutationOutputType = ({ }); let fields = buildNodeOutputFields({ fromType, toType }); if (mutationAction === RelationshipMutation.CREATE) { - // console.log("mutationOutputType: ", mutationOutputType); - // TODO temporary block on cypher field arguments + // TODO temporary block on cypher field arguments - needs translation test const mutationOutputFields = propertyOutputFields.map(field => { if (isCypherField({ directives: field.directives })) { return { @@ -357,6 +402,9 @@ const buildRelationshipMutationOutputType = ({ return generatedTypeMap; }; +/** + * Builds the full name value for a relationship mutation field + */ const buildRelationshipMutationName = ({ mutationAction, typeName, From 2ed9b27635d67849098a081584ac8e6bc162c039 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:35:24 -0700 Subject: [PATCH 26/37] documentation comments --- src/augment/types/temporal.js | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/augment/types/temporal.js b/src/augment/types/temporal.js index debcbe77..1b53c6f7 100644 --- a/src/augment/types/temporal.js +++ b/src/augment/types/temporal.js @@ -2,6 +2,9 @@ import { GraphQLInt, GraphQLString } from 'graphql'; import { Neo4jTypeName, buildNeo4jType } from '../types/types'; import { buildName, buildField, buildNamedType, buildInputValue } from '../ast'; +/** + * An enum describing the names of Neo4j Temporal types + */ export const TemporalType = { TIME: 'Time', DATE: 'Date', @@ -10,6 +13,9 @@ export const TemporalType = { LOCALDATETIME: 'LocalDateTime' }; +/** + * An enum describing the property names of the Neo4j Time type + */ const Neo4jTimeField = { HOUR: 'hour', MINUTE: 'minute', @@ -20,16 +26,28 @@ const Neo4jTimeField = { TIMEZONE: 'timezone' }; +/** + * An enum describing the property names of the Neo4j Date type + */ const Neo4jDateField = { YEAR: 'year', MONTH: 'month', DAY: 'day' }; -const Neo4jTypeFormatted = { +/** + * An enum describing the names of fields computed and added to the input + * and output type definitions representing non-scalar Neo4j property types + * TODO support for the Neo4j Point data type should also use this + */ +export const Neo4jTypeFormatted = { FORMATTED: 'formatted' }; +/** + * A map of the Neo4j Temporal Time type fields to their respective + * GraphQL types + */ const Neo4jTime = { [Neo4jTimeField.HOUR]: GraphQLInt.name, [Neo4jTimeField.MINUTE]: GraphQLInt.name, @@ -40,12 +58,21 @@ const Neo4jTime = { [Neo4jTimeField.TIMEZONE]: GraphQLString.name }; +/** + * A map of the Neo4j Temporal Date type fields to their respective + * GraphQL types + */ const Neo4jDate = { [Neo4jDateField.YEAR]: GraphQLInt.name, [Neo4jDateField.MONTH]: GraphQLInt.name, [Neo4jDateField.DAY]: GraphQLInt.name }; +/** + * The main export for building the GraphQL input and output type definitions + * for the Neo4j Temporal property types. Each TemporalType can be constructed + * using either or both of the Time and Date type fields. + */ export const buildTemporalTypes = ({ typeMap, config = {} }) => { config.temporal = decideTemporalConfig(config); const temporalConfig = config.temporal; @@ -109,6 +136,11 @@ export const buildTemporalTypes = ({ typeMap, config = {} }) => { return typeMap; }; +/** + * A helper function for ensuring a fine-grained temporal + * configmration, used to simplify checking it + * throughout the augmnetation process + */ const decideTemporalConfig = config => { let defaultConfig = { time: true, From e803f09c32d8597d81d451e5e059e98381846649 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:36:10 -0700 Subject: [PATCH 27/37] schema type support, documentation comments --- src/augment/types/types.js | 215 +++++++++++++++++++++++-------------- 1 file changed, 134 insertions(+), 81 deletions(-) diff --git a/src/augment/types/types.js b/src/augment/types/types.js index 10733c14..93054ade 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -12,7 +12,6 @@ import { DirectiveDefinition, getDirective } from '../directives'; -import { shouldAugmentType } from '../augment'; import { buildName, buildNamedType, @@ -25,21 +24,32 @@ import { unwrapNamedType, getFieldDefinition } from '../fields'; +import { augmentNodeType } from './node/node'; import { RelationshipDirectionField } from '../types/relationship/relationship'; +// The prefix added to the name of any type representing a managed Neo4j data type export const Neo4jTypeName = `_Neo4j`; +/** + * An enum describing Neo4j entity types, used in type predicate functions + */ export const Neo4jStructuralType = { NODE: 'Node', RELATIONSHIP: 'Relationship' }; +/** + * An enum describing the semantics of default GraphQL operation types + */ export const OperationType = { QUERY: 'Query', MUTATION: 'Mutation', SUBSCRIPTION: 'Subscription' }; +/** + * A map of the semantics of the GraphQL type system to Neo4j data types + */ // https://neo4j.com/docs/cypher-manual/current/syntax/values/#cypher-values export const Neo4jDataType = { PROPERTY: { @@ -60,43 +70,102 @@ export const Neo4jDataType = { } }; +/** + * A predicate function for identifying type definitions representing + * a Neo4j node entity + */ export const isNodeType = ({ definition }) => interpretType({ definition }) === Neo4jStructuralType.NODE; +/** + * A predicate function for identifying type definitions representing + * a Neo4j relationship entity + */ export const isRelationshipType = ({ definition }) => interpretType({ definition }) === Neo4jStructuralType.RELATIONSHIP; +/** + * A predicate function for identifying a GraphQL Object Type definition + */ export const isObjectTypeDefinition = ({ definition = {} }) => definition.kind === Kind.OBJECT_TYPE_DEFINITION; +/** + * A predicate function for identifying a GraphQL Input Object Type definition + */ export const isInputObjectTypeDefinition = ({ definition = {} }) => definition.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION; +/** + * A predicate function for identifying a GraphQL Interface Type definition + */ export const isInterfaceTypeDefinition = ({ definition = {} }) => definition.kind === Kind.INTERFACE_TYPE_DEFINITION; +/** + * A predicate function for identifying a GraphQL Union definition + */ export const isUnionTypeDefinition = ({ definition = {} }) => definition.kind === Kind.UNION_TYPE_DEFINITION; -export const isOperationTypeDefinition = ({ definition = {} }) => - isQueryTypeDefinition({ definition }) || - isMutationTypeDefinition({ definition }) || - isSubscriptionTypeDefinition({ definition }); +/** + * A predicate function for identifying a GraphQL operation type definition + */ +export const isOperationTypeDefinition = ({ + definition = {}, + operationTypeMap +}) => + isQueryTypeDefinition({ definition, operationTypeMap }) || + isMutationTypeDefinition({ definition, operationTypeMap }) || + isSubscriptionTypeDefinition({ definition, operationTypeMap }); -export const isQueryTypeDefinition = ({ definition }) => - definition.name && definition.name.value === OperationType.QUERY; +/** + * A predicate function for identifying the GraphQL Query type definition + */ +export const isQueryTypeDefinition = ({ definition, operationTypeMap }) => + definition.name && operationTypeMap[OperationType.QUERY] + ? definition.name.value === operationTypeMap[OperationType.QUERY].name.value + : false; -export const isMutationTypeDefinition = ({ definition }) => - definition.name && definition.name.value === OperationType.MUTATION; +/** + * A predicate function for identifying the GraphQL Mutation type definition + */ +export const isMutationTypeDefinition = ({ definition, operationTypeMap }) => + definition.name && operationTypeMap[OperationType.MUTATION] + ? definition.name.value === + operationTypeMap[OperationType.MUTATION].name.value + : false; -export const isSubscriptionTypeDefinition = ({ definition }) => - definition.name && definition.name.value === OperationType.SUBSCRIPTION; +/** + * A predicate function for identifying the GraphQL Subscription type definition + */ +export const isSubscriptionTypeDefinition = ({ + definition, + operationTypeMap +}) => + definition.name && operationTypeMap[OperationType.SUBSCRIPTION] + ? definition.name.value === + operationTypeMap[OperationType.SUBSCRIPTION].name.value + : false; +/** + * A predicate function for identifying a GraphQL type definition representing + * complex Neo4j property types (Temporal, Spatial) managed by the translation process + */ +export const isNeo4jPropertyType = ({ type }) => isNeo4jTemporalType({ type }); + +/** + * A predicate function for identifying a GraphQL type definition representing + * a Neo4j Temporal type (Time, Date, DateTime, LocalTime, LocalDateTime) + * with a name that has already been transformed ('_Neo4j' prefix added) + */ export const isNeo4jTemporalType = ({ type }) => Object.values(TemporalType).some(name => type === `${Neo4jTypeName}${name}`); -export const isNeo4jPropertyType = ({ type }) => isNeo4jTemporalType({ type }); - +/** + * A predicate function for identifying which Neo4j entity type, if any, a given + * GraphQL type definition represents + */ export const interpretType = ({ definition = {} }) => { const kind = definition.kind; // Get the structural types allows for this definition kind @@ -132,7 +201,48 @@ export const interpretType = ({ definition = {} }) => { return neo4jType; }; -export const buildNeo4jTypes = ({ generatedTypeMap, config }) => { +/** + * The main export for the augmentation process over prepared maps of + * GraphQL type definitions + */ +export const augmentTypes = ({ + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + operationTypeMap = {}, + config = {} +}) => { + Object.entries({ + ...typeDefinitionMap, + ...operationTypeMap + }).forEach(([typeName, definition]) => { + if (isNodeType({ definition })) { + [definition, generatedTypeMap, operationTypeMap] = augmentNodeType({ + typeName, + definition, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + generatedTypeMap[typeName] = definition; + } else { + generatedTypeMap[typeName] = definition; + } + return definition; + }); + generatedTypeMap = buildNeo4jTypes({ + generatedTypeMap, + config + }); + return [typeExtensionDefinitionMap, generatedTypeMap, operationTypeMap]; +}; + +/** + * Builds the GraphQL AST type definitions that represent complex Neo4j + * property types (Temporal, Spatial) managed by the translation process + */ +const buildNeo4jTypes = ({ generatedTypeMap, config }) => { generatedTypeMap = buildTemporalTypes({ typeMap: generatedTypeMap, config @@ -140,6 +250,11 @@ export const buildNeo4jTypes = ({ generatedTypeMap, config }) => { return generatedTypeMap; }; +/** + * Builds a GraphQL Object type and Input Object type representing + * the input and output schema for a complex Neo4j property type, + * ex: _Neo4jTime, _Neo4jTimeInput, etc. + */ export const buildNeo4jType = ({ inputTypeName, inputFields, @@ -158,6 +273,11 @@ export const buildNeo4jType = ({ return typeMap; }; +/** + * Applies the Neo4jTypeName prefix to any Field or Input Value definition + * with a type representing a complex Neo4j property type, to align with the + * type names expected by the translation process + */ export const transformNeo4jTypes = ({ definitions = [], config }) => { const inputTypeSuffix = `Input`; return visit(definitions, { @@ -197,70 +317,3 @@ export const transformNeo4jTypes = ({ definitions = [], config }) => { } }); }; - -export const initializeOperationTypes = ({ - typeDefinitionMap, - config = {} -}) => { - const types = Object.keys(typeDefinitionMap); - const queryTypeName = OperationType.QUERY; - const queryTypeNameLower = queryTypeName.toLowerCase(); - const mutationTypeName = OperationType.MUTATION; - const mutationTypeNameLower = mutationTypeName.toLowerCase(); - const subscriptionTypeName = OperationType.SUBSCRIPTION; - let queryType = typeDefinitionMap[queryTypeName]; - let mutationType = typeDefinitionMap[mutationTypeName]; - let subscriptionType = typeDefinitionMap[subscriptionTypeName]; - if ( - hasNonExcludedNodeType( - types, - typeDefinitionMap, - queryTypeNameLower, - config - ) && - !queryType && - config.query - ) { - queryType = buildObjectType({ - name: buildName({ name: queryTypeName }) - }); - } - if ( - hasNonExcludedNodeType( - types, - typeDefinitionMap, - mutationTypeNameLower, - config - ) && - !mutationType && - config.mutation - ) { - mutationType = buildObjectType({ - name: buildName({ name: mutationTypeName }) - }); - } - const operationTypeMap = {}; - if (queryType) { - operationTypeMap[OperationType.QUERY] = queryType; - } - if (mutationType) { - operationTypeMap[OperationType.MUTATION] = mutationType; - } - if (subscriptionType) { - operationTypeMap[OperationType.SUBSCRIPTION] = subscriptionType; - } - return [typeDefinitionMap, operationTypeMap]; -}; - -const hasNonExcludedNodeType = (types, typeMap, rootType, config) => { - return types.find(e => { - const type = typeMap[e]; - const typeName = type.name ? type.name.value : ''; - if (typeName) { - return ( - isNodeType({ definition: type }) && - shouldAugmentType(config, rootType, typeName) - ); - } - }); -}; From 35baa0451161023ebf204db5bb4c12cd789fb8cd Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:37:17 -0700 Subject: [PATCH 28/37] schema type support, subscription persistence --- test/helpers/testSchema.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index ebbb42ed..8284b56c 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -1,5 +1,8 @@ -export const testSchema = ` - type Movie @additionalLabels(labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"]) { +export const testSchema = /* GraphQL */ ` + type Movie + @additionalLabels( + labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] + ) { _id: String movieId: ID! title: String @isAuthenticated @@ -158,7 +161,7 @@ export const testSchema = ` name_asc } - type Query { + type QueryA { Movie( _id: String movieId: ID @@ -204,7 +207,7 @@ export const testSchema = ` @cypher(statement: "RETURN $strInputArg.strArg") } - type Mutation { + type MutationB { currentUserId: String @cypher(statement: "RETURN $cypherParams.currentUserId") computedObjectWithCypherParams: currentUserId @@ -219,6 +222,7 @@ export const testSchema = ` ) customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") + testPublish: Boolean @neo4j_ignore } type currentUserId { @@ -262,4 +266,14 @@ export const testSchema = ` user admin } + + type SubscriptionC { + testSubscribe: Boolean + } + + schema { + query: QueryA + mutation: MutationB + subscription: SubscriptionC + } `; From 72c7f4fa35353f63ce2eb5b02d0550bcdb812d1e Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:43:59 -0700 Subject: [PATCH 29/37] prints directives, persists schema and subscription type --- test/unit/augmentSchemaTest.test.js | 2042 ++++++++++++++++----------- 1 file changed, 1222 insertions(+), 820 deletions(-) diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 7a218245..8ddae4e1 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -1,347 +1,195 @@ import test from 'ava'; -import { printSchema, parse, print } from 'graphql'; -import { makeExecutableSchema } from 'graphql-tools'; +import { parse, print } from 'graphql'; +import { printSchemaDocument } from '../../src/augment/augment'; import { - augmentedSchema, - mapDefinitions, - mergeDefinitionMaps -} from '../../src/augment/augment'; -import { buildDocument } from '../../src/augment/ast'; + makeAugmentedSchema, +} from '../../src/index'; import { testSchema } from '../helpers/testSchema'; -import { augmentDirectiveDefinitions } from '../../src/augment/directives'; +import { Kind } from 'graphql/language'; test.cb('Test augmented schema', t => { - let [ - typeDefinitionMap, - typeExtensionDefinitionMap, - directiveDefinitionMap, - operationTypeMap - ] = mapDefinitions({ - definitions: parse(testSchema).definitions - }); - [typeDefinitionMap, directiveDefinitionMap] = augmentDirectiveDefinitions({ - typeDefinitionMap, - directiveDefinitionMap, + const sourceSchema = makeAugmentedSchema({ + typeDefs: testSchema, config: { auth: true } }); - const mergedDefinitions = mergeDefinitionMaps({ - generatedTypeMap: typeDefinitionMap, - typeExtensionDefinitionMap, - operationTypeMap, - directiveDefinitionMap - }); - const documentAST = buildDocument({ - definitions: mergedDefinitions - }); - const schema = makeExecutableSchema({ - typeDefs: print(documentAST), - resolvers: { - Movie: { - customField(object, params, ctx, resolveInfo) { - return ''; - } - }, - State: { - customField(object, params, ctx, resolveInfo) { - return ''; - } - } - }, - resolverValidationOptions: { - requireResolversForResolveType: false - } - }); - const augmentedTestSchema = augmentedSchema(schema, { - auth: true - }); - const expectedSchema = `directive @cypher(statement: String) on FIELD_DEFINITION + const expectedSchema = /* GraphQL */`directive @cypher(statement: String) on FIELD_DEFINITION + + directive @relation( + name: String + direction: _RelationDirections + from: String + to: String + ) on FIELD_DEFINITION | OBJECT - directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT - directive @additionalLabels(labels: [String]) on OBJECT - - directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION - + + directive @MutationMeta( + relationship: String + from: String + to: String + ) on FIELD_DEFINITION + directive @neo4j_ignore on FIELD_DEFINITION - + directive @isAuthenticated on OBJECT | FIELD_DEFINITION - + directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION - + directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION - - input _ActorFilter { - AND: [_ActorFilter!] - OR: [_ActorFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter - } - - input _ActorInput { - userId: ID! - } - - enum _ActorOrdering { - userId_asc - userId_desc - name_asc - name_desc - _id_asc - _id_desc - } - - type _AddActorMoviesPayload { - from: Actor - to: Movie - } - - type _AddGenreMoviesPayload { - from: Movie - to: Genre - } - - type _AddMovieActorsPayload { - from: Actor - to: Movie - } - - type _AddMovieFilmedInPayload { - from: Movie - to: State - } - - type _AddMovieGenresPayload { - from: Movie - to: Genre - } - - type _AddMovieRatingsPayload { - from: User - to: Movie - currentUserId: String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - } - - type _AddTemporalNodeTemporalNodesPayload { - from: TemporalNode - to: TemporalNode - } - - type _AddUserFavoritesPayload { - from: User - to: Movie - } - - type _AddUserFriendsPayload { - from: User - to: User - currentUserId: String - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - } - - type _AddUserRatedPayload { - from: User - to: Movie + + type QueryA { + Movie( + _id: String + movieId: ID + title: String + year: Int + released: _Neo4jDateTimeInput + plot: String + poster: String + imdbRating: Float + first: Int + offset: Int + orderBy: _MovieOrdering + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MoviesByYear( + year: Int + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MoviesByYears( + year: [Int] + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MovieById(movieId: ID!, filter: _MovieFilter): Movie + MovieBy_Id(_id: String!, filter: _MovieFilter): Movie + GenresBySubstring( + substring: String + first: Int + offset: Int + orderBy: [_GenreOrdering] + ): [Genre] + @cypher( + statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g" + ) + State( + first: Int + offset: Int + orderBy: [_StateOrdering] + filter: _StateFilter + ): [State] + User( + userId: ID + name: String + _id: String + first: Int + offset: Int + orderBy: [_UserOrdering] + filter: _UserFilter + ): [User] + Books( + first: Int + offset: Int + orderBy: [_BookOrdering] + filter: _BookFilter + ): [Book] currentUserId: String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - } - - input _BookFilter { - AND: [_BookFilter!] - OR: [_BookFilter!] - genre: BookGenre - genre_not: BookGenre - genre_in: [BookGenre!] - genre_not_in: [BookGenre!] - } - - input _BookInput { - genre: BookGenre! - } - - enum _BookOrdering { - genre_asc - genre_desc - _id_asc - _id_desc - } - - input _currentUserIdFilter { - AND: [_currentUserIdFilter!] - OR: [_currentUserIdFilter!] - userId: String - userId_not: String - userId_in: [String!] - userId_not_in: [String!] - userId_contains: String - userId_not_contains: String - userId_starts_with: String - userId_not_starts_with: String - userId_ends_with: String - userId_not_ends_with: String - } - - input _currentUserIdInput { - userId: String! - } - - enum _currentUserIdOrdering { - userId_asc - userId_desc - _id_asc - _id_desc - } - - input _FriendOfDirectionsFilter { - from: _FriendOfFilter - to: _FriendOfFilter - } - - input _FriendOfFilter { - AND: [_FriendOfFilter!] - OR: [_FriendOfFilter!] - since: Int - since_not: Int - since_in: [Int!] - since_not_in: [Int!] - since_lt: Int - since_lte: Int - since_gt: Int - since_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter - } - - input _FriendOfInput { - since: Int - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetimes: [_Neo4jDateTimeInput] - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - } - - input _GenreFilter { - AND: [_GenreFilter!] - OR: [_GenreFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter + @cypher(statement: "RETURN $cypherParams.currentUserId AS currentUserId") + computedBoolean: Boolean @cypher(statement: "RETURN true") + computedFloat: Float @cypher(statement: "RETURN 3.14") + computedInt: Int @cypher(statement: "RETURN 1") + computedIntList: [Int] + @cypher(statement: "UNWIND [1, 2, 3] AS intList RETURN intList") + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + computedTemporal: _Neo4jDateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + Genre( + _id: String + name: String + first: Int + offset: Int + orderBy: [_GenreOrdering] + filter: _GenreFilter + orderBy: [_GenreOrdering] + filter: _GenreFilter + ): [Genre] @hasScope(scopes: ["Genre: Read"]) + Actor( + userId: ID + name: String + _id: String + first: Int + offset: Int + orderBy: [_ActorOrdering] + filter: _ActorFilter + orderBy: [_ActorOrdering] + filter: _ActorFilter + ): [Actor] @hasScope(scopes: ["Actor: Read"]) + Book( + genre: BookGenre + _id: String + first: Int + offset: Int + orderBy: [_BookOrdering] + filter: _BookFilter + orderBy: [_BookOrdering] + filter: _BookFilter + ): [Book] @hasScope(scopes: ["Book: Read"]) + TemporalNode( + datetime: _Neo4jDateTimeInput + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: _Neo4jLocalDateTimeInput + computedTimestamp: String + _id: String + first: Int + offset: Int + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + ): [TemporalNode] @hasScope(scopes: ["TemporalNode: Read"]) } - - input _GenreInput { - name: String! + + input _Neo4jDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String } - - enum _GenreOrdering { - name_desc - name_asc + + enum _MovieOrdering { + title_desc + title_asc } - + input _MovieFilter { AND: [_MovieFilter!] OR: [_MovieFilter!] @@ -456,19 +304,225 @@ test.cb('Test augmented schema', t => { ratings_single: _MovieRatedFilter ratings_every: _MovieRatedFilter } - - input _MovieInput { - movieId: ID! + + input _GenreFilter { + AND: [_GenreFilter!] + OR: [_GenreFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter } - - enum _MovieOrdering { - title_desc - title_asc + + input _ActorFilter { + AND: [_ActorFilter!] + OR: [_ActorFilter!] + userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter + } + + input _StateFilter { + AND: [_StateFilter!] + OR: [_StateFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + } + + input _MovieRatedFilter { + AND: [_MovieRatedFilter!] + OR: [_MovieRatedFilter!] + rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter + } + + input _Neo4jTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + input _Neo4jDateInput { + year: Int + month: Int + day: Int + formatted: String + } + + input _Neo4jLocalTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _Neo4jLocalDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _UserFilter { + AND: [_UserFilter!] + OR: [_UserFilter!] + userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + rated: _UserRatedFilter + rated_not: _UserRatedFilter + rated_in: [_UserRatedFilter!] + rated_not_in: [_UserRatedFilter!] + rated_some: _UserRatedFilter + rated_none: _UserRatedFilter + rated_single: _UserRatedFilter + rated_every: _UserRatedFilter + friends: _FriendOfDirectionsFilter + friends_not: _FriendOfDirectionsFilter + friends_in: [_FriendOfDirectionsFilter!] + friends_not_in: [_FriendOfDirectionsFilter!] + friends_some: _FriendOfDirectionsFilter + friends_none: _FriendOfDirectionsFilter + friends_single: _FriendOfDirectionsFilter + friends_every: _FriendOfDirectionsFilter + favorites: _MovieFilter + favorites_not: _MovieFilter + favorites_in: [_MovieFilter!] + favorites_not_in: [_MovieFilter!] + favorites_some: _MovieFilter + favorites_none: _MovieFilter + favorites_single: _MovieFilter + favorites_every: _MovieFilter } - - input _MovieRatedFilter { - AND: [_MovieRatedFilter!] - OR: [_MovieRatedFilter!] + + input _UserRatedFilter { + AND: [_UserRatedFilter!] + OR: [_UserRatedFilter!] rating: Int rating_not: Int rating_in: [Int!] @@ -517,36 +571,137 @@ test.cb('Test augmented schema', t => { localdatetime_lte: _Neo4jLocalDateTimeInput localdatetime_gt: _Neo4jLocalDateTimeInput localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter + Movie: _MovieFilter } - - type _MovieRatings { - currentUserId(strArg: String): String - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - User: User + + input _FriendOfDirectionsFilter { + from: _FriendOfFilter + to: _FriendOfFilter } - - type _Neo4jDate { - year: Int - month: Int - day: Int - formatted: String + + input _FriendOfFilter { + AND: [_FriendOfFilter!] + OR: [_FriendOfFilter!] + since: Int + since_not: Int + since_in: [Int!] + since_not_in: [Int!] + since_lt: Int + since_lte: Int + since_gt: Int + since_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter } - - input _Neo4jDateInput { + + type Movie + @additionalLabels( + labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] + ) { + _id: String + movieId: ID! + title: String @isAuthenticated + someprefix_title_with_underscores: String year: Int - month: Int - day: Int - formatted: String + released: _Neo4jDateTime! + plot: String + poster: String + imdbRating: Float + genres( + first: Int + offset: Int + orderBy: [_GenreOrdering] + filter: _GenreFilter + ): [Genre] @relation(name: "IN_GENRE", direction: "OUT") + similar( + first: Int = 3 + offset: Int = 0 + orderBy: [_MovieOrdering] + ): [Movie] + @cypher( + statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o" + ) + mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this") + degree: Int + @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())") + actors( + first: Int = 3 + offset: Int = 0 + name: String + names: [String] + orderBy: [_ActorOrdering] + filter: _ActorFilter + ): [Actor] @relation(name: "ACTED_IN", direction: "IN") + avgStars: Float + filmedIn(filter: _StateFilter): State + @relation(name: "FILMED_IN", direction: "OUT") + scaleRating(scale: Int = 3): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + scaleRatingFloat(scale: Float = 1.5): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] + @cypher( + statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other" + ) + ratings( + rating: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _MovieRatedFilter + ): [_MovieRatings] + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTime] + customField: String @neo4j_ignore + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) } - + type _Neo4jDateTime { year: Int month: Int @@ -560,47 +715,93 @@ test.cb('Test augmented schema', t => { timezone: String formatted: String } - - input _Neo4jDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String + + enum _GenreOrdering { + name_desc + name_asc } - - type _Neo4jLocalDateTime { - year: Int - month: Int - day: Int + + type Genre { + _id: String + name: String + movies( + first: Int = 3 + offset: Int = 0 + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "IN_GENRE", direction: "IN") + highestRatedMovie: Movie + @cypher( + statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" + ) + } + + enum _ActorOrdering { + userId_asc + userId_desc + name_asc + name_desc + _id_asc + _id_desc + } + + type Actor implements Person { + userId: ID! + name: String + movies( + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "ACTED_IN", direction: "OUT") + _id: String + } + + interface Person { + userId: ID! + name: String + } + + type State { + customField: String @neo4j_ignore + name: String! + _id: String + } + + type _MovieRatings @relation(name: "RATED", from: "User", to: "Movie") { + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + User: User + } + + type _Neo4jTime { hour: Int minute: Int second: Int millisecond: Int microsecond: Int nanosecond: Int + timezone: String formatted: String } - - input _Neo4jLocalDateTimeInput { + + type _Neo4jDate { year: Int month: Int day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int formatted: String } - + type _Neo4jLocalTime { hour: Int minute: Int @@ -610,8 +811,11 @@ test.cb('Test augmented schema', t => { nanosecond: Int formatted: String } - - input _Neo4jLocalTimeInput { + + type _Neo4jLocalDateTime { + year: Int + month: Int + day: Int hour: Int minute: Int second: Int @@ -620,121 +824,159 @@ test.cb('Test augmented schema', t => { nanosecond: Int formatted: String } - - type _Neo4jTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String + + type User implements Person { + userId: ID! + name: String + currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rated( + rating: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _UserRatedFilter + ): [_UserRated] + friends: _UserFriendsDirections + favorites( + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "FAVORITED", direction: "OUT") + _id: String } - - input _Neo4jTimeInput { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String + + input strInput { + strArg: String } - - input _RatedInput { + + type _UserRated @relation(name: "RATED", from: "User", to: "Movie") { + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) rating: Int ratings: [Int] - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - datetimes: [_Neo4jDateTimeInput] - } - - enum _RelationDirections { - IN - OUT - } - - type _RemoveActorMoviesPayload { - from: Actor - to: Movie - } - - type _RemoveGenreMoviesPayload { - from: Movie - to: Genre - } - - type _RemoveMovieActorsPayload { - from: Actor - to: Movie + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + Movie: Movie } - - type _RemoveMovieFilmedInPayload { - from: Movie - to: State + + type _UserFriendsDirections + @relation(name: "FRIEND_OF", from: "User", to: "User") { + from( + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _FriendOfFilter + ): [_UserFriends] + to( + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _FriendOfFilter + ): [_UserFriends] } - - type _RemoveMovieGenresPayload { - from: Movie - to: Genre + + type _UserFriends @relation(name: "FRIEND_OF", from: "User", to: "User") { + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + User: User } - - type _RemoveMovieRatingsPayload { - from: User - to: Movie + + enum _StateOrdering { + name_asc + name_desc + _id_asc + _id_desc } - - type _RemoveTemporalNodeTemporalNodesPayload { - from: TemporalNode - to: TemporalNode + + enum _UserOrdering { + userId_asc + userId_desc + name_asc + name_desc + currentUserId_asc + currentUserId_desc + _id_asc + _id_desc } - - type _RemoveUserFavoritesPayload { - from: User - to: Movie + + enum _BookOrdering { + genre_asc + genre_desc + _id_asc + _id_desc } - - type _RemoveUserFriendsPayload { - from: User - to: User + + input _BookFilter { + AND: [_BookFilter!] + OR: [_BookFilter!] + genre: BookGenre + genre_not: BookGenre + genre_in: [BookGenre!] + genre_not_in: [BookGenre!] } - - type _RemoveUserRatedPayload { - from: User - to: Movie + + enum BookGenre { + Mystery + Science + Math } - - input _StateFilter { - AND: [_StateFilter!] - OR: [_StateFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String + + type Book { + genre: BookGenre + _id: String } - - input _StateInput { - name: String! + + type currentUserId { + userId: String + _id: String } - - enum _StateOrdering { + + enum _TemporalNodeOrdering { + datetime_asc + datetime_desc name_asc name_desc + time_asc + time_desc + date_asc + date_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + computedTimestamp_asc + computedTimestamp_desc _id_asc _id_desc } - + input _TemporalNodeFilter { AND: [_TemporalNodeFilter!] OR: [_TemporalNodeFilter!] @@ -797,113 +1039,366 @@ test.cb('Test augmented schema', t => { temporalNodes_single: _TemporalNodeFilter temporalNodes_every: _TemporalNodeFilter } - - input _TemporalNodeInput { - datetime: _Neo4jDateTimeInput! + + type TemporalNode { + datetime: _Neo4jDateTime + name: String + time: _Neo4jTime + date: _Neo4jDate + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + localdatetimes: [_Neo4jLocalDateTime] + computedTimestamp: String @cypher(statement: "RETURN toString(datetime())") + temporalNodes( + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + first: Int + offset: Int + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) + _id: String } - - enum _TemporalNodeOrdering { - datetime_asc - datetime_desc - name_asc - name_desc - time_asc - time_desc - date_asc - date_desc - localtime_asc - localtime_desc - localdatetime_asc - localdatetime_desc - computedTimestamp_asc - computedTimestamp_desc - _id_asc - _id_desc + + type MutationB { + currentUserId: String + @cypher(statement: "RETURN $cypherParams.currentUserId") + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + computedTemporal: _Neo4jDateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + testPublish: Boolean @neo4j_ignore + AddMovieGenres( + from: _MovieInput! + to: _GenreInput! + ): _AddMovieGenresPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + RemoveMovieGenres( + from: _MovieInput! + to: _GenreInput! + ): _RemoveMovieGenresPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + AddMovieActors( + from: _ActorInput! + to: _MovieInput! + ): _AddMovieActorsPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + RemoveMovieActors( + from: _ActorInput! + to: _MovieInput! + ): _RemoveMovieActorsPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + AddMovieFilmedIn( + from: _MovieInput! + to: _StateInput! + ): _AddMovieFilmedInPayload + @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") + RemoveMovieFilmedIn( + from: _MovieInput! + to: _StateInput! + ): _RemoveMovieFilmedInPayload + @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") + @hasScope(scopes: ["Movie: Delete", "State: Delete"]) + AddMovieRatings( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _AddMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + RemoveMovieRatings( + from: _UserInput! + to: _MovieInput! + ): _RemoveMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + CreateMovie( + movieId: ID + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTimeInput! + plot: String + poster: String + imdbRating: Float + avgStars: Float + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTimeInput] + ): Movie @hasScope(scopes: ["Movie: Create"]) + UpdateMovie( + movieId: ID! + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTimeInput + plot: String + poster: String + imdbRating: Float + avgStars: Float + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTimeInput] + ): Movie @hasScope(scopes: ["Movie: Update"]) + DeleteMovie(movieId: ID!): Movie @hasScope(scopes: ["Movie: Delete"]) + AddGenreMovies( + from: _MovieInput! + to: _GenreInput! + ): _AddGenreMoviesPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + RemoveGenreMovies( + from: _MovieInput! + to: _GenreInput! + ): _RemoveGenreMoviesPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) + DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) + CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) + DeleteState(name: String!): State @hasScope(scopes: ["State: Delete"]) + AddActorMovies( + from: _ActorInput! + to: _MovieInput! + ): _AddActorMoviesPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + RemoveActorMovies( + from: _ActorInput! + to: _MovieInput! + ): _RemoveActorMoviesPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + CreateActor(userId: ID, name: String): Actor + @hasScope(scopes: ["Actor: Create"]) + UpdateActor(userId: ID!, name: String): Actor + @hasScope(scopes: ["Actor: Update"]) + DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) + AddUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _AddUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + RemoveUserRated( + from: _UserInput! + to: _MovieInput! + ): _RemoveUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + AddUserFriends( + from: _UserInput! + to: _UserInput! + data: _FriendOfInput! + ): _AddUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Create", "User: Create"]) + RemoveUserFriends( + from: _UserInput! + to: _UserInput! + ): _RemoveUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Create", "User: Create"]) + AddUserFavorites( + from: _UserInput! + to: _MovieInput! + ): _AddUserFavoritesPayload + @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") + RemoveUserFavorites( + from: _UserInput! + to: _MovieInput! + ): _RemoveUserFavoritesPayload + @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + CreateUser(userId: ID, name: String): User + @hasScope(scopes: ["User: Create"]) + UpdateUser(userId: ID!, name: String): User + @hasScope(scopes: ["User: Update"]) + DeleteUser(userId: ID!): User @hasScope(scopes: ["User: Delete"]) + CreateBook(genre: BookGenre): Book @hasScope(scopes: ["Book: Create"]) + DeleteBook(genre: BookGenre!): Book @hasScope(scopes: ["Book: Delete"]) + CreatecurrentUserId(userId: String): currentUserId + @hasScope(scopes: ["currentUserId: Create"]) + DeletecurrentUserId(userId: String!): currentUserId + @hasScope(scopes: ["currentUserId: Delete"]) + AddTemporalNodeTemporalNodes( + from: _TemporalNodeInput! + to: _TemporalNodeInput! + ): _AddTemporalNodeTemporalNodesPayload + @MutationMeta( + relationship: "TEMPORAL" + from: "TemporalNode" + to: "TemporalNode" + ) + RemoveTemporalNodeTemporalNodes( + from: _TemporalNodeInput! + to: _TemporalNodeInput! + ): _RemoveTemporalNodeTemporalNodesPayload + @MutationMeta( + relationship: "TEMPORAL" + from: "TemporalNode" + to: "TemporalNode" + ) + @hasScope(scopes: ["TemporalNode: Delete", "TemporalNode: Delete"]) + CreateTemporalNode( + datetime: _Neo4jDateTimeInput + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: [_Neo4jLocalDateTimeInput] + ): TemporalNode @hasScope(scopes: ["TemporalNode: Create"]) + UpdateTemporalNode( + datetime: _Neo4jDateTimeInput! + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: [_Neo4jLocalDateTimeInput] + ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) + DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode + @hasScope(scopes: ["TemporalNode: Delete"]) } - - input _UserFilter { - AND: [_UserFilter!] - OR: [_UserFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - rated: _UserRatedFilter - rated_not: _UserRatedFilter - rated_in: [_UserRatedFilter!] - rated_not_in: [_UserRatedFilter!] - rated_some: _UserRatedFilter - rated_none: _UserRatedFilter - rated_single: _UserRatedFilter - rated_every: _UserRatedFilter - friends: _FriendOfDirectionsFilter - friends_not: _FriendOfDirectionsFilter - friends_in: [_FriendOfDirectionsFilter!] - friends_not_in: [_FriendOfDirectionsFilter!] - friends_some: _FriendOfDirectionsFilter - friends_none: _FriendOfDirectionsFilter - friends_single: _FriendOfDirectionsFilter - friends_every: _FriendOfDirectionsFilter - favorites: _MovieFilter - favorites_not: _MovieFilter - favorites_in: [_MovieFilter!] - favorites_not_in: [_MovieFilter!] - favorites_some: _MovieFilter - favorites_none: _MovieFilter - favorites_single: _MovieFilter - favorites_every: _MovieFilter + + input _MovieInput { + movieId: ID! + } + + input _GenreInput { + name: String! } - - type _UserFriends { + + type _AddMovieGenresPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _RemoveMovieGenresPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + input _ActorInput { + userId: ID! + } + + type _AddMovieActorsPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + type _RemoveMovieActorsPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + input _StateInput { + name: String! + } + + type _AddMovieFilmedInPayload + @relation(name: "FILMED_IN", from: "Movie", to: "State") { + from: Movie + to: State + } + + type _RemoveMovieFilmedInPayload + @relation(name: "FILMED_IN", from: "Movie", to: "State") { + from: Movie + to: State + } + + input _UserInput { + userId: ID! + } + + input _RatedInput { + rating: Int + ratings: [Int] + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + datetimes: [_Neo4jDateTimeInput] + } + + type _AddMovieRatingsPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie currentUserId: String - since: Int + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] time: _Neo4jTime date: _Neo4jDate datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime - User: User + datetimes: [_Neo4jDateTime] + } + + type _RemoveMovieRatingsPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + } + + type _AddGenreMoviesPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre } - - type _UserFriendsDirections { - from(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] - to(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] + + type _RemoveGenreMoviesPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre } - - input _UserInput { - userId: ID! + + type _AddActorMoviesPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie } - - enum _UserOrdering { - userId_asc - userId_desc - name_asc - name_desc - currentUserId_asc - currentUserId_desc - _id_asc - _id_desc + + type _RemoveActorMoviesPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie } - - type _UserRated { - currentUserId(strArg: String): String + + type _AddUserRatedPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) rating: Int ratings: [Int] time: _Neo4jTime @@ -912,93 +1407,32 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] - Movie: Movie } - - input _UserRatedFilter { - AND: [_UserRatedFilter!] - OR: [_UserRatedFilter!] - rating: Int - rating_not: Int - rating_in: [Int!] - rating_not_in: [Int!] - rating_lt: Int - rating_lte: Int - rating_gt: Int - rating_gte: Int + + type _RemoveUserRatedPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + } + + input _FriendOfInput { + since: Int time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput + datetimes: [_Neo4jDateTimeInput] localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - Movie: _MovieFilter - } - - type Actor implements Person { - userId: ID! - name: String - movies(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - _id: String - } - - type Book { - genre: BookGenre - _id: String - } - - enum BookGenre { - Mystery - Science - Math - } - - type currentUserId { - userId: String - _id: String } - - scalar Date - - scalar DateTime - - type FriendOf { + + type _AddUserFriendsPayload + @relation(name: "FRIEND_OF", from: "User", to: "User") { from: User + to: User currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) since: Int time: _Neo4jTime date: _Neo4jDate @@ -1006,134 +1440,64 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTime] localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime + } + + type _RemoveUserFriendsPayload + @relation(name: "FRIEND_OF", from: "User", to: "User") { + from: User to: User } - - type Genre { - _id: String - name: String - movies(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - highestRatedMovie: Movie + + type _AddUserFavoritesPayload + @relation(name: "FAVORITED", from: "User", to: "Movie") { + from: User + to: Movie } - - type ignoredType { - ignoredField: String + + type _RemoveUserFavoritesPayload + @relation(name: "FAVORITED", from: "User", to: "Movie") { + from: User + to: Movie } - - scalar LocalDateTime - - scalar LocalTime - - type Movie { - _id: String - movieId: ID! - title: String - someprefix_title_with_underscores: String - year: Int - released: _Neo4jDateTime! - plot: String - poster: String - imdbRating: Float - genres(first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] - similar(first: Int = 3, offset: Int = 0, orderBy: [_MovieOrdering]): [Movie] - mostSimilar: Movie - degree: Int - actors(first: Int = 3, offset: Int = 0, name: String, names: [String], orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] - avgStars: Float - filmedIn(filter: _StateFilter): State - scaleRating(scale: Int = 3): Float - scaleRatingFloat(scale: Float = 1.5): Float - actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] - ratings(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _MovieRatedFilter): [_MovieRatings] - years: [Int] - titles: [String] - imdbRatings: [Float] - releases: [_Neo4jDateTime] - customField: String - currentUserId(strArg: String): String + + input _TemporalNodeInput { + datetime: _Neo4jDateTimeInput! } - - type Mutation { - currentUserId: String - computedObjectWithCypherParams: currentUserId - computedTemporal: _Neo4jDateTime - computedStringList: [String] - customWithArguments(strArg: String, strInputArg: strInput): String - AddMovieGenres(from: _MovieInput!, to: _GenreInput!): _AddMovieGenresPayload - RemoveMovieGenres(from: _MovieInput!, to: _GenreInput!): _RemoveMovieGenresPayload - AddMovieActors(from: _ActorInput!, to: _MovieInput!): _AddMovieActorsPayload - RemoveMovieActors(from: _ActorInput!, to: _MovieInput!): _RemoveMovieActorsPayload - AddMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _AddMovieFilmedInPayload - RemoveMovieFilmedIn(from: _MovieInput!, to: _StateInput!): _RemoveMovieFilmedInPayload - AddMovieRatings(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddMovieRatingsPayload - RemoveMovieRatings(from: _UserInput!, to: _MovieInput!): _RemoveMovieRatingsPayload - CreateMovie(movieId: ID, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput!, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie - UpdateMovie(movieId: ID!, title: String, someprefix_title_with_underscores: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie - DeleteMovie(movieId: ID!): Movie - AddGenreMovies(from: _MovieInput!, to: _GenreInput!): _AddGenreMoviesPayload - RemoveGenreMovies(from: _MovieInput!, to: _GenreInput!): _RemoveGenreMoviesPayload - CreateGenre(name: String): Genre - DeleteGenre(name: String!): Genre - AddActorMovies(from: _ActorInput!, to: _MovieInput!): _AddActorMoviesPayload - RemoveActorMovies(from: _ActorInput!, to: _MovieInput!): _RemoveActorMoviesPayload - CreateActor(userId: ID, name: String): Actor - UpdateActor(userId: ID!, name: String): Actor - DeleteActor(userId: ID!): Actor - CreateState(name: String!): State - DeleteState(name: String!): State - AddUserRated(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddUserRatedPayload - RemoveUserRated(from: _UserInput!, to: _MovieInput!): _RemoveUserRatedPayload - AddUserFriends(from: _UserInput!, to: _UserInput!, data: _FriendOfInput!): _AddUserFriendsPayload - RemoveUserFriends(from: _UserInput!, to: _UserInput!): _RemoveUserFriendsPayload - AddUserFavorites(from: _UserInput!, to: _MovieInput!): _AddUserFavoritesPayload - RemoveUserFavorites(from: _UserInput!, to: _MovieInput!): _RemoveUserFavoritesPayload - CreateUser(userId: ID, name: String): User - UpdateUser(userId: ID!, name: String): User - DeleteUser(userId: ID!): User - CreateBook(genre: BookGenre): Book - DeleteBook(genre: BookGenre!): Book - CreatecurrentUserId(userId: String): currentUserId - DeletecurrentUserId(userId: String!): currentUserId - AddTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _AddTemporalNodeTemporalNodesPayload - RemoveTemporalNodeTemporalNodes(from: _TemporalNodeInput!, to: _TemporalNodeInput!): _RemoveTemporalNodeTemporalNodesPayload - CreateTemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode - UpdateTemporalNode(datetime: _Neo4jDateTimeInput!, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode - DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode + + type _AddTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode } - - interface Person { - userId: ID! - name: String + + type _RemoveTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode } - - type Query { - Movie(_id: String, movieId: ID, title: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MoviesByYear(year: Int, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MoviesByYears(year: [Int], first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - MovieById(movieId: ID!, filter: _MovieFilter): Movie - MovieBy_Id(_id: String!, filter: _MovieFilter): Movie - GenresBySubstring(substring: String, first: Int, offset: Int, orderBy: [_GenreOrdering]): [Genre] - State(first: Int, offset: Int, orderBy: [_StateOrdering], filter: _StateFilter): [State] - User(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_UserOrdering], filter: _UserFilter): [User] - Books(first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] + + type FriendOf @relation { + from: User currentUserId: String - computedBoolean: Boolean - computedFloat: Float - computedInt: Int - computedIntList: [Int] - computedStringList: [String] - computedTemporal: _Neo4jDateTime - computedObjectWithCypherParams: currentUserId - customWithArguments(strArg: String, strInputArg: strInput): String - Genre(_id: String, name: String, first: Int, offset: Int, orderBy: [_GenreOrdering], filter: _GenreFilter): [Genre] - Actor(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_ActorOrdering], filter: _ActorFilter): [Actor] - Book(genre: BookGenre, _id: String, first: Int, offset: Int, orderBy: [_BookOrdering], filter: _BookFilter): [Book] - TemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: _Neo4jLocalDateTimeInput, computedTimestamp: String, _id: String, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + to: User } - - type Rated { + + type Rated @relation { from: User currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) rating: Int ratings: [Int] time: _Neo4jTime @@ -1144,67 +1508,105 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTime] to: Movie } - + + input _BookInput { + genre: BookGenre! + } + + enum _currentUserIdOrdering { + userId_asc + userId_desc + _id_asc + _id_desc + } + + input _currentUserIdFilter { + AND: [_currentUserIdFilter!] + OR: [_currentUserIdFilter!] + userId: String + userId_not: String + userId_in: [String!] + userId_not_in: [String!] + userId_contains: String + userId_not_contains: String + userId_starts_with: String + userId_not_starts_with: String + userId_ends_with: String + userId_not_ends_with: String + } + + input _currentUserIdInput { + userId: String! + } + + type ignoredType { + ignoredField: String @neo4j_ignore + } + + scalar Time + + scalar Date + + scalar DateTime + + scalar LocalTime + + scalar LocalDateTime + enum Role { reader user admin } - - type State { - customField: String - name: String! - _id: String - } - - input strInput { - strArg: String + + type SubscriptionC { + testSubscribe: Boolean } - - type TemporalNode { - datetime: _Neo4jDateTime - name: String - time: _Neo4jTime - date: _Neo4jDate - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - localdatetimes: [_Neo4jLocalDateTime] - computedTimestamp: String - temporalNodes(time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering], filter: _TemporalNodeFilter): [TemporalNode] - _id: String + + enum _RelationDirections { + IN + OUT } - - scalar Time - - type User implements Person { - userId: ID! - name: String - currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String - rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _UserRatedFilter): [_UserRated] - friends: _UserFriendsDirections - favorites(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] - _id: String + + schema { + query: QueryA + mutation: MutationB + subscription: SubscriptionC } `; - - compareSchemas({ + compareSchema({ test: t, - targetSchema: expectedSchema, - sourceSchema: augmentedTestSchema + sourceSchema, + expectedSchema }); t.end(); }); -const compareSchemas = ({ test, sourceSchema = {}, targetSchema = {} }) => { - const definitions = parse(targetSchema).definitions; - const augmentedDefinitions = parse(printSchema(sourceSchema)).definitions; +const compareSchema = ({ test, sourceSchema = {}, expectedSchema = {} }) => { + const definitions = parse(expectedSchema).definitions; + // printSchema is no longer used here, as it simplifies out the schema type and all + // directive instances. printSchemaDocument does not simplify anything out, as it uses + // the graphql print function instead, along with the regeneration of the schema type + const printedSourceSchema = printSchemaDocument({ schema: sourceSchema }); + const augmentedDefinitions = parse(printedSourceSchema).definitions; definitions.forEach(definition => { - const name = definition.name.value; - const augmented = augmentedDefinitions.find( - augmented => name === augmented.name.value - ); - if (!augmented) { - throw new Error(`${name} is missing from the augmented schema`); + const kind = definition.kind; + let augmented = undefined; + if (kind === Kind.SCHEMA_DEFINITION) { + augmented = augmentedDefinitions.find( + def => def.kind === Kind.SCHEMA_DEFINITION + ); + } else { + augmented = augmentedDefinitions.find(augmentedDefinition => { + if (augmentedDefinition.name) { + if (definition.name.value === augmentedDefinition.name.value) { + return augmentedDefinition; + } + } + }); + if (!augmented) { + throw new Error(`${name} is missing from the augmented schema`); + } } test.is(print(augmented), print(definition)); }); From 622274b6e507cca9b34e40c79bc07f34ef43e865 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:45:43 -0700 Subject: [PATCH 30/37] non-default operation type names --- test/helpers/cypherTestHelpers.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 527650ba..951c0718 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -19,7 +19,7 @@ export function cypherTestRunner( const testMovieSchema = testSchema + ` -type Mutation { +type MutationB { CreateGenre(name: String): Genre @cypher(statement: "CREATE (g:Genre) SET g.name = $name RETURN g") CreateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie CreateState(name: String!): State @@ -47,7 +47,7 @@ type Mutation { }; const resolvers = { - Query: { + QueryA: { User: checkCypherQuery, Movie: checkCypherQuery, MoviesByYear: checkCypherQuery, @@ -67,7 +67,7 @@ type Mutation { computedIntList: checkCypherQuery, customWithArguments: checkCypherQuery }, - Mutation: { + MutationB: { CreateGenre: checkCypherMutation, CreateMovie: checkCypherMutation, CreateState: checkCypherMutation, @@ -146,7 +146,7 @@ export function augmentedSchemaCypherTestRunner( }; const resolvers = { - Query: { + QueryA: { User: checkCypherQuery, Movie: checkCypherQuery, MoviesByYear: checkCypherQuery, @@ -176,7 +176,7 @@ export function augmentedSchemaCypherTestRunner( computedIntList: checkCypherQuery, customWithArguments: checkCypherQuery }, - Mutation: { + MutationB: { CreateMovie: checkCypherMutation, CreateState: checkCypherMutation, CreateTemporalNode: checkCypherMutation, From 29af6a0ccb0cc550f484344aebe979d1a50568f9 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:47:38 -0700 Subject: [PATCH 31/37] small refactor --- src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index f2772635..fc4ceb43 100644 --- a/src/index.js +++ b/src/index.js @@ -13,11 +13,10 @@ import { import { augmentedSchema, makeAugmentedExecutableSchema, - augmentTypes, mapDefinitions, mergeDefinitionMaps } from './augment/augment'; -import { transformNeo4jTypes } from './augment/types/types'; +import { augmentTypes, transformNeo4jTypes } from './augment/types/types'; import { buildDocument } from './augment/ast'; import { augmentDirectiveDefinitions } from './augment/directives'; From e6d6bd32b0bac1e0f01fc5ca37ab98e1993b4905 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:02:09 -0700 Subject: [PATCH 32/37] merge resolution --- src/translate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translate.js b/src/translate.js index 342b55b6..66668ce8 100644 --- a/src/translate.js +++ b/src/translate.js @@ -2524,7 +2524,7 @@ const buildRelatedTypeListComprehension = ({ if (rootIsRelationType) { relationVariable = variableName; } - const thisTypeVariable = safeVar(thisType.toLowerCase()); + const thisTypeVariable = safeVar(lowFirstLetter(thisType)); // prevents related node variable from // conflicting with parent variables const relatedTypeVariable = safeVar(`_${relatedType.toLowerCase()}`); From c370b0a8d46a869d815febb689a05aa1ee554f06 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:02:48 -0700 Subject: [PATCH 33/37] CasedType inclusion for merge resolution --- test/helpers/testSchema.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 8284b56c..29a18003 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -5,7 +5,7 @@ export const testSchema = /* GraphQL */ ` ) { _id: String movieId: ID! - title: String @isAuthenticated + title: String someprefix_title_with_underscores: String year: Int released: DateTime! @@ -205,6 +205,7 @@ export const testSchema = /* GraphQL */ ` @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") + CasedType: [CasedType] } type MutationB { @@ -267,6 +268,11 @@ export const testSchema = /* GraphQL */ ` admin } + type CasedType { + name: String + state: State @relation(name: "FILMED_IN", direction: "OUT") + } + type SubscriptionC { testSubscribe: Boolean } From 16246f39a39f7a1075fa3265811e2d6d55a5b5fc Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:03:18 -0700 Subject: [PATCH 34/37] CasedType resolver for merge resolution --- test/helpers/cypherTestHelpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 951c0718..752b6647 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -166,6 +166,7 @@ export function augmentedSchemaCypherTestRunner( t.deepEqual(queryParams, expectedCypherParams); }, State: checkCypherQuery, + CasedType: checkCypherQuery, computedBoolean: checkCypherQuery, computedInt: checkCypherQuery, computedFloat: checkCypherQuery, From 8d148bac0c7d65a1dc4c1ddf04718ff6b16a3384 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:27:48 -0700 Subject: [PATCH 35/37] merge conflict and formatting fix --- test/unit/augmentSchemaTest.test.js | 3086 ++++++++++++++------------- 1 file changed, 1579 insertions(+), 1507 deletions(-) diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 8ddae4e1..deb5e39d 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -1,9 +1,7 @@ import test from 'ava'; import { parse, print } from 'graphql'; import { printSchemaDocument } from '../../src/augment/augment'; -import { - makeAugmentedSchema, -} from '../../src/index'; +import { makeAugmentedSchema } from '../../src/index'; import { testSchema } from '../helpers/testSchema'; import { Kind } from 'graphql/language'; @@ -15,1564 +13,1638 @@ test.cb('Test augmented schema', t => { } }); - const expectedSchema = /* GraphQL */`directive @cypher(statement: String) on FIELD_DEFINITION + const expectedSchema = /* GraphQL */ ` + directive @cypher(statement: String) on FIELD_DEFINITION - directive @relation( - name: String - direction: _RelationDirections - from: String - to: String - ) on FIELD_DEFINITION | OBJECT - - directive @additionalLabels(labels: [String]) on OBJECT - - directive @MutationMeta( - relationship: String - from: String - to: String - ) on FIELD_DEFINITION - - directive @neo4j_ignore on FIELD_DEFINITION - - directive @isAuthenticated on OBJECT | FIELD_DEFINITION + directive @relation( + name: String + direction: _RelationDirections + from: String + to: String + ) on FIELD_DEFINITION | OBJECT + + directive @additionalLabels(labels: [String]) on OBJECT + + directive @MutationMeta( + relationship: String + from: String + to: String + ) on FIELD_DEFINITION + + directive @neo4j_ignore on FIELD_DEFINITION + + directive @isAuthenticated on OBJECT | FIELD_DEFINITION + + directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION + + directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION + + type QueryA { + Movie( + _id: String + movieId: ID + title: String + year: Int + released: _Neo4jDateTimeInput + plot: String + poster: String + imdbRating: Float + first: Int + offset: Int + orderBy: _MovieOrdering + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MoviesByYear( + year: Int + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MoviesByYears( + year: [Int] + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] + MovieById(movieId: ID!, filter: _MovieFilter): Movie + MovieBy_Id(_id: String!, filter: _MovieFilter): Movie + GenresBySubstring( + substring: String + first: Int + offset: Int + orderBy: [_GenreOrdering] + ): [Genre] + @cypher( + statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g" + ) + State( + first: Int + offset: Int + orderBy: [_StateOrdering] + filter: _StateFilter + ): [State] + User( + userId: ID + name: String + _id: String + first: Int + offset: Int + orderBy: [_UserOrdering] + filter: _UserFilter + ): [User] + Books( + first: Int + offset: Int + orderBy: [_BookOrdering] + filter: _BookFilter + ): [Book] + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS currentUserId" + ) + computedBoolean: Boolean @cypher(statement: "RETURN true") + computedFloat: Float @cypher(statement: "RETURN 3.14") + computedInt: Int @cypher(statement: "RETURN 1") + computedIntList: [Int] + @cypher(statement: "UNWIND [1, 2, 3] AS intList RETURN intList") + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + computedTemporal: _Neo4jDateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + CasedType( + first: Int + offset: Int + orderBy: [_CasedTypeOrdering] + filter: _CasedTypeFilter + ): [CasedType] + Genre( + _id: String + name: String + first: Int + offset: Int + orderBy: [_GenreOrdering] + filter: _GenreFilter + orderBy: [_GenreOrdering] + filter: _GenreFilter + ): [Genre] @hasScope(scopes: ["Genre: Read"]) + Actor( + userId: ID + name: String + _id: String + first: Int + offset: Int + orderBy: [_ActorOrdering] + filter: _ActorFilter + orderBy: [_ActorOrdering] + filter: _ActorFilter + ): [Actor] @hasScope(scopes: ["Actor: Read"]) + Book( + genre: BookGenre + _id: String + first: Int + offset: Int + orderBy: [_BookOrdering] + filter: _BookFilter + orderBy: [_BookOrdering] + filter: _BookFilter + ): [Book] @hasScope(scopes: ["Book: Read"]) + TemporalNode( + datetime: _Neo4jDateTimeInput + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: _Neo4jLocalDateTimeInput + computedTimestamp: String + _id: String + first: Int + offset: Int + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + ): [TemporalNode] @hasScope(scopes: ["TemporalNode: Read"]) + } - directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION + input _Neo4jDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } - directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION + enum _MovieOrdering { + title_desc + title_asc + } - type QueryA { - Movie( - _id: String + input _MovieFilter { + AND: [_MovieFilter!] + OR: [_MovieFilter!] movieId: ID + movieId_not: ID + movieId_in: [ID!] + movieId_not_in: [ID!] + movieId_contains: ID + movieId_not_contains: ID + movieId_starts_with: ID + movieId_not_starts_with: ID + movieId_ends_with: ID + movieId_not_ends_with: ID title: String + title_not: String + title_in: [String!] + title_not_in: [String!] + title_contains: String + title_not_contains: String + title_starts_with: String + title_not_starts_with: String + title_ends_with: String + title_not_ends_with: String + someprefix_title_with_underscores: String + someprefix_title_with_underscores_not: String + someprefix_title_with_underscores_in: [String!] + someprefix_title_with_underscores_not_in: [String!] + someprefix_title_with_underscores_contains: String + someprefix_title_with_underscores_not_contains: String + someprefix_title_with_underscores_starts_with: String + someprefix_title_with_underscores_not_starts_with: String + someprefix_title_with_underscores_ends_with: String + someprefix_title_with_underscores_not_ends_with: String year: Int + year_not: Int + year_in: [Int!] + year_not_in: [Int!] + year_lt: Int + year_lte: Int + year_gt: Int + year_gte: Int released: _Neo4jDateTimeInput + released_not: _Neo4jDateTimeInput + released_in: [_Neo4jDateTimeInput!] + released_not_in: [_Neo4jDateTimeInput!] + released_lt: _Neo4jDateTimeInput + released_lte: _Neo4jDateTimeInput + released_gt: _Neo4jDateTimeInput + released_gte: _Neo4jDateTimeInput plot: String + plot_not: String + plot_in: [String!] + plot_not_in: [String!] + plot_contains: String + plot_not_contains: String + plot_starts_with: String + plot_not_starts_with: String + plot_ends_with: String + plot_not_ends_with: String poster: String + poster_not: String + poster_in: [String!] + poster_not_in: [String!] + poster_contains: String + poster_not_contains: String + poster_starts_with: String + poster_not_starts_with: String + poster_ends_with: String + poster_not_ends_with: String imdbRating: Float - first: Int - offset: Int - orderBy: _MovieOrdering - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] - MoviesByYear( - year: Int - first: Int - offset: Int - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] - MoviesByYears( - year: [Int] - first: Int - offset: Int - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] - MovieById(movieId: ID!, filter: _MovieFilter): Movie - MovieBy_Id(_id: String!, filter: _MovieFilter): Movie - GenresBySubstring( - substring: String - first: Int - offset: Int - orderBy: [_GenreOrdering] - ): [Genre] - @cypher( - statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g" - ) - State( - first: Int - offset: Int - orderBy: [_StateOrdering] - filter: _StateFilter - ): [State] - User( - userId: ID - name: String - _id: String - first: Int - offset: Int - orderBy: [_UserOrdering] - filter: _UserFilter - ): [User] - Books( - first: Int - offset: Int - orderBy: [_BookOrdering] - filter: _BookFilter - ): [Book] - currentUserId: String - @cypher(statement: "RETURN $cypherParams.currentUserId AS currentUserId") - computedBoolean: Boolean @cypher(statement: "RETURN true") - computedFloat: Float @cypher(statement: "RETURN 3.14") - computedInt: Int @cypher(statement: "RETURN 1") - computedIntList: [Int] - @cypher(statement: "UNWIND [1, 2, 3] AS intList RETURN intList") - computedStringList: [String] - @cypher( - statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" - ) - computedTemporal: _Neo4jDateTime - @cypher( - statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" - ) - computedObjectWithCypherParams: currentUserId - @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") - customWithArguments(strArg: String, strInputArg: strInput): String - @cypher(statement: "RETURN $strInputArg.strArg") - Genre( - _id: String + imdbRating_not: Float + imdbRating_in: [Float!] + imdbRating_not_in: [Float!] + imdbRating_lt: Float + imdbRating_lte: Float + imdbRating_gt: Float + imdbRating_gte: Float + genres: _GenreFilter + genres_not: _GenreFilter + genres_in: [_GenreFilter!] + genres_not_in: [_GenreFilter!] + genres_some: _GenreFilter + genres_none: _GenreFilter + genres_single: _GenreFilter + genres_every: _GenreFilter + actors: _ActorFilter + actors_not: _ActorFilter + actors_in: [_ActorFilter!] + actors_not_in: [_ActorFilter!] + actors_some: _ActorFilter + actors_none: _ActorFilter + actors_single: _ActorFilter + actors_every: _ActorFilter + avgStars: Float + avgStars_not: Float + avgStars_in: [Float!] + avgStars_not_in: [Float!] + avgStars_lt: Float + avgStars_lte: Float + avgStars_gt: Float + avgStars_gte: Float + filmedIn: _StateFilter + filmedIn_not: _StateFilter + filmedIn_in: [_StateFilter!] + filmedIn_not_in: [_StateFilter!] + ratings: _MovieRatedFilter + ratings_not: _MovieRatedFilter + ratings_in: [_MovieRatedFilter!] + ratings_not_in: [_MovieRatedFilter!] + ratings_some: _MovieRatedFilter + ratings_none: _MovieRatedFilter + ratings_single: _MovieRatedFilter + ratings_every: _MovieRatedFilter + } + + input _GenreFilter { + AND: [_GenreFilter!] + OR: [_GenreFilter!] name: String - first: Int - offset: Int - orderBy: [_GenreOrdering] - filter: _GenreFilter - orderBy: [_GenreOrdering] - filter: _GenreFilter - ): [Genre] @hasScope(scopes: ["Genre: Read"]) - Actor( + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter + } + + input _ActorFilter { + AND: [_ActorFilter!] + OR: [_ActorFilter!] userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID name: String - _id: String - first: Int - offset: Int - orderBy: [_ActorOrdering] - filter: _ActorFilter - orderBy: [_ActorOrdering] - filter: _ActorFilter - ): [Actor] @hasScope(scopes: ["Actor: Read"]) - Book( - genre: BookGenre - _id: String - first: Int - offset: Int - orderBy: [_BookOrdering] - filter: _BookFilter - orderBy: [_BookOrdering] - filter: _BookFilter - ): [Book] @hasScope(scopes: ["Book: Read"]) - TemporalNode( - datetime: _Neo4jDateTimeInput - name: String - time: _Neo4jTimeInput - date: _Neo4jDateInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetimes: _Neo4jLocalDateTimeInput - computedTimestamp: String - _id: String - first: Int - offset: Int - orderBy: [_TemporalNodeOrdering] - filter: _TemporalNodeFilter - orderBy: [_TemporalNodeOrdering] - filter: _TemporalNodeFilter - ): [TemporalNode] @hasScope(scopes: ["TemporalNode: Read"]) - } - - input _Neo4jDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - - enum _MovieOrdering { - title_desc - title_asc - } - - input _MovieFilter { - AND: [_MovieFilter!] - OR: [_MovieFilter!] - movieId: ID - movieId_not: ID - movieId_in: [ID!] - movieId_not_in: [ID!] - movieId_contains: ID - movieId_not_contains: ID - movieId_starts_with: ID - movieId_not_starts_with: ID - movieId_ends_with: ID - movieId_not_ends_with: ID - title: String - title_not: String - title_in: [String!] - title_not_in: [String!] - title_contains: String - title_not_contains: String - title_starts_with: String - title_not_starts_with: String - title_ends_with: String - title_not_ends_with: String - someprefix_title_with_underscores: String - someprefix_title_with_underscores_not: String - someprefix_title_with_underscores_in: [String!] - someprefix_title_with_underscores_not_in: [String!] - someprefix_title_with_underscores_contains: String - someprefix_title_with_underscores_not_contains: String - someprefix_title_with_underscores_starts_with: String - someprefix_title_with_underscores_not_starts_with: String - someprefix_title_with_underscores_ends_with: String - someprefix_title_with_underscores_not_ends_with: String - year: Int - year_not: Int - year_in: [Int!] - year_not_in: [Int!] - year_lt: Int - year_lte: Int - year_gt: Int - year_gte: Int - released: _Neo4jDateTimeInput - released_not: _Neo4jDateTimeInput - released_in: [_Neo4jDateTimeInput!] - released_not_in: [_Neo4jDateTimeInput!] - released_lt: _Neo4jDateTimeInput - released_lte: _Neo4jDateTimeInput - released_gt: _Neo4jDateTimeInput - released_gte: _Neo4jDateTimeInput - plot: String - plot_not: String - plot_in: [String!] - plot_not_in: [String!] - plot_contains: String - plot_not_contains: String - plot_starts_with: String - plot_not_starts_with: String - plot_ends_with: String - plot_not_ends_with: String - poster: String - poster_not: String - poster_in: [String!] - poster_not_in: [String!] - poster_contains: String - poster_not_contains: String - poster_starts_with: String - poster_not_starts_with: String - poster_ends_with: String - poster_not_ends_with: String - imdbRating: Float - imdbRating_not: Float - imdbRating_in: [Float!] - imdbRating_not_in: [Float!] - imdbRating_lt: Float - imdbRating_lte: Float - imdbRating_gt: Float - imdbRating_gte: Float - genres: _GenreFilter - genres_not: _GenreFilter - genres_in: [_GenreFilter!] - genres_not_in: [_GenreFilter!] - genres_some: _GenreFilter - genres_none: _GenreFilter - genres_single: _GenreFilter - genres_every: _GenreFilter - actors: _ActorFilter - actors_not: _ActorFilter - actors_in: [_ActorFilter!] - actors_not_in: [_ActorFilter!] - actors_some: _ActorFilter - actors_none: _ActorFilter - actors_single: _ActorFilter - actors_every: _ActorFilter - avgStars: Float - avgStars_not: Float - avgStars_in: [Float!] - avgStars_not_in: [Float!] - avgStars_lt: Float - avgStars_lte: Float - avgStars_gt: Float - avgStars_gte: Float - filmedIn: _StateFilter - filmedIn_not: _StateFilter - filmedIn_in: [_StateFilter!] - filmedIn_not_in: [_StateFilter!] - ratings: _MovieRatedFilter - ratings_not: _MovieRatedFilter - ratings_in: [_MovieRatedFilter!] - ratings_not_in: [_MovieRatedFilter!] - ratings_some: _MovieRatedFilter - ratings_none: _MovieRatedFilter - ratings_single: _MovieRatedFilter - ratings_every: _MovieRatedFilter - } - - input _GenreFilter { - AND: [_GenreFilter!] - OR: [_GenreFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter - } - - input _ActorFilter { - AND: [_ActorFilter!] - OR: [_ActorFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter - } - - input _StateFilter { - AND: [_StateFilter!] - OR: [_StateFilter!] - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - } - - input _MovieRatedFilter { - AND: [_MovieRatedFilter!] - OR: [_MovieRatedFilter!] - rating: Int - rating_not: Int - rating_in: [Int!] - rating_not_in: [Int!] - rating_lt: Int - rating_lte: Int - rating_gt: Int - rating_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter - } - - input _Neo4jTimeInput { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - - input _Neo4jDateInput { - year: Int - month: Int - day: Int - formatted: String - } - - input _Neo4jLocalTimeInput { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - - input _Neo4jLocalDateTimeInput { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - - input _UserFilter { - AND: [_UserFilter!] - OR: [_UserFilter!] - userId: ID - userId_not: ID - userId_in: [ID!] - userId_not_in: [ID!] - userId_contains: ID - userId_not_contains: ID - userId_starts_with: ID - userId_not_starts_with: ID - userId_ends_with: ID - userId_not_ends_with: ID - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - rated: _UserRatedFilter - rated_not: _UserRatedFilter - rated_in: [_UserRatedFilter!] - rated_not_in: [_UserRatedFilter!] - rated_some: _UserRatedFilter - rated_none: _UserRatedFilter - rated_single: _UserRatedFilter - rated_every: _UserRatedFilter - friends: _FriendOfDirectionsFilter - friends_not: _FriendOfDirectionsFilter - friends_in: [_FriendOfDirectionsFilter!] - friends_not_in: [_FriendOfDirectionsFilter!] - friends_some: _FriendOfDirectionsFilter - friends_none: _FriendOfDirectionsFilter - friends_single: _FriendOfDirectionsFilter - friends_every: _FriendOfDirectionsFilter - favorites: _MovieFilter - favorites_not: _MovieFilter - favorites_in: [_MovieFilter!] - favorites_not_in: [_MovieFilter!] - favorites_some: _MovieFilter - favorites_none: _MovieFilter - favorites_single: _MovieFilter - favorites_every: _MovieFilter - } - - input _UserRatedFilter { - AND: [_UserRatedFilter!] - OR: [_UserRatedFilter!] - rating: Int - rating_not: Int - rating_in: [Int!] - rating_not_in: [Int!] - rating_lt: Int - rating_lte: Int - rating_gt: Int - rating_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - Movie: _MovieFilter - } - - input _FriendOfDirectionsFilter { - from: _FriendOfFilter - to: _FriendOfFilter - } - - input _FriendOfFilter { - AND: [_FriendOfFilter!] - OR: [_FriendOfFilter!] - since: Int - since_not: Int - since_in: [Int!] - since_not_in: [Int!] - since_lt: Int - since_lte: Int - since_gt: Int - since_gte: Int - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - User: _UserFilter - } - - type Movie - @additionalLabels( - labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] - ) { - _id: String - movieId: ID! - title: String @isAuthenticated - someprefix_title_with_underscores: String - year: Int - released: _Neo4jDateTime! - plot: String - poster: String - imdbRating: Float - genres( - first: Int - offset: Int - orderBy: [_GenreOrdering] - filter: _GenreFilter - ): [Genre] @relation(name: "IN_GENRE", direction: "OUT") - similar( - first: Int = 3 - offset: Int = 0 - orderBy: [_MovieOrdering] - ): [Movie] - @cypher( - statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o" - ) - mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this") - degree: Int - @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())") - actors( - first: Int = 3 - offset: Int = 0 + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + movies: _MovieFilter + movies_not: _MovieFilter + movies_in: [_MovieFilter!] + movies_not_in: [_MovieFilter!] + movies_some: _MovieFilter + movies_none: _MovieFilter + movies_single: _MovieFilter + movies_every: _MovieFilter + } + + input _StateFilter { + AND: [_StateFilter!] + OR: [_StateFilter!] name: String - names: [String] - orderBy: [_ActorOrdering] - filter: _ActorFilter - ): [Actor] @relation(name: "ACTED_IN", direction: "IN") - avgStars: Float - filmedIn(filter: _StateFilter): State - @relation(name: "FILMED_IN", direction: "OUT") - scaleRating(scale: Int = 3): Float - @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") - scaleRatingFloat(scale: Float = 1.5): Float - @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") - actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] - @cypher( - statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other" - ) - ratings( + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + } + + input _MovieRatedFilter { + AND: [_MovieRatedFilter!] + OR: [_MovieRatedFilter!] rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - filter: _MovieRatedFilter - ): [_MovieRatings] - years: [Int] - titles: [String] - imdbRatings: [Float] - releases: [_Neo4jDateTime] - customField: String @neo4j_ignore - currentUserId(strArg: String): String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - } - - type _Neo4jDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - - enum _GenreOrdering { - name_desc - name_asc - } - - type Genre { - _id: String - name: String - movies( - first: Int = 3 - offset: Int = 0 - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] @relation(name: "IN_GENRE", direction: "IN") - highestRatedMovie: Movie - @cypher( - statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" - ) - } - - enum _ActorOrdering { - userId_asc - userId_desc - name_asc - name_desc - _id_asc - _id_desc - } - - type Actor implements Person { - userId: ID! - name: String - movies( - first: Int - offset: Int - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] @relation(name: "ACTED_IN", direction: "OUT") - _id: String - } - - interface Person { - userId: ID! - name: String - } - - type State { - customField: String @neo4j_ignore - name: String! - _id: String - } - - type _MovieRatings @relation(name: "RATED", from: "User", to: "Movie") { - currentUserId(strArg: String): String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - User: User - } - - type _Neo4jTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - timezone: String - formatted: String - } - - type _Neo4jDate { - year: Int - month: Int - day: Int - formatted: String - } - - type _Neo4jLocalTime { - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - - type _Neo4jLocalDateTime { - year: Int - month: Int - day: Int - hour: Int - minute: Int - second: Int - millisecond: Int - microsecond: Int - nanosecond: Int - formatted: String - } - - type User implements Person { - userId: ID! - name: String - currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rated( + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter + } + + input _Neo4jTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + input _Neo4jDateInput { + year: Int + month: Int + day: Int + formatted: String + } + + input _Neo4jLocalTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _Neo4jLocalDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + input _UserFilter { + AND: [_UserFilter!] + OR: [_UserFilter!] + userId: ID + userId_not: ID + userId_in: [ID!] + userId_not_in: [ID!] + userId_contains: ID + userId_not_contains: ID + userId_starts_with: ID + userId_not_starts_with: ID + userId_ends_with: ID + userId_not_ends_with: ID + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + rated: _UserRatedFilter + rated_not: _UserRatedFilter + rated_in: [_UserRatedFilter!] + rated_not_in: [_UserRatedFilter!] + rated_some: _UserRatedFilter + rated_none: _UserRatedFilter + rated_single: _UserRatedFilter + rated_every: _UserRatedFilter + friends: _FriendOfDirectionsFilter + friends_not: _FriendOfDirectionsFilter + friends_in: [_FriendOfDirectionsFilter!] + friends_not_in: [_FriendOfDirectionsFilter!] + friends_some: _FriendOfDirectionsFilter + friends_none: _FriendOfDirectionsFilter + friends_single: _FriendOfDirectionsFilter + friends_every: _FriendOfDirectionsFilter + favorites: _MovieFilter + favorites_not: _MovieFilter + favorites_in: [_MovieFilter!] + favorites_not_in: [_MovieFilter!] + favorites_some: _MovieFilter + favorites_none: _MovieFilter + favorites_single: _MovieFilter + favorites_every: _MovieFilter + } + + input _UserRatedFilter { + AND: [_UserRatedFilter!] + OR: [_UserRatedFilter!] rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - filter: _UserRatedFilter - ): [_UserRated] - friends: _UserFriendsDirections - favorites( - first: Int - offset: Int - orderBy: [_MovieOrdering] - filter: _MovieFilter - ): [Movie] @relation(name: "FAVORITED", direction: "OUT") - _id: String - } - - input strInput { - strArg: String - } - - type _UserRated @relation(name: "RATED", from: "User", to: "Movie") { - currentUserId(strArg: String): String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - Movie: Movie - } - - type _UserFriendsDirections - @relation(name: "FRIEND_OF", from: "User", to: "User") { - from( - since: Int - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - filter: _FriendOfFilter - ): [_UserFriends] - to( + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + Movie: _MovieFilter + } + + input _FriendOfDirectionsFilter { + from: _FriendOfFilter + to: _FriendOfFilter + } + + input _FriendOfFilter { + AND: [_FriendOfFilter!] + OR: [_FriendOfFilter!] since: Int + since_not: Int + since_in: [Int!] + since_not_in: [Int!] + since_lt: Int + since_lte: Int + since_gt: Int + since_gte: Int time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - filter: _FriendOfFilter - ): [_UserFriends] - } - - type _UserFriends @relation(name: "FRIEND_OF", from: "User", to: "User") { - currentUserId: String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - User: User - } - - enum _StateOrdering { - name_asc - name_desc - _id_asc - _id_desc - } - - enum _UserOrdering { - userId_asc - userId_desc - name_asc - name_desc - currentUserId_asc - currentUserId_desc - _id_asc - _id_desc - } - - enum _BookOrdering { - genre_asc - genre_desc - _id_asc - _id_desc - } - - input _BookFilter { - AND: [_BookFilter!] - OR: [_BookFilter!] - genre: BookGenre - genre_not: BookGenre - genre_in: [BookGenre!] - genre_not_in: [BookGenre!] - } - - enum BookGenre { - Mystery - Science - Math - } - - type Book { - genre: BookGenre - _id: String - } - - type currentUserId { - userId: String - _id: String - } - - enum _TemporalNodeOrdering { - datetime_asc - datetime_desc - name_asc - name_desc - time_asc - time_desc - date_asc - date_desc - localtime_asc - localtime_desc - localdatetime_asc - localdatetime_desc - computedTimestamp_asc - computedTimestamp_desc - _id_asc - _id_desc - } - - input _TemporalNodeFilter { - AND: [_TemporalNodeFilter!] - OR: [_TemporalNodeFilter!] - datetime: _Neo4jDateTimeInput - datetime_not: _Neo4jDateTimeInput - datetime_in: [_Neo4jDateTimeInput!] - datetime_not_in: [_Neo4jDateTimeInput!] - datetime_lt: _Neo4jDateTimeInput - datetime_lte: _Neo4jDateTimeInput - datetime_gt: _Neo4jDateTimeInput - datetime_gte: _Neo4jDateTimeInput - name: String - name_not: String - name_in: [String!] - name_not_in: [String!] - name_contains: String - name_not_contains: String - name_starts_with: String - name_not_starts_with: String - name_ends_with: String - name_not_ends_with: String - time: _Neo4jTimeInput - time_not: _Neo4jTimeInput - time_in: [_Neo4jTimeInput!] - time_not_in: [_Neo4jTimeInput!] - time_lt: _Neo4jTimeInput - time_lte: _Neo4jTimeInput - time_gt: _Neo4jTimeInput - time_gte: _Neo4jTimeInput - date: _Neo4jDateInput - date_not: _Neo4jDateInput - date_in: [_Neo4jDateInput!] - date_not_in: [_Neo4jDateInput!] - date_lt: _Neo4jDateInput - date_lte: _Neo4jDateInput - date_gt: _Neo4jDateInput - date_gte: _Neo4jDateInput - localtime: _Neo4jLocalTimeInput - localtime_not: _Neo4jLocalTimeInput - localtime_in: [_Neo4jLocalTimeInput!] - localtime_not_in: [_Neo4jLocalTimeInput!] - localtime_lt: _Neo4jLocalTimeInput - localtime_lte: _Neo4jLocalTimeInput - localtime_gt: _Neo4jLocalTimeInput - localtime_gte: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - localdatetime_not: _Neo4jLocalDateTimeInput - localdatetime_in: [_Neo4jLocalDateTimeInput!] - localdatetime_not_in: [_Neo4jLocalDateTimeInput!] - localdatetime_lt: _Neo4jLocalDateTimeInput - localdatetime_lte: _Neo4jLocalDateTimeInput - localdatetime_gt: _Neo4jLocalDateTimeInput - localdatetime_gte: _Neo4jLocalDateTimeInput - temporalNodes: _TemporalNodeFilter - temporalNodes_not: _TemporalNodeFilter - temporalNodes_in: [_TemporalNodeFilter!] - temporalNodes_not_in: [_TemporalNodeFilter!] - temporalNodes_some: _TemporalNodeFilter - temporalNodes_none: _TemporalNodeFilter - temporalNodes_single: _TemporalNodeFilter - temporalNodes_every: _TemporalNodeFilter - } - - type TemporalNode { - datetime: _Neo4jDateTime - name: String - time: _Neo4jTime - date: _Neo4jDate - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - localdatetimes: [_Neo4jLocalDateTime] - computedTimestamp: String @cypher(statement: "RETURN toString(datetime())") - temporalNodes( - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - first: Int - offset: Int - orderBy: [_TemporalNodeOrdering] - filter: _TemporalNodeFilter - ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) - _id: String - } - - type MutationB { - currentUserId: String - @cypher(statement: "RETURN $cypherParams.currentUserId") - computedObjectWithCypherParams: currentUserId - @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") - computedTemporal: _Neo4jDateTime - @cypher( - statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" - ) - computedStringList: [String] - @cypher( - statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" - ) - customWithArguments(strArg: String, strInputArg: strInput): String - @cypher(statement: "RETURN $strInputArg.strArg") - testPublish: Boolean @neo4j_ignore - AddMovieGenres( - from: _MovieInput! - to: _GenreInput! - ): _AddMovieGenresPayload - @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") - RemoveMovieGenres( - from: _MovieInput! - to: _GenreInput! - ): _RemoveMovieGenresPayload - @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") - @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) - AddMovieActors( - from: _ActorInput! - to: _MovieInput! - ): _AddMovieActorsPayload - @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") - RemoveMovieActors( - from: _ActorInput! - to: _MovieInput! - ): _RemoveMovieActorsPayload - @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") - @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) - AddMovieFilmedIn( - from: _MovieInput! - to: _StateInput! - ): _AddMovieFilmedInPayload - @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") - RemoveMovieFilmedIn( - from: _MovieInput! - to: _StateInput! - ): _RemoveMovieFilmedInPayload - @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") - @hasScope(scopes: ["Movie: Delete", "State: Delete"]) - AddMovieRatings( - from: _UserInput! - to: _MovieInput! - data: _RatedInput! - ): _AddMovieRatingsPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Create", "Movie: Create"]) - RemoveMovieRatings( - from: _UserInput! - to: _MovieInput! - ): _RemoveMovieRatingsPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Create", "Movie: Create"]) - CreateMovie( - movieId: ID - title: String - someprefix_title_with_underscores: String - year: Int - released: _Neo4jDateTimeInput! - plot: String - poster: String - imdbRating: Float - avgStars: Float - years: [Int] - titles: [String] - imdbRatings: [Float] - releases: [_Neo4jDateTimeInput] - ): Movie @hasScope(scopes: ["Movie: Create"]) - UpdateMovie( + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter + } + + type Movie + @additionalLabels( + labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] + ) { + _id: String movieId: ID! - title: String + title: String @isAuthenticated someprefix_title_with_underscores: String year: Int - released: _Neo4jDateTimeInput + released: _Neo4jDateTime! plot: String poster: String imdbRating: Float + genres( + first: Int + offset: Int + orderBy: [_GenreOrdering] + filter: _GenreFilter + ): [Genre] @relation(name: "IN_GENRE", direction: "OUT") + similar( + first: Int = 3 + offset: Int = 0 + orderBy: [_MovieOrdering] + ): [Movie] + @cypher( + statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o" + ) + mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this") + degree: Int + @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())") + actors( + first: Int = 3 + offset: Int = 0 + name: String + names: [String] + orderBy: [_ActorOrdering] + filter: _ActorFilter + ): [Actor] @relation(name: "ACTED_IN", direction: "IN") avgStars: Float + filmedIn(filter: _StateFilter): State + @relation(name: "FILMED_IN", direction: "OUT") + scaleRating(scale: Int = 3): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + scaleRatingFloat(scale: Float = 1.5): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] + @cypher( + statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other" + ) + ratings( + rating: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _MovieRatedFilter + ): [_MovieRatings] years: [Int] titles: [String] imdbRatings: [Float] - releases: [_Neo4jDateTimeInput] - ): Movie @hasScope(scopes: ["Movie: Update"]) - DeleteMovie(movieId: ID!): Movie @hasScope(scopes: ["Movie: Delete"]) - AddGenreMovies( - from: _MovieInput! - to: _GenreInput! - ): _AddGenreMoviesPayload - @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") - RemoveGenreMovies( - from: _MovieInput! - to: _GenreInput! - ): _RemoveGenreMoviesPayload - @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") - @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) - CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) - DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) - CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) - DeleteState(name: String!): State @hasScope(scopes: ["State: Delete"]) - AddActorMovies( - from: _ActorInput! - to: _MovieInput! - ): _AddActorMoviesPayload - @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") - RemoveActorMovies( - from: _ActorInput! - to: _MovieInput! - ): _RemoveActorMoviesPayload - @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") - @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) - CreateActor(userId: ID, name: String): Actor - @hasScope(scopes: ["Actor: Create"]) - UpdateActor(userId: ID!, name: String): Actor - @hasScope(scopes: ["Actor: Update"]) - DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) - AddUserRated( - from: _UserInput! - to: _MovieInput! - data: _RatedInput! - ): _AddUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Create", "Movie: Create"]) - RemoveUserRated( - from: _UserInput! - to: _MovieInput! - ): _RemoveUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Create", "Movie: Create"]) - AddUserFriends( - from: _UserInput! - to: _UserInput! - data: _FriendOfInput! - ): _AddUserFriendsPayload - @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") - @hasScope(scopes: ["User: Create", "User: Create"]) - RemoveUserFriends( - from: _UserInput! - to: _UserInput! - ): _RemoveUserFriendsPayload - @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") - @hasScope(scopes: ["User: Create", "User: Create"]) - AddUserFavorites( - from: _UserInput! - to: _MovieInput! - ): _AddUserFavoritesPayload - @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") - RemoveUserFavorites( - from: _UserInput! - to: _MovieInput! - ): _RemoveUserFavoritesPayload - @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Delete", "Movie: Delete"]) - CreateUser(userId: ID, name: String): User - @hasScope(scopes: ["User: Create"]) - UpdateUser(userId: ID!, name: String): User - @hasScope(scopes: ["User: Update"]) - DeleteUser(userId: ID!): User @hasScope(scopes: ["User: Delete"]) - CreateBook(genre: BookGenre): Book @hasScope(scopes: ["Book: Create"]) - DeleteBook(genre: BookGenre!): Book @hasScope(scopes: ["Book: Delete"]) - CreatecurrentUserId(userId: String): currentUserId - @hasScope(scopes: ["currentUserId: Create"]) - DeletecurrentUserId(userId: String!): currentUserId - @hasScope(scopes: ["currentUserId: Delete"]) - AddTemporalNodeTemporalNodes( - from: _TemporalNodeInput! - to: _TemporalNodeInput! - ): _AddTemporalNodeTemporalNodesPayload - @MutationMeta( - relationship: "TEMPORAL" - from: "TemporalNode" - to: "TemporalNode" - ) - RemoveTemporalNodeTemporalNodes( - from: _TemporalNodeInput! - to: _TemporalNodeInput! - ): _RemoveTemporalNodeTemporalNodesPayload - @MutationMeta( - relationship: "TEMPORAL" - from: "TemporalNode" - to: "TemporalNode" - ) - @hasScope(scopes: ["TemporalNode: Delete", "TemporalNode: Delete"]) - CreateTemporalNode( + releases: [_Neo4jDateTime] + customField: String @neo4j_ignore + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + } + + type _Neo4jDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + enum _GenreOrdering { + name_desc + name_asc + } + + type Genre { + _id: String + name: String + movies( + first: Int = 3 + offset: Int = 0 + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "IN_GENRE", direction: "IN") + highestRatedMovie: Movie + @cypher( + statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" + ) + } + + enum _ActorOrdering { + userId_asc + userId_desc + name_asc + name_desc + _id_asc + _id_desc + } + + type Actor implements Person { + userId: ID! + name: String + movies( + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "ACTED_IN", direction: "OUT") + _id: String + } + + interface Person { + userId: ID! + name: String + } + + type State { + customField: String @neo4j_ignore + name: String! + _id: String + } + + type _MovieRatings @relation(name: "RATED", from: "User", to: "Movie") { + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + User: User + } + + type _Neo4jTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String + } + + type _Neo4jDate { + year: Int + month: Int + day: Int + formatted: String + } + + type _Neo4jLocalTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + type _Neo4jLocalDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String + } + + type User implements Person { + userId: ID! + name: String + currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rated( + rating: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _UserRatedFilter + ): [_UserRated] + friends: _UserFriendsDirections + favorites( + first: Int + offset: Int + orderBy: [_MovieOrdering] + filter: _MovieFilter + ): [Movie] @relation(name: "FAVORITED", direction: "OUT") + _id: String + } + + input strInput { + strArg: String + } + + type _UserRated @relation(name: "RATED", from: "User", to: "Movie") { + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + Movie: Movie + } + + type _UserFriendsDirections + @relation(name: "FRIEND_OF", from: "User", to: "User") { + from( + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _FriendOfFilter + ): [_UserFriends] + to( + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + filter: _FriendOfFilter + ): [_UserFriends] + } + + type _UserFriends @relation(name: "FRIEND_OF", from: "User", to: "User") { + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + User: User + } + + enum _StateOrdering { + name_asc + name_desc + _id_asc + _id_desc + } + + enum _UserOrdering { + userId_asc + userId_desc + name_asc + name_desc + currentUserId_asc + currentUserId_desc + _id_asc + _id_desc + } + + enum _BookOrdering { + genre_asc + genre_desc + _id_asc + _id_desc + } + + input _BookFilter { + AND: [_BookFilter!] + OR: [_BookFilter!] + genre: BookGenre + genre_not: BookGenre + genre_in: [BookGenre!] + genre_not_in: [BookGenre!] + } + + enum BookGenre { + Mystery + Science + Math + } + + type Book { + genre: BookGenre + _id: String + } + + type currentUserId { + userId: String + _id: String + } + + enum _CasedTypeOrdering { + name_asc + name_desc + _id_asc + _id_desc + } + + input _CasedTypeFilter { + AND: [_CasedTypeFilter!] + OR: [_CasedTypeFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + state: _StateFilter + state_not: _StateFilter + state_in: [_StateFilter!] + state_not_in: [_StateFilter!] + } + + type CasedType { + name: String + state(filter: _StateFilter): State + @relation(name: "FILMED_IN", direction: "OUT") + _id: String + } + + enum _TemporalNodeOrdering { + datetime_asc + datetime_desc + name_asc + name_desc + time_asc + time_desc + date_asc + date_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + computedTimestamp_asc + computedTimestamp_desc + _id_asc + _id_desc + } + + input _TemporalNodeFilter { + AND: [_TemporalNodeFilter!] + OR: [_TemporalNodeFilter!] datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - localdatetimes: [_Neo4jLocalDateTimeInput] - ): TemporalNode @hasScope(scopes: ["TemporalNode: Create"]) - UpdateTemporalNode( - datetime: _Neo4jDateTimeInput! + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + temporalNodes: _TemporalNodeFilter + temporalNodes_not: _TemporalNodeFilter + temporalNodes_in: [_TemporalNodeFilter!] + temporalNodes_not_in: [_TemporalNodeFilter!] + temporalNodes_some: _TemporalNodeFilter + temporalNodes_none: _TemporalNodeFilter + temporalNodes_single: _TemporalNodeFilter + temporalNodes_every: _TemporalNodeFilter + } + + type TemporalNode { + datetime: _Neo4jDateTime name: String + time: _Neo4jTime + date: _Neo4jDate + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + localdatetimes: [_Neo4jLocalDateTime] + computedTimestamp: String + @cypher(statement: "RETURN toString(datetime())") + temporalNodes( + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + first: Int + offset: Int + orderBy: [_TemporalNodeOrdering] + filter: _TemporalNodeFilter + ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) + _id: String + } + + type MutationB { + currentUserId: String + @cypher(statement: "RETURN $cypherParams.currentUserId") + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + computedTemporal: _Neo4jDateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + testPublish: Boolean @neo4j_ignore + AddMovieGenres( + from: _MovieInput! + to: _GenreInput! + ): _AddMovieGenresPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + RemoveMovieGenres( + from: _MovieInput! + to: _GenreInput! + ): _RemoveMovieGenresPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + AddMovieActors( + from: _ActorInput! + to: _MovieInput! + ): _AddMovieActorsPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + RemoveMovieActors( + from: _ActorInput! + to: _MovieInput! + ): _RemoveMovieActorsPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + AddMovieFilmedIn( + from: _MovieInput! + to: _StateInput! + ): _AddMovieFilmedInPayload + @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") + RemoveMovieFilmedIn( + from: _MovieInput! + to: _StateInput! + ): _RemoveMovieFilmedInPayload + @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") + @hasScope(scopes: ["Movie: Delete", "State: Delete"]) + AddMovieRatings( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _AddMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + RemoveMovieRatings( + from: _UserInput! + to: _MovieInput! + ): _RemoveMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + CreateMovie( + movieId: ID + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTimeInput! + plot: String + poster: String + imdbRating: Float + avgStars: Float + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTimeInput] + ): Movie @hasScope(scopes: ["Movie: Create"]) + UpdateMovie( + movieId: ID! + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTimeInput + plot: String + poster: String + imdbRating: Float + avgStars: Float + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTimeInput] + ): Movie @hasScope(scopes: ["Movie: Update"]) + DeleteMovie(movieId: ID!): Movie @hasScope(scopes: ["Movie: Delete"]) + AddGenreMovies( + from: _MovieInput! + to: _GenreInput! + ): _AddGenreMoviesPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + RemoveGenreMovies( + from: _MovieInput! + to: _GenreInput! + ): _RemoveGenreMoviesPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) + DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) + CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) + DeleteState(name: String!): State @hasScope(scopes: ["State: Delete"]) + AddActorMovies( + from: _ActorInput! + to: _MovieInput! + ): _AddActorMoviesPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + RemoveActorMovies( + from: _ActorInput! + to: _MovieInput! + ): _RemoveActorMoviesPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + CreateActor(userId: ID, name: String): Actor + @hasScope(scopes: ["Actor: Create"]) + UpdateActor(userId: ID!, name: String): Actor + @hasScope(scopes: ["Actor: Update"]) + DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) + AddUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _AddUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + RemoveUserRated( + from: _UserInput! + to: _MovieInput! + ): _RemoveUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + AddUserFriends( + from: _UserInput! + to: _UserInput! + data: _FriendOfInput! + ): _AddUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Create", "User: Create"]) + RemoveUserFriends( + from: _UserInput! + to: _UserInput! + ): _RemoveUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Create", "User: Create"]) + AddUserFavorites( + from: _UserInput! + to: _MovieInput! + ): _AddUserFavoritesPayload + @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") + RemoveUserFavorites( + from: _UserInput! + to: _MovieInput! + ): _RemoveUserFavoritesPayload + @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + CreateUser(userId: ID, name: String): User + @hasScope(scopes: ["User: Create"]) + UpdateUser(userId: ID!, name: String): User + @hasScope(scopes: ["User: Update"]) + DeleteUser(userId: ID!): User @hasScope(scopes: ["User: Delete"]) + CreateBook(genre: BookGenre): Book @hasScope(scopes: ["Book: Create"]) + DeleteBook(genre: BookGenre!): Book @hasScope(scopes: ["Book: Delete"]) + CreatecurrentUserId(userId: String): currentUserId + @hasScope(scopes: ["currentUserId: Create"]) + DeletecurrentUserId(userId: String!): currentUserId + @hasScope(scopes: ["currentUserId: Delete"]) + AddTemporalNodeTemporalNodes( + from: _TemporalNodeInput! + to: _TemporalNodeInput! + ): _AddTemporalNodeTemporalNodesPayload + @MutationMeta( + relationship: "TEMPORAL" + from: "TemporalNode" + to: "TemporalNode" + ) + RemoveTemporalNodeTemporalNodes( + from: _TemporalNodeInput! + to: _TemporalNodeInput! + ): _RemoveTemporalNodeTemporalNodesPayload + @MutationMeta( + relationship: "TEMPORAL" + from: "TemporalNode" + to: "TemporalNode" + ) + @hasScope(scopes: ["TemporalNode: Delete", "TemporalNode: Delete"]) + CreateTemporalNode( + datetime: _Neo4jDateTimeInput + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: [_Neo4jLocalDateTimeInput] + ): TemporalNode @hasScope(scopes: ["TemporalNode: Create"]) + UpdateTemporalNode( + datetime: _Neo4jDateTimeInput! + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: [_Neo4jLocalDateTimeInput] + ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) + DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode + @hasScope(scopes: ["TemporalNode: Delete"]) + AddCasedTypeState( + from: _CasedTypeInput! + to: _StateInput! + ): _AddCasedTypeStatePayload + @MutationMeta(relationship: "FILMED_IN", from: "CasedType", to: "State") + RemoveCasedTypeState( + from: _CasedTypeInput! + to: _StateInput! + ): _RemoveCasedTypeStatePayload + @MutationMeta(relationship: "FILMED_IN", from: "CasedType", to: "State") + @hasScope(scopes: ["CasedType: Delete", "State: Delete"]) + CreateCasedType(name: String): CasedType + @hasScope(scopes: ["CasedType: Create"]) + DeleteCasedType(name: String!): CasedType + @hasScope(scopes: ["CasedType: Delete"]) + } + + input _MovieInput { + movieId: ID! + } + + input _GenreInput { + name: String! + } + + type _AddMovieGenresPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _RemoveMovieGenresPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + input _ActorInput { + userId: ID! + } + + type _AddMovieActorsPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + type _RemoveMovieActorsPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + input _StateInput { + name: String! + } + + type _AddMovieFilmedInPayload + @relation(name: "FILMED_IN", from: "Movie", to: "State") { + from: Movie + to: State + } + + type _RemoveMovieFilmedInPayload + @relation(name: "FILMED_IN", from: "Movie", to: "State") { + from: Movie + to: State + } + + input _UserInput { + userId: ID! + } + + input _RatedInput { + rating: Int + ratings: [Int] time: _Neo4jTimeInput date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput - localdatetimes: [_Neo4jLocalDateTimeInput] - ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) - DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode - @hasScope(scopes: ["TemporalNode: Delete"]) - } - - input _MovieInput { - movieId: ID! - } - - input _GenreInput { - name: String! - } - - type _AddMovieGenresPayload - @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { - from: Movie - to: Genre - } - - type _RemoveMovieGenresPayload - @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { - from: Movie - to: Genre - } - - input _ActorInput { - userId: ID! - } - - type _AddMovieActorsPayload - @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { - from: Actor - to: Movie - } - - type _RemoveMovieActorsPayload - @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { - from: Actor - to: Movie - } - - input _StateInput { - name: String! - } - - type _AddMovieFilmedInPayload - @relation(name: "FILMED_IN", from: "Movie", to: "State") { - from: Movie - to: State - } - - type _RemoveMovieFilmedInPayload - @relation(name: "FILMED_IN", from: "Movie", to: "State") { - from: Movie - to: State - } - - input _UserInput { - userId: ID! - } - - input _RatedInput { - rating: Int - ratings: [Int] - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - datetimes: [_Neo4jDateTimeInput] - } - - type _AddMovieRatingsPayload - @relation(name: "RATED", from: "User", to: "Movie") { - from: User - to: Movie - currentUserId: String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - } - - type _RemoveMovieRatingsPayload - @relation(name: "RATED", from: "User", to: "Movie") { - from: User - to: Movie - } - - type _AddGenreMoviesPayload - @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { - from: Movie - to: Genre - } - - type _RemoveGenreMoviesPayload - @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { - from: Movie - to: Genre - } - - type _AddActorMoviesPayload - @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { - from: Actor - to: Movie - } - - type _RemoveActorMoviesPayload - @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { - from: Actor - to: Movie - } - - type _AddUserRatedPayload - @relation(name: "RATED", from: "User", to: "Movie") { - from: User - to: Movie - currentUserId: String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - } - - type _RemoveUserRatedPayload - @relation(name: "RATED", from: "User", to: "Movie") { - from: User - to: Movie - } - - input _FriendOfInput { - since: Int - time: _Neo4jTimeInput - date: _Neo4jDateInput - datetime: _Neo4jDateTimeInput - datetimes: [_Neo4jDateTimeInput] - localtime: _Neo4jLocalTimeInput - localdatetime: _Neo4jLocalDateTimeInput - } - - type _AddUserFriendsPayload - @relation(name: "FRIEND_OF", from: "User", to: "User") { - from: User - to: User - currentUserId: String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - } - - type _RemoveUserFriendsPayload - @relation(name: "FRIEND_OF", from: "User", to: "User") { - from: User - to: User - } - - type _AddUserFavoritesPayload - @relation(name: "FAVORITED", from: "User", to: "Movie") { - from: User - to: Movie - } - - type _RemoveUserFavoritesPayload - @relation(name: "FAVORITED", from: "User", to: "Movie") { - from: User - to: Movie - } - - input _TemporalNodeInput { - datetime: _Neo4jDateTimeInput! - } - - type _AddTemporalNodeTemporalNodesPayload - @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { - from: TemporalNode - to: TemporalNode - } - - type _RemoveTemporalNodeTemporalNodesPayload - @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { - from: TemporalNode - to: TemporalNode - } - - type FriendOf @relation { - from: User - currentUserId: String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - since: Int - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - datetimes: [_Neo4jDateTime] - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - to: User - } - - type Rated @relation { - from: User - currentUserId(strArg: String): String - @cypher( - statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" - ) - rating: Int - ratings: [Int] - time: _Neo4jTime - date: _Neo4jDate - datetime: _Neo4jDateTime - localtime: _Neo4jLocalTime - localdatetime: _Neo4jLocalDateTime - datetimes: [_Neo4jDateTime] - to: Movie - } - - input _BookInput { - genre: BookGenre! - } - - enum _currentUserIdOrdering { - userId_asc - userId_desc - _id_asc - _id_desc - } - - input _currentUserIdFilter { - AND: [_currentUserIdFilter!] - OR: [_currentUserIdFilter!] - userId: String - userId_not: String - userId_in: [String!] - userId_not_in: [String!] - userId_contains: String - userId_not_contains: String - userId_starts_with: String - userId_not_starts_with: String - userId_ends_with: String - userId_not_ends_with: String - } - - input _currentUserIdInput { - userId: String! - } - - type ignoredType { - ignoredField: String @neo4j_ignore - } - - scalar Time - - scalar Date - - scalar DateTime - - scalar LocalTime - - scalar LocalDateTime - - enum Role { - reader - user - admin - } - - type SubscriptionC { - testSubscribe: Boolean - } - - enum _RelationDirections { - IN - OUT - } - - schema { - query: QueryA - mutation: MutationB - subscription: SubscriptionC - } + datetimes: [_Neo4jDateTimeInput] + } + + type _AddMovieRatingsPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + } + + type _RemoveMovieRatingsPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + } + + type _AddGenreMoviesPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _RemoveGenreMoviesPayload + @relation(name: "IN_GENRE", from: "Movie", to: "Genre") { + from: Movie + to: Genre + } + + type _AddActorMoviesPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + type _RemoveActorMoviesPayload + @relation(name: "ACTED_IN", from: "Actor", to: "Movie") { + from: Actor + to: Movie + } + + type _AddUserRatedPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + } + + type _RemoveUserRatedPayload + @relation(name: "RATED", from: "User", to: "Movie") { + from: User + to: Movie + } + + input _FriendOfInput { + since: Int + time: _Neo4jTimeInput + date: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetimes: [_Neo4jDateTimeInput] + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + } + + type _AddUserFriendsPayload + @relation(name: "FRIEND_OF", from: "User", to: "User") { + from: User + to: User + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + } + + type _RemoveUserFriendsPayload + @relation(name: "FRIEND_OF", from: "User", to: "User") { + from: User + to: User + } + + type _AddUserFavoritesPayload + @relation(name: "FAVORITED", from: "User", to: "Movie") { + from: User + to: Movie + } + + type _RemoveUserFavoritesPayload + @relation(name: "FAVORITED", from: "User", to: "Movie") { + from: User + to: Movie + } + + input _TemporalNodeInput { + datetime: _Neo4jDateTimeInput! + } + + type _AddTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode + } + + type _RemoveTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode + } + + input _CasedTypeInput { + name: String! + } + + type _AddCasedTypeStatePayload + @relation(name: "FILMED_IN", from: "CasedType", to: "State") { + from: CasedType + to: State + } + + type _RemoveCasedTypeStatePayload + @relation(name: "FILMED_IN", from: "CasedType", to: "State") { + from: CasedType + to: State + } + + type SubscriptionC { + testSubscribe: Boolean + } + + type FriendOf @relation { + from: User + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + datetimes: [_Neo4jDateTime] + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + to: User + } + + type Rated @relation { + from: User + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rating: Int + ratings: [Int] + time: _Neo4jTime + date: _Neo4jDate + datetime: _Neo4jDateTime + localtime: _Neo4jLocalTime + localdatetime: _Neo4jLocalDateTime + datetimes: [_Neo4jDateTime] + to: Movie + } + + input _BookInput { + genre: BookGenre! + } + + enum _currentUserIdOrdering { + userId_asc + userId_desc + _id_asc + _id_desc + } + + input _currentUserIdFilter { + AND: [_currentUserIdFilter!] + OR: [_currentUserIdFilter!] + userId: String + userId_not: String + userId_in: [String!] + userId_not_in: [String!] + userId_contains: String + userId_not_contains: String + userId_starts_with: String + userId_not_starts_with: String + userId_ends_with: String + userId_not_ends_with: String + } + + input _currentUserIdInput { + userId: String! + } + + type ignoredType { + ignoredField: String @neo4j_ignore + } + + scalar Time + + scalar Date + + scalar DateTime + + scalar LocalTime + + scalar LocalDateTime + + enum Role { + reader + user + admin + } + + enum _RelationDirections { + IN + OUT + } + + schema { + query: QueryA + mutation: MutationB + subscription: SubscriptionC + } `; compareSchema({ test: t, From a37fed66687cfc3685d232360d09d7ac332006b2 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:28:42 -0700 Subject: [PATCH 36/37] CasedType test addition --- test/unit/cypherTest.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index 783865ef..baf91dd9 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -524,6 +524,25 @@ test('Cypher subquery filters', t => { ]); }); +test('cypher subquery preserves case through filters', t => { + const graphQLQuery = ` + { + CasedType(filter: {state: {name: "hello"}}) { + name + state { + name + } + } + }`, + expectedCypherQuery = + 'MATCH (`casedType`:`CasedType`) WHERE (EXISTS((`casedType`)-[:FILMED_IN]->(:State)) AND ALL(`state` IN [(`casedType`)-[:FILMED_IN]->(`_state`:State) | `_state`] WHERE (`state`.name = $filter.state.name))) RETURN `casedType` { .name ,state: head([(`casedType`)-[:`FILMED_IN`]->(`casedType_state`:`State`) | casedType_state { .name }]) } AS `casedType`'; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + test('Cypher subquery filters with paging', t => { const graphQLQuery = ` { From 730c22a22350059c03be515425c0efdf0e40ef19 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Tue, 22 Oct 2019 15:34:49 -0700 Subject: [PATCH 37/37] put @isAuthenticated back into testing schema --- test/helpers/testSchema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 29a18003..a26d5185 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -5,7 +5,7 @@ export const testSchema = /* GraphQL */ ` ) { _id: String movieId: ID! - title: String + title: String @isAuthenticated someprefix_title_with_underscores: String year: Int released: DateTime!