Skip to content

Commit a783800

Browse files
authored
feat(runtime): support for Prisma 5.14's createManyAndReturn (#1479)
1 parent f606b12 commit a783800

File tree

4 files changed

+200
-26
lines changed

4 files changed

+200
-26
lines changed

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

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -447,44 +447,106 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
447447

448448
// go through create items, statically check input to determine if post-create
449449
// check is needed, and also validate zod schema
450-
let needPostCreateCheck = false;
451-
for (const item of enumerate(args.data)) {
452-
const validationResult = this.validateCreateInputSchema(this.model, item);
453-
if (validationResult !== item) {
454-
this.policyUtils.replace(item, validationResult);
455-
}
456-
457-
const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create');
458-
if (inputCheck === false) {
459-
// unconditionally deny
460-
throw this.policyUtils.deniedByPolicy(
461-
this.model,
462-
'create',
463-
undefined,
464-
CrudFailureReason.ACCESS_POLICY_VIOLATION
465-
);
466-
} else if (inputCheck === true) {
467-
// unconditionally allow
468-
} else if (inputCheck === undefined) {
469-
// static policy check is not possible, need to do post-create check
470-
needPostCreateCheck = true;
471-
}
472-
}
450+
const needPostCreateCheck = this.validateCreateInput(args);
473451

474452
if (!needPostCreateCheck) {
453+
// direct create
475454
return this.modelClient.createMany(args);
476455
} else {
477456
// create entities in a transaction with post-create checks
478457
return this.queryUtils.transaction(this.prisma, async (tx) => {
479458
const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx);
480459
// post-create check
481460
await this.runPostWriteChecks(postWriteChecks, tx);
482-
return result;
461+
return { count: result.length };
483462
});
484463
}
485464
});
486465
}
487466

