Skip to content

Commit 96d0ce5

Browse files
authored
fix(encryption): fixes for createMany and createManyAndReturn operations (#1944)
1 parent 7ed9841 commit 96d0ce5

File tree

7 files changed

+183
-38
lines changed

7 files changed

+183
-38
lines changed

packages/runtime/src/constants.ts

+12
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ export const PRISMA_MINIMUM_VERSION = '5.0.0';
6767
* Prefix for auxiliary relation field generated for delegated models
6868
*/
6969
export const DELEGATE_AUX_RELATION_PREFIX = 'delegate_aux';
70+
71+
/**
72+
* Prisma actions that can have a write payload
73+
*/
74+
export const ACTIONS_WITH_WRITE_PAYLOAD = [
75+
'create',
76+
'createMany',
77+
'createManyAndReturn',
78+
'update',
79+
'updateMany',
80+
'upsert',
81+
];

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

+25-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type { FieldInfo, ModelMeta } from './model-meta';
55
import { resolveField } from './model-meta';
66
import { MaybePromise, PrismaWriteActionType, PrismaWriteActions } from './types';
7-
import { getModelFields } from './utils';
7+
import { enumerate, getModelFields } from './utils';
88

99
type NestingPathItem = { field?: FieldInfo; model: string; where: any; unique: boolean };
1010

@@ -310,31 +310,33 @@ export class NestedWriteVisitor {
310310
payload: any,
311311
nestingPath: NestingPathItem[]
312312
) {
313-
for (const field of getModelFields(payload)) {
314-
const fieldInfo = resolveField(this.modelMeta, model, field);
315-
if (!fieldInfo) {
316-
continue;
317-
}
313+
for (const item of enumerate(payload)) {
314+
for (const field of getModelFields(item)) {
315+
const fieldInfo = resolveField(this.modelMeta, model, field);
316+
if (!fieldInfo) {
317+
continue;
318+
}
318319

319-
if (fieldInfo.isDataModel) {
320-
if (payload[field]) {
321-
// recurse into nested payloads
322-
for (const [subAction, subData] of Object.entries<any>(payload[field])) {
323-
if (this.isPrismaWriteAction(subAction) && subData) {
324-
await this.doVisit(fieldInfo.type, subAction, subData, payload[field], fieldInfo, [
325-
...nestingPath,
326-
]);
320+
if (fieldInfo.isDataModel) {
321+
if (item[field]) {
322+
// recurse into nested payloads
323+
for (const [subAction, subData] of Object.entries<any>(item[field])) {
324+
if (this.isPrismaWriteAction(subAction) && subData) {
325+
await this.doVisit(fieldInfo.type, subAction, subData, item[field], fieldInfo, [
326+
...nestingPath,
327+
]);
328+
}
327329
}
328330
}
329-
}
330-
} else {
331-
// visit plain field
332-
if (this.callback.field) {
333-
await this.callback.field(fieldInfo, action, payload[field], {
334-
parent: payload,
335-
nestingPath,
336-
field: fieldInfo,
337-
});
331+
} else {
332+
// visit plain field
333+
if (this.callback.field) {
334+
await this.callback.field(fieldInfo, action, item[field], {
335+
parent: item,
336+
nestingPath,
337+
field: fieldInfo,
338+
});
339+
}
338340
}
339341
}
340342
}

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

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33

4+
import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants';
45
import {
56
FieldInfo,
67
NestedWriteVisitor,
@@ -50,15 +51,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
5051

5152
// base override
5253
protected async preprocessArgs(action: PrismaProxyActions, args: any) {
53-
const actionsOfInterest: PrismaProxyActions[] = [
54-
'create',
55-
'createMany',
56-
'createManyAndReturn',
57-
'update',
58-
'updateMany',
59-
'upsert',
60-
];
61-
if (actionsOfInterest.includes(action)) {
54+
if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) {
6255
const newArgs = await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
6356
return newArgs;
6457
}

packages/runtime/src/enhancements/node/encryption.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-disable @typescript-eslint/no-unused-vars */
33

44
import { z } from 'zod';
5+
import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants';
56
import {
67
FieldInfo,
78
NestedWriteVisitor,
@@ -211,8 +212,7 @@ class EncryptedHandler extends DefaultPrismaProxyHandler {
211212

212213
// base override
213214
protected async preprocessArgs(action: PrismaProxyActions, args: any) {
214-
const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
215-
if (args && args.data && actionsOfInterest.includes(action)) {
215+
if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) {
216216
await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
217217
}
218218
return args;

packages/runtime/src/enhancements/node/password.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
/* eslint-disable @typescript-eslint/no-unused-vars */
33

4-
import { DEFAULT_PASSWORD_SALT_LENGTH } from '../../constants';
4+
import { ACTIONS_WITH_WRITE_PAYLOAD, DEFAULT_PASSWORD_SALT_LENGTH } from '../../constants';
55
import { NestedWriteVisitor, type PrismaWriteActionType } from '../../cross';
66
import { DbClientContract } from '../../types';
77
import { InternalEnhancementOptions } from './create-enhancement';
@@ -39,8 +39,7 @@ class PasswordHandler extends DefaultPrismaProxyHandler {
3939

4040
// base override
4141
protected async preprocessArgs(action: PrismaProxyActions, args: any) {
42-
const actionsOfInterest: PrismaProxyActions[] = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
43-
if (args && args.data && actionsOfInterest.includes(action)) {
42+
if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) {
4443
await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args);
4544
}
4645
return args;

tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts

+118
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,124 @@ describe('Encrypted test', () => {
5858
expect(read.encrypted_value).toBe('abc123');
5959
expect(sudoRead.encrypted_value).not.toBe('abc123');
6060
expect(rawRead.encrypted_value).not.toBe('abc123');
61+
62+
// update
63+
const updated = await db.user.update({
64+
where: { id: '1' },
65+
data: { encrypted_value: 'abc234' },
66+
});
67+
expect(updated.encrypted_value).toBe('abc234');
68+
await expect(db.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({
69+
encrypted_value: 'abc234',
70+
});
71+
await expect(prisma.user.findUnique({ where: { id: '1' } })).resolves.not.toMatchObject({
72+
encrypted_value: 'abc234',
73+
});
74+
75+
// upsert with create
76+
const upsertCreate = await db.user.upsert({
77+
where: { id: '2' },
78+
create: {
79+
id: '2',
80+
encrypted_value: 'abc345',
81+
},
82+
update: {
83+
encrypted_value: 'abc456',
84+
},
85+
});
86+
expect(upsertCreate.encrypted_value).toBe('abc345');
87+
await expect(db.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({
88+
encrypted_value: 'abc345',
89+
});
90+
await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({
91+
encrypted_value: 'abc345',
92+
});
93+
94+
// upsert with update
95+
const upsertUpdate = await db.user.upsert({
96+
where: { id: '2' },
97+
create: {
98+
id: '2',
99+
encrypted_value: 'abc345',
100+
},
101+
update: {
102+
encrypted_value: 'abc456',
103+
},
104+
});
105+
expect(upsertUpdate.encrypted_value).toBe('abc456');
106+
await expect(db.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({
107+
encrypted_value: 'abc456',
108+
});
109+
await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({
110+
encrypted_value: 'abc456',
111+
});
112+
113+
// createMany
114+
await db.user.createMany({
115+
data: [
116+
{ id: '3', encrypted_value: 'abc567' },
117+
{ id: '4', encrypted_value: 'abc678' },
118+
],
119+
});
120+
await expect(db.user.findUnique({ where: { id: '3' } })).resolves.toMatchObject({
121+
encrypted_value: 'abc567',
122+
});
123+
await expect(prisma.user.findUnique({ where: { id: '3' } })).resolves.not.toMatchObject({
124+
encrypted_value: 'abc567',
125+
});
126+
127+
// createManyAndReturn
128+
await expect(
129+
db.user.createManyAndReturn({
130+
data: [
131+
{ id: '5', encrypted_value: 'abc789' },
132+
{ id: '6', encrypted_value: 'abc890' },
133+
],
134+
})
135+
).resolves.toEqual(
136+
expect.arrayContaining([
137+
{ id: '5', encrypted_value: 'abc789' },
138+
{ id: '6', encrypted_value: 'abc890' },
139+
])
140+
);
141+
await expect(db.user.findUnique({ where: { id: '5' } })).resolves.toMatchObject({
142+
encrypted_value: 'abc789',
143+
});
144+
await expect(prisma.user.findUnique({ where: { id: '5' } })).resolves.not.toMatchObject({
145+
encrypted_value: 'abc789',
146+
});
147+
});
148+
149+
it('Works with nullish values', async () => {
150+
const { enhance, prisma } = await loadSchema(
151+
`
152+
model User {
153+
id String @id @default(cuid())
154+
encrypted_value String? @encrypted()
155+
}`,
156+
{
157+
enhancements: ['encryption'],
158+
enhanceOptions: {
159+
encryption: { encryptionKey },
160+
},
161+
}
162+
);
163+
164+
const db = enhance();
165+
await expect(db.user.create({ data: { id: '1', encrypted_value: '' } })).resolves.toMatchObject({
166+
encrypted_value: '',
167+
});
168+
await expect(prisma.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ encrypted_value: '' });
169+
170+
await expect(db.user.create({ data: { id: '2' } })).resolves.toMatchObject({
171+
encrypted_value: null,
172+
});
173+
await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ encrypted_value: null });
174+
175+
await expect(db.user.create({ data: { id: '3', encrypted_value: null } })).resolves.toMatchObject({
176+
encrypted_value: null,
177+
});
178+
await expect(prisma.user.findUnique({ where: { id: '3' } })).resolves.toMatchObject({ encrypted_value: null });
61179
});
62180

63181
it('Decrypts nested fields', async () => {

tests/integration/tests/enhancements/with-password/with-password.test.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('Password test', () => {
1414
});
1515

1616
it('password tests', async () => {
17-
const { enhance } = await loadSchema(`
17+
const { enhance, prisma } = await loadSchema(`
1818
model User {
1919
id String @id @default(cuid())
2020
password String @password(saltLength: 16)
@@ -38,6 +38,27 @@ describe('Password test', () => {
3838
},
3939
});
4040
expect(compareSync('abc456', r1.password)).toBeTruthy();
41+
42+
await db.user.createMany({
43+
data: [
44+
{ id: '2', password: 'user2' },
45+
{ id: '3', password: 'user3' },
46+
],
47+
});
48+
await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({ password: 'user2' });
49+
const r2 = await db.user.findUnique({ where: { id: '2' } });
50+
expect(compareSync('user2', r2.password)).toBeTruthy();
51+
52+
const [u4] = await db.user.createManyAndReturn({
53+
data: [
54+
{ id: '4', password: 'user4' },
55+
{ id: '5', password: 'user5' },
56+
],
57+
});
58+
expect(compareSync('user4', u4.password)).toBeTruthy();
59+
await expect(prisma.user.findUnique({ where: { id: '4' } })).resolves.not.toMatchObject({ password: 'user4' });
60+
const r4 = await db.user.findUnique({ where: { id: '4' } });
61+
expect(compareSync('user4', r4.password)).toBeTruthy();
4162
});
4263

4364
it('length tests', async () => {

0 commit comments

Comments
 (0)