Skip to content

Commit

Permalink
feat: add support for the in operator with static scalar values
Browse files Browse the repository at this point in the history
Signed-off-by: Lucian Buzzo <[email protected]>
  • Loading branch information
LucianBuzzo committed Jan 16, 2024
1 parent 6876957 commit 6383a40
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 28 deletions.
67 changes: 39 additions & 28 deletions src/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,28 +209,32 @@ const tokenizeWhereExpression = (
break;

case isInStatement:
// This is a bit hokey, but we are going to assume that each value here is static, and
// perform tokenization on each value in the `in` array.
// The ideal solution is to rework this tokenization function so that it recurses until it
// finds a scalar value, and then tokenizes that value, with checking for row/context values.
if (Array.isArray(value.in)) {
const values = [];
for (const item in value.in) {
values.push({
type: "single_quote_string",
value: item,
});
const tokenList = [];

for (const item of value.in) {
let inToken;
do {
inToken = getLargeRandomInt();
} while (tokens[int]);

tokens[inToken] = {
astFragment: {
type: "parameter",
value: escapeLiteral(item),
},
};

tokenList.push(isNumeric ? inToken : `${inToken}`);
}
astFragment = {
type: "binary_expr",
operator: "IN",
left: {
type: "column_ref",
schema: "public",
table: table,
column: field,
},
right: {
type: "expr_list",
value: values,
},
where[field] = {
in: tokenList,
};
continue;
} else {
// If the value of `in` is a context value, we assume that it is an array that has been JSON encoded
// We create an AST fragment representing a function call to `jsonb_array_elements_text` with the context value as the argument
Expand Down Expand Up @@ -329,23 +333,30 @@ export const expressionToSQL = async (getExpression: Expression, table: string):
let param = params[i];
const token = tokens[param];

// If there is no token, we can skip this. The most likely cause of this is that the parameter is for a limit or offset, which we cull from the SQL anyway
if (!token) {
continue;
}

const parameterizedStatement = deepFind(ast, {
right: {
type: "var",
name: i + 1,
prefix: "$",
},
type: "var",
name: i + 1,
prefix: "$",
});

if (!parameterizedStatement) {
continue;
// If we found a matching parameterized statement, we can replace it with the AST fragment.
// This will replace the parameter with the original value.
// We do this by mutating the object returned from the deepfind function.
if (parameterizedStatement) {
// First, scrub all the keys from the parameterized statement
for (const key of Object.keys(parameterizedStatement)) {
Reflect.deleteProperty(parameterizedStatement, key);
}
// Second, add all the keys from the AST fragment to the parameterized statement
for (const key of Object.keys(token.astFragment)) {
parameterizedStatement[key] = token.astFragment[key];
}
}

parameterizedStatement.right = token.astFragment;
}

if (isSubselect) {
Expand Down
66 changes: 66 additions & 0 deletions test/integration/expressions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PrismaClient } from "@prisma/client";
import _ from "lodash";
import { v4 as uuid } from "uuid";
import { setup } from "../../src";
import { Parser } from "node-sql-parser";

jest.setTimeout(30000);

Expand Down Expand Up @@ -516,6 +517,71 @@ describe("expressions", () => {
expect(post.id).toBeDefined();
});

it("should be able to allow access using static values and the `in` keyword", async () => {
const initial = new PrismaClient();

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

const label1 = `test-label-${uuid()}`;
const label2 = `test-label-${uuid()}`;

const client = await setup({
prisma: initial,
customAbilities: {
Post: {
customCreateAbility: {
description: "Read where tag label exists with a specific value",
operation: "INSERT",
expression: (client: PrismaClient) => {
return client.tag.findFirst({
where: {
label: {
in: [label1, label2],
},
},
});
},
},
},
},
getRoles(abilities) {
return {
[role]: [abilities.Post.customCreateAbility, abilities.Post.read, abilities.Tag.read, abilities.Tag.create],
};
},
getContext: () => ({
role,
context: {
"tag.title": "test",
},
}),
});

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

await expect(
client.post.create({
data: {
title: testTitle,
},
}),
).rejects.toThrow();

await client.tag.create({
data: {
label: label1,
},
});

const post = await client.post.create({
data: {
title: testTitle,
},
});

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

it("should be able to allow access using textual row values", async () => {
const initial = new PrismaClient();

Expand Down

0 comments on commit 6383a40

Please sign in to comment.