Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ref: plural props #1536

Closed
wants to merge 5 commits into from
Closed
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
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ The endpoint responds with "Hello, World" or "Hello, {name}" if the name is supp
import { z } from "zod";

const helloWorldEndpoint = defaultEndpointsFactory.build({
method: "get", // or methods: ["get", "post", ...]
methods: "get", // or array: ["get", "post", ...]
input: z.object({
// for empty input use z.object({})
name: z.string().optional(),
Expand Down Expand Up @@ -380,7 +380,7 @@ arrays of numbers.
import { z } from "zod";

const getUserEndpoint = endpointsFactory.build({
method: "get",
methods: "get",
input: z.object({
id: z.string().transform((id) => parseInt(id, 10)),
ids: z
Expand Down Expand Up @@ -429,7 +429,7 @@ import { z } from "zod";
import { ez, defaultEndpointsFactory } from "express-zod-api";

const updateUserEndpoint = defaultEndpointsFactory.build({
method: "post",
methods: "post",
input: z.object({
userId: z.string(),
birthday: ez.dateIn(), // string -> Date
Expand Down Expand Up @@ -614,7 +614,7 @@ You then need to specify these parameters in the endpoint input schema in the us

```typescript
const getUserEndpoint = endpointsFactory.build({
method: "get",
methods: "get",
input: z.object({
// id is the route path param, always string
id: z.string().transform((value) => parseInt(value, 10)),
Expand Down Expand Up @@ -689,7 +689,7 @@ import {
export const yourResultHandler = createResultHandler({
getPositiveResponse: (output: IOSchema) => ({
schema: z.object({ data: output }),
mimeType: "application/json", // optinal, or mimeTypes for array
mimeTypes: "application/json", // optinal, or array
}),
getNegativeResponse: () => z.object({ error: z.string() }),
handler: ({ error, input, output, request, response, logger }) => {
Expand Down Expand Up @@ -734,9 +734,12 @@ const fileStreamingEndpointsFactory = new EndpointsFactory(
createResultHandler({
getPositiveResponse: () => ({
schema: ez.file("buffer"),
mimeType: "image/*",
mimeTypes: "image/*",
}),
getNegativeResponse: () => ({
schema: z.string(),
mimeTypes: "text/plain",
}),
getNegativeResponse: () => ({ schema: z.string(), mimeType: "text/plain" }),
handler: ({ response, error, output }) => {
if (error) {
response.status(400).send(error.message);
Expand Down Expand Up @@ -779,7 +782,7 @@ import { z } from "zod";
import { ez, defaultEndpointsFactory } from "express-zod-api";

const fileUploadEndpoint = defaultEndpointsFactory.build({
method: "post",
methods: "post",
input: z.object({
avatar: ez.upload(), // <--
}),
Expand Down Expand Up @@ -860,7 +863,7 @@ createResultHandler({
}),
getNegativeResponse: () => [
{
statusCode: 409, // conflict: entity already exists
statusCodes: 409, // conflict: entity already exists
schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
},
{
Expand Down Expand Up @@ -903,7 +906,7 @@ createConfig({
});

defaultEndpointsFactory.build({
method: "get",
methods: "get",
input: z.object({
"x-request-id": z.string(), // this one is from request.headers
id: z.string(), // this one is from request.query
Expand All @@ -930,7 +933,7 @@ const config = createConfig({
});

const rawAcceptingEndpoint = defaultEndpointsFactory.build({
method: "post",
methods: "post",
input: ez
.raw() // accepts the featured { raw: Buffer }
.extend({}), // for additional inputs, like route params, if needed
Expand Down Expand Up @@ -1060,7 +1063,7 @@ const taggedEndpointsFactory = new EndpointsFactory({

const exampleEndpoint = taggedEndpointsFactory.build({
// ...
tag: "users", // or tags: ["users", "files"]
tags: "users", // or array: ["users", "files"]
});
```

Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/accept-raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ez } from "../../src";
import { taggedEndpointsFactory } from "../factories";

export const rawAcceptingEndpoint = taggedEndpointsFactory.build({
method: "post",
tag: "files",
methods: "post",
tags: "files",
input: ez
.raw() // requires to enable rawParser option in server config
.extend({}), // additional inputs, route params for example, if needed
Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { statusDependingFactory } from "../factories";

/** @desc depending on the thrown error, the custom result handler of the factory responds slightly differently */
export const createUserEndpoint = statusDependingFactory.build({
method: "post",
tag: "users",
methods: "post",
tags: "users",
input: z.object({
name: z.string().min(1),
}),
Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/list-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { arrayRespondingFactory } from "../factories";
* Avoid doing this in new projects. This feature is only for easier migration of legacy APIs.
* */
export const listUsersEndpoint = arrayRespondingFactory.build({
method: "get",
tag: "users",
methods: "get",
tags: "users",
input: z.object({}),
output: withMeta(
z.object({
Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/retrieve-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const feature: z.ZodType<Feature> = baseFeature.extend({
export const retrieveUserEndpoint = taggedEndpointsFactory
.addMiddleware(methodProviderMiddleware)
.build({
method: "get",
tag: "users",
methods: "get",
tags: "users",
shortDescription: "Retrieves the user.",
description: "Example user retrieval endpoint.",
input: z.object({
Expand Down
2 changes: 1 addition & 1 deletion example/endpoints/send-avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fileSendingEndpointsFactory } from "../factories";
import { readFile } from "node:fs/promises";

export const sendAvatarEndpoint = fileSendingEndpointsFactory.build({
method: "get",
methods: "get",
shortDescription: "Sends a file content.",
tags: ["files", "users"],
input: z.object({
Expand Down
2 changes: 1 addition & 1 deletion example/endpoints/stream-avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from "zod";
import { fileStreamingEndpointsFactory } from "../factories";

export const streamAvatarEndpoint = fileStreamingEndpointsFactory.build({
method: "get",
methods: "get",
shortDescription: "Streams a file content.",
tags: ["users", "files"],
input: z.object({
Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/update-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { keyAndTokenAuthenticatedEndpointsFactory } from "../factories";

export const updateUserEndpoint =
keyAndTokenAuthenticatedEndpointsFactory.build({
method: "patch",
tag: "users",
methods: "patch",
tags: "users",
description: "Changes the user record. Example user update endpoint.",
input: withMeta(
z.object({
Expand Down
4 changes: 2 additions & 2 deletions example/endpoints/upload-avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { createHash } from "node:crypto";
import { taggedEndpointsFactory } from "../factories";

export const uploadAvatarEndpoint = taggedEndpointsFactory.build({
method: "post",
tag: "files",
methods: "post",
tags: "files",
description: "Handles a file upload.",
input: z
.object({
Expand Down
10 changes: 5 additions & 5 deletions example/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export const fileSendingEndpointsFactory = new EndpointsFactory({
resultHandler: createResultHandler({
getPositiveResponse: () => ({
schema: z.string(),
mimeType: "image/svg+xml",
mimeTypes: "image/svg+xml",
}),
getNegativeResponse: () => ({
schema: z.string(),
mimeType: "text/plain",
mimeTypes: "text/plain",
}),
handler: ({ response, error, output }) => {
if (error) {
Expand All @@ -53,11 +53,11 @@ export const fileStreamingEndpointsFactory = new EndpointsFactory({
resultHandler: createResultHandler({
getPositiveResponse: () => ({
schema: ez.file("buffer"),
mimeType: "image/*",
mimeTypes: "image/*",
}),
getNegativeResponse: () => ({
schema: z.string(),
mimeType: "text/plain",
mimeTypes: "text/plain",
}),
handler: ({ response, error, output }) => {
if (error) {
Expand Down Expand Up @@ -97,7 +97,7 @@ export const statusDependingFactory = new EndpointsFactory({
}),
getNegativeResponse: () => [
{
statusCode: 409,
statusCodes: 409,
schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
},
{
Expand Down
47 changes: 23 additions & 24 deletions src/api-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,23 @@ export const defaultStatusCodes = {
export interface ApiResponse<S extends z.ZodTypeAny> {
schema: S;
/**
* @default 200 for a positive response
* @default 400 for a negative response
* @override statusCodes
* @default 200 for positive response
* @default 400 for negative response
* */
statusCode?: number;
/**
* @default [200] for positive response
* @default [400] for negative response
* */
statusCodes?: [number, ...number[]];
/**
* @default "application/json"
* @override mimeTypes
* */
mimeType?: string;
/** @default [ "application/json" ] */
mimeTypes?: [string, ...string[]];
statusCodes?: number | [number, ...number[]];
/** @default "application/json" */
mimeTypes?: string | [string, ...string[]];
/** @deprecated use statusCodes instead */
statusCode?: never;
/** @deprecated use mimeTypes instead */
mimeType?: never;
}

export type NormalizedResponse = Required<
Pick<ApiResponse<z.ZodTypeAny>, "schema" | "statusCodes" | "mimeTypes">
>;
export interface NormalizedResponse {
schema: z.ZodTypeAny;
statusCodes: [number, ...number[]];
mimeTypes: [string, ...string[]];
}

export type AnyResponseDefinition =
| z.ZodTypeAny // plain schema, default status codes applied
Expand All @@ -44,12 +39,16 @@ export const normalizeApiResponse = (
return [{ ...fallback, schema: subject }];
}
return (Array.isArray(subject) ? subject : [subject]).map(
({ schema, statusCodes, statusCode, mimeTypes, mimeType }) => ({
({ schema, statusCodes, mimeTypes }) => ({
schema,
statusCodes: statusCode
? [statusCode]
: statusCodes || fallback.statusCodes,
mimeTypes: mimeType ? [mimeType] : mimeTypes || fallback.mimeTypes,
statusCodes:
typeof statusCodes === "number"
? [statusCodes]
: statusCodes || fallback.statusCodes,
mimeTypes:
typeof mimeTypes === "string"
? [mimeTypes]
: mimeTypes || fallback.mimeTypes,
}),
);
};
26 changes: 10 additions & 16 deletions src/endpoints-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ type BuildProps<
description?: string;
shortDescription?: string;
operationId?: string | ((method: Method) => string);
} & ({ method: Method } | { methods: Method[] }) &
({ scopes?: SCO[] } | { scope?: SCO }) &
({ tags?: TAG[] } | { tag?: TAG });
methods: Method | Method[];
scopes?: SCO | SCO[];
tags?: TAG | TAG[];
};

export class EndpointsFactory<
IN extends IOSchema<"strip"> | null = null,
Expand Down Expand Up @@ -145,7 +146,9 @@ export class EndpointsFactory<
description,
shortDescription,
operationId,
...rest
methods,
scopes,
tags,
}: BuildProps<BIN, BOUT, IN, OUT, SCO, TAG>): Endpoint<
ProbableIntersection<IN, BIN>,
BOUT,
Expand All @@ -154,25 +157,16 @@ export class EndpointsFactory<
TAG
> {
const { middlewares, resultHandler } = this;
const methods = "methods" in rest ? rest.methods : [rest.method];
const getOperationId =
typeof operationId === "function" ? operationId : () => operationId;
const scopes =
"scopes" in rest
? rest.scopes
: "scope" in rest && rest.scope
? [rest.scope]
: [];
const tags =
"tags" in rest ? rest.tags : "tag" in rest && rest.tag ? [rest.tag] : [];
return new Endpoint({
handler,
middlewares,
outputSchema,
resultHandler,
scopes,
tags,
methods,
scopes: typeof scopes === "string" ? [scopes] : scopes || [],
tags: typeof tags === "string" ? [tags] : tags || [],
methods: typeof methods === "string" ? [methods] : methods,
getOperationId,
description,
shortDescription,
Expand Down
4 changes: 2 additions & 2 deletions tests/system/system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("App", async () => {
},
)
.build({
method: "get",
methods: "get",
input: z.object({}),
output: z.object({ corsDone: z.boolean() }),
handler: async ({ options }) => ({
Expand Down Expand Up @@ -59,7 +59,7 @@ describe("App", async () => {
}),
)
.build({
method: "get",
methods: "get",
input: z.object({
epError: z
.any()
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/depends-on-method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("DependsOnMethod", () => {
test("should accept an endpoint with a corresponding method", () => {
const instance = new DependsOnMethod({
post: new EndpointsFactory(defaultResultHandler).build({
method: "post",
methods: "post",
input: z.object({}),
output: z.object({}),
handler: async () => ({}),
Expand Down
Loading
Loading