import type { KyselyPlugin, NoResultErrorConstructor, PluginTransformQueryArgs, PluginTransformResultArgs, QueryNode, QueryResult, RootOperationNode, UnknownRow, } from "kysely"; // --- Type helpers --- /** Converts `string` (and nullable/undefinable variants) to `Date` variants. */ type StringToDate = T extends string ? Date : T; /** * Given type T and a dot-separated path, returns T with the field at that * path converted from `string` to `Date`. Automatically traverses arrays. */ type SetNestedPath< T, Path extends string, > = Path extends `${infer Key}.${infer Rest}` ? { [K in keyof T]: K extends Key ? NonNullable extends (infer U)[] ? | SetNestedPath[] | (null extends T[K] ? null : never) | (undefined extends T[K] ? undefined : never) : | SetNestedPath, Rest> | (null extends T[K] ? null : never) | (undefined extends T[K] ? undefined : never) : T[K]; } : { [K in keyof T]: K extends Path ? StringToDate : T[K]; }; /** Applies all date-path transformations in the Paths tuple to type T. */ export type WithParsedDates< T, Paths extends readonly string[], > = Paths extends readonly [ infer Head extends string, ...infer Tail extends string[], ] ? WithParsedDates, Tail> : T; /** Extracts dot-separated paths leading to a `string` or `Date` leaf, auto-traversing arrays. */ type DateConvertiblePaths< T, Depth extends unknown[] = [], > = Depth["length"] extends 5 ? never : T extends Record ? { [K in keyof T & string]: | (NonNullable extends string | Date ? K : never) | (NonNullable extends readonly (infer U)[] ? `${K}.${DateConvertiblePaths}` : NonNullable extends Record ? `${K}.${DateConvertiblePaths, [...Depth, unknown]>}` : never); }[keyof T & string] : never; // --- Runtime --- type ParsedPath = string[]; function applyDatePath(obj: Record, path: ParsedPath): void { const [key, ...rest] = path; if (key === undefined) return; const value = obj[key]; if (rest.length === 0) { if (typeof value === "string") { obj[key] = new Date(value); } return; } if (Array.isArray(value)) { for (const item of value) { if (item !== null && typeof item === "object") { applyDatePath(item as Record, rest); } } } else if ( value !== null && value !== undefined && typeof value === "object" ) { applyDatePath(value as Record, rest); } } /** * Kysely plugin that converts date strings to `Date` objects for fields * returned inside JSON (e.g. from `jsonArrayFrom` / `jsonObjectFrom`). * * Note: `withPlugin` does not transform TypeScript types. * Use `executeWithParsedDates` for a fully type-safe result. */ export class DateJsonParserPlugin implements KyselyPlugin { private readonly parsedPaths: ParsedPath[]; constructor(readonly paths: Paths) { this.parsedPaths = paths.map((p) => p.split(".")); } transformQuery(args: PluginTransformQueryArgs): RootOperationNode { return args.node; } async transformResult( args: PluginTransformResultArgs, ): Promise> { return { ...args.result, rows: args.result.rows.map((row) => { const newRow = { ...row } as Record; for (const path of this.parsedPaths) { applyDatePath(newRow, path); } return newRow; }), }; } } type NoResultErrorArg = NoResultErrorConstructor | ((node: QueryNode) => Error); type PluggedQuery = { execute(): Promise; executeTakeFirst(): Promise; executeTakeFirstOrThrow(errorConstructor?: NoResultErrorArg): Promise; }; /** * Wraps a Kysely query with date string → `Date` parsing, exposing * `execute`, `executeTakeFirst`, and `executeTakeFirstOrThrow` with * correctly inferred TypeScript types. * * @example * const rows = await withParsedDates( * db * .selectFrom("box") * .select((eb) => [ * jsonArrayFrom( * eb.selectFrom("item").selectAll().whereRef("item.box_id", "=", "box.id"), * ).as("items"), * ]), * ["items.created_at"], * ).execute(); * // rows: { items: { id: number; box_id: number; created_at: Date }[] }[] */ export function withParsedDates< T, const Paths extends readonly DateConvertiblePaths[], >( query: { withPlugin(plugin: KyselyPlugin): PluggedQuery; }, paths: Paths, ): { execute(): Promise[]>; executeTakeFirst(): Promise | undefined>; executeTakeFirstOrThrow( errorConstructor?: NoResultErrorArg, ): Promise>; } { type Out = WithParsedDates; const plugged = query.withPlugin(new DateJsonParserPlugin(paths)); return { execute: () => plugged.execute() as Promise, executeTakeFirst: () => plugged.executeTakeFirst() as Promise, executeTakeFirstOrThrow: (errorConstructor) => plugged.executeTakeFirstOrThrow(errorConstructor) as Promise, }; }