From a471821d29f18848872bc70e8e7a671d36276275 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Mon, 2 Jun 2025 11:15:14 -0600 Subject: [PATCH 1/2] fix(protect): handle undefined and null values in model interfaces --- .changeset/thirty-otters-cough.md | 5 ++ packages/protect/__tests__/protect.test.ts | 93 ++++++++++++++++++++++ packages/protect/src/ffi/model-helpers.ts | 18 ++--- 3 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 .changeset/thirty-otters-cough.md diff --git a/.changeset/thirty-otters-cough.md b/.changeset/thirty-otters-cough.md new file mode 100644 index 000000000..7e40c023f --- /dev/null +++ b/.changeset/thirty-otters-cough.md @@ -0,0 +1,5 @@ +--- +"@cipherstash/protect": minor +--- + +Fixed a bug in the model interface to correclty handle undefined and null values. diff --git a/packages/protect/__tests__/protect.test.ts b/packages/protect/__tests__/protect.test.ts index e8e44ed07..ec54ebe47 100644 --- a/packages/protect/__tests__/protect.test.ts +++ b/packages/protect/__tests__/protect.test.ts @@ -141,6 +141,48 @@ describe('encryption and decryption', () => { address: null, }) }, 30000) + + it('should handle undefined values in a model', async () => { + const protectClient = await protect(users) + + // Create a model with undefined values + const decryptedModel = { + id: '1', + email: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + } + + // Encrypt the model + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Decrypt the model + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + }) + }, 30000) }) describe('bulk encryption', () => { @@ -288,6 +330,57 @@ describe('bulk encryption edge cases', () => { expect(decryptedResult.data).toEqual(decryptedModels) }, 30000) + it('should handle mixed undefined and non-undefined values in bulk operations', async () => { + const protectClient = await protect(users) + const decryptedModels = [ + { + id: '1', + email: 'test1', + address: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, + { + id: '2', + email: null, + address: '123 Main St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + { + id: '3', + email: 'test3', + address: '456 Oak St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 3, + }, + ] + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) + it('should handle empty models in bulk operations', async () => { const protectClient = await protect(users) const decryptedModels = [ diff --git a/packages/protect/src/ffi/model-helpers.ts b/packages/protect/src/ffi/model-helpers.ts index 54e789ecc..390b15b2f 100644 --- a/packages/protect/src/ffi/model-helpers.ts +++ b/packages/protect/src/ffi/model-helpers.ts @@ -128,11 +128,11 @@ function prepareFieldsForOperation>( otherFields: Record operationFields: Record keyMap: Record - nullFields: Record + nullFields: Record } { const otherFields = extractOtherFields(model) const operationFields: Record = {} - const nullFields: Record = {} + const nullFields: Record = {} const keyMap: Record = {} let index = 0 @@ -143,8 +143,8 @@ function prepareFieldsForOperation>( : Object.entries(extractEncryptedFields(model)) for (const [key, value] of fieldsToProcess) { - if (value === null) { - nullFields[key] = null + if (value === null || value === undefined) { + nullFields[key] = value === undefined ? undefined : null continue } @@ -309,11 +309,11 @@ function prepareBulkModelsForOperation>( otherFields: Record[] operationFields: Record[] keyMap: Record - nullFields: Record[] + nullFields: Record[] } { const otherFields: Record[] = [] const operationFields: Record[] = [] - const nullFields: Record[] = [] + const nullFields: Record[] = [] const keyMap: Record = {} let index = 0 @@ -321,7 +321,7 @@ function prepareBulkModelsForOperation>( const model = models[modelIndex] const modelOtherFields = extractOtherFields(model) const modelOperationFields: Record = {} - const modelNullFields: Record = {} + const modelNullFields: Record = {} const fieldsToProcess = table ? Object.entries(model).filter(([key]) => @@ -330,8 +330,8 @@ function prepareBulkModelsForOperation>( : Object.entries(extractEncryptedFields(model)) for (const [key, value] of fieldsToProcess) { - if (value === null) { - modelNullFields[key] = null + if (value === null || value === undefined) { + modelNullFields[key] = value === undefined ? undefined : null continue } From 351ee126c46063aaa1dc9f697501c5f94077f4cd Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Mon, 2 Jun 2025 11:22:49 -0600 Subject: [PATCH 2/2] Update .changeset/thirty-otters-cough.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changeset/thirty-otters-cough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-otters-cough.md b/.changeset/thirty-otters-cough.md index 7e40c023f..410d426f9 100644 --- a/.changeset/thirty-otters-cough.md +++ b/.changeset/thirty-otters-cough.md @@ -2,4 +2,4 @@ "@cipherstash/protect": minor --- -Fixed a bug in the model interface to correclty handle undefined and null values. +Fixed a bug in the model interface to correctly handle undefined and null values.