Skip to content

Commit 64198a3

Browse files
authored
fix: using auth() in @default() is not effective for createManyAndReturn (#1727)
1 parent 738bba6 commit 64198a3

File tree

6 files changed

+136
-19
lines changed

6 files changed

+136
-19
lines changed

packages/runtime/src/cross/nested-write-visitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export class NestedWriteVisitor {
169169
break;
170170

171171
case 'createMany':
172+
case 'createManyAndReturn':
172173
if (data) {
173174
const newContext = pushNewContext(field, model, {});
174175
let callbackResult: any;

packages/runtime/src/cross/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const PrismaWriteActions = [
55
'create',
66
'createMany',
7+
'createManyAndReturn',
78
'connectOrCreate',
89
'update',
910
'updateMany',

packages/runtime/src/enhancements/node/default-auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
4949

5050
// base override
5151
protected async preprocessArgs(action: PrismaProxyActions, args: any) {
52-
const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
52+
const actionsOfInterest: PrismaProxyActions[] = [
53+
'create',
54+
'createMany',
55+
'createManyAndReturn',
56+
'update',
57+
'updateMany',
58+
'upsert',
59+
];
5360
if (actionsOfInterest.includes(action)) {
5461
const newArgs = await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
5562
return newArgs;

packages/runtime/src/enhancements/node/policy/handler.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
NestedWriteVisitorContext,
1313
enumerate,
1414
getIdFields,
15+
getModelInfo,
1516
requireField,
1617
resolveField,
1718
type FieldInfo,
@@ -435,17 +436,16 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
435436

436437
args = this.policyUtils.safeClone(args);
437438

438-
// go through create items, statically check input to determine if post-create
439-
// check is needed, and also validate zod schema
440-
const needPostCreateCheck = this.validateCreateInput(args);
439+
// `createManyAndReturn` may need to be converted to regular `create`s
440+
const shouldConvertToCreate = this.preprocessCreateManyPayload(args);
441441

442-
if (!needPostCreateCheck) {
443-
// direct create
442+
if (!shouldConvertToCreate) {
443+
// direct `createMany`
444444
return this.modelClient.createMany(args);
445445
} else {
446446
// create entities in a transaction with post-create checks
447447
return this.queryUtils.transaction(this.prisma, async (tx) => {
448-
const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx);
448+
const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx, 'createMany');
449449
// post-create check
450450
await this.runPostWriteChecks(postWriteChecks, tx);
451451
return { count: result.length };
@@ -472,14 +472,13 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
472472
const origArgs = args;
473473
args = this.policyUtils.safeClone(args);
474474

475-
// go through create items, statically check input to determine if post-create
476-
// check is needed, and also validate zod schema
477-
const needPostCreateCheck = this.validateCreateInput(args);
475+
// `createManyAndReturn` may need to be converted to regular `create`s
476+
const shouldConvertToCreate = this.preprocessCreateManyPayload(args);
478477

479478
let result: { result: unknown; error?: Error }[];
480479

481-
if (!needPostCreateCheck) {
482-
// direct create
480+
if (!shouldConvertToCreate) {
481+
// direct `createManyAndReturn`
483482
const created = await this.modelClient.createManyAndReturn(args);
484483

485484
// process read-back
@@ -489,7 +488,13 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
489488
} else {
490489
// create entities in a transaction with post-create checks
491490
result = await this.queryUtils.transaction(this.prisma, async (tx) => {
492-
const { result: created, postWriteChecks } = await this.doCreateMany(this.model, args, tx);
491+
const { result: created, postWriteChecks } = await this.doCreateMany(
492+
this.model,
493+
args,
494+
tx,
495+
'createManyAndReturn'
496+
);
497+
493498
// post-create check
494499
await this.runPostWriteChecks(postWriteChecks, tx);
495500

@@ -510,6 +515,46 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
510515
});
511516
}
512517

518+
/**
519+
* Preprocess the payload of `createMany` and `createManyAndReturn` and update in place if needed.
520+
* @returns `true` if the operation should be converted to regular `create`s; false otherwise.
521+
*/
522+
private preprocessCreateManyPayload(args: { data: any; select?: any; skipDuplicates?: boolean }) {
523+
if (!args) {
524+
return false;
525+
}
526+
527+
// if post-create check is needed
528+
const needPostCreateCheck = this.validateCreateInput(args);
529+
530+
// if the payload has any relation fields. Note that other enhancements (`withDefaultInAuth` for now)
531+
// can introduce relation fields into the payload
532+
let hasRelationFields = false;
533+
if (args.data) {
534+
hasRelationFields = this.hasRelationFieldsInPayload(this.model, args.data);
535+
}
536+
537+
return needPostCreateCheck || hasRelationFields;
538+
}
539+
540+
private hasRelationFieldsInPayload(model: string, payload: any) {
541+
const modelInfo = getModelInfo(this.modelMeta, model);
542+
if (!modelInfo) {
543+
return false;
544+
}
545+
546+
for (const item of enumerate(payload)) {
547+
for (const field of Object.keys(item)) {
548+
const fieldInfo = resolveField(this.modelMeta, model, field);
549+
if (fieldInfo?.isDataModel) {
550+
return true;
551+
}
552+
}
553+
}
554+
555+
return false;
556+
}
557+
513558
private validateCreateInput(args: { data: any; skipDuplicates?: boolean | undefined }) {
514559
let needPostCreateCheck = false;
515560
for (const item of enumerate(args.data)) {
@@ -537,7 +582,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
537582
return needPostCreateCheck;
538583
}
539584

540-
private async doCreateMany(model: string, args: { data: any; skipDuplicates?: boolean }, db: CrudContract) {
585+
private async doCreateMany(
586+
model: string,
587+
args: { data: any; skipDuplicates?: boolean },
588+
db: CrudContract,
589+
action: 'createMany' | 'createManyAndReturn'
590+
) {
541591
// We can't call the native "createMany" because we can't get back what was created
542592
// for post-create checks. Instead, do a "create" for each item and collect the results.
543593

@@ -553,7 +603,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
553603
}
554604

555605
if (this.shouldLogQuery) {
556-
this.logger.info(`[policy] \`create\` for \`createMany\` ${model}: ${formatObject(item)}`);
606+
this.logger.info(`[policy] \`create\` for \`${action}\` ${model}: ${formatObject(item)}`);
557607
}
558608
return await db[model].create({ select: this.policyUtils.makeIdSelection(model), data: item });
559609
})

tests/integration/tests/enhancements/with-policy/auth.test.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,11 @@ describe('auth() runtime test', () => {
389389
await expect(userDb.post.create({ data: { title: 'abc' } })).toResolveTruthy();
390390
await expect(userDb.post.findMany()).resolves.toHaveLength(1);
391391
await expect(userDb.post.count({ where: { authorName: 'user1', score: 10 } })).resolves.toBe(1);
392+
393+
await expect(userDb.post.createMany({ data: [{ title: 'def' }] })).resolves.toMatchObject({ count: 1 });
394+
const r = await userDb.post.createManyAndReturn({ data: [{ title: 'xxx' }, { title: 'yyy' }] });
395+
expect(r[0]).toMatchObject({ title: 'xxx', score: 10 });
396+
expect(r[1]).toMatchObject({ title: 'yyy', score: 10 });
392397
});
393398

394399
it('Default auth() data should not override passed args', async () => {
@@ -414,6 +419,12 @@ describe('auth() runtime test', () => {
414419
const userDb = enhance({ id: '1', name: userContextName });
415420
await expect(userDb.post.create({ data: { authorName: overrideName } })).toResolveTruthy();
416421
await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(1);
422+
423+
await expect(userDb.post.createMany({ data: [{ authorName: overrideName }] })).toResolveTruthy();
424+
await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(2);
425+
426+
const r = await userDb.post.createManyAndReturn({ data: [{ authorName: overrideName }] });
427+
expect(r[0]).toMatchObject({ authorName: overrideName });
417428
});
418429

419430
it('Default auth() with foreign key', async () => {
@@ -465,6 +476,15 @@ describe('auth() runtime test', () => {
465476
update: { title: 'post4' },
466477
})
467478
).resolves.toMatchObject({ authorId: 'userId-1' });
479+
480+
// default auth effective for createMany
481+
await expect(db.post.createMany({ data: { title: 'post5' } })).resolves.toMatchObject({ count: 1 });
482+
const r = await db.post.findFirst({ where: { title: 'post5' } });
483+
expect(r).toMatchObject({ authorId: 'userId-1' });
484+
485+
// default auth effective for createManyAndReturn
486+
const r1 = await db.post.createManyAndReturn({ data: { title: 'post6' } });
487+
expect(r1[0]).toMatchObject({ authorId: 'userId-1' });
468488
});
469489

470490
it('Default auth() with nested user context value', async () => {
@@ -631,14 +651,23 @@ describe('auth() runtime test', () => {
631651
const db = enhance({ id: 'userId-1' });
632652
await db.user.create({ data: { id: 'userId-1' } });
633653

634-
// safe
654+
// unsafe
635655
await db.stats.create({ data: { id: 'stats-1', viewCount: 10 } });
636-
await expect(db.post.create({ data: { title: 'title', statsId: 'stats-1' } })).toResolveTruthy();
656+
await expect(db.post.create({ data: { title: 'title1', statsId: 'stats-1' } })).toResolveTruthy();
637657

638-
// unsafe
639658
await db.stats.create({ data: { id: 'stats-2', viewCount: 10 } });
659+
await expect(db.post.createMany({ data: [{ title: 'title2', statsId: 'stats-2' }] })).resolves.toMatchObject({
660+
count: 1,
661+
});
662+
663+
await db.stats.create({ data: { id: 'stats-3', viewCount: 10 } });
664+
const r = await db.post.createManyAndReturn({ data: [{ title: 'title3', statsId: 'stats-3' }] });
665+
expect(r[0]).toMatchObject({ statsId: 'stats-3' });
666+
667+
// safe
668+
await db.stats.create({ data: { id: 'stats-4', viewCount: 10 } });
640669
await expect(
641-
db.post.create({ data: { title: 'title', stats: { connect: { id: 'stats-2' } } } })
670+
db.post.create({ data: { title: 'title4', stats: { connect: { id: 'stats-4' } } } })
642671
).toResolveTruthy();
643672
});
644673
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
describe('issue 1681', () => {
3+
it('regression', async () => {
4+
const { enhance } = await loadSchema(
5+
`
6+
model User {
7+
id Int @id @default(autoincrement())
8+
posts Post[]
9+
@@allow('all', true)
10+
}
11+
12+
model Post {
13+
id Int @id @default(autoincrement())
14+
title String
15+
author User @relation(fields: [authorId], references: [id])
16+
authorId Int @default(auth().id)
17+
@@allow('all', true)
18+
}
19+
`
20+
);
21+
22+
const db = enhance({ id: 1 });
23+
const user = await db.user.create({ data: {} });
24+
await expect(db.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 });
25+
26+
const r = await db.post.createManyAndReturn({ data: [{ title: 'Post2' }] });
27+
expect(r[0].authorId).toBe(user.id);
28+
});
29+
});

0 commit comments

Comments
 (0)