Skip to content

Commit 8bfe400

Browse files
authored
Merge pull request #301 from cipherstash/usability-1
feat(stack): usability improvements
2 parents 7ba9119 + c447e26 commit 8bfe400

28 files changed

+192
-1020
lines changed

.changeset/fresh-phones-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/stack": minor
3+
---
4+
5+
Remove null support from encrypt and bulk encrypt operations to improve typescript support and reduce operation complexity.

docs/reference/model-operations.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ The type system ensures:
155155

156156
- Schema-defined fields are typed as `Encrypted` in the return value
157157
- Non-schema fields retain their original types
158-
- Correct handling of optional and nullable fields
159158
- Preservation of nested object structures
160159

161160
### Using explicit type parameters

docs/reference/searchable-encryption-postgres.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ By default, `encryptQuery` returns an `Encrypted` object (the raw EQL JSON paylo
126126
| `'composite-literal'` | `string` | PostgreSQL composite literal format `("json")`. Use with Supabase `.eq()` or other APIs that require a string value. |
127127
| `'escaped-composite-literal'` | `string` | Escaped composite literal `"(\"json\")"`. Use when the query string will be embedded inside another string or JSON value. |
128128

129-
The return type of `encryptQuery` is `EncryptedQueryResult`, which is `Encrypted | string | null` depending on the `returnType` and whether the input was `null`.
129+
The return type of `encryptQuery` is `EncryptedQueryResult`, which is `Encrypted | string` depending on the `returnType`.
130130

131131
**Single query with `returnType`:**
132132

@@ -193,7 +193,6 @@ const documents = encryptedTable('documents', {
193193
| `string` (e.g. `'$.user.email'`) | `steVecSelector` | JSONPath selector queries |
194194
| `object` (e.g. `{ role: 'admin' }`) | `steVecTerm` | Containment queries |
195195
| `array` (e.g. `['admin', 'user']`) | `steVecTerm` | Containment queries |
196-
| `null` | Returns `null` | Null handling |
197196

198197
#### JSONPath selector queries
199198

packages/stack/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ All bulk methods make a single call to ZeroKMS regardless of the number of recor
170170
const plaintexts = [
171171
{ id: "u1", plaintext: "alice@example.com" },
172172
{ id: "u2", plaintext: "bob@example.com" },
173-
{ id: "u3", plaintext: null }, // null values are preserved
173+
{ id: "u3", plaintext: "charlie@example.com" },
174174
]
175175

176176
const encrypted = await client.bulkEncrypt(plaintexts, {

packages/stack/__tests__/bulk-protect.test.ts

Lines changed: 2 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -96,66 +96,8 @@ describe('bulk encryption and decryption', () => {
9696
expect(encryptedData.data[2].data).toHaveProperty('c')
9797
}, 30000)
9898

99-
it('should handle null values in bulk encrypt', async () => {
100-
const plaintexts = [
101-
{ id: 'user1', plaintext: 'alice@example.com' },
102-
{ id: 'user2', plaintext: null },
103-
{ id: 'user3', plaintext: 'charlie@example.com' },
104-
]
105-
106-
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
107-
column: users.email,
108-
table: users,
109-
})
110-
111-
if (encryptedData.failure) {
112-
throw new Error(`[protect]: ${encryptedData.failure.message}`)
113-
}
114-
115-
// Verify structure
116-
expect(encryptedData.data).toHaveLength(3)
117-
expect(encryptedData.data[0]).toHaveProperty('id', 'user1')
118-
expect(encryptedData.data[0]).toHaveProperty('data')
119-
expect(encryptedData.data[0].data).toHaveProperty('c')
120-
expect(encryptedData.data[1]).toHaveProperty('id', 'user2')
121-
expect(encryptedData.data[1]).toHaveProperty('data')
122-
expect(encryptedData.data[1].data).toBeNull()
123-
expect(encryptedData.data[2]).toHaveProperty('id', 'user3')
124-
expect(encryptedData.data[2]).toHaveProperty('data')
125-
expect(encryptedData.data[2].data).toHaveProperty('c')
126-
}, 30000)
127-
128-
it('should handle all null values in bulk encrypt', async () => {
129-
const plaintexts = [
130-
{ id: 'user1', plaintext: null },
131-
{ id: 'user2', plaintext: null },
132-
{ id: 'user3', plaintext: null },
133-
]
134-
135-
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
136-
column: users.email,
137-
table: users,
138-
})
139-
140-
if (encryptedData.failure) {
141-
throw new Error(`[protect]: ${encryptedData.failure.message}`)
142-
}
143-
144-
// Verify structure
145-
expect(encryptedData.data).toHaveLength(3)
146-
expect(encryptedData.data[0]).toHaveProperty('id', 'user1')
147-
expect(encryptedData.data[0]).toHaveProperty('data')
148-
expect(encryptedData.data[0].data).toBeNull()
149-
expect(encryptedData.data[1]).toHaveProperty('id', 'user2')
150-
expect(encryptedData.data[1]).toHaveProperty('data')
151-
expect(encryptedData.data[1].data).toBeNull()
152-
expect(encryptedData.data[2]).toHaveProperty('id', 'user3')
153-
expect(encryptedData.data[2]).toHaveProperty('data')
154-
expect(encryptedData.data[2].data).toBeNull()
155-
}, 30000)
156-
15799
it('should handle empty array in bulk encrypt', async () => {
158-
const plaintexts: Array<{ id?: string; plaintext: string | null }> = []
100+
const plaintexts: Array<{ id?: string; plaintext: string }> = []
159101

160102
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
161103
column: users.email,
@@ -245,77 +187,6 @@ describe('bulk encryption and decryption', () => {
245187
)
246188
}, 30000)
247189

248-
it('should handle null values in bulk decrypt', async () => {
249-
// First encrypt some data with nulls
250-
const plaintexts = [
251-
{ id: 'user1', plaintext: 'alice@example.com' },
252-
{ id: 'user2', plaintext: null },
253-
{ id: 'user3', plaintext: 'charlie@example.com' },
254-
]
255-
256-
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
257-
column: users.email,
258-
table: users,
259-
})
260-
261-
if (encryptedData.failure) {
262-
throw new Error(`[protect]: ${encryptedData.failure.message}`)
263-
}
264-
265-
// Now decrypt the data
266-
const decryptedData = await protectClient.bulkDecrypt(encryptedData.data)
267-
268-
if (decryptedData.failure) {
269-
throw new Error(`[protect]: ${decryptedData.failure.message}`)
270-
}
271-
272-
// Verify structure
273-
expect(decryptedData.data).toHaveLength(3)
274-
expect(decryptedData.data[0]).toHaveProperty('id', 'user1')
275-
expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com')
276-
expect(decryptedData.data[1]).toHaveProperty('id', 'user2')
277-
expect(decryptedData.data[1]).toHaveProperty('data', null)
278-
expect(decryptedData.data[2]).toHaveProperty('id', 'user3')
279-
expect(decryptedData.data[2]).toHaveProperty(
280-
'data',
281-
'charlie@example.com',
282-
)
283-
}, 30000)
284-
285-
it('should handle all null values in bulk decrypt', async () => {
286-
// First encrypt some data with all nulls
287-
const plaintexts = [
288-
{ id: 'user1', plaintext: null },
289-
{ id: 'user2', plaintext: null },
290-
{ id: 'user3', plaintext: null },
291-
]
292-
293-
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
294-
column: users.email,
295-
table: users,
296-
})
297-
298-
if (encryptedData.failure) {
299-
throw new Error(`[protect]: ${encryptedData.failure.message}`)
300-
}
301-
302-
// Now decrypt the data
303-
const decryptedData = await protectClient.bulkDecrypt(encryptedData.data)
304-
305-
if (decryptedData.failure) {
306-
throw new Error(`[protect]: ${decryptedData.failure.message}`)
307-
}
308-
309-
// Verify structure
310-
expect(decryptedData.data).toHaveLength(3)
311-
expect(decryptedData.data[0]).toHaveProperty('id', 'user1')
312-
expect(decryptedData.data[0]).toHaveProperty('data', null)
313-
expect(decryptedData.data[1]).toHaveProperty('id', 'user2')
314-
expect(decryptedData.data[1]).toHaveProperty('data', null)
315-
expect(decryptedData.data[2]).toHaveProperty('id', 'user3')
316-
expect(decryptedData.data[2]).toHaveProperty('data', null)
317-
}, 30000)
318-
319190
it('should handle empty array in bulk decrypt', async () => {
320191
const encryptedPayloads: Array<{ id?: string; data: Encrypted }> = []
321192

@@ -399,57 +270,6 @@ describe('bulk encryption and decryption', () => {
399270
)
400271
}, 30000)
401272

402-
it('should handle null values with lock context', async () => {
403-
const userJwt = process.env.USER_JWT
404-
405-
if (!userJwt) {
406-
console.log('Skipping lock context test - no USER_JWT provided')
407-
return
408-
}
409-
410-
const lc = new LockContext()
411-
const lockContext = await lc.identify(userJwt)
412-
413-
if (lockContext.failure) {
414-
throw new Error(`[protect]: ${lockContext.failure.message}`)
415-
}
416-
417-
const plaintexts = [
418-
{ id: 'user1', plaintext: 'alice@example.com' },
419-
{ id: 'user2', plaintext: null },
420-
{ id: 'user3', plaintext: 'charlie@example.com' },
421-
]
422-
423-
// Encrypt with lock context
424-
const encryptedData = await protectClient
425-
.bulkEncrypt(plaintexts, {
426-
column: users.email,
427-
table: users,
428-
})
429-
.withLockContext(lockContext.data)
430-
431-
if (encryptedData.failure) {
432-
throw new Error(`[protect]: ${encryptedData.failure.message}`)
433-
}
434-
435-
// Verify null is preserved
436-
expect(encryptedData.data[1]).toHaveProperty('data')
437-
expect(encryptedData.data[1].data).toBeNull()
438-
439-
// Decrypt with lock context
440-
const decryptedData = await protectClient
441-
.bulkDecrypt(encryptedData.data)
442-
.withLockContext(lockContext.data)
443-
444-
if (decryptedData.failure) {
445-
throw new Error(`[protect]: ${decryptedData.failure.message}`)
446-
}
447-
448-
// Verify null is preserved
449-
expect(decryptedData.data[1]).toHaveProperty('data')
450-
expect(decryptedData.data[1].data).toBeNull()
451-
}, 30000)
452-
453273
it('should decrypt mixed lock context payloads with specific lock context', async () => {
454274
const userJwt = process.env.USER_JWT
455275
const user2Jwt = process.env.USER_2_JWT
@@ -533,8 +353,7 @@ describe('bulk encryption and decryption', () => {
533353
const originalData = [
534354
{ id: 'user1', plaintext: 'alice@example.com' },
535355
{ id: 'user2', plaintext: 'bob@example.com' },
536-
{ id: 'user3', plaintext: null },
537-
{ id: 'user4', plaintext: 'dave@example.com' },
356+
{ id: 'user3', plaintext: 'dave@example.com' },
538357
]
539358

540359
// Encrypt

0 commit comments

Comments
 (0)