diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5bce8d5fd..ad55e75a2 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
on:
push:
- branches: [ master, v21, v22, v23, v24 ]
+ branches: [ master, v21, v22, v23, v24, make-v26 ]
pull_request:
# The branches below must be a subset of the branches above
- branches: [ master, v21, v22, v23, v24 ]
+ branches: [ master, v21, v22, v23, v24, make-v26 ]
schedule:
- cron: '26 8 * * 1'
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 3a4aa9323..e5005e2d8 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -5,9 +5,9 @@ name: Node.js CI
on:
push:
- branches: [ master, v21, v22, v23, v24 ]
+ branches: [ master, v21, v22, v23, v24, make-v26 ]
pull_request:
- branches: [ master, v21, v22, v23, v24 ]
+ branches: [ master, v21, v22, v23, v24, make-v26 ]
jobs:
build:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7168c55ee..f7a46dae4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changelog
+## Version 26
+
+### v26.0.0
+
+- `DependsOnMethod` removed: use flat syntax with explicit method and a slash;
+- `options` property renamed to `ctx` in argument of:
+ - `Middleware::handler()`,
+ - `ResultHandler::handler()`,
+ - `handler` of `EndpointsFactory::build()` argument,
+ - `testMiddleware()`;
+- `EndpointsFactory::addOptions()` renamed to `addContext()`;
+
+```patch
+ const routing: Routing = {
+- "/v1/users": new DependsOnMethod({
++ "/v1/users": {
+- get: getUserEndpoint,
++ "get /": getUserEndpoint,
+- }).nest({
+ create: makeUserEndpoint
+- }),
++ },
+ };
+```
+
## Version 25
### v25.6.0
diff --git a/README.md b/README.md
index 2ac4b428d..d41d8bca6 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Start your API server with I/O schema validation and custom middlewares in minut
4. [Basic features](#basic-features)
1. [Routing](#routing) including static file serving
2. [Middlewares](#middlewares)
- 3. [Options](#options)
+ 3. [Context](#context)
4. [Using native express middlewares](#using-native-express-middlewares)
5. [Refinements](#refinements)
6. [Query string parser](#query-string-parser)
@@ -149,7 +149,7 @@ These people contributed to the improvement of the framework by reporting bugs,
The API operates object schemas for input and output validation.
The object being validated is the combination of certain `request` properties.
It is available to the endpoint handler as the `input` parameter.
-Middlewares have access to all `request` properties, they can provide endpoints with `options`.
+Middlewares have access to all `request` properties, they can provide endpoints with `ctx` (context).
The object returned by the endpoint handler is called `output`. It goes to the `ResultHandler` which is
responsible for transmitting consistent responses containing the `output` or possible error.
Much can be customized to fit your needs.
@@ -233,8 +233,8 @@ const helloWorldEndpoint = defaultEndpointsFactory.build({
output: z.object({
greetings: z.string(),
}),
- handler: async ({ input: { name }, options, logger }) => {
- logger.debug("Options:", options); // middlewares provide options
+ handler: async ({ input: { name }, ctx, logger }) => {
+ logger.debug("Context:", ctx); // middlewares provide ctx
return { greetings: `Hello, ${name || "World"}. Happy coding!` };
},
});
@@ -288,7 +288,7 @@ in one place, illustrating how you can structure your API using whichever method
architecture — or even mix them seamlessly.
```ts
-import { Routing, DependsOnMethod, ServeStatic } from "express-zod-api";
+import { Routing, ServeStatic } from "express-zod-api";
const routing: Routing = {
// flat syntax — /v1/users
@@ -306,12 +306,12 @@ const routing: Routing = {
// mixed syntax with explicit method — /v1/user/:id
"delete /user/:id": deleteUserEndpoint,
// method-based routing — /v1/account
- account: new DependsOnMethod({
- get: endpointA,
- delete: endpointA,
- post: endpointB,
- patch: endpointB,
- }),
+ account: {
+ "get /": endpointA,
+ "delete /": endpointA,
+ "post /": endpointB,
+ "patch /": endpointB,
+ },
},
// static file serving — /public serves files from ./assets
public: new ServeStatic("assets", {
@@ -329,7 +329,7 @@ When the method is not specified, the one(s) supported by the Endpoint applied (
## Middlewares
-Middleware can authenticate using input or `request` headers, and can provide endpoint handlers with `options`.
+Middleware can authenticate using input or `request` headers, and can provide endpoint handlers with `ctx`.
Inputs of middlewares are also available to endpoint handlers within `input`.
Here is an example of the authentication middleware, that checks a `key` from input and `token` from headers:
@@ -356,7 +356,7 @@ const authMiddleware = new Middleware({
if (!user) throw createHttpError(401, "Invalid key");
if (request.headers.token !== user.token)
throw createHttpError(401, "Invalid token");
- return { user }; // provides endpoints with options.user
+ return { user }; // provides endpoints with ctx.user
},
});
```
@@ -367,7 +367,7 @@ By using `.addMiddleware()` method before `.build()` you can connect it to the e
const yourEndpoint = defaultEndpointsFactory
.addMiddleware(authMiddleware)
.build({
- handler: async ({ options: { user } }) => {
+ handler: async ({ ctx: { user } }) => {
// user is the one returned by authMiddleware
}, // ...
});
@@ -375,7 +375,7 @@ const yourEndpoint = defaultEndpointsFactory
You can create a new factory by connecting as many middlewares as you want — they will be executed in the specified
order for all the endpoints produced on that factory. You may also use a shorter inline syntax within the
-`.addMiddleware()` method, and have access to the output of the previously executed middlewares in chain as `options`:
+`.addMiddleware()` method, and have access to the output of the previously executed middlewares in chain as `ctx`:
```ts
import { defaultEndpointsFactory } from "express-zod-api";
@@ -383,20 +383,20 @@ import { defaultEndpointsFactory } from "express-zod-api";
const factory = defaultEndpointsFactory
.addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:
.addMiddleware({
- handler: async ({ options: { user } }) => ({}), // user from authMiddleware
+ handler: async ({ ctx: { user } }) => ({}), // user from authMiddleware
});
```
-## Options
+## Context
-In case you'd like to provide your endpoints with options that do not depend on Request, like non-persistent connection
-to a database, consider shorthand method `addOptions`. For static options consider reusing `const` across your files.
+If you need to provide your endpoints with a context that does not depend on Request, like non-persistent database
+connection, consider shorthand method `addContext`. For static values consider reusing a `const` across your files.
```ts
import { readFile } from "node:fs/promises";
import { defaultEndpointsFactory } from "express-zod-api";
-const endpointsFactory = defaultEndpointsFactory.addOptions(async () => {
+const endpointsFactory = defaultEndpointsFactory.addContext(async () => {
// caution: new connection on every request:
const db = mongoose.connect("mongodb://connection.string");
const privateKey = await readFile("private-key.pem", "utf-8");
@@ -411,10 +411,10 @@ custom [Result Handler](#response-customization):
import { ResultHandler } from "express-zod-api";
const resultHandlerWithCleanup = new ResultHandler({
- handler: ({ options }) => {
- // necessary to check for certain option presence:
- if ("db" in options && options.db) {
- options.db.connection.close(); // sample cleanup
+ handler: ({ ctx }) => {
+ // necessary to check the presence of a certain property:
+ if ("db" in ctx && ctx.db) {
+ ctx.db.connection.close(); // sample cleanup
}
},
});
@@ -446,7 +446,7 @@ const config = createConfig({
In case you need a special processing of `request`, or to modify the `response` for selected endpoints, use the method
`addExpressMiddleware()` of `EndpointsFactory` (or its alias `use()`). The method has two optional features: a provider
-of [options](#options) and an error transformer for adjusting the response status code.
+of a [context](#context) and an error transformer for adjusting the response status code.
```ts
import { defaultEndpointsFactory } from "express-zod-api";
@@ -1045,8 +1045,8 @@ test("should respond successfully", async () => {
## Testing middlewares
-Middlewares can also be tested individually using the `testMiddleware()` method. You can also pass `options` collected
-from outputs of previous middlewares, if the one being tested somehow depends on them. Possible errors would be handled
+Middlewares can also be tested individually using the `testMiddleware()` method. You can also pass `ctx` collected
+from returns of previous middlewares, if the one being tested somehow depends on it. Possible errors would be handled
either by `errorHandler` configured within given `configProps` or `defaultResultHandler`.
```ts
@@ -1055,8 +1055,8 @@ import { Middleware, testMiddleware } from "express-zod-api";
const middleware = new Middleware({
input: z.object({ test: z.string() }),
- handler: async ({ options, input: { test } }) => ({
- collectedOptions: Object.keys(options),
+ handler: async ({ ctx, input: { test } }) => ({
+ collectedContext: Object.keys(ctx),
testLength: test.length,
}),
});
@@ -1064,10 +1064,10 @@ const middleware = new Middleware({
const { output, responseMock, loggerMock } = await testMiddleware({
middleware,
requestProps: { method: "POST", body: { test: "something" } },
- options: { prev: "accumulated" }, // responseOptions, configProps, loggerProps
+ ctx: { prev: "accumulated" }, // responseOptions, configProps, loggerProps
});
expect(loggerMock._getLogs().error).toHaveLength(0);
-expect(output).toEqual({ collectedOptions: ["prev"], testLength: 9 });
+expect(output).toEqual({ collectedContext: ["prev"], testLength: 9 });
```
# Integration and Documentation
@@ -1183,11 +1183,11 @@ new Documentation({
## Deprecated schemas and routes
As your API evolves, you may need to mark some parameters or routes as deprecated before deleting them. For this
-purpose, the `.deprecated()` method is available on each schema, `Endpoint` and `DependsOnMethod`, it's immutable.
+purpose, the `.deprecated()` method is available on each schema and `Endpoint`, it's immutable.
You can also deprecate all routes the `Endpoint` assigned to by setting `EndpointsFactory::build({ deprecated: true })`.
```ts
-import { Routing, DependsOnMethod } from "express-zod-api";
+import { Routing } from "express-zod-api";
import { z } from "zod";
const someEndpoint = factory.build({
@@ -1199,8 +1199,7 @@ const someEndpoint = factory.build({
const routing: Routing = {
v1: oldEndpoint.deprecated(), // deprecates the /v1 path
- v2: new DependsOnMethod({ get: oldEndpoint }).deprecated(), // deprecates the /v2 path
- v3: someEndpoint, // the path is assigned with initially deprecated endpoint (also deprecated)
+ v2: someEndpoint, // the path is assigned with initially deprecated endpoint (also deprecated)
};
```
@@ -1378,7 +1377,7 @@ const subscriptionEndpoint = new EventStreamFactory({
time: z.int().positive(),
}).buildVoid({
input: z.object({}), // optional input schema
- handler: async ({ options: { emit, isClosed, signal } }) => {
+ handler: async ({ ctx: { emit, isClosed, signal } }) => {
while (!isClosed()) {
emit("time", Date.now());
await setTimeout(1000);
diff --git a/compat-test/eslint.config.js b/compat-test/eslint.config.js
index a34ce502d..6be0df64e 100644
--- a/compat-test/eslint.config.js
+++ b/compat-test/eslint.config.js
@@ -3,5 +3,5 @@ import migration from "@express-zod-api/migration";
export default [
{ languageOptions: { parser }, plugins: { migration } },
- { files: ["**/*.ts"], rules: { "migration/v25": "error" } },
+ { files: ["**/*.ts"], rules: { "migration/v26": "error" } },
];
diff --git a/compat-test/migration.spec.ts b/compat-test/migration.spec.ts
index 46c0e742f..bcc9cc5f2 100644
--- a/compat-test/migration.spec.ts
+++ b/compat-test/migration.spec.ts
@@ -4,6 +4,6 @@ import { describe, test, expect } from "vitest";
describe("Migration", () => {
test("should fix the import", async () => {
const fixed = await readFile("./sample.ts", "utf-8");
- expect(fixed).toBe('import {} from "zod";\n');
+ expect(fixed).toBe(`const route = {\n"get /": someEndpoint,\n}\n`);
});
});
diff --git a/compat-test/package.json b/compat-test/package.json
index a4b08508c..574e7dcb0 100644
--- a/compat-test/package.json
+++ b/compat-test/package.json
@@ -3,7 +3,7 @@
"type": "module",
"private": true,
"scripts": {
- "pretest": "echo 'import {} from \"zod/v4\";' > sample.ts",
+ "pretest": "echo 'const route = new DependsOnMethod({ get: someEndpoint })' > sample.ts",
"test": "eslint --fix && vitest --run",
"posttest": "rm sample.ts"
},
diff --git a/dataflow.svg b/dataflow.svg
index ac6f75040..72886fee4 100644
--- a/dataflow.svg
+++ b/dataflow.svg
@@ -1,4 +1,4 @@
-
+
diff --git a/example/endpoints/retrieve-user.ts b/example/endpoints/retrieve-user.ts
index 20e6bfd42..38ff2ac59 100644
--- a/example/endpoints/retrieve-user.ts
+++ b/example/endpoints/retrieve-user.ts
@@ -31,7 +31,7 @@ export const retrieveUserEndpoint = defaultEndpointsFactory
name: z.string(),
features: feature.array(), // @link https://github.com/colinhacks/zod/issues/4592
}),
- handler: async ({ input: { id }, options: { method }, logger }) => {
+ handler: async ({ input: { id }, ctx: { method }, logger }) => {
logger.debug(`Requested id: ${id}, method ${method}`);
const name = "John Doe";
assert(id <= 100, createHttpError(404, "User not found"));
diff --git a/example/endpoints/time-subscription.ts b/example/endpoints/time-subscription.ts
index 5a2b98a5b..9416aedf4 100644
--- a/example/endpoints/time-subscription.ts
+++ b/example/endpoints/time-subscription.ts
@@ -12,11 +12,7 @@ export const subscriptionEndpoint = eventsFactory.buildVoid({
.deprecated()
.describe("for testing error response"),
}),
- handler: async ({
- input: { trigger },
- options: { emit, isClosed },
- logger,
- }) => {
+ handler: async ({ input: { trigger }, ctx: { emit, isClosed }, logger }) => {
if (trigger === "failure") throw new Error("Intentional failure");
while (!isClosed()) {
logger.debug("emitting");
diff --git a/example/endpoints/update-user.ts b/example/endpoints/update-user.ts
index 56ca40dd8..0abc67df4 100644
--- a/example/endpoints/update-user.ts
+++ b/example/endpoints/update-user.ts
@@ -30,7 +30,7 @@ export const updateUserEndpoint =
}),
handler: async ({
input: { id, name },
- options: { authorized }, // comes from authMiddleware
+ ctx: { authorized }, // comes from authMiddleware
logger,
}) => {
logger.debug(`${authorized} is changing user #${id}`);
diff --git a/example/routing.ts b/example/routing.ts
index 0d9ef98a7..a6691fab3 100644
--- a/example/routing.ts
+++ b/example/routing.ts
@@ -1,4 +1,4 @@
-import { DependsOnMethod, Routing, ServeStatic } from "express-zod-api";
+import { Routing, ServeStatic } from "express-zod-api";
import { rawAcceptingEndpoint } from "./endpoints/accept-raw.ts";
import { createUserEndpoint } from "./endpoints/create-user.ts";
import { deleteUserEndpoint } from "./endpoints/delete-user.ts";
@@ -16,12 +16,12 @@ export const routing: Routing = {
user: {
// syntax 1: methods are defined within the endpoint
retrieve: retrieveUserEndpoint, // path: /v1/user/retrieve
- // syntax 2: methods are defined within the route (id is the route path param by the way)
- ":id": new DependsOnMethod({
- patch: updateUserEndpoint, // demonstrates authentication
- }).nest({
+ // id is the route path param
+ ":id": {
remove: deleteUserEndpoint, // nested path: /v1/user/:id/remove
- }),
+ // syntax 2: methods are defined within the route
+ "patch /": updateUserEndpoint, // demonstrates authentication
+ },
// demonstrates different response schemas depending on status code
create: createUserEndpoint,
// this one demonstrates the legacy array based response
diff --git a/express-zod-api/src/depends-on-method.ts b/express-zod-api/src/depends-on-method.ts
deleted file mode 100644
index 191d8c132..000000000
--- a/express-zod-api/src/depends-on-method.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as R from "ramda";
-import { AbstractEndpoint } from "./endpoint.ts";
-import { Method } from "./method.ts";
-import { Routable } from "./routable.ts";
-
-export class DependsOnMethod extends Routable {
- readonly #endpoints: ConstructorParameters[0];
-
- constructor(endpoints: Partial>) {
- super();
- this.#endpoints = endpoints;
- }
-
- /**
- * @desc [method, endpoint]
- * @internal
- * */
- public get entries() {
- const nonempty = R.filter(
- (pair): pair is [Method, AbstractEndpoint] => Boolean(pair[1]),
- Object.entries(this.#endpoints),
- );
- return Object.freeze(nonempty);
- }
-
- public override deprecated() {
- const deprecatedEndpoints = Object.entries(this.#endpoints).reduce(
- (agg, [method, endpoint]) =>
- Object.assign(agg, { [method]: endpoint.deprecated() }),
- {} as ConstructorParameters[0],
- );
- return new DependsOnMethod(deprecatedEndpoints) as this;
- }
-}
diff --git a/express-zod-api/src/endpoint.ts b/express-zod-api/src/endpoint.ts
index 535a6fef8..080dd34dc 100644
--- a/express-zod-api/src/endpoint.ts
+++ b/express-zod-api/src/endpoint.ts
@@ -27,21 +27,27 @@ import { AbstractMiddleware, ExpressMiddleware } from "./middleware.ts";
import { ContentType } from "./content-type.ts";
import { ezRawBrand } from "./raw-schema.ts";
import { DiscriminatedResult, pullResponseExamples } from "./result-helpers.ts";
-import { Routable } from "./routable.ts";
import { AbstractResultHandler } from "./result-handler.ts";
import { Security } from "./security.ts";
import { ezUploadBrand } from "./upload-schema.ts";
+import type { Routing } from "./routing.ts";
-export type Handler = (params: {
+export type Handler = (params: {
/** @desc The inputs from the enabled input sources validated against the final input schema (incl. Middlewares) */
input: IN;
/** @desc The returns of the assigned Middlewares */
- options: OPT;
+ ctx: CTX;
/** @desc The instance of the configured logger */
logger: ActualLogger;
}) => Promise;
-export abstract class AbstractEndpoint extends Routable {
+export abstract class AbstractEndpoint {
+ /** @desc Enables nested routes within the path assigned to the subject */
+ public nest(routing: Routing): Routing {
+ return { ...routing, "": this };
+ }
+ /** @desc Marks the route as deprecated (makes a copy of the endpoint) */
+ public abstract deprecated(): this;
public abstract execute(params: {
request: Request;
response: Response;
@@ -79,9 +85,9 @@ export abstract class AbstractEndpoint extends Routable {
export class Endpoint<
IN extends IOSchema,
OUT extends IOSchema,
- OPT extends FlatObject,
+ CTX extends FlatObject,
> extends AbstractEndpoint {
- readonly #def: ConstructorParameters>[0];
+ readonly #def: ConstructorParameters>[0];
/** considered expensive operation, only required for generators */
#ensureOutputExamples = R.once(() => {
@@ -100,7 +106,7 @@ export class Endpoint<
middlewares?: AbstractMiddleware[];
inputSchema: IN;
outputSchema: OUT;
- handler: Handler, z.input, OPT>;
+ handler: Handler, z.input, CTX>;
resultHandler: AbstractResultHandler;
description?: string;
shortDescription?: string;
@@ -114,7 +120,7 @@ export class Endpoint<
}
#clone(
- inc?: Partial>[0]>,
+ inc?: Partial>[0]>,
) {
return new Endpoint({ ...this.#def, ...inc });
}
@@ -208,7 +214,7 @@ export class Endpoint<
async #runMiddlewares({
method,
logger,
- options,
+ ctx,
response,
...rest
}: {
@@ -217,7 +223,7 @@ export class Endpoint<
request: Request;
response: Response;
logger: ActualLogger;
- options: Partial;
+ ctx: Partial;
}) {
for (const mw of this.#def.middlewares || []) {
if (
@@ -225,14 +231,11 @@ export class Endpoint<
!(mw instanceof ExpressMiddleware)
)
continue;
- Object.assign(
- options,
- await mw.execute({ ...rest, options, response, logger }),
- );
+ Object.assign(ctx, await mw.execute({ ...rest, ctx, response, logger }));
if (response.writableEnded) {
logger.warn(
- "A middleware has closed the stream. Accumulated options:",
- options,
+ "A middleware has closed the stream. Accumulated context:",
+ ctx,
);
break;
}
@@ -244,7 +247,7 @@ export class Endpoint<
...rest
}: {
input: Readonly;
- options: OPT;
+ ctx: CTX;
logger: ActualLogger;
}) {
let finalInput: z.output; // final input types transformations for handler
@@ -264,7 +267,7 @@ export class Endpoint<
response: Response;
logger: ActualLogger;
input: FlatObject;
- options: Partial;
+ ctx: Partial;
},
) {
try {
@@ -292,7 +295,7 @@ export class Endpoint<
config: CommonConfig;
}) {
const method = getActualMethod(request);
- const options: Partial = {};
+ const ctx: Partial = {};
let result: DiscriminatedResult = { output: {}, error: null };
const input = getInput(request, config.inputSources);
try {
@@ -302,7 +305,7 @@ export class Endpoint<
request,
response,
logger,
- options,
+ ctx,
});
if (response.writableEnded) return;
if (method === ("options" satisfies CORSMethod))
@@ -312,7 +315,7 @@ export class Endpoint<
await this.#parseAndRunHandler({
input,
logger,
- options: options as OPT, // ensured the complete OPT by writableEnded condition and try-catch
+ ctx: ctx as CTX, // ensured the complete CTX by writableEnded condition and try-catch
}),
),
error: null,
@@ -326,7 +329,7 @@ export class Endpoint<
request,
response,
logger,
- options,
+ ctx,
});
}
}
diff --git a/express-zod-api/src/endpoints-factory.ts b/express-zod-api/src/endpoints-factory.ts
index eebbc6385..c4ed55e66 100644
--- a/express-zod-api/src/endpoints-factory.ts
+++ b/express-zod-api/src/endpoints-factory.ts
@@ -31,7 +31,7 @@ interface BuildProps<
IN extends IOSchema,
OUT extends IOSchema | z.ZodVoid,
MIN extends IOSchema | undefined,
- OPT extends FlatObject,
+ CTX extends FlatObject,
SCO extends string,
> {
/**
@@ -42,8 +42,8 @@ interface BuildProps<
input?: IN;
/** @desc The schema by which the returns of the Endpoint handler is validated */
output: OUT;
- /** @desc The Endpoint handler receiving the validated inputs, returns of added Middlewares (options) and a logger */
- handler: Handler>, z.input, OPT>;
+ /** @desc The Endpoint handler receiving the validated inputs, returns of added Middlewares (ctx) and a logger */
+ handler: Handler>, z.input, CTX>;
/** @desc The operation description for the generated Documentation */
description?: string;
/** @desc The operation summary for the generated Documentation (50 symbols max) */
@@ -52,8 +52,7 @@ interface BuildProps<
operationId?: string | ((method: ClientMethod) => string);
/**
* @desc HTTP method(s) this endpoint can handle
- * @default "get" unless the Endpoint is assigned within DependsOnMethod
- * @see DependsOnMethod
+ * @default "get" unless method is explicitly defined in Routing keys
* */
method?: Method | [Method, ...Method[]];
/**
@@ -72,7 +71,7 @@ interface BuildProps<
export class EndpointsFactory<
IN extends IOSchema | undefined = undefined,
- OUT extends FlatObject = EmptyObject,
+ CTX extends FlatObject = EmptyObject,
SCO extends string = string,
> {
protected schema = undefined as IN;
@@ -81,12 +80,12 @@ export class EndpointsFactory<
#extend<
AIN extends IOSchema | undefined,
- AOUT extends FlatObject,
+ RET extends FlatObject,
ASCO extends string,
- >(middleware: Middleware) {
+ >(middleware: Middleware) {
const factory = new EndpointsFactory<
Extension,
- OUT & AOUT,
+ CTX & RET,
SCO & ASCO
>(this.resultHandler);
factory.middlewares = this.middlewares.concat(middleware);
@@ -95,13 +94,13 @@ export class EndpointsFactory<
}
public addMiddleware<
- AOUT extends FlatObject,
+ RET extends FlatObject,
ASCO extends string,
AIN extends IOSchema | undefined = undefined,
>(
subject:
- | Middleware
- | ConstructorParameters>[0],
+ | Middleware
+ | ConstructorParameters>[0],
) {
return this.#extend(
subject instanceof Middleware ? subject : new Middleware(subject),
@@ -118,8 +117,8 @@ export class EndpointsFactory<
return this.#extend(new ExpressMiddleware(...params));
}
- public addOptions(getOptions: () => Promise) {
- return this.#extend(new Middleware({ handler: getOptions }));
+ public addContext(getContext: () => Promise) {
+ return this.#extend(new Middleware({ handler: getContext }));
}
public build({
@@ -130,7 +129,7 @@ export class EndpointsFactory<
tag,
method,
...rest
- }: BuildProps) {
+ }: BuildProps) {
const { middlewares, resultHandler } = this;
const methods = typeof method === "string" ? [method] : method;
const getOperationId =
@@ -157,7 +156,7 @@ export class EndpointsFactory<
public buildVoid({
handler,
...rest
- }: Omit, "output">) {
+ }: Omit, "output">) {
return this.build({
...rest,
output: emptySchema,
diff --git a/express-zod-api/src/index.ts b/express-zod-api/src/index.ts
index 6bbd6ccde..5aef7b7c0 100644
--- a/express-zod-api/src/index.ts
+++ b/express-zod-api/src/index.ts
@@ -14,7 +14,6 @@ export {
defaultResultHandler,
arrayResultHandler,
} from "./result-handler.ts";
-export { DependsOnMethod } from "./depends-on-method.ts";
export { ServeStatic } from "./serve-static.ts";
export { createServer, attachRouting } from "./server.ts";
export { Documentation } from "./documentation.ts";
diff --git a/express-zod-api/src/middleware.ts b/express-zod-api/src/middleware.ts
index e62034462..e2c7f758e 100644
--- a/express-zod-api/src/middleware.ts
+++ b/express-zod-api/src/middleware.ts
@@ -7,21 +7,21 @@ import { LogicalContainer } from "./logical-container.ts";
import { Security } from "./security.ts";
import { ActualLogger } from "./logger-helpers.ts";
-type Handler = (params: {
+type Handler = (params: {
/** @desc The inputs from the enabled input sources validated against the input schema of the Middleware */
input: IN;
/**
* @desc The returns of the previously executed Middlewares (typed when chaining Middlewares)
* @link https://github.com/RobinTail/express-zod-api/discussions/1250
* */
- options: OPT;
+ ctx: CTX;
/** @link https://expressjs.com/en/5x/api.html#req */
request: Request;
/** @link https://expressjs.com/en/5x/api.html#res */
response: Response;
/** @desc The instance of the configured logger */
logger: ActualLogger;
-}) => Promise;
+}) => Promise;
export abstract class AbstractMiddleware {
/** @internal */
@@ -30,7 +30,7 @@ export abstract class AbstractMiddleware {
public abstract get schema(): IOSchema | undefined;
public abstract execute(params: {
input: unknown;
- options: FlatObject;
+ ctx: FlatObject;
request: Request;
response: Response;
logger: ActualLogger;
@@ -38,8 +38,8 @@ export abstract class AbstractMiddleware {
}
export class Middleware<
- OPT extends FlatObject,
- OUT extends FlatObject,
+ CTX extends FlatObject,
+ RET extends FlatObject,
SCO extends string,
IN extends IOSchema | undefined = undefined,
> extends AbstractMiddleware {
@@ -47,7 +47,7 @@ export class Middleware<
readonly #security?: LogicalContainer<
Security, string>, SCO>
>;
- readonly #handler: Handler, OPT, OUT>;
+ readonly #handler: Handler, CTX, RET>;
constructor({
input,
@@ -64,8 +64,8 @@ export class Middleware<
security?: LogicalContainer<
Security, string>, SCO>
>;
- /** @desc The handler returning options available to Endpoints */
- handler: Handler, OPT, OUT>;
+ /** @desc The handler returning a context available to Endpoints */
+ handler: Handler, CTX, RET>;
}) {
super();
this.#schema = input as IN;
@@ -89,7 +89,7 @@ export class Middleware<
...rest
}: {
input: unknown;
- options: OPT;
+ ctx: CTX;
request: Request;
response: Response;
logger: ActualLogger;
@@ -108,22 +108,22 @@ export class Middleware<
export class ExpressMiddleware<
R extends Request,
S extends Response,
- OUT extends FlatObject,
-> extends Middleware {
+ RET extends FlatObject,
+> extends Middleware {
constructor(
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- issue #2824, assignment compatibility fix
nativeMw: (request: R, response: S, next: NextFunction) => any,
{
- provider = () => ({}) as OUT,
+ provider = () => ({}) as RET,
transformer = (err: Error) => err,
}: {
- provider?: (request: R, response: S) => OUT | Promise;
+ provider?: (request: R, response: S) => RET | Promise;
transformer?: (err: Error) => Error;
} = {},
) {
super({
handler: async ({ request, response }) =>
- new Promise((resolve, reject) => {
+ new Promise((resolve, reject) => {
const next = (err?: unknown) => {
if (err && err instanceof Error) return reject(transformer(err));
resolve(provider(request as R, response as S));
diff --git a/express-zod-api/src/result-handler.ts b/express-zod-api/src/result-handler.ts
index a41cf18b8..51520a95d 100644
--- a/express-zod-api/src/result-handler.ts
+++ b/express-zod-api/src/result-handler.ts
@@ -23,7 +23,7 @@ type Handler = (
/** null in case of failure to parse or to find the matching endpoint (error: not found) */
input: FlatObject | null;
/** can be empty: check presence of the required property using "in" operator */
- options: FlatObject;
+ ctx: FlatObject;
request: Request;
response: Response;
logger: ActualLogger;
diff --git a/express-zod-api/src/routable.ts b/express-zod-api/src/routable.ts
deleted file mode 100644
index 8800157df..000000000
--- a/express-zod-api/src/routable.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Routing } from "./routing.ts";
-
-export abstract class Routable {
- /** @desc Marks the route as deprecated (makes a copy of the endpoint) */
- public abstract deprecated(): this;
-
- /** @desc Enables nested routes within the path assigned to the subject */
- public nest(routing: Routing): Routing {
- return Object.assign(routing, { "": this });
- }
-}
diff --git a/express-zod-api/src/routing-walker.ts b/express-zod-api/src/routing-walker.ts
index 906f93721..e8348a4df 100644
--- a/express-zod-api/src/routing-walker.ts
+++ b/express-zod-api/src/routing-walker.ts
@@ -1,4 +1,3 @@
-import { DependsOnMethod } from "./depends-on-method.ts";
import { AbstractEndpoint } from "./endpoint.ts";
import { RoutingError } from "./errors.ts";
import { ClientMethod, isMethod, Method } from "./method.ts";
@@ -98,13 +97,6 @@ export const walkRouting = ({
if (explicitMethod) prohibit(explicitMethod, path);
if (element instanceof ServeStatic) {
if (onStatic) element.apply(path, onStatic);
- } else if (element instanceof DependsOnMethod) {
- for (const [method, endpoint] of element.entries) {
- const { methods } = endpoint;
- checkDuplicate(method, path, visited);
- checkMethodSupported(method, path, methods);
- onEndpoint(method, path, endpoint);
- }
} else {
stack.unshift(...processEntries(element, path));
}
diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts
index 6c85de88c..d0d5a1dcf 100644
--- a/express-zod-api/src/routing.ts
+++ b/express-zod-api/src/routing.ts
@@ -3,7 +3,6 @@ import createHttpError from "http-errors";
import { isProduction } from "./common-helpers.ts";
import { CommonConfig } from "./config-type.ts";
import { ContentType } from "./content-type.ts";
-import { DependsOnMethod } from "./depends-on-method.ts";
import { Diagnostics } from "./diagnostics.ts";
import { AbstractEndpoint } from "./endpoint.ts";
import { CORSMethod, isMethod } from "./method.ts";
@@ -17,9 +16,10 @@ import * as R from "ramda";
* @example { "v1/books/:bookId": getBookEndpoint }
* @example { "get /v1/books/:bookId": getBookEndpoint }
* @example { v1: { "patch /books/:bookId": changeBookEndpoint } }
+ * @example { dependsOnMethod: { "get /": retrieveEndpoint, "post /": createEndpoint } }
* */
export interface Routing {
- [K: string]: Routing | DependsOnMethod | AbstractEndpoint | ServeStatic;
+ [K: string]: Routing | AbstractEndpoint | ServeStatic;
}
export type Parsers = Partial>;
diff --git a/express-zod-api/src/server-helpers.ts b/express-zod-api/src/server-helpers.ts
index 1db70773c..32b5bd885 100644
--- a/express-zod-api/src/server-helpers.ts
+++ b/express-zod-api/src/server-helpers.ts
@@ -39,7 +39,7 @@ export const createCatcher =
response,
input: null,
output: null,
- options: {},
+ ctx: {},
logger: getLogger(request),
});
};
@@ -60,7 +60,7 @@ export const createNotFoundHandler =
error,
input: null,
output: null,
- options: {},
+ ctx: {},
});
} catch (e) {
lastResortHandler({
diff --git a/express-zod-api/src/testing.ts b/express-zod-api/src/testing.ts
index 6ca2177a3..8408c35d8 100644
--- a/express-zod-api/src/testing.ts
+++ b/express-zod-api/src/testing.ts
@@ -120,13 +120,13 @@ export const testMiddleware = async <
REQ extends RequestOptions,
>({
middleware,
- options = {},
+ ctx = {},
...rest
}: TestingProps & {
/** @desc The middleware to test */
middleware: AbstractMiddleware;
- /** @desc The aggregated output from previously executed middlewares */
- options?: FlatObject;
+ /** @desc The aggregated returns of previously executed middlewares */
+ ctx?: FlatObject;
}) => {
const {
requestMock,
@@ -140,7 +140,7 @@ export const testMiddleware = async <
response: responseMock,
logger: loggerMock,
input,
- options,
+ ctx,
};
try {
const output = await middleware.execute(commons);
diff --git a/express-zod-api/tests/__snapshots__/index.spec.ts.snap b/express-zod-api/tests/__snapshots__/index.spec.ts.snap
index 4d1dc663a..2cc396c49 100644
--- a/express-zod-api/tests/__snapshots__/index.spec.ts.snap
+++ b/express-zod-api/tests/__snapshots__/index.spec.ts.snap
@@ -2,8 +2,6 @@
exports[`Index Entrypoint > exports > BuiltinLogger should have certain value 1`] = `[Function]`;
-exports[`Index Entrypoint > exports > DependsOnMethod should have certain value 1`] = `[Function]`;
-
exports[`Index Entrypoint > exports > Documentation should have certain value 1`] = `[Function]`;
exports[`Index Entrypoint > exports > DocumentationError should have certain value 1`] = `[Function]`;
@@ -84,7 +82,6 @@ exports[`Index Entrypoint > exports > should have certain entities exposed 1`] =
"ResultHandler",
"defaultResultHandler",
"arrayResultHandler",
- "DependsOnMethod",
"ServeStatic",
"createServer",
"attachRouting",
diff --git a/express-zod-api/tests/__snapshots__/routing.spec.ts.snap b/express-zod-api/tests/__snapshots__/routing.spec.ts.snap
index 578fe381f..4dcb415a6 100644
--- a/express-zod-api/tests/__snapshots__/routing.spec.ts.snap
+++ b/express-zod-api/tests/__snapshots__/routing.spec.ts.snap
@@ -10,7 +10,7 @@ RoutingError({
})
`;
-exports[`Routing > initRouting() > Should check if endpoint supports the method it's assigned to within DependsOnMethod 1`] = `
+exports[`Routing > initRouting() > Should check if endpoint supports the method it's assigned to 1`] = `
RoutingError({
"cause": {
"method": "post",
@@ -20,7 +20,7 @@ RoutingError({
})
`;
-exports[`Routing > initRouting() > Should prohibit DependsOnMethod for a route having explicit method 1`] = `
+exports[`Routing > initRouting() > Should prohibit ServeStatic for a route having explicit method 1`] = `
RoutingError({
"cause": {
"method": "get",
@@ -30,27 +30,27 @@ RoutingError({
})
`;
-exports[`Routing > initRouting() > Should prohibit ServeStatic for a route having explicit method 1`] = `
+exports[`Routing > initRouting() > Should prohibit duplicated routes 1`] = `
RoutingError({
"cause": {
"method": "get",
- "path": "/v1/user/retrieve",
+ "path": "/v1/test",
},
- "message": "Route with explicit method can only be assigned with Endpoint",
+ "message": "Route has a duplicate",
})
`;
-exports[`Routing > initRouting() > Should prohibit duplicated routes 1`] = `
+exports[`Routing > initRouting() > Should prohibit nested routing within a route having explicit method 1`] = `
RoutingError({
"cause": {
"method": "get",
- "path": "/v1/test",
+ "path": "/v1/user/retrieve",
},
- "message": "Route has a duplicate",
+ "message": "Route with explicit method can only be assigned with Endpoint",
})
`;
-exports[`Routing > initRouting() > Should prohibit nested routing within a route having explicit method 1`] = `
+exports[`Routing > initRouting() > Should prohibit nesting for a route having explicit method 1`] = `
RoutingError({
"cause": {
"method": "get",
diff --git a/express-zod-api/tests/depends-on-method.spec.ts b/express-zod-api/tests/depends-on-method.spec.ts
deleted file mode 100644
index 6eb6c8d69..000000000
--- a/express-zod-api/tests/depends-on-method.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { z } from "zod";
-import {
- DependsOnMethod,
- EndpointsFactory,
- defaultResultHandler,
-} from "../src/index.ts";
-import { AbstractEndpoint } from "../src/endpoint.ts";
-
-describe("DependsOnMethod", () => {
- test("should accept empty object", () => {
- const instance = new DependsOnMethod({});
- expect(instance).toBeInstanceOf(DependsOnMethod);
- expect(instance.entries).toEqual([]);
- });
-
- test("should accept an endpoint with a corresponding method", () => {
- const instance = new DependsOnMethod({
- post: new EndpointsFactory(defaultResultHandler).build({
- method: "post",
- output: z.object({}),
- handler: async () => ({}),
- }),
- });
- expect(instance.entries).toEqual([["post", expect.any(AbstractEndpoint)]]);
- });
-
- test.each([{ methods: ["get", "post"] } as const, {}])(
- "should accept an endpoint capable to handle multiple methods %#",
- (inc) => {
- const endpoint = new EndpointsFactory(defaultResultHandler).build({
- ...inc,
- output: z.object({}),
- handler: async () => ({}),
- });
- const instance = new DependsOnMethod({ get: endpoint, post: endpoint });
- expect(instance.entries).toEqual([
- ["get", expect.any(AbstractEndpoint)],
- ["post", expect.any(AbstractEndpoint)],
- ]);
- },
- );
-
- test("should reject empty assignments", () => {
- const instance = new DependsOnMethod({
- get: undefined,
- post: undefined,
- });
- expect(instance.entries).toEqual([]);
- });
-
- test("should be able to deprecate the assigned endpoints within a copy of itself", () => {
- const instance = new DependsOnMethod({
- post: new EndpointsFactory(defaultResultHandler).build({
- method: "post",
- output: z.object({}),
- handler: async () => ({}),
- }),
- });
- expect(instance.entries[0][1].isDeprecated).toBe(false);
- const copy = instance.deprecated();
- expect(copy.entries[0][1].isDeprecated).toBe(true);
- expect(copy).not.toBe(instance);
- });
-});
diff --git a/express-zod-api/tests/endpoint.spec.ts b/express-zod-api/tests/endpoint.spec.ts
index 99252021e..8887e94be 100644
--- a/express-zod-api/tests/endpoint.spec.ts
+++ b/express-zod-api/tests/endpoint.spec.ts
@@ -46,8 +46,8 @@ describe("Endpoint", () => {
});
const handlerMock = vi
.fn()
- .mockImplementationOnce(async ({ input, options }) => ({
- inc2: (options as { inc: number }).inc + 1,
+ .mockImplementationOnce(async ({ input, ctx }) => ({
+ inc2: (ctx as { inc: number }).inc + 1,
str: input.n.toFixed(2),
transform: "test",
}));
@@ -71,8 +71,8 @@ describe("Endpoint", () => {
expect(middlewareMock).toHaveBeenCalledTimes(1);
expect(middlewareMock).toHaveBeenCalledWith({
input: { n: 453 },
- options: {
- inc: 454, // due to reassignment of options
+ ctx: {
+ inc: 454, // due to reassignment
},
request: requestMock,
response: responseMock,
@@ -81,7 +81,7 @@ describe("Endpoint", () => {
expect(handlerMock).toHaveBeenCalledTimes(1);
expect(handlerMock).toHaveBeenCalledWith({
input: { n: 453 },
- options: { inc: 454 },
+ ctx: { inc: 454 },
logger: loggerMock,
});
expect(loggerMock._getLogs().error).toHaveLength(0);
@@ -89,7 +89,7 @@ describe("Endpoint", () => {
error: null,
input: { n: 453 },
logger: loggerMock,
- options: { inc: 454 },
+ ctx: { inc: 454 },
output: { inc2: 455, str: "453.00", transform: 4 },
request: requestMock,
response: responseMock,
@@ -131,6 +131,20 @@ describe("Endpoint", () => {
});
});
+ describe(".nest()", () => {
+ test("should return Routing arrangement", () => {
+ const subject = defaultEndpointsFactory.build({
+ output: z.object({}),
+ handler: vi.fn(),
+ });
+ expect(subject).toHaveProperty("nest", expect.any(Function));
+ expect(subject.nest({ subpath: subject })).toEqual({
+ "": subject,
+ subpath: subject,
+ });
+ });
+ });
+
describe("#parseOutput", () => {
test("Should throw on output validation failure", async () => {
const endpoint = defaultEndpointsFactory.build({
@@ -196,7 +210,7 @@ describe("Endpoint", () => {
expect(loggerMock._getLogs().error).toHaveLength(0);
expect(loggerMock._getLogs().warn).toEqual([
[
- "A middleware has closed the stream. Accumulated options:",
+ "A middleware has closed the stream. Accumulated context:",
{ inc: 454 },
],
]);
@@ -226,7 +240,7 @@ describe("Endpoint", () => {
error: null,
logger: loggerMock,
input: {},
- options: {},
+ ctx: {},
output: { test: "OK" },
request: requestMock,
response: responseMock,
diff --git a/express-zod-api/tests/endpoints-factory.spec.ts b/express-zod-api/tests/endpoints-factory.spec.ts
index 27b12ecb0..3d6a9abeb 100644
--- a/express-zod-api/tests/endpoints-factory.spec.ts
+++ b/express-zod-api/tests/endpoints-factory.spec.ts
@@ -54,18 +54,16 @@ describe("EndpointsFactory", () => {
expect(newFactory["resultHandler"]).toStrictEqual(resultHandlerMock);
});
- test("Should maintain the chain of options", () => {
+ test("Should maintain the chain of context", () => {
defaultEndpointsFactory
.addMiddleware(
- new Middleware({
- handler: async () => ({ test: "fist option" }),
- }),
+ new Middleware({ handler: async () => ({ test: "fist" }) }),
)
.addMiddleware(
new Middleware({
- handler: async ({ options }) => {
- expectTypeOf(options.test).toEqualTypeOf();
- return { second: `another option, ${options.test}` };
+ handler: async ({ ctx }) => {
+ expectTypeOf(ctx.test).toEqualTypeOf();
+ return { second: `another, ${ctx.test}` };
},
}),
);
@@ -73,7 +71,7 @@ describe("EndpointsFactory", () => {
test("Should accept creation props without input schema", () => {
const factory = defaultEndpointsFactory.addMiddleware({
- handler: async () => ({ test: "fist option" }),
+ handler: async () => ({ test: "fist" }),
});
expectTypeOf(factory).toEqualTypeOf<
EndpointsFactory
@@ -103,10 +101,10 @@ describe("EndpointsFactory", () => {
});
});
- describe(".addOptions()", () => {
+ describe(".addContext()", () => {
test("Should create a new factory with an empty-input middleware and the same result handler", async () => {
const factory = new EndpointsFactory(resultHandlerMock);
- const newFactory = factory.addOptions(async () => ({
+ const newFactory = factory.addContext(async () => ({
option1: "some value",
option2: "other value",
}));
@@ -131,118 +129,115 @@ describe("EndpointsFactory", () => {
});
});
- describe.each(["addExpressMiddleware" as const, "use" as const])(
- ".%s()",
- (method) => {
- test("Should create a new factory with a native express middleware wrapper", async () => {
- const factory = new EndpointsFactory(resultHandlerMock);
- const middleware: RequestHandler = vi.fn((req, {}, next) => {
- req.body.test = "Here is the test";
- next();
- });
- const newFactory = factory[method](middleware, {
- provider: (req) => ({ result: req.body.test }),
- });
- expect(newFactory["middlewares"].length).toBe(1);
- expect(newFactory["middlewares"][0].schema).toBeUndefined();
- const {
- output: options,
- responseMock,
- requestMock,
- } = await testMiddleware({
- middleware: newFactory["middlewares"][0],
- });
- expect(middleware).toHaveBeenCalledTimes(1);
- expect(middleware).toHaveBeenCalledWith(
- requestMock,
- responseMock,
- expect.any(Function),
- );
- expect(requestMock.body).toHaveProperty("test");
- expect(requestMock.body.test).toBe("Here is the test");
- expect(options).toEqual({ result: "Here is the test" });
+ describe.each(["addExpressMiddleware", "use"] as const)(".%s()", (method) => {
+ test("Should create a new factory with a native express middleware wrapper", async () => {
+ const factory = new EndpointsFactory(resultHandlerMock);
+ const middleware: RequestHandler = vi.fn((req, {}, next) => {
+ req.body.test = "Here is the test";
+ next();
+ });
+ const newFactory = factory[method](middleware, {
+ provider: (req) => ({ result: req.body.test }),
+ });
+ expect(newFactory["middlewares"].length).toBe(1);
+ expect(newFactory["middlewares"][0].schema).toBeUndefined();
+ const {
+ output: options,
+ responseMock,
+ requestMock,
+ } = await testMiddleware({
+ middleware: newFactory["middlewares"][0],
});
+ expect(middleware).toHaveBeenCalledTimes(1);
+ expect(middleware).toHaveBeenCalledWith(
+ requestMock,
+ responseMock,
+ expect.any(Function),
+ );
+ expect(requestMock.body).toHaveProperty("test");
+ expect(requestMock.body.test).toBe("Here is the test");
+ expect(options).toEqual({ result: "Here is the test" });
+ });
- test("Should handle rejects from async middlewares", async () => {
- const factory = new EndpointsFactory(resultHandlerMock);
- const middleware: RequestHandler = vi.fn(async () =>
- assert.fail("Rejected"),
- );
- const newFactory = factory[method](middleware);
- const { responseMock } = await testMiddleware({
- middleware: newFactory["middlewares"][0],
- });
- expect(responseMock._getStatusCode()).toBe(500);
- expect(responseMock._getJSONData()).toEqual({
- error: { message: "Rejected" },
- status: "error",
- });
- expect(middleware).toHaveBeenCalledTimes(1);
+ test("Should handle rejects from async middlewares", async () => {
+ const factory = new EndpointsFactory(resultHandlerMock);
+ const middleware: RequestHandler = vi.fn(async () =>
+ assert.fail("Rejected"),
+ );
+ const newFactory = factory[method](middleware);
+ const { responseMock } = await testMiddleware({
+ middleware: newFactory["middlewares"][0],
+ });
+ expect(responseMock._getStatusCode()).toBe(500);
+ expect(responseMock._getJSONData()).toEqual({
+ error: { message: "Rejected" },
+ status: "error",
});
+ expect(middleware).toHaveBeenCalledTimes(1);
+ });
- test("Should operate without options provider", async () => {
- const factory = new EndpointsFactory(resultHandlerMock);
- const middleware: RequestHandler = vi.fn((req, {}, next) => {
- req.body.test = "Here is the test";
- next();
- });
- const newFactory = factory[method](middleware);
- expect(newFactory["middlewares"].length).toBe(1);
- const {
- output: options,
- responseMock,
- requestMock,
- } = await testMiddleware({
- middleware: newFactory["middlewares"][0],
- });
- expect(middleware).toHaveBeenCalledTimes(1);
- expect(middleware).toHaveBeenCalledWith(
- requestMock,
- responseMock,
- expect.any(Function),
- );
- expect(requestMock.body).toHaveProperty("test");
- expect(requestMock.body.test).toBe("Here is the test");
- expect(options).toEqual({});
+ test("Should operate without context provider", async () => {
+ const factory = new EndpointsFactory(resultHandlerMock);
+ const middleware: RequestHandler = vi.fn((req, {}, next) => {
+ req.body.test = "Here is the test";
+ next();
});
+ const newFactory = factory[method](middleware);
+ expect(newFactory["middlewares"].length).toBe(1);
+ const {
+ output: options,
+ responseMock,
+ requestMock,
+ } = await testMiddleware({
+ middleware: newFactory["middlewares"][0],
+ });
+ expect(middleware).toHaveBeenCalledTimes(1);
+ expect(middleware).toHaveBeenCalledWith(
+ requestMock,
+ responseMock,
+ expect.any(Function),
+ );
+ expect(requestMock.body).toHaveProperty("test");
+ expect(requestMock.body.test).toBe("Here is the test");
+ expect(options).toEqual({});
+ });
- test("Should handle errors", async () => {
- const factory = new EndpointsFactory(resultHandlerMock);
- const middleware: RequestHandler = vi.fn(({}, {}, next) => {
- next(new Error("This one has failed"));
- });
- const newFactory = factory[method](middleware);
- const { responseMock } = await testMiddleware({
- middleware: newFactory["middlewares"][0],
- });
- expect(responseMock._getStatusCode()).toBe(500);
- expect(responseMock._getJSONData()).toEqual({
- error: { message: "This one has failed" },
- status: "error",
- });
- expect(middleware).toHaveBeenCalledTimes(1);
+ test("Should handle errors", async () => {
+ const factory = new EndpointsFactory(resultHandlerMock);
+ const middleware: RequestHandler = vi.fn(({}, {}, next) => {
+ next(new Error("This one has failed"));
+ });
+ const newFactory = factory[method](middleware);
+ const { responseMock } = await testMiddleware({
+ middleware: newFactory["middlewares"][0],
});
+ expect(responseMock._getStatusCode()).toBe(500);
+ expect(responseMock._getJSONData()).toEqual({
+ error: { message: "This one has failed" },
+ status: "error",
+ });
+ expect(middleware).toHaveBeenCalledTimes(1);
+ });
- test("Should transform errors", async () => {
- const factory = new EndpointsFactory(resultHandlerMock);
- const middleware: RequestHandler = vi.fn(({}, {}, next) => {
- next(new Error("This one has failed"));
- });
- const newFactory = factory[method](middleware, {
- transformer: (err) => createHttpError(401, err.message),
- });
- const { responseMock } = await testMiddleware({
- middleware: newFactory["middlewares"][0],
- });
- expect(responseMock._getStatusCode()).toBe(401);
- expect(responseMock._getJSONData()).toEqual({
- error: { message: "This one has failed" },
- status: "error",
- });
- expect(middleware).toHaveBeenCalledTimes(1);
+ test("Should transform errors", async () => {
+ const factory = new EndpointsFactory(resultHandlerMock);
+ const middleware: RequestHandler = vi.fn(({}, {}, next) => {
+ next(new Error("This one has failed"));
});
- },
- );
+ const newFactory = factory[method](middleware, {
+ transformer: (err) => createHttpError(401, err.message),
+ });
+ const { responseMock } = await testMiddleware({
+ middleware: newFactory["middlewares"][0],
+ });
+ expect(responseMock._getStatusCode()).toBe(401);
+ expect(responseMock._getJSONData()).toEqual({
+ error: { message: "This one has failed" },
+ status: "error",
+ });
+ expect(middleware).toHaveBeenCalledTimes(1);
+ });
+ });
describe(".build()", () => {
test("Should create an endpoint with simple middleware", () => {
diff --git a/express-zod-api/tests/middleware.spec.ts b/express-zod-api/tests/middleware.spec.ts
index f25b58d9d..8945a58b7 100644
--- a/express-zod-api/tests/middleware.spec.ts
+++ b/express-zod-api/tests/middleware.spec.ts
@@ -45,7 +45,7 @@ describe("Middleware", () => {
await expect(() =>
mw.execute({
input: { test: 123 },
- options: {},
+ ctx: {},
logger: makeLoggerMock(),
request: makeRequestMock(),
response: makeResponseMock(),
@@ -65,7 +65,7 @@ describe("Middleware", () => {
expect(
await mw.execute({
input: { test: "something" },
- options: { opt: "anything " },
+ ctx: { one: "anything " },
logger: loggerMock,
request: requestMock,
response: responseMock,
@@ -73,7 +73,7 @@ describe("Middleware", () => {
).toEqual({ result: "test" });
expect(handlerMock).toHaveBeenCalledWith({
input: { test: "something" },
- options: { opt: "anything " },
+ ctx: { one: "anything " },
logger: loggerMock,
request: requestMock,
response: responseMock,
diff --git a/express-zod-api/tests/result-handler.spec.ts b/express-zod-api/tests/result-handler.spec.ts
index 7d9fa6802..463d03515 100644
--- a/express-zod-api/tests/result-handler.spec.ts
+++ b/express-zod-api/tests/result-handler.spec.ts
@@ -91,7 +91,7 @@ describe("ResultHandler", () => {
request: requestMock,
response: responseMock,
logger: loggerMock,
- options: {},
+ ctx: {},
});
expect(loggerMock._getLogs().error).toMatchSnapshot();
expect(responseMock._getStatusCode()).toBe(500);
@@ -123,7 +123,7 @@ describe("ResultHandler", () => {
),
input: { something: 453 },
output: null,
- options: {},
+ ctx: {},
request: requestMock,
response: responseMock,
logger: loggerMock,
@@ -148,7 +148,7 @@ describe("ResultHandler", () => {
error: createHttpError(404, "Something not found"),
input: { something: 453 },
output: null,
- options: {},
+ ctx: {},
request: requestMock,
response: responseMock,
logger: loggerMock,
@@ -173,7 +173,7 @@ describe("ResultHandler", () => {
error: null,
input: { something: 453 },
output: { anything: 118, items: ["One", "Two", "Three"] },
- options: {},
+ ctx: {},
request: requestMock,
response: responseMock,
logger: loggerMock,
@@ -226,7 +226,7 @@ describe("ResultHandler", () => {
error: null,
input: { something: 453 },
output: { anything: 118 },
- options: {},
+ ctx: {},
request: requestMock,
response: responseMock,
logger: loggerMock,
diff --git a/express-zod-api/tests/routable.spec.ts b/express-zod-api/tests/routable.spec.ts
deleted file mode 100644
index 6573fff4d..000000000
--- a/express-zod-api/tests/routable.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { z } from "zod";
-import { defaultEndpointsFactory, DependsOnMethod } from "../src/index.ts";
-
-const endpoint = defaultEndpointsFactory.build({
- output: z.object({}),
- handler: vi.fn(),
-});
-
-const methodDepending = new DependsOnMethod({ get: endpoint });
-
-describe.each([methodDepending, endpoint])("Routable mixin %#", (subject) => {
- describe(".nest()", () => {
- test("should return Routing arrangement", () => {
- expect(subject).toHaveProperty("nest", expect.any(Function));
- expect(subject.nest({ subpath: endpoint })).toEqual({
- "": subject,
- subpath: endpoint,
- });
- });
- });
-});
diff --git a/express-zod-api/tests/routing.spec.ts b/express-zod-api/tests/routing.spec.ts
index 21ed597a1..ad0f3c684 100644
--- a/express-zod-api/tests/routing.spec.ts
+++ b/express-zod-api/tests/routing.spec.ts
@@ -6,7 +6,6 @@ import {
} from "./express-mock.ts";
import { z } from "zod";
import {
- DependsOnMethod,
EndpointsFactory,
Routing,
ServeStatic,
@@ -109,7 +108,7 @@ describe("Routing", () => {
expect(appMock.use).toHaveBeenCalledWith("/public", staticHandler);
});
- test("Should accept DependsOnMethod", () => {
+ test("Should handle method depending assignments", () => {
const handlerMock = vi.fn();
const factory = new EndpointsFactory(defaultResultHandler);
const getEndpoint = factory.build({
@@ -126,12 +125,12 @@ describe("Routing", () => {
});
const routing: Routing = {
v1: {
- user: new DependsOnMethod({
- get: getEndpoint,
- post: postEndpoint,
- put: putAndPatchEndpoint,
- patch: putAndPatchEndpoint,
- }),
+ user: {
+ "get /": getEndpoint,
+ "post /": postEndpoint,
+ "put /": putAndPatchEndpoint,
+ "patch /": putAndPatchEndpoint,
+ },
},
};
const logger = makeLoggerMock();
@@ -154,7 +153,7 @@ describe("Routing", () => {
expect(appMock.options.mock.calls[0][0]).toBe("/v1/user");
});
- test("Should check if endpoint supports the method it's assigned to within DependsOnMethod", () => {
+ test("Should check if endpoint supports the method it's assigned to", () => {
const factory = new EndpointsFactory(defaultResultHandler);
const putAndPatchEndpoint = factory.build({
method: ["put", "patch"],
@@ -163,11 +162,11 @@ describe("Routing", () => {
});
const routing: Routing = {
v1: {
- user: new DependsOnMethod({
- put: putAndPatchEndpoint,
- patch: putAndPatchEndpoint,
- post: putAndPatchEndpoint, // intentional
- }),
+ user: {
+ "put /": putAndPatchEndpoint,
+ "patch /": putAndPatchEndpoint,
+ "post /": putAndPatchEndpoint, // intentional
+ },
},
};
const logger = makeLoggerMock();
@@ -181,7 +180,7 @@ describe("Routing", () => {
).toThrowErrorMatchingSnapshot();
});
- test("Issue 705: should set all DependsOnMethod' methods for CORS", async () => {
+ test("Issue 705: should set all assigned methods to CORS response header", async () => {
const handler = vi.fn(async () => ({}));
const configMock = {
cors: (params: { defaultHeaders: Record }) => ({
@@ -208,12 +207,12 @@ describe("Routing", () => {
handler,
});
const routing: Routing = {
- hello: new DependsOnMethod({
- get: getEndpoint,
- post: postEndpoint,
- put: putAndPatchEndpoint,
- patch: putAndPatchEndpoint,
- }),
+ hello: {
+ "get /": getEndpoint,
+ "post /": postEndpoint,
+ "put /": putAndPatchEndpoint,
+ "patch /": putAndPatchEndpoint,
+ },
};
const logger = makeLoggerMock();
initRouting({
@@ -408,7 +407,7 @@ describe("Routing", () => {
).toThrowErrorMatchingSnapshot();
});
- test("Should prohibit DependsOnMethod for a route having explicit method", () => {
+ test("Should prohibit nesting for a route having explicit method", () => {
const logger = makeLoggerMock();
expect(() =>
initRouting({
@@ -417,7 +416,7 @@ describe("Routing", () => {
config: { cors: false },
routing: {
v1: {
- "get /user/retrieve": new DependsOnMethod({}),
+ "get /user/retrieve": {},
},
},
}),
@@ -501,7 +500,7 @@ describe("Routing", () => {
).toHaveLength(0);
expect(handlerMock).toHaveBeenCalledWith({
input: { test: 123 },
- options: {},
+ ctx: {},
logger: getLoggerMock.mock.results.pop()!.value,
});
expect(responseMock._getStatusCode()).toBe(200);
diff --git a/express-zod-api/tests/sse.spec.ts b/express-zod-api/tests/sse.spec.ts
index 5260defb1..473d4367d 100644
--- a/express-zod-api/tests/sse.spec.ts
+++ b/express-zod-api/tests/sse.spec.ts
@@ -76,7 +76,7 @@ describe("SSE", () => {
describe("makeMiddleware()", () => {
// with and without response.flush()
test.each([vi.fn(), undefined])(
- "should create a Middleware providing options for emission %#",
+ "should create a Middleware providing context for emission %#",
async (flushMock) => {
const middleware = makeMiddleware({ test: z.string() });
expect(middleware).toBeInstanceOf(Middleware);
@@ -128,7 +128,7 @@ describe("SSE", () => {
const positiveResponse = makeResponseMock();
const commons = {
input: {},
- options: {},
+ ctx: {},
request: makeRequestMock(),
logger: makeLoggerMock(),
};
@@ -163,9 +163,9 @@ describe("SSE", () => {
test("should combine SSE Middleware with corresponding ResultHandler and return Endpoint", async () => {
const endpoint = new EventStreamFactory({ test: z.string() }).buildVoid({
input: z.object({ some: z.string().optional() }),
- handler: async ({ input, options }) => {
+ handler: async ({ input, ctx }) => {
expectTypeOf(input).toExtend<{ some?: string }>();
- expectTypeOf(options.emit)
+ expectTypeOf(ctx.emit)
.parameter(0)
.toEqualTypeOf("test" as const);
},
diff --git a/express-zod-api/tests/system.spec.ts b/express-zod-api/tests/system.spec.ts
index 7b76d19ed..b3a57b136 100644
--- a/express-zod-api/tests/system.spec.ts
+++ b/express-zod-api/tests/system.spec.ts
@@ -35,7 +35,7 @@ describe("App in production mode", async () => {
)
.build({
output: z.object({ corsDone: z.boolean() }),
- handler: async ({ options: { corsDone } }) => ({ corsDone }),
+ handler: async ({ ctx: { corsDone } }) => ({ corsDone }),
});
const faultyResultHandler = new ResultHandler({
positive: z.object({}),
@@ -75,7 +75,7 @@ describe("App in production mode", async () => {
handler: async () => ({ user: { id: 354 } }),
})
.addMiddleware({
- handler: async ({ request, options: { user } }) => ({
+ handler: async ({ request, ctx: { user } }) => ({
method: request.method.toLowerCase() as Method,
permissions: user.id === 354 ? ["any"] : [],
}),
@@ -86,7 +86,7 @@ describe("App in production mode", async () => {
output: z.looseObject({ anything: z.number().positive() }), // allow excessive keys
handler: async ({
input: { key, something },
- options: { user, permissions, method },
+ ctx: { user, permissions, method },
}) => {
// Problem 787: should lead to ZodError that is NOT considered as the IOSchema validation error
if (something === "internal_zod_error") z.number().parse("");
diff --git a/express-zod-api/tests/testing.spec.ts b/express-zod-api/tests/testing.spec.ts
index 65b9e4841..60bbf074d 100644
--- a/express-zod-api/tests/testing.spec.ts
+++ b/express-zod-api/tests/testing.spec.ts
@@ -61,17 +61,17 @@ describe("Testing", () => {
test("Should test a middleware", async () => {
const { output } = await testMiddleware({
requestProps: { method: "POST", body: { test: "something" } },
- options: { prev: "accumulated" },
+ ctx: { prev: "accumulated" },
middleware: new Middleware({
input: z.object({ test: z.string() }),
- handler: async ({ options, input: { test } }) => ({
- optKeys: Object.keys(options),
+ handler: async ({ ctx, input: { test } }) => ({
+ ctxKeys: Object.keys(ctx),
inpLen: test.length,
}),
}),
});
expect(output).toEqual({
- optKeys: ["prev"],
+ ctxKeys: ["prev"],
inpLen: 9,
});
});
diff --git a/migration/README.md b/migration/README.md
index 7c65012b0..e57bc91fd 100644
--- a/migration/README.md
+++ b/migration/README.md
@@ -18,6 +18,6 @@ import migration from "@express-zod-api/migration";
export default [
{ languageOptions: { parser }, plugins: { migration } },
- { files: ["**/*.ts"], rules: { "migration/v25": "error" } },
+ { files: ["**/*.ts"], rules: { "migration/v26": "error" } },
];
```
diff --git a/migration/__snapshots__/index.spec.ts.snap b/migration/__snapshots__/index.spec.ts.snap
index 0de70f538..b557c5e5d 100644
--- a/migration/__snapshots__/index.spec.ts.snap
+++ b/migration/__snapshots__/index.spec.ts.snap
@@ -3,7 +3,7 @@
exports[`Migration > should consist of one rule being the major version of the package 1`] = `
{
"rules": {
- "v25": {
+ "v26": {
"create": [Function],
"defaultOptions": [],
"meta": {
diff --git a/migration/index.spec.ts b/migration/index.spec.ts
index ed36ad323..d07020383 100644
--- a/migration/index.spec.ts
+++ b/migration/index.spec.ts
@@ -23,56 +23,144 @@ describe("Migration", async () => {
tester.run(ruleName, theRule, {
valid: [
- `import {} from "zod";`,
- `ez.dateIn({ examples: ["1963-04-21"] });`,
- `ez.dateOut({ examples: ["2021-12-31T00:00:00.000Z"] });`,
- `schema.meta()?.examples;`,
+ `const routing = { "get /": someEndpoint };`,
+ `factory.build({ handler: async ({ ctx }) => {} });`,
+ `factory.addContext();`,
+ `new Middleware({ handler: async ({ ctx }) => {} });`,
+ `new ResultHandler({ handler: ({ ctx }) => {} });`,
+ `testMiddleware({ ctx: {} });`,
],
invalid: [
{
- code: `import {} from "zod/v4";`,
- output: `import {} from "zod";`,
+ name: "basic DependsOnMethod",
+ code: `const routing = new DependsOnMethod({ get: someEndpoint });`,
+ output: `const routing = {\n"get /": someEndpoint,\n};`,
errors: [
{
messageId: "change",
- data: { subject: "import", from: "zod/v4", to: "zod" },
+ data: {
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
+ },
},
],
},
{
- code: `ez.dateIn({ example: "1963-04-21" });`,
- output: `ez.dateIn({ examples: ["1963-04-21"] });`,
+ name: "DependsOnMethod with literals",
+ code: `const routing = new DependsOnMethod({ "get": someEndpoint });`,
+ output: `const routing = {\n"get /": someEndpoint,\n};`,
errors: [
{
messageId: "change",
- data: { subject: "property", from: "example", to: "examples" },
+ data: {
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
+ },
},
],
},
{
- code: `ez.dateOut({ example: "2021-12-31T00:00:00.000Z" });`,
- output: `ez.dateOut({ examples: ["2021-12-31T00:00:00.000Z"] });`,
+ name: "deprecated DependsOnMethod",
+ code: `const routing = new DependsOnMethod({ get: someEndpoint }).deprecated();`,
+ output: `const routing = {\n"get /": someEndpoint.deprecated(),\n};`,
errors: [
{
messageId: "change",
- data: { subject: "property", from: "example", to: "examples" },
+ data: {
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
+ },
},
],
},
{
- code: `getExamples(schema);`,
- output: `(schema.meta()?.examples || []);`,
+ name: "DependsOnMethod with nesting",
+ code: `const routing = new DependsOnMethod({ get: someEndpoint }).nest({ some: otherEndpoint });`,
+ output: `const routing = {\n"get /": someEndpoint,\n"some": otherEndpoint,\n};`,
errors: [
{
messageId: "change",
data: {
- subject: "method",
- from: "getExamples()",
- to: ".meta()?.examples || []",
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
},
},
],
},
+ {
+ name: "DependsOnMethod both deprecated and with nesting",
+ code: `const routing = new DependsOnMethod({ get: someEndpoint }).deprecated().nest({ some: otherEndpoint });`,
+ output: `const routing = {\n"get /": someEndpoint.deprecated(),\n"some": otherEndpoint,\n};`,
+ errors: [
+ {
+ messageId: "change",
+ data: {
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
+ },
+ },
+ ],
+ },
+ {
+ name: "options in handler",
+ code: `factory.build({ handler: async ({ options }) => {} });`,
+ output: `factory.build({ handler: async ({ ctx }) => {} });`,
+ errors: [
+ {
+ messageId: "change",
+ data: { subject: "property", from: "options", to: "ctx" },
+ },
+ ],
+ },
+ {
+ name: "renamed options in handler",
+ code: `new Middleware({ handler: async ({ options: ttt }) => {} });`,
+ output: `new Middleware({ handler: async ({ ctx: ttt }) => {} });`,
+ errors: [
+ {
+ messageId: "change",
+ data: { subject: "property", from: "options", to: "ctx" },
+ },
+ ],
+ },
+ {
+ name: "destructed options in handler",
+ code: `new ResultHandler({ handler: ({ options: { method } }) => {} });`,
+ output: `new ResultHandler({ handler: ({ ctx: { method } }) => {} });`,
+ errors: [
+ {
+ messageId: "change",
+ data: { subject: "property", from: "options", to: "ctx" },
+ },
+ ],
+ },
+ {
+ name: "addOptions method",
+ code: `factory.addOptions(() => {});`,
+ output: `factory.addContext(() => {});`,
+ errors: [
+ {
+ messageId: "change",
+ data: { subject: "method", from: "addOptions", to: "addContext" },
+ },
+ ],
+ },
+ {
+ name: "testMiddleware options property",
+ code: `testMiddleware({ options: {} });`,
+ output: `testMiddleware({ ctx: {} });`,
+ errors: [
+ {
+ messageId: "change",
+ data: { subject: "property", from: "options", to: "ctx" },
+ },
+ ],
+ },
],
});
});
diff --git a/migration/index.ts b/migration/index.ts
index 5c6fda15e..acc88fa56 100644
--- a/migration/index.ts
+++ b/migration/index.ts
@@ -6,25 +6,40 @@ import {
} from "@typescript-eslint/utils"; // eslint-disable-line allowed/dependencies -- assumed transitive dependency
type NamedProp = TSESTree.PropertyNonComputedName & {
- key: TSESTree.Identifier;
+ key: TSESTree.Identifier | TSESTree.StringLiteral;
};
interface Queries {
- zod: TSESTree.ImportDeclaration;
- dateInOutExample: NamedProp;
- getExamples: TSESTree.CallExpression;
+ dependsOnMethod: TSESTree.NewExpression;
+ handlerOptions: TSESTree.Property;
+ addOptions: TSESTree.Identifier;
+ testMiddlewareOptions: TSESTree.Property;
}
type Listener = keyof Queries;
const queries: Record = {
- zod: `${NT.ImportDeclaration}[source.value='zod/v4']`,
- dateInOutExample:
- `${NT.CallExpression}[callee.object.name='ez'][callee.property.name=/date(In|Out)/] >` +
- `${NT.ObjectExpression} > ${NT.Property}[key.name='example']`,
- getExamples: `${NT.CallExpression}[callee.name='getExamples']`,
+ dependsOnMethod: `${NT.NewExpression}[callee.name='DependsOnMethod']`,
+ handlerOptions:
+ `${NT.ObjectExpression} > ${NT.Property}[key.name='handler'] > ` +
+ `${NT.ArrowFunctionExpression} > ${NT.ObjectPattern} > ${NT.Property}[key.name='options']`,
+ addOptions:
+ `${NT.CallExpression}:has( ${NT.ArrowFunctionExpression} ) > ` +
+ `${NT.MemberExpression} > ${NT.Identifier}[name='addOptions']`,
+ testMiddlewareOptions:
+ `${NT.CallExpression}[callee.name='testMiddleware'] > ` +
+ `${NT.ObjectExpression} > ${NT.Property}[key.name='options']`,
};
+const isNamedProp = (prop: TSESTree.ObjectLiteralElement): prop is NamedProp =>
+ prop.type === NT.Property &&
+ !prop.computed &&
+ (prop.key.type === NT.Identifier ||
+ (prop.key.type === NT.Literal && typeof prop.key.value === "string"));
+
+const getPropName = (prop: NamedProp): string =>
+ prop.key.type === NT.Identifier ? prop.key.name : prop.key.value;
+
const listen = <
S extends { [K in Listener]: TSESLint.RuleFunction },
>(
@@ -56,39 +71,79 @@ const theRule = ESLintUtils.RuleCreator.withoutDocs({
defaultOptions: [],
create: (ctx) =>
listen({
- zod: (node) =>
+ dependsOnMethod: (node) => {
+ if (node.arguments.length !== 1) return;
+ const argument = node.arguments[0];
+ if (argument.type !== NT.ObjectExpression) return;
+ let isDeprecated = false;
+ let nested: TSESTree.ObjectExpression | undefined = undefined;
+ let cursor: TSESTree.Node = node;
+ while (
+ cursor &&
+ cursor.parent &&
+ cursor.parent.type === NT.MemberExpression &&
+ cursor.parent.property.type === NT.Identifier &&
+ cursor.parent.parent &&
+ cursor.parent.parent.type === NT.CallExpression
+ ) {
+ const name = cursor.parent.property.name;
+ const call = cursor.parent.parent as TSESTree.CallExpression;
+ if (name === "deprecated") isDeprecated = true;
+ if (
+ name === "nest" &&
+ call.arguments[0] &&
+ call.arguments[0].type === NT.ObjectExpression
+ )
+ nested = call.arguments[0];
+ cursor = call;
+ }
+ ctx.report({
+ node: cursor,
+ messageId: "change",
+ data: {
+ subject: "value",
+ from: "new DependsOnMethod(...)",
+ to: "its argument object and append its keys with ' /'",
+ },
+ fix: (fixer) => {
+ const makeMapper =
+ (feat?: "deprecated" | "nest") =>
+ (prop: TSESTree.ObjectLiteralElement) =>
+ isNamedProp(prop)
+ ? `"${getPropName(prop)}${feat === "nest" ? "" : " /"}": ${ctx.sourceCode.getText(prop.value)}${feat === "deprecated" ? ".deprecated()" : ""},`
+ : `${ctx.sourceCode.getText(prop)}, /** @todo migrate manually */`;
+ const nextProps = argument.properties
+ .map(makeMapper(isDeprecated ? "deprecated" : undefined))
+ .concat(nested?.properties.map(makeMapper("nest")) ?? [])
+ .join("\n");
+ return fixer.replaceText(cursor, `{\n${nextProps}\n}`);
+ },
+ });
+ },
+ handlerOptions: (node) => {
ctx.report({
- node: node.source,
+ node,
messageId: "change",
- data: { subject: "import", from: "zod/v4", to: "zod" },
- fix: (fixer) => fixer.replaceText(node.source, `"zod"`),
- }),
- dateInOutExample: (node) =>
+ data: { subject: "property", from: "options", to: "ctx" },
+ fix: (fixer) => fixer.replaceText(node.key, "ctx"),
+ });
+ },
+ addOptions: (node) => {
ctx.report({
node,
messageId: "change",
- data: { subject: "property", from: "example", to: "examples" },
- fix: (fixer) =>
- fixer.replaceText(
- node,
- `examples: [${ctx.sourceCode.getText(node.value)}]`,
- ),
- }),
- getExamples: (node) =>
+ data: { subject: "method", from: "addOptions", to: "addContext" },
+ fix: (fixer) => fixer.replaceText(node, "addContext"),
+ });
+ },
+ testMiddlewareOptions: (node) => {
ctx.report({
node,
messageId: "change",
- data: {
- subject: "method",
- from: "getExamples()",
- to: ".meta()?.examples || []",
- },
- fix: (fixer) =>
- fixer.replaceText(
- node,
- `(${ctx.sourceCode.getText(node.arguments[0])}.meta()?.examples || [])`,
- ),
- }),
+ data: { subject: "property", from: "options", to: "ctx" },
+ fix: (fixer) => fixer.replaceText(node.key, "ctx"),
+ });
+ },
}),
});
diff --git a/migration/package.json b/migration/package.json
index ad455c2a9..0771bff23 100644
--- a/migration/package.json
+++ b/migration/package.json
@@ -1,6 +1,6 @@
{
"name": "@express-zod-api/migration",
- "version": "25.1.1",
+ "version": "26.0.0-beta.0",
"license": "MIT",
"description": "Migration scripts for express-zod-api",
"repository": {