Skip to content
7 changes: 5 additions & 2 deletions example/example.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,9 +663,12 @@ const defaultImplementation: Implementation = async (method, path, params) => {
};

export class Client<T> {
protected readonly implementation: Implementation<T>;
public constructor(
protected readonly implementation: Implementation<T> = defaultImplementation,
) {}
implementation: Implementation<T> = defaultImplementation,
) {
this.implementation = implementation;
}
public provide<K extends Request>(
request: K,
params: Input[K],
Expand Down
5 changes: 4 additions & 1 deletion express-zod-api/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ export class Diagnostics {
AbstractEndpoint,
{ flat: ReturnType<typeof flattenIO>; paths: string[] }
>();
protected logger: ActualLogger;

constructor(protected logger: ActualLogger) {}
constructor(logger: ActualLogger) {
this.logger = logger;
}

public checkSchema(endpoint: AbstractEndpoint, ctx: FlatObject): void {
if (this.#verifiedEndpoints.has(endpoint)) return;
Expand Down
5 changes: 4 additions & 1 deletion express-zod-api/src/endpoints-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export class EndpointsFactory<
> {
protected schema = undefined as IN;
protected middlewares: AbstractMiddleware[] = [];
constructor(protected resultHandler: AbstractResultHandler) {}
protected resultHandler: AbstractResultHandler;
constructor(resultHandler: AbstractResultHandler) {
this.resultHandler = resultHandler;
}

#extend<
AIN extends IOSchema | undefined,
Expand Down
25 changes: 16 additions & 9 deletions express-zod-api/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,47 +43,54 @@ export class IOSchemaError extends Error {

export class DeepCheckError extends IOSchemaError {
public override name = "DeepCheckError";
public override readonly cause: z.core.$ZodType;

constructor(public override readonly cause: z.core.$ZodType) {
constructor(cause: z.core.$ZodType) {
super("Found", { cause });
this.cause = cause;
}
}

/** @desc An error of validating the Endpoint handler's returns against the Endpoint output schema */
export class OutputValidationError extends IOSchemaError {
public override name = "OutputValidationError";
public override readonly cause: z.ZodError;

constructor(public override readonly cause: z.ZodError) {
constructor(cause: z.ZodError) {
const prefixedPath = new z.ZodError(
cause.issues.map(({ path, ...rest }) => ({
...rest,
path: ["output", ...path],
})),
);
super(getMessageFromError(prefixedPath), { cause });
this.cause = cause;
}
}

/** @desc An error of validating the input sources against the Middleware or Endpoint input schema */
export class InputValidationError extends IOSchemaError {
public override name = "InputValidationError";
public override readonly cause: z.ZodError;

constructor(public override readonly cause: z.ZodError) {
constructor(cause: z.ZodError) {
super(getMessageFromError(cause), { cause });
this.cause = cause;
}
}

/** @desc An error related to the execution or incorrect configuration of ResultHandler */
export class ResultHandlerError extends Error {
public override name = "ResultHandlerError";
/** @desc The error thrown from ResultHandler */
public override readonly cause: Error;
/** @desc The error being processed by ResultHandler when it failed */
public readonly handled?: Error;

constructor(
/** @desc The error thrown from ResultHandler */
public override readonly cause: Error,
/** @desc The error being processed by ResultHandler when it failed */
public readonly handled?: Error,
) {
constructor(cause: Error, handled?: Error) {
super(getMessageFromError(cause), { cause });
this.cause = cause;
this.handled = handled;
}
}

Expand Down
48 changes: 33 additions & 15 deletions express-zod-api/src/integration-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { contentTypes } from "./content-type.ts";
import { ClientMethod, clientMethods } from "./method.ts";
import type { makeEventSchema } from "./sse.ts";
import {
accessModifiers,
ensureTypeNode,
f,
makeArrowFn,
Expand All @@ -32,7 +31,7 @@ import {
propOf,
recordStringAny,
makeAssignment,
makePublicProperty,
makeProperty,
makeIndexed,
makeMaybeAsync,
Typeable,
Expand All @@ -56,6 +55,8 @@ export abstract class IntegrationBase {
{ store: Store; isDeprecated: boolean }
>();

readonly #serverUrl: string;

readonly #ids = {
pathType: f.createIdentifier("Path"),
implementationType: f.createIdentifier("Implementation"),
Expand Down Expand Up @@ -119,7 +120,9 @@ export abstract class IntegrationBase {
{ expose: true },
);

protected constructor(private readonly serverUrl: string) {}
protected constructor(serverUrl: string) {
this.#serverUrl = serverUrl;
}

/**
* @example SomeOf<_>
Expand Down Expand Up @@ -323,22 +326,37 @@ export abstract class IntegrationBase {
* @example export class Client { ___ }
* @internal
* */
protected makeClientClass = (name: string) =>
makePublicClass(
protected makeClientClass = (name: string) => {
const genericImplType = ensureTypeNode(this.#ids.implementationType, ["T"]);
return makePublicClass(
name,
[
// public constructor(protected readonly implementation: Implementation = defaultImplementation) {}
makePublicConstructor([
makeParam(this.#ids.implementationArgument, {
type: ensureTypeNode(this.#ids.implementationType, ["T"]),
mod: accessModifiers.protectedReadonly,
init: this.#ids.defaultImplementationConst,
}),
]),
// protected readonly implementation: Implementation<T>;
makeProperty(this.#ids.implementationArgument, genericImplType),
// public constructor(implementation: Implementation<T> = defaultImplementation) {}
makePublicConstructor(
[
makeParam(this.#ids.implementationArgument, {
type: genericImplType,
init: this.#ids.defaultImplementationConst,
}),
],
[
// this.implementation = implementation;
makeAssignment(
f.createPropertyAccessExpression(
f.createThis(),
this.#ids.implementationArgument,
),
this.#ids.implementationArgument,
),
],
),
this.#makeProvider(),
],
{ typeParams: ["T"] },
);
};

// `?${new URLSearchParams(____)}`
#makeSearchParams = (from: ts.Expression) =>
Expand All @@ -353,7 +371,7 @@ export abstract class IntegrationBase {
[this.#ids.pathParameter],
[this.#ids.searchParamsConst],
),
literally(this.serverUrl),
literally(this.#serverUrl),
);

/**
Expand Down Expand Up @@ -595,7 +613,7 @@ export abstract class IntegrationBase {
makePublicClass(
name,
[
makePublicProperty(this.#ids.sourceProp, "EventSource"),
makeProperty(this.#ids.sourceProp, "EventSource", { expose: true }),
this.#makeSubscriptionConstructor(),
this.#makeOnMethod(),
],
Expand Down
7 changes: 4 additions & 3 deletions express-zod-api/src/typescript-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const exportModifier = [f.createModifier(ts.SyntaxKind.ExportKeyword)];

const asyncModifier = [f.createModifier(ts.SyntaxKind.AsyncKeyword)];

export const accessModifiers = {
const accessModifiers = {
public: [f.createModifier(ts.SyntaxKind.PublicKeyword)],
protectedReadonly: [
f.createModifier(ts.SyntaxKind.ProtectedKeyword),
Expand Down Expand Up @@ -222,12 +222,13 @@ export const makeType = (
return comment ? addJsDoc(node, comment) : node;
};

export const makePublicProperty = (
export const makeProperty = (
name: string | ts.PropertyName,
type: Typeable,
{ expose }: { expose?: boolean } = {},
) =>
f.createPropertyDeclaration(
accessModifiers.public,
expose ? accessModifiers.public : accessModifiers.protectedReadonly,
name,
undefined,
ensureTypeNode(type),
Expand Down
7 changes: 5 additions & 2 deletions express-zod-api/tests/__snapshots__/integration.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,12 @@ const defaultImplementation: Implementation = async (method, path, params) => {
};

export class Client<T> {
protected readonly implementation: Implementation<T>;
public constructor(
protected readonly implementation: Implementation<T> = defaultImplementation,
) {}
implementation: Implementation<T> = defaultImplementation,
) {
this.implementation = implementation;
}
public provide<K extends Request>(
request: K,
params: Input[K],
Expand Down
18 changes: 18 additions & 0 deletions express-zod-api/tests/__snapshots__/result-handler.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,21 @@ exports[`ResultHandler > arrayResultHandler should attempt to take examples from
],
}
`;

exports[`ResultHandler > getters > should throw when result is defined as an empty array 1`] = `
ResultHandlerError({
"cause": Error({
"message": "At least one positive response schema required.",
}),
"message": "At least one positive response schema required.",
})
`;

exports[`ResultHandler > getters > should throw when result is defined as an empty array 2`] = `
ResultHandlerError({
"cause": Error({
"message": "At least one negative response schema required.",
}),
"message": "At least one negative response schema required.",
})
`;
7 changes: 2 additions & 5 deletions express-zod-api/tests/__snapshots__/zts.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ exports[`zod-to-ts > Example > should produce the expected results 1`] = `
promise: any;
optDefaultString?: string | undefined;
refinedStringWithSomeBullshit: (string | number) & (bigint | null);
nativeEnum: "A" | "apple" | "banana" | "cantaloupe" | 5;
lazy: SomeType;
discUnion: {
kind: "circle";
Expand Down Expand Up @@ -169,11 +168,9 @@ exports[`zod-to-ts > PrimitiveSchema (isResponse=true) > outputs correct typescr
}"
`;

exports[`zod-to-ts > enums > handles 'numeric' literals 1`] = `""Red" | "Green" | "Blue" | 0 | 1 | 2"`;
exports[`zod-to-ts > enums > handles enum-like literals 0 1`] = `"0 | 1 | 2"`;

exports[`zod-to-ts > enums > handles 'quoted string' literals 1`] = `""Two Words" | "'Quotes\\"" | "\\\\\\"Escaped\\\\\\"" | 0 | 1 | 2"`;

exports[`zod-to-ts > enums > handles 'string' literals 1`] = `""apple" | "banana" | "cantaloupe""`;
exports[`zod-to-ts > enums > handles enum-like literals 1 1`] = `""apple" | "banana" | "cantaloupe""`;

exports[`zod-to-ts > ez.buffer() > should be Buffer 1`] = `"Buffer"`;

Expand Down
13 changes: 2 additions & 11 deletions express-zod-api/tests/result-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
defaultResultHandler,
ResultHandler,
} from "../src/index.ts";
import { ResultHandlerError } from "../src/errors.ts";
import { AbstractResultHandler, Result } from "../src/result-handler.ts";
import {
makeLoggerMock,
Expand Down Expand Up @@ -46,22 +45,14 @@ describe("ResultHandler", () => {
negative: vi.fn(),
handler: vi.fn(),
}).getPositiveResponse(z.object({})),
).toThrow(
new ResultHandlerError(
new Error("At least one positive response schema required."),
),
);
).toThrowErrorMatchingSnapshot();
expect(() =>
new ResultHandler({
positive: vi.fn(),
negative: [] as Result,
handler: vi.fn(),
}).getNegativeResponse(),
).toThrow(
new ResultHandlerError(
new Error("At least one negative response schema required."),
),
);
).toThrowErrorMatchingSnapshot();
});
});

Expand Down
37 changes: 3 additions & 34 deletions express-zod-api/tests/zts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,45 +38,15 @@ describe("zod-to-ts", () => {
});

describe("enums", () => {
// noinspection JSUnusedGlobalSymbols
enum Color {
Red,
Green,
Blue,
}

// noinspection JSUnusedGlobalSymbols
enum Fruit {
Apple = "apple",
Banana = "banana",
Cantaloupe = "cantaloupe",
}

// noinspection JSUnusedGlobalSymbols
enum StringLiteral {
"Two Words",
"'Quotes\"",
'\\"Escaped\\"',
}

test.each([
{ schema: z.enum(Color), feature: "numeric" },
{ schema: z.enum(Fruit), feature: "string" },
{ schema: z.enum(StringLiteral), feature: "quoted string" },
])("handles $feature literals", ({ schema }) => {
z.enum({ red: 0, green: 1, blue: 2 }),
z.enum(["apple", "banana", "cantaloupe"]),
])("handles enum-like literals %#", (schema) => {
expect(printNodeTest(zodToTs(schema, { ctx }))).toMatchSnapshot();
});
});

describe("Example", () => {
// noinspection JSUnusedGlobalSymbols
enum Fruits {
Apple = "apple",
Banana = "banana",
Cantaloupe = "cantaloupe",
A = 5,
}

const pickedSchema = z
.object({
string: z.string(),
Expand Down Expand Up @@ -158,7 +128,6 @@ describe("zod-to-ts", () => {
.refine((val) => val.length > 10)
.or(z.number())
.and(z.bigint().nullish()),
nativeEnum: z.enum(Fruits),
lazy: z.lazy(() => z.string()),
discUnion: z.discriminatedUnion("kind", [
z.object({ kind: z.literal("circle"), radius: z.number() }),
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"noImplicitOverride": true,
"noUncheckedSideEffectImports": true,
"resolveJsonModule": true,
"erasableSyntaxOnly": true,
"types": ["node", "vitest/globals"]
}
}