Skip to content

Commit

Permalink
fix: 🐛 create correct extended instance of getAbilities
Browse files Browse the repository at this point in the history
  • Loading branch information
dennemark committed Sep 30, 2024
1 parent f933cbf commit 83f45e0
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 107 deletions.
216 changes: 110 additions & 106 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,108 @@ export function useCaslAbilities(getAbilityFactory: () => AbilityBuilder<PureAbi


return Prisma.defineExtension((client) => {
let getAbilities = () => getAbilityFactory()
const allOperations = (getAbilities: () => AbilityBuilder<PureAbility<AbilityTuple, PrismaQuery>>) => ({
async $allOperations<T>({ args, query, model, operation, ...rest }: { args: any, query: any, model: any, operation: any }) {
const op = operation === 'createMany' ? 'createManyAndReturn' : operation
const transaction = (rest as any).__internalParams.transaction
const debug = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && args.debugCasl
delete args.debugCasl
const perf = debug ? performance : undefined
const logger = debug ? console : undefined
perf?.clearMeasures('prisma-casl-extension-Overall')
perf?.clearMeasures('prisma-casl-extension-Create Abilities')
perf?.clearMeasures('prisma-casl-extension-Create Casl Query')
perf?.clearMeasures('prisma-casl-extension-Finish Query')
perf?.clearMeasures('prisma-casl-extension-Filtering Results')
perf?.clearMarks('prisma-casl-extension-0')
perf?.clearMarks('prisma-casl-extension-1')
perf?.clearMarks('prisma-casl-extension-2')
perf?.clearMarks('prisma-casl-extension-3')
perf?.clearMarks('prisma-casl-extension-4')

if (!(op in caslOperationDict)) {
return query(args)
}


perf?.mark('prisma-casl-extension-0')
const abilities = transaction?.abilities ?? getAbilities().build()
if (transaction) {
transaction.abilities = abilities
}
perf?.mark('prisma-casl-extension-1')


const caslQuery = applyCaslToQuery(operation, args, abilities, model, permissionField ? true : false)


perf?.mark('prisma-casl-extension-2')
logger?.log('Query Args', JSON.stringify(caslQuery.args))
logger?.log('Query Mask', JSON.stringify(caslQuery.mask))

const cleanupResults = (result: any) => {

perf?.mark('prisma-casl-extension-3')
const fluentModel = getFluentModel(model, rest)

if (fluentModel !== model && caslQuery.mask) {
// on fluent models we need to take mask of the relation
const relation = Object.entries(relationFieldsByModel[model]).find(([k, v]) => v.type === fluentModel)?.[0]
caslQuery.mask = relation && relation in caslQuery.mask ? caslQuery.mask[relation] : {}
}
const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, permissionField)

if (perf) {
perf.mark('prisma-casl-extension-4')
logger?.log(
[perf.measure('prisma-casl-extension-Overall', 'prisma-casl-extension-0', 'prisma-casl-extension-4'),
perf.measure('prisma-casl-extension-Create Abilities', 'prisma-casl-extension-0', 'prisma-casl-extension-1'),
perf.measure('prisma-casl-extension-Create Casl Query', 'prisma-casl-extension-1', 'prisma-casl-extension-2'),
perf.measure('prisma-casl-extension-Finish Query', 'prisma-casl-extension-2', 'prisma-casl-extension-3'),
perf.measure('prisma-casl-extension-Filtering Results', 'prisma-casl-extension-3', 'prisma-casl-extension-4')
].map((measure) => {
return `${measure.name.replace('prisma-casl-extension-', '')}: ${measure.duration}`
})
)
}

return operation === 'createMany' ? { count: filteredResult.length } : filteredResult
}
const operationAbility = caslOperationDict[operation as PrismaCaslOperation]
/**
* on update or create we need to create a transaction
* since there can be errors if newly created db entries
* are not permitted by abilities
*
* for reads and deletes we skip the transaction
*/
if (operationAbility.action === 'update' || operationAbility.action === 'create') {
if (transaction) {
if (transaction.kind === 'itx') {
const transactionClient = (client as any)._createItxClient(transaction)
return transactionClient[model][op](caslQuery.args).then(cleanupResults)
} else if (transaction.kind === 'batch') {
//@ts-ignore
throw new Error('Sequential transactions are not supported in prisma-extension-casl.')
// const extendedRequest = request.then(cleanupResults)
// extendedRequest.requestTransaction = request.requestTransaction
//@ts-ignore
// return client._createPrismaPromise(new Promise((resolve, reject) => {
// query(caslQuery.args).then(cleanupResults).then((result: any) => resolve(result)).catch(((e: any) => reject(e)))
// })
}
} else {

return client.$transaction(async (tx) => {
//@ts-ignore
return tx[model][op](caslQuery.args).then(cleanupResults)
})
}
} else {
return query(caslQuery.args).then(cleanupResults)
}
}
})
return client.$extends({
name: "prisma-extension-casl",
client: {
Expand All @@ -35,116 +136,19 @@ export function useCaslAbilities(getAbilityFactory: () => AbilityBuilder<PureAbi
// });
// },
$casl(extendFactory: (factory: AbilityBuilder<PureAbility<AbilityTuple, PrismaQuery>>) => AbilityBuilder<PureAbility<AbilityTuple, PrismaQuery>>) {
const ctx = Prisma.getExtensionContext(this)
// alter the getAblities function shortly
getAbilities = () => extendFactory(getAbilityFactory())
return ctx as typeof client
return client.$extends({
query: {
$allModels: {
...allOperations(() => extendFactory(getAbilityFactory()))
}
}
})
}
},
query: {
$allModels: {
async $allOperations<T>({ args, query, model, operation, ...rest }: { args: any, query: any, model: any, operation: any }) {
const op = operation === 'createMany' ? 'createManyAndReturn' : operation
const transaction = (rest as any).__internalParams.transaction
const debug = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && args.debugCasl
delete args.debugCasl
const perf = debug ? performance : undefined
const logger = debug ? console : undefined
perf?.clearMeasures('prisma-casl-extension-Overall')
perf?.clearMeasures('prisma-casl-extension-Create Abilities')
perf?.clearMeasures('prisma-casl-extension-Create Casl Query')
perf?.clearMeasures('prisma-casl-extension-Finish Query')
perf?.clearMeasures('prisma-casl-extension-Filtering Results')
perf?.clearMarks('prisma-casl-extension-0')
perf?.clearMarks('prisma-casl-extension-1')
perf?.clearMarks('prisma-casl-extension-2')
perf?.clearMarks('prisma-casl-extension-3')
perf?.clearMarks('prisma-casl-extension-4')

if (!(op in caslOperationDict)) {
return query(args)
}


perf?.mark('prisma-casl-extension-0')
const abilities = transaction?.abilities ?? getAbilities().build()
if (transaction) {
transaction.abilities = abilities
}
// reset alteration of getAblities function
getAbilities = () => getAbilityFactory()
perf?.mark('prisma-casl-extension-1')


const caslQuery = applyCaslToQuery(operation, args, abilities, model, permissionField ? true : false)


perf?.mark('prisma-casl-extension-2')
logger?.log('Query Args', JSON.stringify(caslQuery.args))
logger?.log('Query Mask', JSON.stringify(caslQuery.mask))

const cleanupResults = (result: any) => {

perf?.mark('prisma-casl-extension-3')
const fluentModel = getFluentModel(model, rest)

if (fluentModel !== model && caslQuery.mask) {
// on fluent models we need to take mask of the relation
const relation = Object.entries(relationFieldsByModel[model]).find(([k, v]) => v.type === fluentModel)?.[0]
caslQuery.mask = relation && relation in caslQuery.mask ? caslQuery.mask[relation] : {}
}
const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, permissionField)

if (perf) {
perf.mark('prisma-casl-extension-4')
logger?.log(
[perf.measure('prisma-casl-extension-Overall', 'prisma-casl-extension-0', 'prisma-casl-extension-4'),
perf.measure('prisma-casl-extension-Create Abilities', 'prisma-casl-extension-0', 'prisma-casl-extension-1'),
perf.measure('prisma-casl-extension-Create Casl Query', 'prisma-casl-extension-1', 'prisma-casl-extension-2'),
perf.measure('prisma-casl-extension-Finish Query', 'prisma-casl-extension-2', 'prisma-casl-extension-3'),
perf.measure('prisma-casl-extension-Filtering Results', 'prisma-casl-extension-3', 'prisma-casl-extension-4')
].map((measure) => {
return `${measure.name.replace('prisma-casl-extension-', '')}: ${measure.duration}`
})
)
}

return operation === 'createMany' ? { count: filteredResult.length } : filteredResult
}
const operationAbility = caslOperationDict[operation as PrismaCaslOperation]
/**
* on update or create we need to create a transaction
* since there can be errors if newly created db entries
* are not permitted by abilities
*
* for reads and deletes we skip the transaction
*/
if (operationAbility.action === 'update' || operationAbility.action === 'create') {
if (transaction) {
if (transaction.kind === 'itx') {
const transactionClient = (client as any)._createItxClient(transaction)
return transactionClient[model][op](caslQuery.args).then(cleanupResults)
} else if (transaction.kind === 'batch') {
//@ts-ignore
throw new Error('Sequential transactions are not supported in prisma-extension-casl.')
// const extendedRequest = request.then(cleanupResults)
// extendedRequest.requestTransaction = request.requestTransaction
//@ts-ignore
// return client._createPrismaPromise(new Promise((resolve, reject) => {
// query(caslQuery.args).then(cleanupResults).then((result: any) => resolve(result)).catch(((e: any) => reject(e)))
// })
}
} else {

return client.$transaction(async (tx) => {
//@ts-ignore
return tx[model][op](caslQuery.args).then(cleanupResults)
})
}
} else {
return query(caslQuery.args).then(cleanupResults)
}
},
...allOperations(getAbilityFactory)
},
}
})
Expand Down
26 changes: 25 additions & 1 deletion test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1583,7 +1583,31 @@ describe('prisma extension casl', () => {
await expect(client.user.findUnique({ where: { id: 0 } }).posts()).rejects.toThrow()

})

it('can do chained queries with local abilities', async () => {
function builderFactory() {
const builder = abilityBuilder()
const { can, cannot } = builder
can('read', 'User')
return builder
}
const client = seedClient.$extends(
useCaslAbilities(builderFactory)
)
// await expect(await client.user.findUnique({ where: { id: 0 } }).posts()).rejects.toThrow()

const result = await client.$casl((abilities) => {
abilities.can('read', 'Post')
return abilities
}).user.findUnique({ where: { id: 0 } }).posts()
expect(result).toEqual([{ authorId: 0, text: '', id: 0, threadId: 0 },
{
authorId: 0,
id: 3,
text: '',
threadId: 2,
},
])
})
})
describe('store permissions', () => {
it('has permissions on custom prop on chained queries', async () => {
Expand Down

0 comments on commit 83f45e0

Please sign in to comment.