infoalloggi-monorepo/apps/infoalloggi/src/lib/dateParserJsonKysely.ts

169 lines
4.5 KiB
TypeScript
Raw Normal View History

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> = 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<T[K]> extends (infer U)[]
?
| SetNestedPath<U, Rest>[]
| (null extends T[K] ? null : never)
| (undefined extends T[K] ? undefined : never)
:
| SetNestedPath<NonNullable<T[K]>, 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]> : 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<SetNestedPath<T, Head>, Tail>
: T;
// --- Runtime ---
type ParsedPath = string[];
function applyDatePath(obj: Record<string, unknown>, 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<string, unknown>, rest);
}
}
} else if (
value !== null &&
value !== undefined &&
typeof value === "object"
) {
applyDatePath(value as Record<string, unknown>, 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<const Paths extends readonly string[]>
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<QueryResult<UnknownRow>> {
return {
...args.result,
rows: args.result.rows.map((row) => {
const newRow = { ...row } as Record<string, unknown>;
for (const path of this.parsedPaths) {
applyDatePath(newRow, path);
}
return newRow;
}),
};
}
}
type NoResultErrorArg = NoResultErrorConstructor | ((node: QueryNode) => Error);
type PluggedQuery<T> = {
execute(): Promise<T[]>;
executeTakeFirst(): Promise<T | undefined>;
executeTakeFirstOrThrow(errorConstructor?: NoResultErrorArg): Promise<T>;
};
/**
* 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<const Paths extends readonly string[], T>(
query: {
withPlugin(plugin: KyselyPlugin): PluggedQuery<T>;
},
paths: Paths,
): {
execute(): Promise<WithParsedDates<T, Paths>[]>;
executeTakeFirst(): Promise<WithParsedDates<T, Paths> | undefined>;
executeTakeFirstOrThrow(
errorConstructor?: NoResultErrorArg,
): Promise<WithParsedDates<T, Paths>>;
} {
type Out = WithParsedDates<T, Paths>;
const plugged = query.withPlugin(new DateJsonParserPlugin(paths));
return {
execute: () => plugged.execute() as Promise<Out[]>,
executeTakeFirst: () =>
plugged.executeTakeFirst() as Promise<Out | undefined>,
executeTakeFirstOrThrow: (errorConstructor) =>
plugged.executeTakeFirstOrThrow(errorConstructor) as Promise<Out>,
};
}