11/* eslint-disable @typescript-eslint/no-explicit-any */
22import type { } from "effect/Equal"
33import type { } from "effect/Hash"
4- import { Array , Chunk , Context , Effect , Equivalence , flow , type NonEmptyReadonlyArray , Option , pipe , Pipeable , PubSub , S , Unify } from "effect-app"
4+ import { Array , Chunk , Context , Effect , Either , Equivalence , flow , type NonEmptyReadonlyArray , Option , pipe , Pipeable , PubSub , S , Unify } from "effect-app"
55import { toNonEmptyArray } from "effect-app/Array"
66import { NotFoundError } from "effect-app/client/errors"
77import { flatMapOption } from "effect-app/Effect"
@@ -12,6 +12,7 @@ import { getContextMap } from "../../../Store/ContextMapContainer.js"
1212import type { FieldValues } from "../../filter/types.js"
1313import * as Q from "../../query.js"
1414import type { Repository } from "../service.js"
15+ import { ValidationError , ValidationResult } from "../validation.js"
1516
1617const dedupe = Array . dedupeWith ( Equivalence . string )
1718
@@ -322,6 +323,64 @@ export function makeRepoInternal<
322323 )
323324 } ) as any
324325
326+ const validateSample = Effect . fn ( "validateSample" ) ( function * ( options ?: {
327+ percentage ?: number
328+ maxItems ?: number
329+ } ) {
330+ const percentage = options ?. percentage ?? 0.1 // default 10%
331+ const maxItems = options ?. maxItems
332+
333+ // 1. get all IDs with projection (bypasses main schema decode)
334+ const allIds = yield * store . filter ( {
335+ t : null as unknown as Encoded ,
336+ select : [ idKey as keyof Encoded ]
337+ } )
338+
339+ // 2. random subset
340+ const shuffled = [ ...allIds ] . sort ( ( ) => Math . random ( ) - 0.5 )
341+ const sampleSize = Math . min (
342+ maxItems ?? Infinity ,
343+ Math . ceil ( allIds . length * percentage )
344+ )
345+ const sample = shuffled . slice ( 0 , sampleSize )
346+
347+ // 3. validate each item
348+ const errors : ValidationError [ ] = [ ]
349+
350+ for ( const item of sample ) {
351+ const id = item [ idKey ]
352+ const rawResult = yield * store . find ( id )
353+
354+ if ( Option . isNone ( rawResult ) ) continue
355+
356+ const rawData = rawResult . value as Encoded
357+ const jitMResult = mapFrom ( rawData ) // apply jitM
358+
359+ const decodeResult = yield * S . decode ( schema ) ( jitMResult ) . pipe (
360+ Effect . either ,
361+ provideRctx
362+ )
363+
364+ if ( Either . isLeft ( decodeResult ) ) {
365+ errors . push (
366+ new ValidationError ( {
367+ id,
368+ rawData,
369+ jitMResult,
370+ error : decodeResult . left
371+ } )
372+ )
373+ }
374+ }
375+
376+ return new ValidationResult ( {
377+ total : NonNegativeInt ( allIds . length ) ,
378+ sampled : NonNegativeInt ( sample . length ) ,
379+ valid : NonNegativeInt ( sample . length - errors . length ) ,
380+ errors
381+ } )
382+ } )
383+
325384 const r : Repository < T , Encoded , Evt , ItemType , IdKey , Exclude < R , RCtx > , RPublish > = {
326385 changeFeed,
327386 itemType : name ,
@@ -331,6 +390,7 @@ export function makeRepoInternal<
331390 saveAndPublish,
332391 removeAndPublish,
333392 removeById,
393+ validateSample,
334394 queryRaw ( schema , q ) {
335395 const dec = S . decode ( S . Array ( schema ) )
336396 return store . queryRaw ( q ) . pipe ( Effect . flatMap ( dec ) )
0 commit comments