diff --git a/packages/runtime/src/enhancements/node/encryption.ts b/packages/runtime/src/enhancements/node/encryption.ts index d5db6669..3d0f738d 100644 --- a/packages/runtime/src/enhancements/node/encryption.ts +++ b/packages/runtime/src/enhancements/node/encryption.ts @@ -138,21 +138,29 @@ class EncryptedHandler extends DefaultPrismaProxyHandler { const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData); for (const field of getModelFields(entityData)) { - const fieldInfo = await resolveField(this.options.modelMeta, realModel, field); + // Don't decrypt null, undefined or empty string values + if (!entityData[field]) continue; + const fieldInfo = await resolveField(this.options.modelMeta, realModel, field); if (!fieldInfo) { continue; } - const shouldDecrypt = fieldInfo.attributes?.find((attr) => attr.name === '@encrypted'); - if (shouldDecrypt) { - // Don't decrypt null, undefined or empty string values - if (!entityData[field]) continue; - - try { - entityData[field] = await this.decrypt(fieldInfo, entityData[field]); - } catch (error) { - this.logger.warn(`Decryption failed, keeping original value: ${error}`); + if (fieldInfo.isDataModel) { + const items = + fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]]; + for (const item of items) { + // recurse + await this.doPostProcess(item, fieldInfo.type); + } + } else { + const shouldDecrypt = fieldInfo.attributes?.find((attr) => attr.name === '@encrypted'); + if (shouldDecrypt) { + try { + entityData[field] = await this.decrypt(fieldInfo, entityData[field]); + } catch (error) { + this.logger.warn(`Decryption failed, keeping original value: ${error}`); + } } } } diff --git a/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts b/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts index 9b630782..3f02c3f7 100644 --- a/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts +++ b/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts @@ -20,8 +20,6 @@ describe('Encrypted test', () => { model User { id String @id @default(cuid()) encrypted_value String @encrypted() - - @@allow('all', true) }`, { enhancements: ['encryption'], @@ -62,6 +60,52 @@ describe('Encrypted test', () => { expect(rawRead.encrypted_value).not.toBe('abc123'); }); + it('Decrypts nested fields', async () => { + const { enhance, prisma } = await loadSchema( + ` + model User { + id String @id @default(cuid()) + posts Post[] + } + + model Post { + id String @id @default(cuid()) + title String @encrypted() + author User @relation(fields: [authorId], references: [id]) + authorId String + } + `, + { + enhancements: ['encryption'], + enhanceOptions: { + encryption: { encryptionKey }, + }, + } + ); + + const db = enhance(); + + const create = await db.user.create({ + data: { + id: '1', + posts: { create: { title: 'Post1' } }, + }, + include: { posts: true }, + }); + expect(create.posts[0].title).toBe('Post1'); + + const read = await db.user.findUnique({ + where: { + id: '1', + }, + include: { posts: true }, + }); + expect(read.posts[0].title).toBe('Post1'); + + const rawRead = await prisma.user.findUnique({ where: { id: '1' }, include: { posts: true } }); + expect(rawRead.posts[0].title).not.toBe('Post1'); + }); + it('Multi-field encryption test', async () => { const { enhance } = await loadSchema( ` @@ -69,8 +113,6 @@ describe('Encrypted test', () => { id String @id @default(cuid()) x1 String @encrypted() x2 String @encrypted() - - @@allow('all', true) }`, { enhancements: ['encryption'], @@ -105,8 +147,6 @@ describe('Encrypted test', () => { model User { id String @id @default(cuid()) encrypted_value String @encrypted() - - @@allow('all', true) }`); const sudoDb = enhance(undefined, { kinds: [] }); @@ -203,7 +243,6 @@ describe('Encrypted test', () => { model User { id String @id @default(cuid()) encrypted_value String @encrypted() @length(0, 6) - @@allow('all', true) }`, {