import type { KyselyPlugin, UnknownRow } from "kysely"; import { createQueryId, DummyDriver, Kysely, NoResultError, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, } from "kysely"; import { jsonArrayFrom } from "kysely/helpers/postgres"; import { describe, expect, expectTypeOf, it } from "vitest"; import { DateJsonParserPlugin, type WithParsedDates, withParsedDates, } from "~/lib/dateParserJsonKysely"; // --------------------------------------------------------------------------- // Schema // --------------------------------------------------------------------------- interface Database { box: BoxTable; item: ItemTable; } interface BoxTable { id: number; } interface ItemTable { id: number; box_id: number; created_at: Date; updated_at: Date | null; } // --------------------------------------------------------------------------- // DummyDriver db — used only for compile-time type tests // --------------------------------------------------------------------------- const db = new Kysely({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), // biome-ignore lint/suspicious/noExplicitAny: required by Kysely dialect contract createIntrospector: (db: Kysely) => new PostgresIntrospector(db), createQueryCompiler: () => new PostgresQueryCompiler(), }, }); // --------------------------------------------------------------------------- // Fake query helper — pipes synthetic rows through the plugin at runtime // --------------------------------------------------------------------------- class TestNoResultError extends Error { constructor() { super("No result (test)"); this.name = "TestNoResultError"; } } function makeFakeQuery(rows: T[]) { return { withPlugin(plugin: KyselyPlugin) { async function run(): Promise { const result = await plugin.transformResult({ queryId: createQueryId(), result: { rows: rows as UnknownRow[] }, }); return result.rows as T[]; } return { execute: run, async executeTakeFirst() { return (await run())[0]; }, async executeTakeFirstOrThrow( errorConstructor?: (new () => Error) | ((node: unknown) => Error), ) { const row = (await run())[0]; if (row !== undefined) return row; if (typeof errorConstructor === "function") { try { // try constructor form first throw new (errorConstructor as new () => Error)(); } catch (e) { if (e instanceof Error) throw e; // it was a factory function, not a constructor throw (errorConstructor as (node: unknown) => Error)({}); } } throw new NoResultError({} as never); }, }; }, }; } // --------------------------------------------------------------------------- // DateJsonParserPlugin — direct unit tests // --------------------------------------------------------------------------- describe("DateJsonParserPlugin", () => { it("parses a top-level date string", async () => { const plugin = new DateJsonParserPlugin(["created_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [{ created_at: "2024-01-15T10:00:00.000Z" }] }, }); expect(rows[0]?.created_at).toBeInstanceOf(Date); expect((rows[0]?.created_at as Date).toISOString()).toBe( "2024-01-15T10:00:00.000Z", ); }); it("parses dates nested inside a JSON array", async () => { const plugin = new DateJsonParserPlugin(["items.created_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [ { items: [ { id: 1, created_at: "2024-01-01T00:00:00.000Z" }, { id: 2, created_at: "2024-06-15T12:30:00.000Z" }, ], }, ], }, }); const items = rows[0]?.items as { id: number; created_at: Date }[]; expect(items[0]?.created_at).toBeInstanceOf(Date); expect(items[0]?.created_at.toISOString()).toBe("2024-01-01T00:00:00.000Z"); expect(items[1]?.created_at).toBeInstanceOf(Date); expect(items[1]?.created_at.toISOString()).toBe("2024-06-15T12:30:00.000Z"); }); it("parses a date nested inside a JSON object", async () => { const plugin = new DateJsonParserPlugin(["meta.published_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [{ meta: { published_at: "2024-03-01T00:00:00.000Z" } }], }, }); const meta = rows[0]?.meta as { published_at: Date }; expect(meta.published_at).toBeInstanceOf(Date); }); it("leaves null values untouched", async () => { const plugin = new DateJsonParserPlugin(["items.created_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [{ items: [{ id: 1, created_at: null }] }] }, }); const items = rows[0]?.items as { created_at: null }[]; expect(items[0]?.created_at).toBeNull(); }); it("leaves undefined values untouched", async () => { const plugin = new DateJsonParserPlugin(["created_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [{ created_at: undefined }] }, }); expect(rows[0]?.created_at).toBeUndefined(); }); it("leaves non-string primitive values untouched", async () => { const plugin = new DateJsonParserPlugin(["count"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [{ count: 42 }] }, }); expect(rows[0]?.count).toBe(42); }); it("applies multiple paths independently", async () => { const plugin = new DateJsonParserPlugin(["created_at", "updated_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [ { created_at: "2024-01-01T00:00:00.000Z", updated_at: "2024-06-01T00:00:00.000Z", }, ], }, }); expect(rows[0]?.created_at).toBeInstanceOf(Date); expect(rows[0]?.updated_at).toBeInstanceOf(Date); }); it("parses the same path across multiple result rows", async () => { const plugin = new DateJsonParserPlugin(["created_at"]); const { rows } = await plugin.transformResult({ queryId: createQueryId(), result: { rows: [ { created_at: "2024-01-01T00:00:00.000Z" }, { created_at: "2024-02-01T00:00:00.000Z" }, { created_at: "2024-03-01T00:00:00.000Z" }, ], }, }); for (const row of rows) { expect(row.created_at).toBeInstanceOf(Date); } }); it("does not mutate the original rows array", async () => { const original = [{ created_at: "2024-01-01T00:00:00.000Z" }]; const plugin = new DateJsonParserPlugin(["created_at"]); await plugin.transformResult({ queryId: createQueryId(), result: { rows: original }, }); // original row object should be untouched (plugin shallow-copies each row) expect(original[0]?.created_at).toBe("2024-01-01T00:00:00.000Z"); }); }); // --------------------------------------------------------------------------- // withParsedDates — runtime behaviour // --------------------------------------------------------------------------- describe("withParsedDates – runtime", () => { it("execute() returns all rows with dates parsed", async () => { const rows = await withParsedDates( makeFakeQuery([ { id: 1, items: [{ created_at: "2024-01-01T00:00:00.000Z" }] }, { id: 2, items: [{ created_at: "2024-06-01T00:00:00.000Z" }] }, ]), ["items.created_at"], ).execute(); expect(rows).toHaveLength(2); expect(rows[0]?.items[0]?.created_at).toBeInstanceOf(Date); expect(rows[1]?.items[0]?.created_at).toBeInstanceOf(Date); }); it("executeTakeFirst() returns the first row with date parsed", async () => { const row = await withParsedDates( makeFakeQuery([ { items: [{ created_at: "2024-01-01T00:00:00.000Z" }] }, { items: [{ created_at: "2099-01-01T00:00:00.000Z" }] }, ]), ["items.created_at"], ).executeTakeFirst(); expect(row).toBeDefined(); expect(row?.items[0]?.created_at).toBeInstanceOf(Date); expect(row?.items[0]?.created_at.toISOString()).toBe( "2024-01-01T00:00:00.000Z", ); }); it("executeTakeFirst() returns undefined when there are no rows", async () => { const row = await withParsedDates( makeFakeQuery<{ items: { created_at: string }[] }>([]), ["items.created_at"], ).executeTakeFirst(); expect(row).toBeUndefined(); }); it("executeTakeFirstOrThrow() returns the first row with date parsed", async () => { const row = await withParsedDates( makeFakeQuery([{ items: [{ created_at: "2024-03-23T00:00:00.000Z" }] }]), ["items.created_at"], ).executeTakeFirstOrThrow(); expect(row.items[0]?.created_at).toBeInstanceOf(Date); expect(row.items[0]?.created_at.toISOString()).toBe( "2024-03-23T00:00:00.000Z", ); }); it("executeTakeFirstOrThrow() throws NoResultError by default when empty", async () => { await expect( withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [ "items.created_at", ]).executeTakeFirstOrThrow(), ).rejects.toBeInstanceOf(NoResultError); }); it("executeTakeFirstOrThrow() throws a custom error class when provided", async () => { await expect( withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [ "items.created_at", ]).executeTakeFirstOrThrow(TestNoResultError), ).rejects.toBeInstanceOf(TestNoResultError); }); it("executeTakeFirstOrThrow() custom error has the expected message", async () => { await expect( withParsedDates(makeFakeQuery<{ items: { created_at: string }[] }>([]), [ "items.created_at", ]).executeTakeFirstOrThrow(TestNoResultError), ).rejects.toThrow("No result (test)"); }); }); // --------------------------------------------------------------------------- // withParsedDates — type-level tests (compile-time only, no runtime assertions) // --------------------------------------------------------------------------- describe("withParsedDates – types", () => { it("transforms string → Date for a nested array field", () => { const fakeRows: { items: { created_at: string }[] }[] = []; const query = withParsedDates(makeFakeQuery(fakeRows), [ "items.created_at", ]); type Result = Awaited>; expectTypeOf().toEqualTypeOf<{ items: { created_at: Date }[] }[]>(); }); it("preserves unrelated fields unchanged", () => { const fakeRows: { id: number; items: { created_at: string; name: string }[]; }[] = []; const query = withParsedDates(makeFakeQuery(fakeRows), [ "items.created_at", ]); type Result = Awaited>; expectTypeOf().toEqualTypeOf< { id: number; items: { created_at: Date; name: string }[] }[] >(); }); it("executeTakeFirst() returns Row | undefined", () => { const fakeRows: { items: { created_at: string }[] }[] = []; const query = withParsedDates(makeFakeQuery(fakeRows), [ "items.created_at", ]); type Result = Awaited>; expectTypeOf().toEqualTypeOf< { items: { created_at: Date }[] } | undefined >(); }); it("executeTakeFirstOrThrow() returns Row (never undefined)", () => { const fakeRows: { items: { created_at: string }[] }[] = []; const query = withParsedDates(makeFakeQuery(fakeRows), [ "items.created_at", ]); type Result = Awaited>; expectTypeOf().toEqualTypeOf<{ items: { created_at: Date }[] }>(); }); it("WithParsedDates utility type transforms correctly", () => { type Input = { items: { created_at: string; id: number }[] }; type Output = WithParsedDates; expectTypeOf().toEqualTypeOf<{ items: { created_at: Date; id: number }[]; }>(); }); it("WithParsedDates preserves null unions on the array field", () => { type Input = { items: { created_at: string }[] | null }; type Output = WithParsedDates; expectTypeOf().toEqualTypeOf<{ items: { created_at: Date }[] | null; }>(); }); it("type-checks against real DummyDriver Kysely instance", () => { const query = withParsedDates( db .selectFrom("box") .select((eb) => [ jsonArrayFrom( eb .selectFrom("item") .selectAll() .whereRef("item.box_id", "=", "box.id"), ).as("items"), ]), ["items.created_at"], ); // created_at inside items must be Date, not string — compile-time check only type Result = Awaited>; expectTypeOf().toExtend<{ items: { created_at: Date }[] }[]>(); }); });