diff --git a/example/example.client.ts b/example/example.client.ts index 2f401f783..71db27e77 100644 --- a/example/example.client.ts +++ b/example/example.client.ts @@ -663,9 +663,12 @@ const defaultImplementation: Implementation = async (method, path, params) => { }; export class Client { + protected readonly implementation: Implementation; public constructor( - protected readonly implementation: Implementation = defaultImplementation, - ) {} + implementation: Implementation = defaultImplementation, + ) { + this.implementation = implementation; + } public provide( request: K, params: Input[K], diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index bc69fb606..78a76da35 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -13,8 +13,11 @@ export class Diagnostics { AbstractEndpoint, { flat: ReturnType; 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; diff --git a/express-zod-api/src/endpoints-factory.ts b/express-zod-api/src/endpoints-factory.ts index eebbc6385..8d6f625d7 100644 --- a/express-zod-api/src/endpoints-factory.ts +++ b/express-zod-api/src/endpoints-factory.ts @@ -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, diff --git a/express-zod-api/src/errors.ts b/express-zod-api/src/errors.ts index 5bd0947b3..95b574315 100644 --- a/express-zod-api/src/errors.ts +++ b/express-zod-api/src/errors.ts @@ -43,17 +43,20 @@ 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, @@ -61,29 +64,33 @@ export class OutputValidationError extends IOSchemaError { })), ); 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; } } diff --git a/express-zod-api/src/integration-base.ts b/express-zod-api/src/integration-base.ts index a80453b8a..d9e7dcab5 100644 --- a/express-zod-api/src/integration-base.ts +++ b/express-zod-api/src/integration-base.ts @@ -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, @@ -32,7 +31,7 @@ import { propOf, recordStringAny, makeAssignment, - makePublicProperty, + makeProperty, makeIndexed, makeMaybeAsync, Typeable, @@ -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"), @@ -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<_> @@ -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; + makeProperty(this.#ids.implementationArgument, genericImplType), + // public constructor(implementation: Implementation = 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) => @@ -353,7 +371,7 @@ export abstract class IntegrationBase { [this.#ids.pathParameter], [this.#ids.searchParamsConst], ), - literally(this.serverUrl), + literally(this.#serverUrl), ); /** @@ -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(), ], diff --git a/express-zod-api/src/typescript-api.ts b/express-zod-api/src/typescript-api.ts index b4aa43df9..bf9be54f4 100644 --- a/express-zod-api/src/typescript-api.ts +++ b/express-zod-api/src/typescript-api.ts @@ -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), @@ -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), diff --git a/express-zod-api/tests/__snapshots__/integration.spec.ts.snap b/express-zod-api/tests/__snapshots__/integration.spec.ts.snap index fd3c5e966..e738ccd83 100644 --- a/express-zod-api/tests/__snapshots__/integration.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/integration.spec.ts.snap @@ -536,9 +536,12 @@ const defaultImplementation: Implementation = async (method, path, params) => { }; export class Client { + protected readonly implementation: Implementation; public constructor( - protected readonly implementation: Implementation = defaultImplementation, - ) {} + implementation: Implementation = defaultImplementation, + ) { + this.implementation = implementation; + } public provide( request: K, params: Input[K], diff --git a/express-zod-api/tests/__snapshots__/result-handler.spec.ts.snap b/express-zod-api/tests/__snapshots__/result-handler.spec.ts.snap index 6860e519a..8f52b1ee9 100644 --- a/express-zod-api/tests/__snapshots__/result-handler.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/result-handler.spec.ts.snap @@ -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.", +}) +`; diff --git a/express-zod-api/tests/__snapshots__/zts.spec.ts.snap b/express-zod-api/tests/__snapshots__/zts.spec.ts.snap index fc11f6622..67df87c61 100644 --- a/express-zod-api/tests/__snapshots__/zts.spec.ts.snap +++ b/express-zod-api/tests/__snapshots__/zts.spec.ts.snap @@ -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"; @@ -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"`; diff --git a/express-zod-api/tests/result-handler.spec.ts b/express-zod-api/tests/result-handler.spec.ts index 7d9fa6802..1e2dbd9d1 100644 --- a/express-zod-api/tests/result-handler.spec.ts +++ b/express-zod-api/tests/result-handler.spec.ts @@ -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, @@ -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(); }); }); diff --git a/express-zod-api/tests/zts.spec.ts b/express-zod-api/tests/zts.spec.ts index 63fa4bbc8..1e30066bd 100644 --- a/express-zod-api/tests/zts.spec.ts +++ b/express-zod-api/tests/zts.spec.ts @@ -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(), @@ -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() }), diff --git a/tsconfig.json b/tsconfig.json index 82820a127..48a68e211 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitOverride": true, "noUncheckedSideEffectImports": true, "resolveJsonModule": true, + "erasableSyntaxOnly": true, "types": ["node", "vitest/globals"] } }