Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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 @@ -8,6 +8,7 @@
"noImplicitOverride": true,
"noUncheckedSideEffectImports": true,
"resolveJsonModule": true,
"erasableSyntaxOnly": true,
"types": ["node", "vitest/globals"]
}
}
Loading