Skip to content

Commit

Permalink
fix: allow non-scalar values when using client expressions
Browse files Browse the repository at this point in the history
This fixes an issue where client expressions fail if the value is not
a scalar, e.g. if using `some`.

Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
  • Loading branch information
LucianBuzzo committed Dec 14, 2023
1 parent 7802d6b commit 9fc0a52
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 1 deletion.
26 changes: 25 additions & 1 deletion src/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ const tokenizeWhereExpression = (
let astFragment = {};

const value = where[field];

// Check if the field is an object, if so, we need to recurse
// This is a fairly simple approach but covers most cases like "some", "every", "none" etc.
if (fieldData.kind === "object") {
for (const subField in value) {
const subValue = value[subField];

const { tokens: subTokens, where: subWhere } = tokenizeWhereExpression(
client,
subValue,
table,
fieldData.type,
tokens,
);

tokens = {
...tokens,
...subTokens,
};

where[field][subField] = subWhere;
}
continue;
}
const isNumeric = PRISMA_NUMERIC_TYPES.includes(fieldData.type);
const isColumnName = typeof value === "string" && !!value.match(/^___yates_row_/);
const isContext = typeof value === "string" && !!value.match(/^___yates_context_/);
Expand Down Expand Up @@ -230,7 +254,7 @@ export const expressionToSQL = async (getExpression: Expression, table: string):
expressionContext,
);
// If the raw expression is a promise, then this is a client subselect,
// as opoosed to a plain SQL expression or "where" object
// as opposed to a plain SQL expression or "where" object
const isSubselect = typeof rawExpression === "object" && typeof rawExpression.then === "function";

baseClient.$on("query", (e: any) => {
Expand Down
207 changes: 207 additions & 0 deletions test/integration/expressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,5 +625,212 @@ describe("expressions", () => {

expect(post.id).toBeDefined();
});

// This test case creates an ability that uses a nested "some" clause that filters on a related model
it("should be able to handle expressions that are multi-level objects", async () => {
const initial = new PrismaClient();

const role = `USER_${uuid()}`;

const client = await setup({
prisma: initial,
customAbilities: {
User: {
customReadAbility: {
description: "Read user that made a post with a tag labeled with the tag context value",
operation: "SELECT",
expression: (client: PrismaClient, row, context) => {
return client.post.findFirst({
where: {
authorId: row("id"),
tags: {
some: {
label: context("ctx.label"),
},
},
},
});
},
},
},
},
getRoles(abilities) {
return {
[role]: [abilities.Post.create, abilities.Post.read, abilities.User.customReadAbility, abilities.Tag.read],
};
},
getContext: () => ({
role,
context: {
"ctx.label": "foo",
},
}),
});

const testTitle = `test_${uuid()}`;

const user1 = await adminClient.user.create({
data: {
email: `test-user-${uuid()}@example.com`,
posts: {
create: {
title: testTitle,
tags: {
create: {
label: "foo",
},
},
},
},
},
});

const user2 = await adminClient.user.create({
data: {
email: `test-user-${uuid()}@example.com`,
posts: {
create: {
title: testTitle,
tags: {
create: {
label: "bar",
},
},
},
},
},
});

const result1 = await client.user.findFirst({
where: {
id: user1.id,
},
});

expect(result1).not.toBeNull();

const result2 = await client.user.findFirst({
where: {
id: user2.id,
},
});

expect(result2).toBeNull();
});

// This test case creates an ability that uses multiple nested "some" clauses that span models
it("should be able to handle expressions that are deep multi-level objects", async () => {
const initial = new PrismaClient();

const role = `USER_${uuid()}`;

const testTitle = `test_${uuid()}`;

const client = await setup({
prisma: initial,
customAbilities: {
User: {
customReadAbility: {
description:
"Read user that made a post with a tag that is also attached to a post with the title context value",
operation: "SELECT",
expression: (client: PrismaClient, row, context) => {
return client.post.findFirst({
where: {
authorId: row("id"),
tags: {
some: {
posts: {
some: {
title: context("ctx.title"),
},
},
},
},
},
});
},
},
},
},
getRoles(abilities) {
return {
[role]: [abilities.Post.create, abilities.Post.read, abilities.User.customReadAbility, abilities.Tag.read],
};
},
getContext: () => ({
role,
context: {
"ctx.title": testTitle,
},
}),
});

const user1 = await adminClient.user.create({
data: {
email: `test-user-${uuid()}@example.com`,
posts: {
create: {
title: testTitle,
tags: {
create: {
label: uuid(),
},
},
},
},
},
include: {
posts: {
include: {
tags: true,
},
},
},
});

await adminClient.post.create({
data: {
title: testTitle,
tags: {
connect: {
id: user1.posts[0].tags[0].id,
},
},
},
});

const user2 = await adminClient.user.create({
data: {
email: `test-user-${uuid()}@example.com`,
posts: {
create: {
title: `test_${uuid()}`,
tags: {
create: {
label: "bar",
},
},
},
},
},
});

const result1 = await client.user.findFirst({
where: {
id: user1.id,
},
});

expect(result1).not.toBeNull();

const result2 = await client.user.findFirst({
where: {
id: user2.id,
},
});

expect(result2).toBeNull();
});
});
});

0 comments on commit 9fc0a52

Please sign in to comment.