Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support client expressions that query 1:1 relationships #76

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,20 @@ When defining an ability you need to provide the following properties:
Yates uses a transaction to apply the RLS policies to each query. This means that if you are using transactions in your application, rollbacks will not work as expected. This is because [Prisma has poor support for nested transactions](https://github.com/prisma/prisma/issues/15212) and will `COMMIT` the inner transaction even if the outer transaction is rolled back.
If you need this functionality and you are using Yates, you can return `null` from the `getContext()` setup method to bypass the internal transaction, and therefore the RLS policies for the current request. see the `nested-transactions.spec.ts` test case for an example of how to do this.

### Unsupported Prisma Client query features

If you are using the Prisma client to construct an ability expression, the following `where` keywords are not supported.

- `AND`
- `OR`
- `NOT`
- `is`
- `isNot`

Additionally, using context or row values to query Prisma Enums is not supported.

If you need to use these expressions, you can use the `expression` property of the ability to write a raw SQL expression instead.

## License

The project is licensed under the MIT license.
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ services:
interval: 5s
timeout: 5s
retries: 5
# command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Warnings:

- You are about to drop the column `role` on the `User` table. All the data in the column will be lost.

*/
-- CreateEnum
CREATE TYPE "RoleEnum" AS ENUM ('USER', 'ORGANIZATION_ADMIN', 'ADMIN');

-- AlterTable
ALTER TABLE "User" DROP COLUMN "role";

-- DropEnum
DROP TYPE "Role";

-- CreateTable
CREATE TABLE "Organization" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT,

CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "RoleAssignment" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"organizationId" INTEGER NOT NULL,
"roleId" INTEGER NOT NULL,

CONSTRAINT "RoleAssignment_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Role" (
"id" SERIAL NOT NULL,
"name" "RoleEnum" NOT NULL,

CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Organization_name_key" ON "Organization"("name");

-- CreateIndex
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
75 changes: 75 additions & 0 deletions prisma/migrations/20240115145706_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Warnings:

- The primary key for the `Organization` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `Role` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `RoleAssignment` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.

*/
-- DropForeignKey
ALTER TABLE "Hat" DROP CONSTRAINT "Hat_userId_fkey";

-- DropForeignKey
ALTER TABLE "Post" DROP CONSTRAINT "Post_authorId_fkey";

-- DropForeignKey
ALTER TABLE "RoleAssignment" DROP CONSTRAINT "RoleAssignment_organizationId_fkey";

-- DropForeignKey
ALTER TABLE "RoleAssignment" DROP CONSTRAINT "RoleAssignment_roleId_fkey";

-- DropForeignKey
ALTER TABLE "RoleAssignment" DROP CONSTRAINT "RoleAssignment_userId_fkey";

-- AlterTable
ALTER TABLE "Hat" ALTER COLUMN "userId" SET DATA TYPE TEXT;

-- AlterTable
ALTER TABLE "Organization" DROP CONSTRAINT "Organization_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Organization_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Organization_id_seq";

-- AlterTable
ALTER TABLE "Post" ALTER COLUMN "authorId" SET DATA TYPE TEXT;

-- AlterTable
ALTER TABLE "Role" DROP CONSTRAINT "Role_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Role_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Role_id_seq";

-- AlterTable
ALTER TABLE "RoleAssignment" DROP CONSTRAINT "RoleAssignment_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "userId" SET DATA TYPE TEXT,
ALTER COLUMN "organizationId" SET DATA TYPE TEXT,
ALTER COLUMN "roleId" SET DATA TYPE TEXT,
ADD CONSTRAINT "RoleAssignment_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "RoleAssignment_id_seq";

-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "User_id_seq";

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "RoleAssignment" ADD CONSTRAINT "RoleAssignment_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Hat" ADD CONSTRAINT "Hat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
15 changes: 15 additions & 0 deletions prisma/migrations/20240115154022_drop_role_enum/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
Warnings:

- Changed the type of `name` on the `Role` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.

*/
-- AlterTable
ALTER TABLE "Role" DROP COLUMN "name",
ADD COLUMN "name" TEXT NOT NULL;

-- DropEnum
DROP TYPE "RoleEnum";

-- CreateIndex
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
48 changes: 34 additions & 14 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,38 @@ generator client {
}

model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String?
role Role @default(USER)
posts Post[]
hat Hat?
id String @id @default(uuid())
createdAt DateTime @default(now())
email String @unique
name String?
posts Post[]
hat Hat?
roleAssignment RoleAssignment[]
}

model Organization {
id String @id @default(uuid())
createdAt DateTime @default(now())
name String? @unique
roleAssignment RoleAssignment[]
}

model RoleAssignment {
id String @id @default(uuid())
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
role Role @relation(fields: [roleId], references: [id])

userId String
organizationId String
roleId String
}

model Role {
id String @id @default(uuid())
name String @unique
roleAssignment RoleAssignment[]
}

model Post {
Expand All @@ -27,7 +52,7 @@ model Post {
published Boolean @default(false)
title String @db.VarChar(255)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
authorId String?
tags Tag[]
}

Expand All @@ -48,16 +73,11 @@ model Hat {
id Int @id @default(autoincrement())
style String?
user User? @relation(fields: [userId], references: [id])
userId Int? @unique
userId String? @unique
}

model Account {
id Int @id @default(autoincrement())
balance Int @default(0)
email String @unique
}

enum Role {
USER
ADMIN
}
29 changes: 24 additions & 5 deletions src/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,31 @@ const tokenizeWhereExpression = (
// 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];
// List queries will always have a sub-object of "every", "some" or "none", so we need to dropdown and iterate through them
if (fieldData.isList) {
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;
} else {
const { tokens: subTokens, where: subWhere } = tokenizeWhereExpression(
client,
subValue,
value,
table,
fieldData.type,
tokens,
Expand All @@ -112,9 +131,9 @@ const tokenizeWhereExpression = (
...subTokens,
};

where[field][subField] = subWhere;
where[field] = subWhere;
continue;
}
continue;
}
const isNumeric = PRISMA_NUMERIC_TYPES.includes(fieldData.type);
const isColumnName = typeof value === "string" && !!value.match(/^___yates_row_/);
Expand Down
Loading
Loading