prisma-extension-casl
Version:
Enforce casl abilities on prisma client
1,471 lines (1,367 loc) • 80.4 kB
text/typescript
import { seedClient } from './client'
import { seed } from './seed'
import { prismaQuery } from '@casl/prisma'
import { useCaslAbilities } from '../src/index'
import { abilityBuilder } from './abilities'
beforeEach(async () => {
await seed(seedClient)
})
describe('prisma extension casl', () => {
describe('override rules', () => {
it('overwrites abilities', async () => {
function builderFactory() {
const builder = abilityBuilder()
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.$casl((a) => {
a.can('read', 'User')
return a
}).user.findMany()
expect(result.length).toEqual(2)
expect(await client.user.findMany()).toEqual([])
expect(await client.$casl((a) => a).user.findMany()).toEqual([])
})
})
describe('transaction', () => {
it('has $transaction on $casl client', async () => {
function builderFactory() {
const builder = abilityBuilder()
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(client.$transaction).toBeDefined()
expect(client.$casl((abilities) => abilities).$transaction).toBeDefined()
let transactionClient = undefined
await client.$casl((abilities) => abilities).$transaction(async (tx) => {
//@ts-ignore
transactionClient = tx.$casl((abilities) => abilities)
return
})
expect(transactionClient).toBeDefined()
//@ts-ignore
expect(transactionClient?.$casl).toBeDefined()
//@ts-ignore
expect(transactionClient?.$transaction).toBeDefined()
})
it('reverts create within an existing batch transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('create', 'User', {
email: 'third-mail'
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.create({
data: {
email: 'first-mail',
}
})).rejects.toThrow()
await expect(
client.$transaction([
client.user.create({
data: {
email: 'third-mail'
}
}),
client.user.create({
data: {
email: 'second-mail',
}
}),
])
).rejects.toThrow()
const firstResult = await client.user.findFirst({
where: {
email: 'first-mail'
}
})
const secondResult = await client.user.findFirst({
where: {
email: 'second-mail'
}
})
const thirdResult = await client.user.findFirst({
where: {
email: 'third-mail'
}
})
expect(firstResult).toBeNull()
expect(secondResult).toBeNull()
expect(thirdResult).toBeNull()
})
it('read and delete work with an existing batch transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
can('create', 'User')
can('delete', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
// expect(await client.user.count()).toEqual(2)
await expect(client.$transaction([
client.user.delete({
where: {
id: 0
}
}),
client.user.findUnique({ where: { id: 0 } }),
])
).rejects.toThrow()
// expect(await client.user.count()).toEqual(1)
})
it('reverts read and delete if they error with an existing batch transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
can('create', 'User')
can('delete', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.user.count()).toEqual(2)
await expect(
client.$transaction([
client.user.delete({
where: {
id: 0
}
}),
//@ts-ignore
client.user.findUnique({ where: { email: 0 } }),
])
).rejects.toThrow()
expect(await client.user.count()).toEqual(2)
})
it('creates within an existing batch transaction fails', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('create', 'User')
cannot('create', 'User', {
email: 'first-mail'
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.create({
data: {
email: 'first-mail',
}
})).rejects.toThrow()
const firstResult = await client.user.findFirst({
where: {
email: 'first-mail'
}
})
expect(firstResult).toBeNull()
await expect(
client.$transaction([
client.user.create({
data: {
email: 'second-mail',
}
}),
client.user.create({
data: {
email: 'third-mail'
}
})
])
).rejects.toThrow('Sequential transactions are not supported in prisma-extension-casl.')
})
it('reverts create within an existing interactive transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('create', 'User', {
email: 'third-mail'
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.create({
data: {
email: 'first-mail',
}
})).rejects.toThrow()
await expect(
//@ts-ignore
client.$transaction(async (tx) => {
await tx.user.create({
data: {
email: 'third-mail'
}
}),
await tx.user.create({
data: {
email: 'second-mail',
}
})
})
).rejects.toThrow()
const firstResult = await client.user.findFirst({
where: {
email: 'first-mail'
}
})
const secondResult = await client.user.findFirst({
where: {
email: 'second-mail'
}
})
const thirdResult = await client.user.findFirst({
where: {
email: 'third-mail'
}
})
expect(firstResult).toBeNull()
expect(secondResult).toBeNull()
expect(thirdResult).toBeNull()
})
it('creates within an existing interactive transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('create', 'User')
cannot('create', 'User', {
email: 'first-mail'
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.create({
data: {
email: 'first-mail',
}
})).rejects.toThrow()
const firstResult = await client.user.findFirst({
where: {
email: 'first-mail'
}
})
expect(firstResult).toBeNull()
expect(await
//@ts-ignore
client.$transaction(async (tx) => {
return [await tx.user.create({
data: {
email: 'second-mail',
}
}),
await tx.user.create({
data: {
email: 'third-mail'
}
})]
})
).toEqual([{ "email": "second-mail" }, { "email": "third-mail" }])
})
it('creates within an existing interactive transaction with $casl application on transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await
client.$casl((abilities) => {
abilities.can('read', 'User', 'email')
abilities.can('create', 'User')
return abilities
}).$transaction(async (tx) => {
return [await tx.user.create({
data: {
email: 'second-mail',
}
}),
await tx.user.create({
data: {
email: 'third-mail'
}
})]
})
).toEqual([{ "email": "second-mail" }, { "email": "third-mail" }])
})
it('creates within an existing interactive transaction with $casl application in transaction', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await
client.$casl((abilities) => {
abilities.can('read', 'User', 'email')
return abilities
}).$transaction(async (tx) => {
try {
await tx.user.create({
data: {
email: 'third-mail'
}
})
} catch (e) {
}
//@ts-ignore
return [await tx.$casl((abilities) => {
abilities.can('read', 'User', 'email')
abilities.can('create', 'User')
return abilities
}).user.create({
data: {
email: 'second-mail',
}
})]
})
).toEqual([{ "email": "second-mail" }])
})
it('allows to use transaction beforeQuery', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post')
can('create', 'Post')
can('update', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory, {
beforeQuery: async (tx) => {
await tx.post.create({
data: {
text: 'abc',
authorId: 0
}
})
}
})
)
expect(await seedClient.post.count()).toBe(4)
await
client.post.create({
data: {
text: 'abc',
authorId: 0
}
})
expect(await seedClient.post.count()).toBe(6)
})
it('rollsback to use transaction beforeQuery', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post')
can('create', 'Post')
can('update', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory, {
beforeQuery: async (tx) => {
await tx.post.create({
data: {
text: 'abc',
authorId: 0
}
})
throw new Error('')
}
})
)
expect(await seedClient.post.count()).toBe(4)
await expect(
client.post.create({
data: {
text: 'abc',
authorId: 0
}
})).rejects.toThrow()
expect(await seedClient.post.count()).toBe(4)
})
})
describe('findUnique', () => {
it('returns only permitted fields with conditional cannot rule', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email', 'id'])
cannot('read', 'User', ['email'], {
posts: {
some: {
threadId: 2
}
}
})
return builder
}
const client = seedClient.$extends(
//@ts-ignore
useCaslAbilities(builderFactory)
)
const result = await client.user.findUnique({
where: {
id: 0
}
})
expect(result).toEqual({ id: 0 })
const result2 = await client.user.findUnique({
where: {
id: 1
}
})
expect(result2).toEqual({ email: '1', id: 1 })
})
it('returns only permitted fields with conditional can rule', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email'])
can('read', 'User', ['email', 'id'], {
posts: {
some: {
threadId: 2
}
}
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findUnique({
where: {
id: 0
}
})
expect(result).toEqual({ email: '0', id: 0 })
const result2 = await client.user.findUnique({
where: {
id: 1
}
})
expect(result2).toEqual({ email: '1' })
})
it('returns nested includes for 1 to 1 relation with permissions on findUnique', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email', 'id'], {
posts: {
some: {
id: 0
}
}
})
can('read', 'Post', ['id'], {
id: 0
})
can('read', 'Thread', ['id'], {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({
where: {
id: 0
},
include: {
author: {
include: {
favorite: true
}
}
}
})
expect(result).toEqual({ id: 0, author: { id: 0, email: '0', favorite: { id: 0 } } })
})
it('returns limited fields only', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email', 'id'], {
id: 1
})
can('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findUnique({
where: {
id: 0
}
})
expect(result).toEqual({ email: '0' })
const result2 = await client.user.findUnique({
where: {
id: 1
}
})
expect(result2).toEqual({ email: '1', id: 1 })
})
it('cannot return omitted property', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
cannot('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.user.findUnique({
where: {
id: 0
}
})).toEqual({ id: 0, favoriteId: 0, })
})
it('omit query works on property', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.user.findUnique({
where: {
id: 0
},
omit: {
email: true
}
})).toEqual({ id: 0, favoriteId: 0, })
})
it('select with limited fields for selects', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', ['id'], {
thread: {
//https://casl.js.org/v6/en/package/casl-prisma#note-on-prisma-query-runtime-interpreter
is: {
creatorId: 0
}
}
})
can('read', 'User', ['email', 'id'], {
id: 1
})
can('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({
where: {
id: 1
},
select: {
id: true,
author: true
},
})
expect(result).toEqual({ id: 1, author: { email: '1', id: 1 } })
const result2 = await client.post.findUnique({
where: {
id: 0
},
select: {
id: true,
author: true
},
})
expect(result2).toEqual({ id: 0, author: { email: '0' } })
})
it('does include nested fields if query does not include properties to check for rules', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', ['id'], {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', ['creatorId'])
can('read', 'User', ['email', 'id'], {
id: 2
})
can('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({
where: {
id: 1
},
select: {
author: {
select: {
email: true,
posts: true
},
}
},
})
expect(result).toEqual({ author: { email: '1', posts: [{ id: 1 }] } })
})
it('includes nested fields if query does not include properties to check for rules', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', ['id'], {
thread: {
is: {
creatorId: { lte: 0 }
}
}
})
can('read', 'Thread', ['creatorId'])
can('read', 'User', ['email', 'id'], {
id: 2
})
can('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result2 = await client.post.findUnique({
where: {
id: 1
},
select: {
author: {
select: {
email: true,
posts: {
include: {
thread: {
select: {
creatorId: true
}
}
},
where: {
thread: {
creatorId: 0
}
}
}
},
}
},
})
expect(result2).toEqual({ author: { email: '1', posts: [{ 'id': 1, thread: { creatorId: 0 } }] } })
})
it('includes nested fields if query does not include properties to check for rules', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', ['id'], {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', ['id'])
can('read', 'User', ['email', 'id'], {
id: 2
})
can('read', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result2 = await client.post.findUnique({
where: {
id: 1
},
select: {
author: {
select: {
email: true,
posts: {
include: {
thread: true
},
where: {
thread: {
creatorId: 0
}
}
}
},
}
},
})
expect(result2).toEqual({ author: { email: '1', posts: [{ 'id': 1, thread: { id: 0 } }] } })
})
it('ignores conditional rule', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User' as any)
can('read', 'User', ['email'], {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ email: '0', favoriteId: 0, id: 0 }, { email: '1', favoriteId: null, id: 1 }])
})
it('ignores conditional rule', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
cannot('read', 'User', 'email', {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ id: 0, favoriteId: 0, }, { email: '1', favoriteId: null, id: 1 }])
})
it('applies filter props and ignores weaker can rule', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('read', 'User', {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ email: '0', favoriteId: 0, id: 0 }, { email: '1' }])
})
it('allows to see more props on a condition', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('read', 'User', ['id'], { id: 0 })
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ email: '0', id: 0 }, { email: '1' }])
})
it('allows to see only specified props on a condition', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email', { id: 0 })
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ 'email': '0' }])
})
it('allows to see more props on a condition', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'id', { id: 1 })
can('read', 'User', 'email', { id: 0 })
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ 'email': '0' }, { id: 1 }])
})
it('can findUnique if nested id is correct and included', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', 'id')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({ where: { id: 0 }, include: { thread: true } })
expect(result).toEqual({ authorId: 0, id: 0, threadId: 0, text: '', thread: { id: 0 } })
})
it('can findUnique if nested id is correct and included, but makes nested null if it has no read rights', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.post.findUnique({ where: { id: 0 }, include: { thread: true } })).toEqual({
authorId: 0,
id: 0,
text: '',
thread: null,
threadId: 0
})
})
it('cannot findUnique if nested id is not correct and included', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', 'id')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.post.findUnique({
where: { id: 2 },
include: {
thread: true
}
})).toBeNull()
})
it('cannot findUnique if nested id is not readable', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', 'id')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
expect(await client.post.findUnique({
where: { id: 2 },
})).toBeNull()
})
it('can findUnique include', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'User', {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({
where: { id: 0 },
include: {
author: true
}
})
expect(result?.author?.id).toBe(0)
expect(await client.post.findUnique({
where: { id: 1 },
include: {
author: true
}
})).toEqual({
author: null,
authorId: 1,
id: 1,
text: '',
threadId: 0
})
})
it('cannot findUnique', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post')
cannot('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findUnique({ where: { id: 2 } })
expect(result?.authorId).toBe(1)
expect(await client.post.findUnique({
where: { id: 1 }
})).toBeNull()
})
})
describe('findMany', () => {
it('filters only readable posts', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'Post', {
thread: {
is: {
creatorId: 0
}
}
})
can('read', 'Thread', 'id')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.post.findMany({ include: { thread: true } })
expect(result).toEqual([{ authorId: 0, id: 0, text: '', threadId: 0, thread: { id: 0 } }, { authorId: 1, id: 1, text: '', threadId: 0, thread: { id: 0 } }, { authorId: 0, id: 3, text: '', threadId: 2, thread: { id: 2 } }])
})
it('checks post permission but does not include it in output', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email', {
posts: {
some: {
authorId: 0
}
}
})
cannot('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.findMany()
expect(result).toEqual([{ email: "0" }])
})
it('read performance', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email', {
posts: {
some: {
authorId: 0
}
}
})
cannot('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
console.log("read performance")
const t1 = performance.now()
await seedClient.user.findMany({
where: {
posts: {
some: {
authorId: 0
}
}
}
})
console.log("plain prisma performance", performance.now() - t1)
const result = await client.user.findMany({
//@ts-ignore
debugCasl: true
})
expect(result).toEqual([{ email: "0" }])
})
})
describe('update', () => {
it('cannot update if no abiltiy exists', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
const { build } = builder
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.update({
data: {
email: '2'
},
where: {
id: 0
}
})).rejects.toThrow()
})
it('can update with ability, but cannot read', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new'
},
where: {
id: 0
}
})
expect(result).toBeNull()
})
it('can update with ability but only read permitted values', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email'])
can('update', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new'
},
where: {
id: 0
}
})
expect(result).toEqual({ email: 'new' })
})
it('can update permitted property with ability', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
can('update', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new'
},
where: {
id: 0
}
})
expect(result).toEqual({ id: 0, favoriteId: 0, email: 'new' })
})
it('cannot update non-permitted property with ability', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'User', ['id'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.update({
data: {
email: 'new'
},
where: {
id: 0
}
})).rejects.toThrow()
})
it('cannot update omitted property with ability', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'User')
cannot('update', 'User', ['email'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.user.update({
data: {
email: 'new'
},
where: {
id: 0
}
})).rejects.toThrow()
})
it('can update permitted property with ability and query included data', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', ['email'])
can('update', 'User', ['email'])
can('read', 'Post', ['id'])
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new'
},
where: {
id: 0
},
include: {
posts: true
}
})
expect(result).toEqual({ email: 'new', posts: [{ id: 0 }, { id: 3 }] })
})
it('can do nested updates', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('update', 'User')
can('update', 'Post')
can('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new',
posts: {
update: {
data: {
text: '1'
},
where: {
id: 0
}
}
}
},
where: {
id: 0
},
include: {
posts: {
select: { id: true, text: true }
}
}
})
expect(result).toEqual({ email: 'new', posts: [{ id: 0, text: '1' }, { id: 3, text: '' }] })
})
it('cannot do nested updates on restricted fields with conditions', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'Thread', 'id')
can('update', 'Post', ['threadId'], {
id: 0
})
can('update', 'Post', {
text: '-',
id: 0
})
can('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.post.update({
data: {
text: '1'
},
where: {
id: 0
}
})).rejects.toThrow()
expect(await client.post.update({
data: {
threadId: 1
},
where: {
id: 0
}
})).toEqual({ id: 0, text: "", threadId: 1, authorId: 0, })
})
it('cannot do updates on restricted fields with conditions', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'Thread', 'id')
can('update', 'Post', {
id: 0
})
// cannot needs to be after can rule. https://casl.js.org/v6/en/guide/intro#inverted-rules
cannot('update', 'Post', ['text'])
can('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.post.update({
data: {
text: '1'
},
where: {
id: 0
}
})).rejects.toThrow()
})
it('cannot do updates on restricted fields with cannot conditions', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('update', 'Thread', 'id')
can('update', 'Post', {
id: 0
})
can('read', 'Post')
// cannot needs to be after can rule. https://casl.js.org/v6/en/guide/intro#inverted-rules
cannot('update', 'Post', ['text'], {
id: 0
})
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
await expect(client.post.update({
data: {
text: '1'
},
where: {
id: 0
}
})).rejects.toThrow()
})
it('can do nested updates with conditions', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User', 'email')
can('update', 'User')
can('update', 'Post', {
id: 0
})
can('read', 'Post')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
const result = await client.user.update({
data: {
email: 'new',
posts: {
update: {
data: {
text: '1'
},
where: {
id: 0
}
}
}
},
where: {
id: 0
},
include: {
posts: {
select: