Skip to content

Commit

Permalink
Commit to get branch to the point to which filtering will work. Howev…
Browse files Browse the repository at this point in the history
…er, the code here will not work for the `single` case, as it will try to add subqueries inside a list comprehension.
  • Loading branch information
darrellwarde committed Feb 27, 2025
1 parent 27b3fd6 commit 837275e
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ export class ConnectionFilter extends Filter {
const returnVar = new Cypher.Variable();
const innerFiltersPredicates: Cypher.Predicate[] = [];

const authFilterSubqueries = this.getAuthFilterSubqueries(this.target.name, queryASTContext).map((sq) =>
new Cypher.Call(sq).importWith(queryASTContext.target)
);

const subqueries = this.innerFilters.flatMap((f) => {
const nestedSubqueries = f
.getSubqueries(queryASTContext)
Expand All @@ -231,7 +235,7 @@ export class ConnectionFilter extends Filter {
return clauses;
});

if (subqueries.length === 0) return []; // Hack logic to change predicates logic
// if (subqueries.length === 0) return []; // Hack logic to change predicates logic

const comparisonValue = this.operator === "NONE" ? Cypher.false : Cypher.true;
this.subqueryPredicate = Cypher.eq(returnVar, comparisonValue);
Expand All @@ -244,7 +248,7 @@ export class ConnectionFilter extends Filter {
const withPredicateReturn = new Cypher.With("*")
.where(Cypher.and(...innerFiltersPredicates))
.return([countComparisonPredicate, returnVar]);
return [Cypher.utils.concat(match, ...subqueries, withPredicateReturn)];
return [Cypher.utils.concat(match, ...authFilterSubqueries, ...subqueries, withPredicateReturn)];
}

// This method has a big deal of complexity due to a couple of factors:
Expand Down Expand Up @@ -311,4 +315,11 @@ export class ConnectionFilter extends Filter {

return filterTruthy(authFilters.map((f) => f.getPredicate(context)));
}

protected getAuthFilterSubqueries(name: string, context: QueryASTContext): Cypher.Clause[] {
const authFilters = this.authFilters[name];
if (!authFilters) return [];

return filterTruthy(authFilters.flatMap((f) => f.getSubqueries(context)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ export class RelationshipFilter extends Filter {

const nestedSubqueries = this.targetNodeFilters.flatMap((f) => f.getSubqueries(nestedContext));
const nestedSelection = this.getNestedSelectionSubqueries(nestedContext);
const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext);

if (nestedSubqueries.length > 0) {
subqueries.push(...this.getNestedSubqueries(nestedContext));
Expand All @@ -102,10 +101,6 @@ export class RelationshipFilter extends Filter {
subqueries.push(...nestedSelection);
}

if (authFilterSubqueries.length > 0) {
subqueries.push(...authFilterSubqueries);
}

return subqueries;
}

Expand Down Expand Up @@ -160,6 +155,11 @@ export class RelationshipFilter extends Filter {
context: QueryASTContext
): Cypher.Predicate | undefined {
const predicates = this.targetNodeFilters.map((c) => c.getPredicate(context));

const authFilterSubqueries = this.getAuthFilterSubqueries(context).map((sq) =>
new Cypher.Call(sq).importWith("*")
);

const authPredicates = this.getAuthFilterPredicate(context);
const innerPredicate = Cypher.and(...authPredicates, ...predicates);

Expand All @@ -186,12 +186,23 @@ export class RelationshipFilter extends Filter {
}
case "NONE":
case "SOME": {
let exists: Cypher.Exists;

const match = new Cypher.Match(pattern);

if (innerPredicate) {
match.where(innerPredicate);
if (authFilterSubqueries.length > 0) {
const withPredicateReturn = new Cypher.With("*").where(Cypher.and(innerPredicate));
const clause = Cypher.utils.concat(match, ...authFilterSubqueries, withPredicateReturn);
exists = new Cypher.Exists(clause);
} else {
match.where(innerPredicate);
exists = new Cypher.Exists(match);
}
} else {
exists = new Cypher.Exists(match);
}

const exists = new Cypher.Exists(match);
if (this.operator === "NONE") {
return Cypher.not(exists);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -940,5 +940,155 @@ describe("auth/where", () => {

expect(gqlResult.data).toEqual({ [Post.plural]: [{ id: postId1 }] });
});

test("should add filter to relationship filter for users with over 2 posts", async () => {
const typeDefs = /* GraphQL */ `
type ${User} @node {
id: ID
name: String
posts: [${Post}!]! @relationship(type: "HAS_POST", direction: OUT)
}
type ${Post} @node {
id: ID
creator: [${User}!]! @relationship(type: "HAS_POST", direction: IN)
}
extend type ${User} @authorization(filter: [{ operations: [FILTER], where: { node: { postsAggregate: { count: { gt: 2 } } } } }])
`;

const userId1 = generate({
charset: "alphabetic",
});

const userId2 = generate({
charset: "alphabetic",
});

const postId1 = generate({
charset: "alphabetic",
});

const postId2 = generate({
charset: "alphabetic",
});

const postId3 = generate({
charset: "alphabetic",
});

const postId4 = generate({
charset: "alphabetic",
});

const query = /* GraphQL */ `
{
${Post.plural}(where: { creator: { some: { name: { eq: "darrell" } } } }) {
id
}
}
`;

await testHelper.initNeo4jGraphQL({
typeDefs,
features: {
authorization: {
key: secret,
},
},
});

await testHelper.executeCypher(`
CREATE (:${User} {id: "${userId1}", name: "darrell"})-[:HAS_POST]->(:${Post} {id: "${postId1}"})
CREATE (u:${User} {id: "${userId2}", name: "darrell"})-[:HAS_POST]->(:${Post} {id: "${postId2}"})
CREATE (u)-[:HAS_POST]->(:${Post} {id: "${postId3}"})
CREATE (u)-[:HAS_POST]->(:${Post} {id: "${postId4}"})
`);

const token = createBearerToken(secret, { sub: userId1 });

const gqlResult = await testHelper.executeGraphQLWithToken(query, token);

expect(gqlResult.errors).toBeUndefined();

expect(gqlResult.data).toEqual({
[Post.plural]: expect.toIncludeSameMembers([{ id: postId2 }, { id: postId3 }, { id: postId4 }]),
});
});

test("should add filter to connection filter for users with over 2 posts", async () => {
const typeDefs = /* GraphQL */ `
type ${User} @node {
id: ID
name: String
posts: [${Post}!]! @relationship(type: "HAS_POST", direction: OUT)
}
type ${Post} @node {
id: ID
creator: [${User}!]! @relationship(type: "HAS_POST", direction: IN)
}
extend type ${User} @authorization(filter: [{ operations: [FILTER], where: { node: { postsAggregate: { count: { gt: 2 } } } } }])
`;

const userId1 = generate({
charset: "alphabetic",
});

const userId2 = generate({
charset: "alphabetic",
});

const postId1 = generate({
charset: "alphabetic",
});

const postId2 = generate({
charset: "alphabetic",
});

const postId3 = generate({
charset: "alphabetic",
});

const postId4 = generate({
charset: "alphabetic",
});

const query = /* GraphQL */ `
{
${Post.plural}(where: { creatorConnection: { some: { node: { name: { eq: "darrell" } } } } }) {
id
}
}
`;

await testHelper.initNeo4jGraphQL({
typeDefs,
features: {
authorization: {
key: secret,
},
},
});

await testHelper.executeCypher(`
CREATE (:${User} {id: "${userId1}", name: "darrell"})-[:HAS_POST]->(:${Post} {id: "${postId1}"})
CREATE (u:${User} {id: "${userId2}", name: "darrell"})-[:HAS_POST]->(:${Post} {id: "${postId2}"})
CREATE (u)-[:HAS_POST]->(:${Post} {id: "${postId3}"})
CREATE (u)-[:HAS_POST]->(:${Post} {id: "${postId4}"})
`);

const token = createBearerToken(secret, { sub: userId1 });

const gqlResult = await testHelper.executeGraphQLWithToken(query, token);

expect(gqlResult.errors).toBeUndefined();

expect(gqlResult.data).toEqual({
[Post.plural]: expect.toIncludeSameMembers([{ id: postId2 }, { id: postId3 }, { id: postId4 }]),
});
});
});
});

0 comments on commit 837275e

Please sign in to comment.