import {CamelCase} from "type-fest"; import {ZodRawShape} from "zod/lib/types"; import {z, ZodDiscriminatedUnion, ZodLiteral, ZodObject, ZodSchema} from "zod"; import { EntityCore, EntityCoreShape, EntityCoreShapeType, EntitySymbol, Extender, UnsafeEntitySymbol, UnsafeZentity, Zentity, ZodShapeType } from "./zentity"; import {toCamelCase} from "./utils"; type ExtractType = T extends { [K in D]: infer V } ? { [K in keyof T]: K extends D ? V : T[K] } : never; type ShapeRecord = Record type KeyedExtender, R extends object> = { [K in keyof KS]?: Extender } type Flatten, D extends string> = ValueUnion<{ [K in keyof O]: ZodShapeType & { [Q in D]: K } }> type CC

= CamelCase<`${P}-${K}`> type Clean = T extends undefined ? {} : T type OShape, K extends keyof O> = O[K] & Clean type OType, K extends keyof O> = ZodShapeType> type ReturnType = E extends (arg: any) => infer R ? R : {} type ToZodD, D extends string> = ZodObject } }> & BS & EntityCoreShapeType> type ZentityNewFn, D extends string, KSE extends KeyedExtender, TD extends TransitionDef> = { [key in (keyof KS & string) as CC<`new`, key>]: (params: OType & { id: string }) => Full } type ZentityCreateFn, D extends string, KSE extends KeyedExtender, TD extends TransitionDef> = { [key in (keyof KS & string) as CC<`create`, key>]: (params: OType & EntityCore) => Full } type ZentityUnsafeFn, D extends string, KSE extends KeyedExtender, TD extends TransitionDef> = { [key in (keyof KS & string) as CC<`unsafe`, key>]: (params: OType & EntityCore) => UnsafeFull } type ZentityGuardFn, D extends string, KSE extends KeyedExtender, TD extends TransitionDef> = { [key in (keyof KS & string) as CC<'is', key>]: (data: any) => data is Full } type ZentityShape, D extends string> = { [key in (keyof KS & string) as CC]: OShape & { [K in D]: ZodLiteral } & typeof EntityCoreShape } type ZentitySchema, D extends string> = { [key in (keyof KS & string) as CC]: ZodObject & { [K in D]: ZodLiteral } & typeof EntityCoreShape> } type Full, D extends string, KE extends KeyedExtender, TD extends TransitionDef, key extends keyof KS> = Zentity<{ [Q in D]: key } & OType, ReturnType & Transition> type UnsafeFull, D extends string, KE extends KeyedExtender, TD extends TransitionDef, key extends keyof KS> = UnsafeZentity<{ [Q in D]: key } & OType, ReturnType & Transition> type DiscriminatedZentityClass, D extends string, KE extends KeyedExtender, TD extends TransitionDef> = { create: >(params: T & EntityCore & ZodShapeType) => Zentity & ExtractType, ReturnType & Transition>, new: >(params: T & ZodShapeType & { id: string }) => Zentity, ReturnType & Transition>, unsafe: >(params: T & ZodShapeType) => UnsafeZentity, ReturnType & Transition>, baseShape: BS & typeof EntityCoreShape, schema: ZodDiscriminatedUnion, ...ToZodD[]]>, } & ZentityNewFn & ZentityCreateFn & ZentityUnsafeFn & ZentityGuardFn & ZentitySchema & ZentityShape type ValueUnion = Required; type TransitionDef> = { [key in keyof KS]: (keyof Omit) [] } type ExclusiveToB = { [K in keyof B as K extends keyof A ? never : K]: B[K]; }; type Transition, KE extends KeyedExtender, TD extends TransitionDef, K extends keyof KS> = { [key in (TD[K][number] & string) as CC<`to`, key>]: (param: ExclusiveToB, ZodShapeType>) => Zentity, ReturnType & Transition> } function typedObjectKeys(object: O): (keyof O)[] { return Object.keys(object) as (keyof O)[] } function typedObjectEntries(object: O): [keyof O, O[keyof O]][] { return Object.entries(object) as [keyof O, O[keyof O]][] } function toObject(list: T[], fn: (item: T) => [K, Y]): Record { return list.reduce((acc, item) => { const [key, value] = fn(item) return { ...acc, [String(key)]: value } }, {} as Record) } export function statefulEntity, D extends string, KSE extends KeyedExtender, T extends TransitionDef>(options: { discriminator: D, base: BS, states: KS, extend: KSE, transitions: T, }): DiscriminatedZentityClass { const baseShape = { ...EntityCoreShape, ...options.base, } const unionSchema = z.discriminatedUnion( options.discriminator, typedObjectEntries(options.states).map(([key, shape]) => { return z.object({ ...shape, ...baseShape, [options.discriminator]: z.literal(key) }) }) ) const genEntityCore = () => ({ createdAt: new Date(), updatedAt: new Date(), }) const createFn = (schema: ZodSchema, params: any, unsafe: boolean = false) => { const data = unsafe ? params : schema.parse(params) return { _brand: UnsafeEntitySymbol, _safe: EntitySymbol, ...data, copy: copyFn(schema, data), validate: validateFn(schema, data), toPlain: () => data, ...options.extend[params[options.discriminator]]?.(data), ...transitionFnsForState(params[options.discriminator], data), } } const copyFn = (schema: ZodSchema, params: any) => { return (copyParams: any) => { return createFn(schema, { ...params, ...copyParams, }) } } const validateFn = (schema: ZodSchema, params: any) => () => { return createFn(schema, params) } const transitionFnsForState = (key: keyof KS, params: any) => { const toKeys = options.transitions[key] ?? [] return toKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`to-${String(key)}`)]: transitionFn(key, params) } }, {}) } const transitionFn = (to: keyof KS, params: any) => { return (extraParams: any) => { const destSchema = z.object({ ...baseShape, [options.discriminator]: z.literal(to), ...options.states[to] }) return createFn(destSchema, { ...params, ...extraParams, [options.discriminator]: to }) } } const stateKeys = typedObjectKeys(options.states) const stateShapes: ZentityShape = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`${String(key)}-shape`)]: { ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), } } }, {}) as any; const stateSchemas: ZentitySchema = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`${String(key)}-schema`)]: z.object({ ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), }) } }, {}) as any; const stateCreateFn: ZentityCreateFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`create-${String(key)}`)]: (params: OType & EntityCore) => { return createFn(z.object({ ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), }), {...params, [options.discriminator]: key}) } } }, {}) as any; const stateNewFn: ZentityNewFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`new-${String(key)}`)]: (params: OType) => { return createFn(z.object({ ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), }), { ...params, [options.discriminator]: key, ...genEntityCore() }, ) } } }, {}) as any; const stateUnsafeFn: ZentityUnsafeFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`unsafe-${String(key)}`)]: (params: OType & EntityCore) => { return createFn(z.object({ ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), }), { ...params, [options.discriminator]: key, ...genEntityCore() }, true, ) } } }, {}) as any; const stateIsFn: ZentityGuardFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`is-${String(key)}`)]: (params: any): params is Full => { const schema = z.object({ ...baseShape, ...options.states[key], [options.discriminator]: z.literal(key), }) return schema.safeParse(params).success } } }, {}) as any; return { ...stateCreateFn, ...stateNewFn, ...stateUnsafeFn, ...stateShapes, ...stateSchemas, ...stateIsFn, baseShape, schema: unionSchema, create: >(params: T & EntityCore & ZodShapeType) => { return createFn(unionSchema, params) }, new: >(params: T & ZodShapeType & { id: string }) => { return createFn(unionSchema, { ...params, ...genEntityCore(), }) }, unsafe: >(params: T & ZodShapeType) => { return createFn(unionSchema, params, true) }, } } type UnionOfStateShapeTypesWithDiscriminator, D extends string> = ValueUnion<{ [key in keyof KS]: ZodShapeType & { [Q in D]: key } }> export type inferStatefulZentityTypeFromClass = T extends DiscriminatedZentityClass ? Zentity & Clean, ReturnType & Transition> : never;