diff --git a/package.json b/package.json index e32794d..0bdf7d3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "prisma generate && tsup src/index.ts --format=cjs,esm --dts", "watch": "prisma generate && tsup src/index.ts --format=cjs,esm --dts --watch", - "test": "jest --watch", + "test": "jest --watch --runInBand", "release": "release-it" }, "keywords": [ diff --git a/src/applyCaslToQuery.ts b/src/applyCaslToQuery.ts index 165bf10..f092cc9 100644 --- a/src/applyCaslToQuery.ts +++ b/src/applyCaslToQuery.ts @@ -27,7 +27,7 @@ export function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abil if (operationAbility.whereQuery && !args.where) { args.where = {} } - const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operationAbility.action, model) + const { args: dataArgs, creationTree: dataCreationTree } = applyDataQuery(abilities, args, operation, operationAbility.action, model) creationTree = dataCreationTree args = dataArgs if (operation === 'updateMany') { diff --git a/src/applyDataQuery.ts b/src/applyDataQuery.ts index c99b6f0..2395350 100644 --- a/src/applyDataQuery.ts +++ b/src/applyDataQuery.ts @@ -4,7 +4,7 @@ import { Prisma } from '@prisma/client' import { applyAccessibleQuery } from './applyAccessibleQuery' import { applyWhereQuery } from './applyWhereQuery' import { CreationTree } from './convertCreationTreeToSelect' -import { caslNestedOperationDict, getPermittedFields, propertyFieldsByModel, relationFieldsByModel } from './helpers' +import { caslNestedOperationDict, getPermittedFields, PrismaCaslOperation, propertyFieldsByModel, relationFieldsByModel } from './helpers' import './polyfills' /** @@ -26,6 +26,7 @@ import './polyfills' export function applyDataQuery( abilities: PureAbility, args: any, + operation: PrismaCaslOperation, action: string, model: string, creationTree?: CreationTree @@ -91,11 +92,12 @@ export function applyDataQuery( mutationArgs.map((mutation: any) => { // get all mutation arg fields and if they are short code connect (userId instead of user: { connect: id }), we convert it + // except if it is a createMany or updateMany operation const queriedFields = (mutation ? Object.keys(mutation) : []).map((field) => { const relationModelId = propertyFieldsByModel[model][field] if (relationModelId && mutation[field] !== null) { const fieldId = relationFieldsByModel[model][relationModelId].relationToFields?.[0] - if (fieldId) { + if (fieldId && operation !== 'createMany' && operation !== 'createManyAndReturn') { mutation[relationModelId] = { connect: { [fieldId]: mutation[field] } } delete mutation[field] } @@ -120,7 +122,7 @@ export function applyDataQuery( tree.children[field] = { action: mutationAction, model: relationModel.type as Prisma.ModelName, children: {}, mutation: [] } if (nestedAction !== 'disconnect' && nestedArgs !== true) { - const dataQuery = applyDataQuery(abilities, nestedArgs, mutationAction, relationModel.type, tree.children[field]) + const dataQuery = applyDataQuery(abilities, nestedArgs, operation, mutationAction, relationModel.type, tree.children[field]) mutation[field][nestedAction] = dataQuery.args // connection works like a where query, so we apply it if (isConnection) { diff --git a/test/applyDataQuery.test.ts b/test/applyDataQuery.test.ts index dc033d1..9741a1d 100644 --- a/test/applyDataQuery.test.ts +++ b/test/applyDataQuery.test.ts @@ -9,7 +9,7 @@ describe('apply data query', () => { const { can, build } = abilityBuilder() can('update', 'Post') can('update', 'User') - const result = applyDataQuery(build(), { data: { authorId: 0 }, where: { id: 0 } }, 'update', 'Post') + const result = applyDataQuery(build(), { data: { authorId: 0 }, where: { id: 0 } }, 'update', 'update', 'Post') expect(result.args).toEqual({ data: { author: { connect: { id: 0, } } }, where: { id: 0, } }) expect(result.creationTree).toEqual({ children: { author: { mutation: [], children: {}, model: 'User', action: "update" } }, model: 'Post', action: "update", mutation: [{ fields: ['authorId'], where: { id: 0 } }], }) }) @@ -17,9 +17,9 @@ describe('apply data query', () => { const { can, cannot, build } = abilityBuilder() can('update', 'Post') cannot('update', 'User') - expect(() => applyDataQuery(build(), { data: { authorId: 0 }, where: { id: 0 } }, 'update', 'Post')).toThrow(`It's not allowed to "update" "id" on "User"`) + expect(() => applyDataQuery(build(), { data: { authorId: 0 }, where: { id: 0 } }, 'update', 'update', 'Post')).toThrow(`It's not allowed to "update" "id" on "User"`) }) - ;['update', 'create'].map((mutation) => { + ; (['update', 'create'] as const).map((mutation) => { describe(mutation, () => { it('adds where clause to query', () => { @@ -27,7 +27,7 @@ describe('apply data query', () => { can(mutation, 'User', { id: 0 }) - const result = applyDataQuery(build(), { data: { id: 0 }, where: { id: 1 } }, mutation, 'User') + const result = applyDataQuery(build(), { data: { id: 0 }, where: { id: 1 } }, mutation, mutation, 'User') expect(result.args).toEqual({ data: { id: 0 }, where: { id: 1, AND: [{ OR: [{ id: 0 }] }] } @@ -43,7 +43,7 @@ describe('apply data query', () => { data: { email: '-1' }, where: { id: 0 } - }, mutation, 'User')).toThrow(`It's not allowed to "${mutation}" "email" on "User"`) + }, mutation, mutation, 'User')).toThrow(`It's not allowed to "${mutation}" "email" on "User"`) }) it('throws error if mutation of property is not permitted', () => { @@ -53,7 +53,7 @@ describe('apply data query', () => { data: { email: '-1' }, where: { id: 0 } - }, mutation, 'User')).toThrow(`It's not allowed to run "${mutation}" on "User"`) + }, mutation, mutation, 'User')).toThrow(`It's not allowed to run "${mutation}" on "User"`) }) }) }) @@ -63,7 +63,7 @@ describe('apply data query', () => { can('update', 'Thread') can('update', 'User', { id: 0 }) can('create', 'User') - const result = applyDataQuery(build(), { data: { creator: { upsert: { create: { email: '-1' }, update: { email: '-1' }, where: { id: 1 } } } }, where: { id: 0 } }, 'update', 'Thread') + const result = applyDataQuery(build(), { data: { creator: { upsert: { create: { email: '-1' }, update: { email: '-1' }, where: { id: 1 } } } }, where: { id: 0 } }, 'update', 'update', 'Thread') expect(result.args).toEqual({ data: { creator: { upsert: { create: { email: '-1' }, update: { email: '-1' }, where: { id: 1, AND: [{ OR: [{ id: 0 }] }] } } } }, where: { id: 0, } }) expect(result.creationTree).toEqual({ action: 'update', @@ -86,7 +86,7 @@ describe('apply data query', () => { can('create', 'User') cannot('create', 'User', 'email') - expect(() => applyDataQuery(build(), { data: { creator: { upsert: { create: { email: '-1' }, update: { email: '-1' }, where: { id: 1 } } } }, where: { id: 0 } }, 'update', 'Thread')) + expect(() => applyDataQuery(build(), { data: { creator: { upsert: { create: { email: '-1' }, update: { email: '-1' }, where: { id: 1 } } } }, where: { id: 0 } }, 'update', 'update', 'Thread')) .toThrow(`It's not allowed to "create" "email" on "User"`) }) }) @@ -100,7 +100,7 @@ describe('apply data query', () => { can('update', 'Post', { id: 1 }) - const result = applyDataQuery(build(), { data: { id: 1, posts: { connect: { id: 0 } } }, where: { id: 0 } }, 'update', 'User') + const result = applyDataQuery(build(), { data: { id: 1, posts: { connect: { id: 0 } } }, where: { id: 0 } }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { connect: { id: 0, AND: [{ OR: [{ id: 1 }] }] } } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) expect(result.creationTree).toEqual({ children: { posts: { children: {}, action: 'update', model: 'Post', mutation: [] } }, model: 'User', action: "update", mutation: [{ fields: ['id'], where: { id: 0 } }], }) }) @@ -113,7 +113,7 @@ describe('apply data query', () => { id: 1 }) - const result = applyDataQuery(build(), { data: { id: 1, posts: { connect: [{ id: 0 }] } }, where: { id: 0 } }, 'update', 'User') + const result = applyDataQuery(build(), { data: { id: 1, posts: { connect: [{ id: 0 }] } }, where: { id: 0 } }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { connect: [{ id: 0, AND: [{ OR: [{ id: 1 }] }] }] } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) expect(result.creationTree).toEqual({ children: { posts: { children: {}, model: 'Post', action: "update", mutation: [] } }, model: 'User', action: "update", mutation: [{ fields: ['id'], where: { id: 0 } }] }) }) @@ -122,7 +122,7 @@ describe('apply data query', () => { can('update', 'User') cannot('update', 'Post') - expect(() => applyDataQuery(build(), { data: { id: 1, posts: { connect: { id: 0 } } }, where: { id: 0 } }, 'update', 'User')) + expect(() => applyDataQuery(build(), { data: { id: 1, posts: { connect: { id: 0 } } }, where: { id: 0 } }, 'update', 'update', 'User')) .toThrow(`It's not allowed to "update" "id" on "Post"`) }) @@ -136,7 +136,7 @@ describe('apply data query', () => { can('update', 'Post', { id: 1 }) - const result = applyDataQuery(build(), { data: { id: 1, posts: { disconnect: true } }, where: { id: 0 } }, 'update', 'User') + const result = applyDataQuery(build(), { data: { id: 1, posts: { disconnect: true } }, where: { id: 0 } }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { disconnect: true } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) expect(result.creationTree).toEqual({ children: { posts: { children: {}, action: 'update', model: 'Post', mutation: [] } }, model: 'User', action: "update", mutation: [{ fields: ['id'], where: { id: 0 } }] }) }) @@ -168,7 +168,7 @@ describe('apply data query', () => { where: { id: 0 } - }, 'update', 'User') + }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { connectOrCreate: { create: { text: '' }, where: { id: 1, AND: [{ OR: [{ id: 2 }] }] } } } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) expect(result.creationTree).toEqual({ action: 'update', model: 'User', children: { posts: { model: 'Post', action: 'create', children: {}, mutation: [{ fields: ['text'], where: { id: 1 } }], } }, mutation: [{ fields: ['id'], where: { id: 0 } }], }) @@ -197,7 +197,7 @@ describe('apply data query', () => { where: { id: 0 } - }, 'update', 'User') + }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { connectOrCreate: [{ create: { text: '' }, where: { id: 0, AND: [{ OR: [{ id: 2 }] }] } }] } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) expect(result.creationTree).toEqual({ action: 'update', model: 'User', children: { posts: { model: 'Post', action: 'create', children: {}, mutation: [{ fields: ['text'], where: { id: 0 } }] } }, mutation: [{ fields: ['id'], where: { id: 0 } }] }) }) @@ -228,7 +228,7 @@ describe('apply data query', () => { where: { id: 0 } - }, 'update', 'User')) + }, 'update', 'update', 'User')) .toThrow(`It's not allowed to "create" "text" on "Post"`) }) it('throws error if data in nested create property in connection is not allowed', () => { @@ -258,7 +258,7 @@ describe('apply data query', () => { where: { id: 0 } - }, 'update', 'User')) + }, 'update', 'update', 'User')) .toThrow(`It's not allowed to "create" "text" on "Post"`) }) }) @@ -269,7 +269,7 @@ describe('apply data query', () => { can('update', 'Post') can('update', 'Thread') - const result = applyDataQuery(build(), { data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0 } } } }, where: { id: 0 } }, 'update', 'User') + const result = applyDataQuery(build(), { data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0 } } } }, where: { id: 0 } }, 'update', 'update', 'User') expect(result.args).toEqual({ data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0, } } } }, where: { id: 0, } }) expect(result.creationTree).toEqual({ children: { posts: { children: { thread: { children: {}, model: 'Thread', action: "update", mutation: [] } }, model: 'Post', action: "update", mutation: [{ fields: [], where: { id: 0 } }] } }, model: 'User', action: "update", mutation: [{ fields: ['id'], where: { id: 0 } }] }) }) @@ -279,10 +279,51 @@ describe('apply data query', () => { can('update', 'Post') cannot('update', 'Thread') - expect(() => applyDataQuery(build(), { data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0 } } } }, where: { id: 0 } }, 'update', 'User')) + expect(() => applyDataQuery(build(), { data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0 } } } }, where: { id: 0 } }, 'update', 'update', 'User')) .toThrow(`It's not allowed to "update" "id" on "Thread"`) }) }) + describe('nested delete', () => { + it('can update nested nested delete', () => { + const { can, build } = abilityBuilder() + can('update', 'User') + can('delete', 'Post') + + const result = applyDataQuery(build(), { data: { id: 1, posts: { delete: { id: 0 } } }, where: { id: 0 } }, 'update', 'update', 'User') + expect(result.args).toEqual({ data: { id: 1, posts: { delete: { id: 0 } } }, where: { id: 0, } }) + expect(result.creationTree).toEqual({ + action: "update", + children: { + posts: { + action: "delete", + children: {}, + model: "Post", + mutation: [], + }, + }, + model: "User", + mutation: [ + { + fields: [ + "id", + ], + where: { + id: 0, + }, + }, + ], + }) + }) + // it('throws error if data in nested nested update is not allowed', () => { + // const { can, cannot, build } = abilityBuilder() + // can('update', 'User') + // can('update', 'Post') + // cannot('update', 'Thread') + + // expect(() => applyDataQuery(build(), { data: { id: 1, posts: { update: { data: { thread: { update: { id: 0 } } }, where: { id: 0 } } } }, where: { id: 0 } }, 'update', 'User')) + // .toThrow(`It's not allowed to "update" "id" on "Thread"`) + // }) + }) describe('createMany', () => { it('adds where and connection clause in nested connection update', () => { const { can, build } = abilityBuilder() @@ -299,7 +340,7 @@ describe('apply data query', () => { } } } - }, 'create', 'User') + }, 'create', 'create', 'User') expect(result.args) .toEqual({ data: { id: 0, posts: { createMany: { data: { text: '' } } } } }) expect(result.creationTree).toEqual({ action: 'create', model: 'User', children: { posts: { model: 'Post', action: 'create', children: {}, mutation: [] } }, mutation: [] }) @@ -326,7 +367,7 @@ describe('apply data query', () => { } } } - }, 'create', 'User')) + }, 'create', 'create', 'User')) .toThrow(`It's not allowed to "create" "text" on "Post"`) }) diff --git a/test/subset.test.ts b/test/subset.test.ts index 1241c2c..7a11b06 100644 --- a/test/subset.test.ts +++ b/test/subset.test.ts @@ -28,4 +28,17 @@ describe('subset', () => { } )).toBeFalsy() }) + it('is no subset', () => { + expect(isSubset({ + a: [{ c: [{ d: 1 }] }], + e: 0, + f: 2 + }, + { + a: [{ b: 0 }, { c: [{ d: 0 }, { d: 1 }] }], + // e: 1, + f: 2 + } + )).toBeFalsy() + }) })