467+
createManyAndReturn(args: { select: any; include: any; data: any; skipDuplicates?: boolean }) {
468+
if (!args) {
469+
throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required');
470+
}
471+
if (!args.data) {
472+
throw prismaClientValidationError(
473+
this.prisma,
474+
this.prismaModule,
475+
'data field is required in query argument'
476+
);
477+
}
478+
479+
return createDeferredPromise(async () => {
480+
this.policyUtils.tryReject(this.prisma, this.model, 'create');
481+
482+
const origArgs = args;
483+
args = clone(args);
484+
485+
// go through create items, statically check input to determine if post-create
486+
// check is needed, and also validate zod schema
487+
const needPostCreateCheck = this.validateCreateInput(args);
488+
489+
let result: { result: unknown; error?: Error }[];
490+
491+
if (!needPostCreateCheck) {
492+
// direct create
493+
const created = await this.modelClient.createManyAndReturn(args);
494+
495+
// process read-back
496+
result = await Promise.all(
497+
created.map((item) => this.policyUtils.readBack(this.prisma, this.model, 'create', origArgs, item))
498+
);
499+
} else {
500+
// create entities in a transaction with post-create checks
501+
result = await this.queryUtils.transaction(this.prisma, async (tx) => {
502+
const { result: created, postWriteChecks } = await this.doCreateMany(this.model, args, tx);
503+
// post-create check
504+
await this.runPostWriteChecks(postWriteChecks, tx);
505+
506+
// process read-back
507+
return Promise.all(
508+
created.map((item) => this.policyUtils.readBack(tx, this.model, 'create', origArgs, item))
509+
);
510+
});
511+
}
512+
513+
// throw read-back error if any of create result read-back fails
514+
const error = result.find((r) => !!r.error)?.error;
515+
if (error) {
516+
throw error;
517+
} else {
518+
return result.map((r) => r.result);
519+
}
520+
});
521+
}
522+
523+
private validateCreateInput(args: { data: any; skipDuplicates?: boolean | undefined }) {
524+
let needPostCreateCheck = false;
525+
for (const item of enumerate(args.data)) {
526+
const validationResult = this.validateCreateInputSchema(this.model, item);
527+
if (validationResult !== item) {
528+
this.policyUtils.replace(item, validationResult);
529+
}
530+
531+
const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create');
532+
if (inputCheck === false) {
533+
// unconditionally deny
534+
throw this.policyUtils.deniedByPolicy(
535+
this.model,
536+
'create',
537+
undefined,
538+
CrudFailureReason.ACCESS_POLICY_VIOLATION
539+
);
540+
} else if (inputCheck === true) {
541+
// unconditionally allow
542+
} else if (inputCheck === undefined) {
543+
// static policy check is not possible, need to do post-create check
544+
needPostCreateCheck = true;
545+
}
546+
}
547+
return needPostCreateCheck;
548+
}
549+
488550
private async doCreateMany(model: string, args: { data: any; skipDuplicates?: boolean }, db: CrudContract) {
489551
// We can't call the native "createMany" because we can't get back what was created
490552
// for post-create checks. Instead, do a "create" for each item and collect the results.
@@ -511,7 +573,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
511573
createResult = createResult.filter((p) => !!p);
512574

513575
return {
514-
result: { count: createResult.length },
576+
result: createResult,
515577
postWriteChecks: createResult.map((item) => ({
516578
model,
517579
operation: 'create' as PolicyOperationKind,

packages/runtime/src/enhancements/proxy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export interface PrismaProxyHandler {
3535

3636
createMany(args: { data: any; skipDuplicates?: boolean }): Promise<BatchResult>;
3737

38+
createManyAndReturn(args: { data: any; select: any; include: any; skipDuplicates?: boolean }): Promise<unknown[]>;
39+
3840
update(args: any): Promise<unknown>;
3941

4042
updateMany(args: any): Promise<BatchResult>;
@@ -122,6 +124,10 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
122124
return this.deferred<{ count: number }>('createMany', args, false);
123125
}
124126

127+
createManyAndReturn(args: { data: any; select: any; include: any; skipDuplicates?: boolean }) {
128+
return this.deferred<unknown[]>('createManyAndReturn', args);
129+
}
130+
125131
update(args: any) {
126132
return this.deferred('update', args);
127133
}

packages/runtime/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export interface DbOperations {
1212
findUnique(args: unknown): PrismaPromise<any>;
1313
findUniqueOrThrow(args: unknown): PrismaPromise<any>;
1414
create(args: unknown): Promise<any>;
15-
createMany(args: unknown, skipDuplicates?: boolean): Promise<{ count: number }>;
15+
createMany(args: unknown): Promise<{ count: number }>;
16+
createManyAndReturn(args: unknown): Promise<unknown[]>;
1617
update(args: unknown): Promise<any>;
1718
updateMany(args: unknown): Promise<{ count: number }>;
1819
upsert(args: unknown): Promise<any>;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('Test API createManyAndReturn', () => {
4+
it('model-level policies', async () => {
5+
const { prisma, enhance } = await loadSchema(
6+
`
7+
model User {
8+
id Int @id @default(autoincrement())
9+
posts Post[]
10+
level Int
11+
12+
@@allow('read', level > 0)
13+
}
14+
15+
model Post {
16+
id Int @id @default(autoincrement())
17+
title String
18+
published Boolean @default(false)
19+
userId Int
20+
user User @relation(fields: [userId], references: [id])
21+
22+
@@allow('read', published)
23+
@@allow('create', contains(title, 'hello'))
24+
}
25+
`
26+
);
27+
28+
await prisma.user.createMany({
29+
data: [
30+
{ id: 1, level: 1 },
31+
{ id: 2, level: 0 },
32+
],
33+
});
34+
35+
const db = enhance();
36+
37+
// create rule violation
38+
await expect(
39+
db.post.createManyAndReturn({
40+
data: [{ title: 'foo', userId: 1 }],
41+
})
42+
).toBeRejectedByPolicy();
43+
44+
// success
45+
let r = await db.post.createManyAndReturn({
46+
data: [{ id: 1, title: 'hello1', userId: 1, published: true }],
47+
});
48+
expect(r.length).toBe(1);
49+
50+
// read-back check
51+
await expect(
52+
db.post.createManyAndReturn({
53+
data: [
54+
{ id: 2, title: 'hello2', userId: 1, published: true },
55+
{ id: 3, title: 'hello3', userId: 1, published: false },
56+
],
57+
})
58+
).toBeRejectedByPolicy(['result is not allowed to be read back']);
59+
await expect(prisma.post.findMany()).resolves.toHaveLength(3);
60+
61+
// return relation
62+
await prisma.post.deleteMany();
63+
r = await db.post.createManyAndReturn({
64+
include: { user: true },
65+
data: [{ id: 1, title: 'hello1', userId: 1, published: true }],
66+
});
67+
expect(r[0]).toMatchObject({ user: { id: 1 } });
68+
69+
// relation filtered
70+
await prisma.post.deleteMany();
71+
await expect(
72+
db.post.createManyAndReturn({
73+
include: { user: true },
74+
data: [{ id: 1, title: 'hello1', userId: 2, published: true }],
75+
})
76+
).toBeRejectedByPolicy(['result is not allowed to be read back']);
77+
await expect(prisma.post.findMany()).resolves.toHaveLength(1);
78+
});
79+
80+
it('field-level policies', async () => {
81+
const { prisma, enhance } = await loadSchema(
82+
`
83+
model Post {
84+
id Int @id @default(autoincrement())
85+
title String @allow('read', published)
86+
published Boolean @default(false)
87+
88+
@@allow('all', true)
89+
}
90+
`
91+
);
92+
93+
const db = enhance();
94+
95+
const r = await db.post.createManyAndReturn({
96+
data: [
97+
{ title: 'post1', published: true },
98+
{ title: 'post2', published: false },
99+
],
100+
});
101+
expect(r).toHaveLength(2);
102+
expect(r[0].title).toBe('post1');
103+
expect(r[1].title).toBeUndefined();
104+
});
105+
});

0 commit comments

Comments
 (0)