feat: add DateJsonParserPlugin to convert date strings to Date objects in Kysely queries
This commit is contained in:
parent
1d1051f25a
commit
d306548209
2 changed files with 565 additions and 0 deletions
397
apps/infoalloggi/src/lib/dateParserJsonKysely.test.ts
Normal file
397
apps/infoalloggi/src/lib/dateParserJsonKysely.test.ts
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
import type { KyselyPlugin, UnknownRow } from "kysely";
|
||||
import {
|
||||
createQueryId,
|
||||
DummyDriver,
|
||||
type Insertable,
|
||||
Kysely,
|
||||
NoResultError,
|
||||
PostgresAdapter,
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
type Selectable,
|
||||
type Updateable,
|
||||
} 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;
|
||||
}
|
||||
|
||||
export type Box = Selectable<BoxTable>;
|
||||
export type Item = Selectable<ItemTable>;
|
||||
export type NewItem = Insertable<ItemTable>;
|
||||
export type ItemUpdate = Updateable<ItemTable>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 }[] }[]>();
|
||||
});
|
||||
});
|
||||
168
apps/infoalloggi/src/lib/dateParserJsonKysely.ts
Normal file
168
apps/infoalloggi/src/lib/dateParserJsonKysely.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
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>,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue