infoalloggi-monorepo/apps/infoalloggi/src/lib/dateParserJsonKysely.test.ts
Marco Pedone 5bff7219e8 refactor: remove ServiziHeader component and integrate its functionality directly into RicercaUser page
feat: update Servizio component to handle interruzioneDays and acceptedInterr logic

refactor: simplify servizio_actions by removing unused InterruzioneServizio and EditParametri components

feat: enhance FormEditServizioAdmin to include interruzioneDays and acceptedInterr fields

fix: adjust email service to delete obsolete emails and handle invalid email types gracefully

chore: update stripe controller to utilize interruzioneDays for lock expiration logic

fix: ensure correct handling of servizio expiration and interruzione logic in service controller
2026-03-24 17:01:40 +01:00

389 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Database>({
dialect: {
createAdapter: () => new PostgresAdapter(),
createDriver: () => new DummyDriver(),
// biome-ignore lint/suspicious/noExplicitAny: required by Kysely dialect contract
createIntrospector: (db: Kysely<any>) => 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<T>(rows: T[]) {
return {
withPlugin(plugin: KyselyPlugin) {
async function run(): Promise<T[]> {
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<ReturnType<typeof query.execute>>;
expectTypeOf<Result>().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<ReturnType<typeof query.execute>>;
expectTypeOf<Result>().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<ReturnType<typeof query.executeTakeFirst>>;
expectTypeOf<Result>().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<ReturnType<typeof query.executeTakeFirstOrThrow>>;
expectTypeOf<Result>().toEqualTypeOf<{ items: { created_at: Date }[] }>();
});
it("WithParsedDates utility type transforms correctly", () => {
type Input = { items: { created_at: string; id: number }[] };
type Output = WithParsedDates<Input, ["items.created_at"]>;
expectTypeOf<Output>().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<Input, ["items.created_at"]>;
expectTypeOf<Output>().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<ReturnType<typeof query.execute>>;
expectTypeOf<Result>().toExtend<{ items: { created_at: Date }[] }[]>();
});
});