feat: type-safe CRUD sensitive field definitions (AEG-V16-021)

Constrain sensitive fields to row keys and preserve generic resource-definition contracts.
This commit is contained in:
2026-08-08 15:00:57 +09:00
parent 6f47d71c4a
commit 3287ae399e
4 changed files with 24 additions and 4 deletions
@@ -2,6 +2,7 @@ import type { ZodType } from 'zod'
import type { UiGridColumn } from '../ui/adapter/contracts'
export type CrudConcurrencyMode = 'NONE' | 'ETAG_IF_MATCH'
export type CrudIdempotencyMode = 'NONE' | 'IDEMPOTENCY_KEY'
export type CrudRowField<TRow> = Extract<keyof TRow, string>
export interface CrudResourceDefinition<TQuery, TRow, TForm> {
resourceId: string
querySchemaVersion: string
@@ -10,12 +11,12 @@ export interface CrudResourceDefinition<TQuery, TRow, TForm> {
rowSchema: ZodType<TRow>
formSchema: ZodType<TForm>
columns: readonly UiGridColumn[]
sensitiveFields: readonly string[]
sensitiveFields: readonly CrudRowField<TRow>[]
permissionPolicy: string
concurrencyMode: CrudConcurrencyMode
idempotencyMode: CrudIdempotencyMode
}
export function assertCrudResourceDefinition(definition: CrudResourceDefinition<unknown, unknown, unknown>): void {
export function assertCrudResourceDefinition<TQuery, TRow, TForm>(definition: CrudResourceDefinition<TQuery, TRow, TForm>): void {
if (!definition.resourceId.trim()) throw new Error('resourceId is required')
if (!definition.querySchemaVersion.trim() || !definition.responseSchemaVersion.trim()) throw new Error('schema versions are required')
if (!definition.permissionPolicy.trim()) throw new Error('permissionPolicy is required')
@@ -2,9 +2,11 @@ import { z } from 'zod'
import { describe, expect, it } from 'vitest'
import { assertCrudResourceDefinition, type CrudResourceDefinition } from '../resourceDefinition'
const definition: CrudResourceDefinition<unknown, unknown, unknown> = {
interface ClientRow { email: string }
const definition: CrudResourceDefinition<unknown, ClientRow, unknown> = {
resourceId: 'client-advisory', querySchemaVersion: '1', responseSchemaVersion: '1',
querySchema: z.unknown(), rowSchema: z.unknown(), formSchema: z.unknown(),
querySchema: z.unknown(), rowSchema: z.object({ email: z.string() }), formSchema: z.unknown(),
columns: [{ field: 'email', header: 'Email', sensitive: true }], sensitiveFields: ['email'],
permissionPolicy: 'ClientAdvisory.Read', concurrencyMode: 'ETAG_IF_MATCH', idempotencyMode: 'IDEMPOTENCY_KEY'
}