neo4j / graphql Goto Github PK
View Code? Open in Web Editor NEWA GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
Home Page: https://neo4j.com/docs/graphql-manual/current/
License: Apache License 2.0
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
Home Page: https://neo4j.com/docs/graphql-manual/current/
License: Apache License 2.0
Please consider adding autogenerated merge mutations similar to those in neo4j-graphql-js.
Merge mutations are very convenient for my use case of ingesting event data since it simplifies the code and allows idempotent replay of events if needed.
This feature would help transition from neo4j-graphql-js to neo4j/graphql
Currently querying nodes, which are not accessible by the user will throw an forbidden error.
For the given schema:
type User {
id: ID!
name: String!
}
extend type User @auth(
rules: [
{
operations: ["read"],
allow: { id: "$jwt.sub" }
}
]
)
The query:
{
users {
id
}
}
with the cypher:
MATCH (this:User)
CALL apoc.util.validate(NOT(EXISTS(this.id) AND this.id = $this_auth_allow0_id), "@neo4j/graphql/FORBIDDEN", [0])
RETURN this {
.id
} as this
will always fail if there are multiple users in the database.
It would be useful to have a mechanism to control the behavior for non matching nodes, like Error
and Filter
.
Hello everyone, I want to know if the function inferSchema() that was available for the previous release neo4j-graphql-js still also available for this release, if yet how can use it?
thanks for your help
Describe the regression
I am currently migrating an existing application from neo4j-graphql-js to this official library and can't seem to find a way to implement the following scenario:
cypherParams.myValue
and inject into the cypher
when specifying the GraphQL schemaAs far as I can see, there is only functionality to get values from JWT, but no way of (synchronously or asynchronously) fetching and injecting custom parameters.
Last working version
neo4j-graphql-js
@neo4j/[email protected]
System (please complete the following information):
Any
Is your feature request related to a problem? Please describe.
I would like a way to support data isolation across multiple projects in a single database. My concrete use case is to be able to support multiple "tenants" with their own schemas on a single Neo4j Aura instance.
Describe the solution you'd like
In neo4j-graphql-js
there is the @additionalLabels
directive, which adds the option to inject extra labels on both queries and mutations. Using this I can add a tenant-id
as an additional label, which gives me the data isolation I need.
Describe alternatives you've considered
Data isolation using multiple databases
This is an option that is (mostly) supported in the neo4j-graphql-js
library. However, Neo4j aura does not support this
feature. We have also experienced performance issues with this approach when using a large number of databases.
Data isolation using multiple neo4j aura instances
This solution is not practically possible today, as there is no way to programmatically manage neo4j aura instances. It will also introduce a lot of extra overhead to have to manage all the instances, and it increases the cost significantly.
The GraphQL Manual is not yet available on the Neo4j Documentation page.
I'm working on a project with a team that want to utilize Graphql with neo4j. I think this library is nice but i would like to request the subscriptions and Relationship properties as they will be crucial in the project.
Describe the bug
The autogenerated CreateInput of a type defined in the schema converts a field of type [String!] to [String!]!, making it a required input, even though it should not be.
Type definitions
type Category {
categoryId: ID! @id
name: String!
description: String! @default(value: "")
exampleImageLocations: [String!]
}
To Reproduce
Steps to reproduce the behavior:
type Category {
categoryId: ID! @id
name: String!
description: String! @default(value: "")
exampleImageLocations: [String!]
}
mutation {
createCategories(
input: [
{ name: "Category 1"}
{ name: "Category 2", exampleImageLocations: [] }
]
) {
categories {
categoryId
}
}
}
Expected behavior
An input value for exampleImageLocations
should not be required.
System (please complete the following information):
Additional context
N/A
Given the schema the schema
type Movie {
id: ID
title: String
actorCount: Int
genres: [Genre] @relation(name: "IN_GENRE", direction: OUT)
}
type Genre {
name: String
movies: [Movie] @relation(name: "IN_GENRE", direction: IN)
}
and the query:
{
movie(where: { genres: { name: "some genre" } }) {
actorCount
}
}
this library is currently generating the following cypher:
MATCH (this:Movie)
WHERE EXISTS((this)-[:IN_GENRE]->(:Genre)) AND ALL(this_genres IN [(this)-[:IN_GENRE]->(this_genres:Genre) | this_genres] WHERE this_genres.name = $this_genres_name)
RETURN this { .actorCount } as this
The java version would instead generate:
MATCH (movie:Movie)
WHERE all(whereMovieGenreCond IN [(movie)-[:IN_GENRE]->(whereMovieGenre:Genre) | whereMovieGenre.name = $whereMovieGenreName] WHERE whereMovieGenreCond) RETURN movie { .actorCount } AS movie
According to @jexp:
Ours [The java version] is better as the planner is able to pull the condition into the expansion and doesn't have to build up a full list and post-filter
see also: neo4j-graphql/neo4j-graphql-java#220 (comment)
btw. in a further step we will migrate to subqueries, to improve the usage of the planner
Describe the bug
Using any@exclude
directives breaks apollo server when using's OGM's neoSchema vs Neo4JGraphQL's neoSchema
Type:
type Error @exclude {
code: String!
message: String!
}
type ErrorResponse @exclude {
error: Error!
}
Error:
[ERROR] 11:33:51 Error: Input Object type ErrorResponseCreateInput must define one or more fields.
To Reproduce
Steps to reproduce the behavior:
export const ogm = new OGM({
typeDefs,
driver,
resolvers,
config: {
jwt: {
secret: process.env.JWT_SECRET!,
},
},
});
export const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
resolvers,
config: {
jwt: {
secret: process.env.JWT_SECRET!,
},
},
});
Works:
const server = new ApolloServer({
schema: neoSchema.schema
});
Returns Error:
const server = new ApolloServer({
schema: ogm.neoSchema.schema
});
Previously you were able to define relationship attributes like this:
type Cause {
id: ID! @id
"Every Cause has at least one Effect"
effects: [Effect]
}
type Effect @relation(
from: "causedBy",
to: "causes"
) {
"Every Effect is caused by a Cause"
causedBy: Cause
"Every Effect causes a Cause"
causes: Cause
description: String
}
It doesn't seem possible to define a Type with an @relationship directive.
Will this feature come back in subsequent releases?
Hi, I would love to help contribute and support this library.
I had some working proposals to add additional features to the GRAND stack like bulk (which seems solved here) and aggregation (cognitedata/neo4j-graphql-js#5).
I would love to help make the graphql integration even better 🤩
I have an AuthenticatedUser
type defined as:
type AuthenticatedUser {
userId: ID! @autogenerate
name: String
sub: String
}
I have a sample currentUser
query defined which should match the AuthenticatedUser.sub
field to the sub
field in the decoded JWT:
"""
A sample query to return details for the authenticated user currently logged in to our system
"""
currentUser: AuthenticatedUser
@cypher(
statement: """
MATCH (u:AuthenticatedUser {sub: $jwt.sub})
RETURN u
"""
),
When I run this query with a valid JWT in the Authorization
HTTP header, the result is:
Is your feature request related to a problem? Please describe.
Currently, if you want to add a computed field on the Node through a custom resolver you need the client to fetch all the dependent fields in order to compute the result. For example, to combine and first and last name as a fullName (as shown in the docs), the client needs to request firstname and lastname in the query in order for the server to have access to the first and last name. This is inefficient from a client / graphql standpoint as we should only be fetching the data we need from the server. It would be better if we had access to all, or certain, of the primitive fields on the server such that there is a parent reference or object to extend.
Describe the solution you'd like
Ideally one of the following: (i) return all primitive fields on the node from the db to the GraphQL layer, (ii) a directive to include fields in the server request, and/or (iii) at least return the primary key or indicated @id for the node.
Describe alternatives you've considered
N/A. Without a reference to the parent node, one can't even query for the additional fields.
Additional context
This may also help you reduce the demand for the relationship properties as users like myself could build whatever connections we wanted rather than having to rely on the enhanced schema. This is akin to more traditional GraphQL endpoints where a query on a Model or document returns the row or document reference.
Is your feature request related to a problem? Please describe.
neo4j-graphql-js
had support for Apollo Federation to build a subgraph, this version has a regression in that regard.
Describe alternatives you've considered
It's impossible to use alternatives besides staying on the old version, as this generates the schema internally instead of exposing generated resolvers.
Describe the bug
Trying to serialize count query I receive: "Int cannot represent non-integer value: { low: 2000, high: 0 }"
where 2000
is the expected value.
When running in Neo4J Browser and the equivalent query in GraphQL Architect the count is as expected. This currently appears to just fail when using neo4j/graphql
.
Type definitions
import { gql } from 'apollo-server-express'
export const typeDefs = gql`
type Event @exclude(operations: [CREATE, UPDATE, DELETE]) {
id: ID! @id
type: String!
createdAt: DateTime! @timestamp(operations: [CREATE])
}
type TopEventResult @exclude {
eventType: String!
eventCount: Int!
}
type Query {
topEvents(): [TopEventResult]!
@cypher(
statement: """
MATCH (e:Event)
WITH DISTINCT e.type AS eventType, COUNT(*) as eventCount
RETURN { eventType: eventType, eventCount: eventCount }
"""
)
}
`
To Reproduce
Steps to reproduce the behavior:
const app = express()
const startApolloServer = async () => {
const driver = neo4j.driver(
NEO4J_URL,
neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD)
)
const neoSchema = new Neo4jGraphQL({
typeDefs,
resolvers,
driver,
})
const server = new ApolloServer({
schema: neoSchema.schema,
context: ({ req }) => ({ req }),
})
await server.start()
server.applyMiddleware({ app })
await new Promise((resolve: (reason?: any) => void) =>
app.listen({ port: 4000 }, resolve)
)
console.log(
`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
)
return { server, app }
}
startApolloServer()
{
topEvents {
eventType
eventCount
}
}
FWIW: Removing eventCount
from the query works.
{
"errors": [
{
"message": "Int cannot represent non-integer value: { low: 2000, high: 0 }",
"locations": [
{
"line": 8,
"column": 5
}
],
"path": [
"topEvents",
0,
"eventCount"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"message": "Int cannot represent non-integer value: { low: 2000, high: 0 }",
"stacktrace": [
"GraphQLError: Int cannot represent non-integer value: { low: 2000, high: 0 }",
" at GraphQLScalarType.serializeInt [as serialize] (/var/task/node_modules/graphql/type/scalars.js:49:11)",
" at completeLeafValue (/var/task/node_modules/graphql/execution/execute.js:653:37)",
" at completeValue (/var/task/node_modules/graphql/execution/execute.js:578:12)",
" at completeValue (/var/task/node_modules/graphql/execution/execute.js:556:21)",
" at resolveField (/var/task/node_modules/graphql/execution/execute.js:472:19)",
" at executeFields (/var/task/node_modules/graphql/execution/execute.js:292:18)",
" at collectAndExecuteSubfields (/var/task/node_modules/graphql/execution/execute.js:748:10)",
" at completeObjectValue (/var/task/node_modules/graphql/execution/execute.js:738:10)",
" at completeValue (/var/task/node_modules/graphql/execution/execute.js:590:12)",
" at /var/task/node_modules/graphql/execution/execute.js:620:25"
]
}
}
}, ...
}
Expected behavior
Should have returned the count value of 2000 rather than trying to parse { low: 2000, high: 0 }
.
System (please complete the following information):
Additional context
@darrellwarde stated on a discussion on Discord in the #graphql channel:
"The only thing that really stands out to me here is that the error is being thrown from the default GraphQL Int scalar, but we have our custom scalar which would be located at /var/task/node_modules/@neo4j/graphql/dist/schema/scalars/Int.js on your machine. So the error itself is valid, the unusual behaviour is that our custom scalar isn't being used... Those are my initial thoughts"
It should be the goal that the API for the java and javascript version are compatible.
That means especially that the used directives and the generated schema are identical or at least compatible.
To achieve this, I propose to set up an umbrella project that will take care of the specification of the API.
This umbrella project will contain the definitions for the directives, documentation and test-cases for the augmented schema.
The TCK for the generated cypher should not be the focus of this project.
I really like the review process we started here, but would like to separate it from @neo4j/graphql
.
I believe I've found a bug in @neo4j/graphql
. To reproduce it, see below.
package.json
{
"dependencies": {
"@neo4j/graphql": "1.0.0-beta.2",
"apollo-server": "2.23.0",
"graphql": "15.5.0",
"neo4j-driver": "4.2.3"
}
}
server.js
'use strict';
const { Neo4jGraphQL } = require('@neo4j/graphql');
const neo4j = require('neo4j-driver');
const driver = neo4j.driver(
'bolt://localhost:7687',
neo4j.auth.basic('*****', '*****'),
);
const typeDefs = `
type Tiger {
x: Int
}
type TigerJawLevel2 {
id: ID
part1: TigerJawLevel2Part1 @relationship(type: "REL1", direction: OUT)
}
type TigerJawLevel2Part1 {
id: ID
tiger: Tiger @relationship(type: "REL2", direction: OUT)
}
`;
new Neo4jGraphQL({
driver,
typeDefs,
});
npm install --no-save
.node server.js
.Nothing. That is, the command should exit almost immediately, with no output.
The following error message:
/app/node_modules/graphql/validation/validate.js:107
throw new Error(errors.map(function (error) {
^
Error: There can be only one type named "TigerJawLevel2Part1DeleteInput".
at assertValidSDL (/app/node_modules/graphql/validation/validate.js:107:11)
at Object.buildASTSchema (/app/node_modules/graphql/utilities/buildASTSchema.js:45:34)
at buildSchemaFromTypeDefinitions (/app/node_modules/@graphql-tools/schema/index.cjs.js:203:26)
at Object.makeExecutableSchema (/app/node_modules/@graphql-tools/schema/index.cjs.js:749:18)
at Object.makeAugmentedSchema (/app/node_modules/@neo4j/graphql/dist/schema/make-augmented-schema.js:594:29)
at new Neo4jGraphQL (/app/node_modules/@neo4j/graphql/dist/classes/Neo4jGraphQL.js:40:44)
at Object.<anonymous> (/app/server.js:27:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
TigerJawLevel2Part1DeleteInput
, the type that appears in the error message, is obviously a type created by @neo4j/graphql
. (See the documentation here.) It appears that @neo4j/graphql
creates two instances of this type. That behavior is surely a bug.
Is your feature request related to a problem? Please describe.
One the features in neo4j-graphql-js
which I found mind-blowingly powerful was the super simple way to add support for full-text search, using the @search
directive.
Describe the solution you'd like
I would really like a solution similar to the current @search
directive, where a directive on the type definition adds the necessary indexes and adds a search-parameter to the generated queries.
Describe alternatives you've considered
Using regex-filters would give the possibility to create a case-insensitive "search". However, it lacks many of the really powerful features of the lucene based search experience, like fuzzy matching.
Is your feature request related to a problem? Please describe.
Looking for the ability to subscribe to events via the Neo4j GraphQL API.
Additional context
Came from here #179
Is your feature request related to a problem? Please describe.
Ability to add properties on relationships
Additional context
Came from here #179
for the given schema
type Movie {
id: ID
}
the following schema will be generated:
// ... the rest
type CreateMoviesMutationResponse {
movies: [Movie!]!
}
type UpdateMoviesMutationResponse {
movies: [Movie!]!
}
type Mutation {
createMovies(input: [MovieCreateInput]!): CreateMoviesMutationResponse!
deleteMovies(where: MovieWhere): DeleteInfo!
updateMovies(where: MovieWhere, update: MovieUpdateInput): UpdateMoviesMutationResponse!
}
Is there a reason, why you encapsulate the result of the mutations in a separate type like CreateMoviesMutationResponse
or UpdateMoviesMutationResponse
and not generate a schema like this:
type Mutation {
createMovies(input: [MovieCreateInput]!): [Movie!]!
deleteMovies(where: MovieWhere): DeleteInfo!
updateMovies(where: MovieWhere, update: MovieUpdateInput): [Movie!]!
}
?
e.g. a use case for this might be that there is a @cypher
-defined property that fetches a list of records. It would be useful to be able to add parameters that can be used by the user-defined query e.g. pass in a number, X, and use it to only retrieve records newer than X days old.
It seems to me that at the database and resolver level this would be fairly straightforward to implement, but more problematic when it comes to designing the directive and schema. I'm not gonna even try to propose what it might look like as I can't think of anything that isn't quite ugly/hacky.
Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
I would like to enforce the uniqueness of certain fields, which I was able to do using neoj4-graphql-js.
Describe the solution you'd like
A clear and concise description of what you want to happen.
@unique directive should give the nodes property a unique constraint
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Otherwise, I have to rely on developers not writing bugging code
Additional context
Add any other context or screenshots about the feature request here.
As mentioned, unions no longer work at the top level, and interfaces no longer work at the top level or on relationships.
This is a major issue for me, and I hope that we can find a way to maintain this functionality in the new library. I'd like to highlight several use cases that are relevant to me:
Search is the canonical example for unions, and it's not clear how we can replicate this behavior without them.
union SearchResult =
Album
| Song
| Artist
| UserPlaylist
| FeaturedPlaylist
| Podcast
type Query {
Search(string: String!): [SearchResult!]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('names', $string+'~') YIELD node, score
RETURN node
"""
)
}
Assuming the below schema excerpt, we may want to query for all of a user's activity, ordered by createdAt
time
interface Activity {
id: ID!
createdAt: _Neo4jDateTime!
user: User! @relation(name: "BY_USER", direction: "OUT")
}
type AudioListen implements Activity {
id: ID!
createdAt: _Neo4jDateTime!
user: User! @relation(name: "BY_USER", direction: "OUT")
audio: Audio! @relation(name: "AUDIO_LISTENED", direction: "OUT")
}
type VideoView implements Activity {
id: ID!
createdAt: _Neo4jDateTime!
user: User! @relation(name: "BY_USER", direction: "OUT")
video: Video! @relation(name: "VIDEO_VIEWED", direction: "OUT")
}
type User {
id: ID!
name: String
createdAt: _Neo4jDateTime
activity: [Activity!]!
@cypher(
statement: """
// get all activity, sorted by createdAt field
"""
)
}
Assuming the below schema excerpt, we may wish to query across all Audio
types.
interface Audio {
id: ID!
name: String!
url: String!
listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}
type Song implements Audio {
id: ID!
name: String!
url: String!
artists: [Artist!]! @relation(name: "BY_ARTIST", direction: "OUT")
albums: [Album!]! @relation(name: "HAS_SONG", direction: "IN")
listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}
type Podcast implements Audio {
id: ID!
name: String!
url: String!
host: User!
listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}
Each of these works fine with the existing neo4j-graphql-js
package. I would advocate strongly for ensuring that these and similar use cases can be easily implemented with the new package.
I would suggest to make the values of the operations
argument an enum. This makes it clearer to the user what values are available.
So for *
an enum value of ALL
could be used.
So the directive declaration would be:
directive @exclude(operation:[ExcludeOptions!]! = [ALL]) on OBJECT
enum ExcludeOptions{
CREATE,
READ,
UPDATE,
DELETE
ALL
}
setting the default value to ALL
would allow to use the exclude directive without argument, so one can use it like:
type Actor @exclude {
name: String
}
A user in slack asked for an ability to enforce the maximum number of returned entities.
To achieve this, I would propose the following new directives:
directive @minValue(value:Float!) on ARGUMENT_DEFINITION | FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @maxValue(value:Float!) on ARGUMENT_DEFINITION | FIELD_DEFINITION | INPUT_FIELD_DEFINITION
so it can be used like this:
type User{
name: String
size: Float @minValue(value: 20) @maxValue(value: 250)
}
input UserOptions {
limit: Int @minValue(value: 0) @maxValue(value: 100)
skip: Int @minValue(value: 0)
}
Is your feature request related to a problem? Please describe.
Currently security setup for Update mutation is all or none, we cannot define permissions for certain fields
Describe the bug
When using a custom Query
or Mutation
and you have a reference type, that has a DateTime
:
type Node {
createdAt: DateTime
}
type Query {
nodes: [Node] @cypher(statement: "")
}
Error is thrown:
node_modules\graphql-compose\lib\TypeStorage.js:44
throw new Error(`Type with name ${(0, _misc.inspect)(typeName)} does not exists`);
^
Error: Type with name "DateTime" does not exists
at SchemaComposer.get (node_modules\graphql-compose\lib\TypeStorage.js:44:13)
If you remove the type Query
or similar type Mutation
it works!
System
Describe the bug
When sorting a nested selection the ordering is not applied if the field used for ordering is not included in the selection set. Sorting works as expected when the field is included in the selection set.
Type definitions
This Codesandbox can be used to demonstrate the issue.
To Reproduce
This query (note that year
is not included in the acted_in
selection set:
{
movies(where: { title_CONTAINS: "Matrix Reloaded, The" }) {
title
actors(options: { limit: 1 }) {
name
acted_in(options: { limit: 3, sort: { year: DESC } }) {
title
}
}
}
}
produces this result:
{
"data": {
"movies": [
{
"title": "Matrix Reloaded, The",
"actors": [
{
"name": "Keanu Reeves",
"acted_in": [
{
"title": "Matrix Reloaded, The"
},
{
"title": "Side by Side"
},
{
"title": "Matrix Revolutions, The"
}
]
}
]
}
]
}
}
however, when year
is included in the selection set:
{
movies(where: { title_CONTAINS: "Matrix Reloaded, The" }) {
title
actors(options: { limit: 1 }) {
name
acted_in(options: { limit: 3, sort: { year: DESC } }) {
title
year
}
}
}
}
then ordering of the acted_in
array is properly sorted in the result:
{
"data": {
"movies": [
{
"title": "Matrix Reloaded, The",
"actors": [
{
"name": "Keanu Reeves",
"acted_in": [
{
"title": "The Neon Demon",
"year": 2016
},
{
"title": "John Wick",
"year": 2014
},
{
"title": "47 Ronin",
"year": 2013
}
]
}
]
}
]
}
}
Expected behavior
I expect ordering to be applied to the nested field even if the field used to order is not included in the selection set.
System (please complete the following information):
In the java version we generate an additionally NOT
filter allowing to negate an complex filter.
type Movie {
id: ID
actorCount: Int
averageRating: Float
isActive: Boolean
}
would result in
input _MovieFilter {
AND: [_MovieFilter!]
NOT: [_MovieFilter!]
OR: [_MovieFilter!]
actorCount: Int
actorCount_gt: Int
actorCount_gte: Int
actorCount_in: [Int]
actorCount_lt: Int
actorCount_lte: Int
actorCount_not: Int
actorCount_not_in: [Int]
averageRating: Float
averageRating_gt: Float
averageRating_gte: Float
averageRating_in: [Float]
averageRating_lt: Float
averageRating_lte: Float
averageRating_not: Float
averageRating_not_in: [Float]
id: ID
id_contains: ID
id_ends_with: ID
id_gt: ID
id_gte: ID
id_in: [ID]
id_lt: ID
id_lte: ID
id_matches: ID
id_not: ID
id_not_contains: ID
id_not_ends_with: ID
id_not_in: [ID]
id_not_starts_with: ID
id_starts_with: ID
isActive: Boolean
isActive_not: Boolean
}
Note also that the OR
, AND
and NOT
filters have not nullable items!
While the new library is in alpha I thought it's a good point to address IMHO the biggest niggle in neo4j-graphql-js. The following filter:
{
Systems(filter: {
lifecycleStage_not: Decommissioned
}) {
name
lifecycleStage
}
}
excludes Systems whose lifecycleStage is not set, which is quite unintuitive and is pretty much never what any of the users of our API want or expect. As a consequence, our codebases are littered with long-winded filters such as the following:
{
OR: [{lifecycleStage_not: Decommissioned}, {lifecycleStage: null}]
}
In the java world (JPA) we have annotations -which are directives in graphql - for auditing data
Instead of using one annotation to cover all usecases like: @autogenerate(operations: ["create"])
I would suggest to use different directives like:
@CreatedDate
@LastModifiedDate
In future also:
@CreatedBy
@LastModifiedBy
In the java version we currently support the relation filters _some
, _none
, _single
and _every
As stated by @danstarns in slack:
Good point -
_single
and_some
are missing. We removed_none
because its the same as:{ User(filter: { posts_not: { id: 1 } }) { id } }And
_every
is the same as:{ User(filter: { posts: { id: 1 } }) { id } }
I would suggest to distinguish between n..1
and n..m
relations:
n..m
-relationsI realized that users may use the _every
but expecting it to act like the _some
filter:
{
User(filter: { posts: { id: 1 } }) {
id
}
}
That's why I would vote for make all 4 filters explicit:
_every
: Filters only those ${type.name}
for which all ${field.name}
-relationships matches this filter_some
: Filters only those ${type.name}
for which at least one ${field.name}
-relationship matches this filter_single
: Filters only those ${type.name}
for which exactly one ${field.name}
-relationship matches this filter_none
: Filters only those ${type.name}
for which none of the ${field.name}
-relationships matches this filterand remove <field>_not
and <field>
for n..m
relations!
It's much clearer to the user what he can expect as result, especially with the given description on each field.
n..1
-relationsThe filter above does not make sense for n..1
relations, so I would keep the current API with <field>_not
and <field>
for these types of relations.
Is your feature request related to a problem? Please describe.
GraphQL support for Neo4j Fabric mode.
Describe the solution you'd like
It would be great to see an example with this package demonstrating Fabric support.
Describe alternatives you've considered
As I know previous effors on neo4j-graphql-js didn't support Fabric mode.
Describe the bug
We have several tools for linting and code formatting, and as it stands they are clashing somewhat.
We need to decide on a standard, configure it uniformly across each tool, and then format the whole codebase to get ourselves on a level playing field across the board.
Hi there! I'm using v1.0.0-alpha.2
and am experiencing an issue with auto-generated mutations erroring out on the @neo4j/graphql
side.
I have an example type AuthenticatedUser
that is defined as follows:
I see that the library has generated the expected queries and mutations for it:
However, when I try to run the mutation from within GraphIQL, it gives me the following error:
{
"errors": [
{
"message": "Invalid input '{': expected whitespace, comment, namespace of a procedure or a procedure name (line 1, column 6 (offset: 5))\n\"CALL {\"\n ^",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createAuthenticatedUsers"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"code": "Neo.ClientError.Statement.SyntaxError",
"name": "Neo4jError",
"stacktrace": [
"Neo4jError: Invalid input '{': expected whitespace, comment, namespace of a procedure or a procedure name (line 1, column 6 (offset: 5))",
"\"CALL {\"",
" ^",
"",
" at captureStacktrace (/usr/src/app/node_modules/neo4j-driver/lib/result.js:275:15)",
" at new Result (/usr/src/app/node_modules/neo4j-driver/lib/result.js:66:19)",
" at newCompletedResult (/usr/src/app/node_modules/neo4j-driver/lib/transaction.js:446:10)",
" at Object.run (/usr/src/app/node_modules/neo4j-driver/lib/transaction.js:285:14)",
" at Transaction.run (/usr/src/app/node_modules/neo4j-driver/lib/transaction.js:121:32)",
" at /usr/src/app/node_modules/@neo4j/graphql/dist/cjs/utils/execute.js:64:39",
" at TransactionExecutor._safeExecuteTransactionWork (/usr/src/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:132:22)",
" at TransactionExecutor._executeTransactionInsidePromise (/usr/src/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:120:32)",
" at /usr/src/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:59:15",
" at new Promise (<anonymous>)",
" at TransactionExecutor.execute (/usr/src/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:58:14)",
" at Session._runTransaction (/usr/src/app/node_modules/neo4j-driver/lib/session.js:300:40)",
" at Session.writeTransaction (/usr/src/app/node_modules/neo4j-driver/lib/session.js:293:19)",
" at Object.<anonymous> (/usr/src/app/node_modules/@neo4j/graphql/dist/cjs/utils/execute.js:63:104)",
" at step (/usr/src/app/node_modules/@neo4j/graphql/dist/cjs/utils/execute.js:33:23)",
" at Object.next (/usr/src/app/node_modules/@neo4j/graphql/dist/cjs/utils/execute.js:14:53)"
]
}
}
}
],
"data": null
}
In the java version the user can define undirected relations.
This feature is currently missing here.
Schema:
type User
{
name: String!
uuid: ID!
associates: [User!] @relation(name:"ASSOCIATES_WITH", direction:BOTH)
}
Query:
query {
user(uuid: $uuid) {
uuid
name
associates { name uuid }
}
}
Cypher:
MATCH (user: User)
WHERE user.uuid = $uuid
RETURN user {
.uuid,
.name,
associates: [(user)-[: ASSOCIATES_WITH]-(userAssociates: User) | userAssociates {
.name,
.uuid
}]
} AS user
Probably related to #167
Describe the bug
A standalone Apollo Server, stop working if I instantiate Neo4jGraphQL (eg new Neo4jGraphQL(...)
) even if the resulting schema is never used. This seems to happen in conjunction with mergeSchemas()
- even if no Neo4jGraphQL schema is ever used.
To Reproduce
Consider this example:
import {
ApolloServer,
makeExecutableSchema,
mergeSchemas,
} from 'apollo-server-express';
import express from 'express';
import { Neo4jGraphQL } from '@neo4j/graphql';
const baseSchema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Query {
stuff: String
}
type Mutation {
mutateSomething(input: Int!): String!
}
`,
resolvers: {
Mutation: {
mutateSomething: () => 'response',
},
},
});
// START FAULTY CODE
// NOTE: The Neo4jGraphQL instance is never even used.
// NOTE: If you remove the instantiation of Neo4jGraphQL here `new
// Neo4jGraphQL(...)` the issue disappears
new Neo4jGraphQL({
typeDefs: /* GraphQL */ `
interface Thing {
id: ID!
}
type SomeType {
someField: String!
}
`,
});
// END FAULTY CODE
const schema = mergeSchemas({
// schemas: [baseSchema, neo4jGraphQL.schema],
schemas: [baseSchema],
});
const port = 4000;
const apolloServer = new ApolloServer({ schema });
const app = express();
apolloServer.applyMiddleware({ app });
app.listen({ port }, () =>
console.log(
`🚀 Server ready at http://localhost:${port}${apolloServer.graphqlPath}`,
),
);
Now run this query:
mutation {
mutateSomething(input: 1)
}
Which will fail with:
"Variable \"$_v0_input\" got invalid value { low: 1, high: 0 }; Expected type \"Int\". Cannot represent non number as Int"
As you can see, the result of new Neo4jGraphQL()
is not even collected in a variable. Still the Apollo server breaks. If you comment out the lines between START FAULTY CODE
and END FAULTY CODE
, the Apollo server works like a charm and the example query will return:
{
"data": {
"mutateSomething": "response"
}
}
Alternatively you can also just comment out the type definition, to make the vanilla Apollo server happy:
type SomeType {
someField: String!
}
System (please complete the following information):
"@neo4j/graphql": "1.0.0",
"apollo-server-express": "2.24.0",
"express": "4.17.1",
Describe the bug
I have a use case where one node (e.g. User) is linked to multiple nodes of the same label (e.g. User Demographics), each User can be linked to one or more different types of demographics depending on the client the users are under. I wanted to write a query to fetch users that have specific demographic data. But the results I am getting for the related queries is not expected. Below explains the use case and the result I am getting and what I actually expected.
Type definitions
type User {
client_id: String
uid: String
demographics: [UserDemographics] @relationship(type: "HAS_DEMOGRAPHIC", direction: OUT)
}
type UserDemographics {
client_id: String,
type: String,
value: String,
users: [User] @relationship(type: "HAS_DEMOGRAPHIC", direction: IN)
}
To Reproduce
Steps to reproduce the behavior:
Create (:User {uid: "user1"}),(:User {uid: "user2"}),(:UserDemographics{type:"Gender",value:"Female"}),(:UserDemographics{type:"Gender",value:"Male"}),(:UserDemographics{type:"Age",value:"50+"}),(:UserDemographics{type:"State",value:"VIC"})
MATCH (u:User {uid: 'user1'}) MATCH (d:UserDemographics {type: 'Gender', value:'Female'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
MATCH (u:User {uid: 'user2'}) MATCH (d:UserDemographics {type: 'Gender', value:'Male'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
MATCH (u:User {uid: 'user1'}) MATCH (d:UserDemographics {type: 'Age', value:'50+'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
MATCH (u:User {uid: 'user2'}) MATCH (d:UserDemographics {type: 'Age', value:'50+'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
MATCH (u:User {uid: 'user1'}) MATCH (d:UserDemographics {type: 'State', value:'VIC'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
MATCH (u:User {uid: 'user2'}) MATCH (d:UserDemographics {type: 'State', value:'VIC'}) create (u)-[:HAS_DEMOGRAPHIC]->(d)
query{
users(where: {demographics:{
type:"Gender", value:"Female"}
}){
uid
demographics{
type
value
}
}
}
{
"data": {
"users": []
}
}
query{
users(where: {demographics:{
OR:[
{type:"Gender", value:"Female"},
{type:"State"}
{type:"Age"},
]}
}){
uid
demographics{
type
value
}
}
}
{
"data": {
"users": [
{
"uid": "user1",
"demographics": [
{
"type": "State",
"value": "VIC"
},
{
"type": "Age",
"value": "50+"
},
{
"type": "Gender",
"value": "Female"
}
]
}
]
}
}
Expected behavior
I expected to see the result shown in step 6 to be returned when running the query in step 3. I also expected to see all users listed as a result of the query in step 5 since there is an OR operation
System Details:
Additional context
When running server with Debug Logging DEBUG=@neo4j/graphql:* node src/index.js
I can see the generated cypher for the query is using ALL when I replace ALL with ANY and run the same cypher query in neo4j browser, I get the expected result for both queries. Is it done intentionally? From my perspective it doesn't seem intuitive and I don't feel it would be right to list all the possible demographic types when doing the query as this list can change per client and can get updated over time.
Cypher:
MATCH (this:User)
WHERE EXISTS((this)-[:HAS_DEMOGRAPHIC]->(:UserDemographics)) AND ALL(this_demographics IN [(this)-[:HAS_DEMOGRAPHIC]->(this_demographics:UserDemographics) | this_demographics] WHERE this_demographics.type = $this_demographics_type AND this_demographics.value = $this_demographics_value)
RETURN this { .uid, demographics: [ (this)-[:HAS_DEMOGRAPHIC]->(this_demographics:UserDemographics) | this_demographics { .type, .value } ] } as this
Params:
{
"this_demographics_type": "Gender",
"this_demographics_value": "Female"
}
I'd like to be able to create custom authorization checks utilizing the @cypher
directive. An example of this would be to check if one user is connected to another user via a matched path (that can't be matched via a @relationship
):
type Group {
id: ID! @id
name: String!
members: [User!] @relationship(type: ":MEMBER_OF", direction: IN)
}
type User {
id: ID! @id
name: String!
groups: [Group!] @relationship(type: ":MEMBER_OF", direction: OUT)
isConnected: Boolean @cypher(statement: """
MATCH (this)-[:MEMBER_OF]->(:Group)<-[:MEMBER_OF]-(u:User)
WHERE u.id = $auth.jwt.sub
RETURN count(u) > 0
""")
}
extend type User {
@auth(rules: [{
operations: [READ]
allow: {isConnected: true}
}])
}
This would allow the current user to view another user only if they were connected via a shared Group
.
I can think of many other situations where this kind of flexibility would be useful and prevent the need for creating custom resolvers. It might even solve #159.
In the examples I saw that the @relationship
directive is used with a string value as the directions' argument.
I would suggest to make this field an enum value, so the user knows the possible values.
directive @relationship(type:String!, direction: RelationDirection = OUT) on FIELD_DEFINITION
enum RelationDirection {
IN
OUT
}
As explained in neo4j-graphql/neo4j-graphql-java#64 I would suggest to change the way sorting is defined.
Given the Schema:
type Movie { title: String publishedBy: Publisher @relation(name: "PUBLISHED_BY", direction: OUT) } type Publisher { name: ID! }The augmentation would be:
type Query { movie(sort: [_MovieSorting!]): [Movie!]! } enum SortDirection{ asc, desc, } input _MovieSorting { title: SortDirection publishedBy: [_PublisherSorting!] } input _PublisherSorting { name: SortDirection }So a user can get all movies ordered by
Publisher.name
andtitle
by callingquery { movie(sort: [{publishedBy: [{name: desc}]}, {title: asc}]) { title } }which results in this query
MATCH (m:Movie) WITH m ORDER BY head([(m)-[:PUBLISHED_BY]->(p:Publisher) | p.name]) DESC, m.name ASC RETURN m{.title}This sorting should be possible for all 1..1 relations.
Is your feature request related to a problem? Please describe.
As a developer, I need to have a flexible option validate date as a condition for authenticity.
Describe the solution you'd like
I would like to have an option to have a rule called "query" who's value is a graphql query. The query is evaluated just as any other query as a condition. If the condition is true, then the operation is authenticated.
@auth(rules: [{ // Authenticated
operations: [CREATE],
isAuthenticated: true,
query: {
match: "bob",
gql: `users(where: {name: "bob"}) { name }`
}
}])
@auth(rules: [{ // NOT Authenticated
operations: [CREATE],
isAuthenticated: true,
query: {
match: "allen",
gql: `users(where: {name: "bob"}) { name }`
}
}])
const myAuthQuery = gql`
users(where: {
name: "bob"
}) {
name
}
`
@auth(rules: [{ // Authenticated
operations: [CREATE],
isAuthenticated: true,
query: {
match: "bob",
gql: myAuthQuery
}
}])
@auth(rules: [{ // Authenticated
operations: [CREATE],
isAuthenticated: true,
query: {
match: "bob",
cypher: "MATCH (u:User {name: "bob"}) return u.name"
}
}])
@auth(rules: [{ // NOT Authenticated
operations: [CREATE],
isAuthenticated: true,
query: {
match: "allen",
cypher: "MATCH (u:User {name: "bob"}) return u.name"
}
}])
Describe alternatives you've considered
I have implemented on the client by calling similar queries as validation, but that still potentially leave the endpoint compromised, or I have to limit application functionality.
Additional context
Nope... just the code example above. PS... this is a direct functionality lift of another graphq DB's graphql solution.
Currently the @auth
directive is highly dynamical through itsallow
and bind
arguments. This usage cannot be used in the java version since the compiled schema will not be valid.
The arguments of a graphql directive must be of known types!
So I would suggest to change the @auth
directive to the following format:
# You can put the `@auth` directive also on a field with the `@cypher` directive.
# Functionality like allow and bind will not work but you can still utilize `isAuthenticated` and `roles`.
# Notice you don't need to specify operations for `@auth` directives on `@cypher` fields.
directive @auth(
# You can have many rules for many operations.
# We fallthrough each rule, on the corresponding operation, until a match.
# On no match, an error is thrown. You can think of rules as a big OR.
rules: [AuthRule!]!
) on FIELD_DEFINITION|OBJECT
input AuthRule{
# Operations is an array, you can re-use the same rule for many operations.
operations: [AuthOperations!]
# This is the most basic of auth. Used to ensure that there is a valid decoded JWT in the request
isAuthenticated: Boolean
# Use the roles property to specify the allowed roles for an operation.
roles: [String!]
# Use allow to ensure, on matched nodes, a connection exists between a value on the JWT vs a property on each matched node.
# Allow is used on the following operations:
# * read
# * update
# * connect
# * disconnect
# * delete
# When you specify allow on a relationship you can select fields on the referenced node.
# It's worth pointing out that allow on a relationship will perform an `ANY` on the matched nodes: to see if there is a match.
# Allow works the same as it does on Type Definitions although its context is the Field.
# So instead of enforcing auth rules when the node is matched and or upserted, it would instead be called when the Field is selected or upserted.
allow: AllowValue
# Use bind to ensure, on creating or updating nodes, a connection exists between a value on the JWT vs a property on a matched node.
# This validation is done after the operation but inside a transaction.
# Bind is used on the following operations:
# * read
# * update
# * connect
# * disconnect
# * delete
# There may be a reason where you need to traverse across relationships to satisfy your Auth implementation.
# One example of this could be "Ensure that users only create Posts related to themselves"
# When you specify `bind` on a relationship you can select fields on the referenced node.
# It's worth pointing out that allow on a relationship will perform an `ALL` on the matched nodes; to see if there is a match.
# This means you can only use `bind` to enforce a single relationship to a single node.
# You can use bind on a field. The root is still considered the node.
bind: [PathValue!]
OR: [AuthRule!]
AND: [AuthRule!]
}
enum AuthOperations {
# MATCH
read
# CREATE
create
# SET
update
# DELETE
delete
# MATCH & MERGE
connect
# MATCH & DELETE
disconnect
# permit for all operations
all
}
input AllowValue {
AND: [PathValue!]
OR: [PathValue!]
path: String
value: String
}
input PathValue {
path: String!
value: String!
}
The previous example of:
type User {
id: ID
name: String
}
type Post {
content: String
moderators: [User] @relationship(type: "MODERATES_POST", direction: "IN")
creator: User @relationship(type: "HAS_POST", direction: "IN")
}
extend type Post
@auth(
rules: [
{
operations: ["update"],
allow: { OR: [{ moderators: { id: "$jwt.sub" } }, { creator: { id: "$jwt.sub" } }] }
}
]
)
would then be migrated to:
extend type Post
type User {
id: ID
name: String
}
type Post {
content: String
moderators: [User] @relationship(type: "MODERATES_POST", direction: "IN")
creator: User @relationship(type: "HAS_POST", direction: "IN")
}
@auth(
rules: [
{
operations: [update],
allow: {
OR: [
{ path: "moderators.id", value: "$jwt.sub"}
{ path: "creator.id", value: "$jwt.sub"}
]
}
}
]
)
Note 1
recursion for input arguments for directive arguments seems not to work
so changing:
input AllowValue {
AND: [PathValue!]
OR: [PathValue!]
path: String
value: String
}
to:
input AllowValue {
AND: [AllowValue!]
OR: [AllowValue!]
path: String
value: String
}
results in errors.
Note 2
Note that I changed operations
to an enum type as well
Provide an @property
directive to change the name used for the database mappings.
With schema:
type Person {
id : ID! @property(name:"an-id")
name: String @property(name:" a name ")
}
And query:
{ person(id:"32",name:"Jane") { name }}
Should generate something like:
MATCH (person:Person)
WHERE (person.`an-id` = $personId
AND person.` a name ` = $personName)
RETURN person {
name: person.` a name `
} AS person
Error
[2021-02-18T07:25:42.150Z] Worker was unable to load function graphql: 'Error: Unexpected error: Unable to resolve a valid GraphQLSchema. Please file an issue with a reproduction of this error, if possible.'
[2021-02-18T07:25:42.154Z] Worker failed to function id eb16320b-aebf-4b61-b38d-9182e11d37da.
[2021-02-18T07:25:42.164Z] Result: Failure
Exception: Worker was unable to load function graphql: 'Error: Unexpected error: Unable to resolve a valid GraphQLSchema. Please file an issue with a reproduction of this error, if possible.'
Stack: Error: Unexpected error: Unable to resolve a valid GraphQLSchema. Please file an issue with a reproduction of this error, if possible.
at new ApolloServerBase (C:\workPlease\node_modules\apollo-server-core\dist\ApolloServer.js:169:19)
at new ApolloServer (C:\workPlease\node_modules\apollo-server-azure-functions\dist\ApolloServer.js:16:1)
at Object.<anonymous> (C:\workPlease\graphql\index.js:38:16)
at Module._compile (internal/modules/cjs/loader.js:1251:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1272:10)
at Module.load (internal/modules/cjs/loader.js:1100:32)
at Function.Module._load (internal/modules/cjs/loader.js:962:14)
at Module.require (internal/modules/cjs/loader.js:1140:19)
at require (internal/modules/cjs/helpers.js:75:18)
at FunctionLoader.load (C:\Program Files\Microsoft\Azure Functions Core Tools\workers\node\worker-bundle.js:20979:22).
"**dependencies**": {
"@neo4j/graphql": "^1.0.0-alpha.4",
"apollo-server-azure-functions": "^2.19.0",
"dotenv": "^8.2.0",
"find-duplicate-dependencies": "^2.1.1",
"graphql": "^15.5.0",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.12.0",
"neo4j-driver": "^4.2.1"
}
**Schema**
type Owner {
id:ID!
}
requested Server construction
const typeDefs = require("./graphql-schema");
...
const schema = makeAugmentedSchema({
typeDefs
});
...
const server = new ApolloServer(
{
schema,
playground:{
settings:{
'schema.polling.interval': 90000,
},
},
context: ({ req }) => ({ req, driver })
});
Hey guys, while I am really excited about this new release, I find that there are a few powerful features I really enjoy in the previous neo4j-graphql.js
library are missing in this release. I personally use assertSchema
and seachSchema
a lot, so I wonder if there would be a further update so that I can keep my use of @index
@unique
@search
in my graphql schema.
Describe the bug
The following tests in packages/graphql/tests/integration/auth/roles.int.test.ts
fail if they are run against an empty database:
This bug, and the fact that the behaviour varies based on whether the tests are run in isolation, suggests that individual tests are relying on data produced by other tests to pass.
More worryingly, it seems that if there are no records for a type labelled with an @auth
directive, it seems that the library will allow unauthenticated/unauthorized users to figure out the fact that there is no data. Not a massive breach, but they shouldn't even be able to do that.
To Reproduce
Steps to reproduce the the top two failures:
MATCH (n) DETACH DELETE n
yarn test:int
Steps to reproduce the the bottom two failures:
MATCH (n) DETACH DELETE n
yarn test:int -t "should throw if missing role on type definition"
Expected behavior
Tests to pass, @auth
directive to block unauthenticated/unauthorized users from seeing if there is no data for a certain type.
System
Is your feature request related to a problem? Please describe.
As a user of the OGM, I would like to see autogenerated typescript types. Currently, I have to use generics and cross-check with the GraphQL API.
Describe the solution you'd like
A script I can call to produce typings for the OGM.
Additional context
Please express interest in this issue so we can gauge demand.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.