Skip to content

Commit 7d7e990

Browse files
committed
feat(prisma-next): derive stack schema for v3 string columns
1 parent 6da16b1 commit 7d7e990

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

packages/prisma-next/src/stack/derive-schemas.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import {
3131
CIPHERSTASH_JSON_CODEC_ID,
3232
CIPHERSTASH_STRING_CODEC_ID,
3333
isCipherstashCodecId,
34+
isCipherstashV3CodecId,
3435
} from '../extension-metadata/constants'
36+
import { type V3Index, v3CastAs } from '../v3/domain-map'
3537

3638
/**
3739
* Structural shape of the subset of `contract.json` this derivation
@@ -98,7 +100,19 @@ export function deriveStackSchemas(
98100
const builders: Record<string, EncryptedColumn> = {}
99101
for (const [columnName, column] of Object.entries(columns)) {
100102
const codecId = column.codecId
101-
if (codecId == null || !isCipherstashCodecId(codecId)) continue
103+
if (codecId == null || !(isCipherstashCodecId(codecId) || isCipherstashV3CodecId(codecId))) continue
104+
105+
// v3 columns take a SINGLE index (one domain per column), carried as
106+
// `typeParams.index`; the v2 boolean-flag walk does not apply.
107+
if (isCipherstashV3CodecId(codecId)) {
108+
builders[columnName] = applyV3Index(
109+
encryptedColumn(columnName).dataType(v3CastAs('text')),
110+
column.typeParams,
111+
tableName,
112+
columnName,
113+
)
114+
continue
115+
}
102116

103117
const dataType = CODEC_ID_TO_DATA_TYPE[codecId]
104118
builders[columnName] = applyTypeParams(
@@ -140,3 +154,33 @@ function applyTypeParams(
140154
function isCipherstashFlag(value: string): value is CipherstashFlag {
141155
return value in FLAG_DISPATCH
142156
}
157+
158+
const V3_INDEX_VALUES = ['equality', 'freeTextSearch', 'orderAndRange'] as const
159+
160+
function isV3Index(value: unknown): value is V3Index {
161+
return typeof value === 'string' && (V3_INDEX_VALUES as readonly string[]).includes(value)
162+
}
163+
164+
/**
165+
* Apply a v3 column's single index. `typeParams.index` is `unknown` (the contract
166+
* view types typeParams as `Record<string, unknown> | null`), so narrow with
167+
* `isV3Index` before dispatching. A v3 column is exactly one domain ⇒ exactly one
168+
* index builder.
169+
*/
170+
function applyV3Index(
171+
builder: EncryptedColumn,
172+
typeParams: Readonly<Record<string, unknown>> | null | undefined,
173+
tableName: string,
174+
columnName: string,
175+
): EncryptedColumn {
176+
const index = typeParams?.['index']
177+
if (!isV3Index(index)) {
178+
throw new Error(
179+
`deriveStackSchemas: v3 column "${tableName}"."${columnName}" requires a valid 'index' typeParam ` +
180+
`(one of ${V3_INDEX_VALUES.join(', ')}), got ${String(index)}.`,
181+
)
182+
}
183+
if (index === 'equality') return builder.equality()
184+
if (index === 'freeTextSearch') return builder.freeTextSearch()
185+
return builder.orderAndRange()
186+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { deriveStackSchemas } from '../../src/stack/derive-schemas'
3+
import { CIPHERSTASH_STRING_V3_CODEC_ID } from '../../src/extension-metadata/constants'
4+
5+
function makeContract(tables: Record<string, Record<string, { codecId: string; typeParams?: unknown }>>) {
6+
return {
7+
storage: {
8+
tables: Object.fromEntries(
9+
Object.entries(tables).map(([name, cols]) => [name, { columns: cols }]),
10+
),
11+
},
12+
} as never
13+
}
14+
15+
describe('deriveStackSchemas (v3)', () => {
16+
it('maps each v3 string column to a string-cast column with exactly its one index', () => {
17+
const contract = makeContract({
18+
user_v3: {
19+
email: { codecId: CIPHERSTASH_STRING_V3_CODEC_ID, typeParams: { index: 'equality' } },
20+
bio: { codecId: CIPHERSTASH_STRING_V3_CODEC_ID, typeParams: { index: 'freeTextSearch' } },
21+
name: { codecId: CIPHERSTASH_STRING_V3_CODEC_ID, typeParams: { index: 'orderAndRange' } },
22+
},
23+
})
24+
const schemas = deriveStackSchemas(contract)
25+
expect(schemas).toHaveLength(1)
26+
const built = schemas[0]!.build()
27+
28+
// dataType is the string cast for every v3 text column.
29+
expect(built.columns.email?.cast_as).toBe('string')
30+
31+
// equality → unique only; freeTextSearch → match only; orderAndRange → ore only.
32+
expect(Object.keys(built.columns.email?.indexes ?? {})).toEqual(['unique'])
33+
expect(Object.keys(built.columns.bio?.indexes ?? {})).toEqual(['match'])
34+
expect(Object.keys(built.columns.name?.indexes ?? {})).toEqual(['ore'])
35+
})
36+
37+
it('throws on a v3 column with a missing/invalid index typeParam', () => {
38+
const missing = makeContract({ t: { c: { codecId: CIPHERSTASH_STRING_V3_CODEC_ID, typeParams: {} } } })
39+
expect(() => deriveStackSchemas(missing)).toThrow(/index/)
40+
const bad = makeContract({ t: { c: { codecId: CIPHERSTASH_STRING_V3_CODEC_ID, typeParams: { index: 'nope' } } } })
41+
expect(() => deriveStackSchemas(bad)).toThrow(/index/)
42+
})
43+
})

0 commit comments

Comments
 (0)