UNPKG

@tsed/schema

Version:
222 lines (221 loc) 7.16 kB
import { deepMerge, uniq, uniqBy } from "@tsed/core"; import { isRedirectionStatus, isSuccessStatus } from "../utils/isSuccessStatus.js"; import { JsonMap } from "./JsonMap.js"; import { JsonParameter } from "./JsonParameter.js"; import { JsonResponse } from "./JsonResponse.js"; import { JsonSchema } from "./JsonSchema.js"; /** * Represents an HTTP operation path with metadata for OpenAPI specifications. * * This class associates an HTTP method and path with operation-level metadata * such as summary and description. It's used internally to manage routing * information for API endpoints. * * @public */ export class JsonMethodPath extends JsonMap { constructor(method, path) { super(); this.method = method; this.path = path; } summary(summary) { super.set("summary", summary); return this; } description(description) { super.set("description", description); return this; } } /** * Represents an HTTP operation (endpoint) with complete OpenAPI metadata. * * JsonOperation is the core class for defining HTTP endpoints in Ts.ED, storing all * operation-level information required for OpenAPI specification generation including: * * - Request parameters (path, query, header, body) * - Response definitions with status codes * - Security requirements * - Tags and categorization * - Media type handling (consumes/produces) * - Operation metadata (summary, description, operationId) * * ### Usage * * ```typescript * const operation = new JsonOperation() * .summary("Create a user") * .description("Creates a new user in the system") * .addTags([{name: "Users"}]) * .addParameter(0, parameterMetadata) * .addResponse(201, responseMetadata); * ``` * * ### Key Features * * - **Parameters**: Manage operation parameters by index or name * - **Responses**: Define responses for different HTTP status codes * - **Security**: Configure authentication and authorization * - **Media Types**: Specify accepted and produced content types * - **Routing**: Multiple path/method combinations per operation * * @public */ export class JsonOperation extends JsonMap { #status; #redirection; constructor(obj = {}) { super({ parameters: [], responses: new JsonMap(), ...obj }); this.$kind = "operation"; this.operationPaths = new Map(); this.#redirection = false; } get response() { return this.getResponses().get(this.getStatus().toString()); } get status() { return this.#status; } tags(tags) { super.set("tags", tags); return this; } addTags(tags) { tags = uniqBy([...(this.get("tags") || []), ...tags], "name"); return this.tags(tags); } summary(summary) { super.set("summary", summary); return this; } operationId(operationId) { this.set("operationId", operationId); return this; } responses(responses) { this.set("responses", responses); return this; } defaultStatus(status) { this.#status = status; return this; } getStatus() { return this.#status || 200; } setRedirection(status = 302) { this.#redirection = true; this.#status = status; return this; } isRedirection(status) { if (this.#redirection) { if (status) { return isRedirectionStatus(status); } } return this.#redirection; } addResponse(statusCode, response) { if ((isSuccessStatus(statusCode) || isRedirectionStatus(statusCode)) && !this.#status) { const res = this.getResponseOf(200); this.getResponses().set(statusCode.toString(), res).delete("200"); this.defaultStatus(Number(statusCode)); } const currentCode = statusCode === "default" ? this.getStatus().toString() : statusCode.toString(); const currentResponse = this.getResponses().get(currentCode); if (!currentResponse) { response.status = Number(currentCode); this.getResponses().set(currentCode, response); } else { response.forEach((value, key) => { if (!["content"].includes(key)) { currentResponse.set(key, deepMerge(currentResponse.get(key), value)); } }); currentResponse.status = Number(currentCode); } return this; } getResponses() { return this.get("responses"); } getResponseOf(status) { return (status === "default" ? this.response : this.getResponses().get(String(status))) || new JsonResponse(); } ensureResponseOf(status) { this.addResponse(status, this.getResponseOf(status)); return this.getResponseOf(status); } getHeadersOf(status) { return this.getResponseOf(status).get("headers") || {}; } getContentTypeOf(status) { return [...this.getResponseOf(status).get("content").keys()].slice(-1)[0]; } security(security) { this.set("security", security); return this; } addSecurityScopes(name, scopes) { const security = this.get("security") || {}; security[name] = uniq([...(security[name] || []), ...scopes]); return this.security(security); } description(description) { super.set("description", description); return this; } deprecated(deprecated) { super.set("deprecated", deprecated); return this; } addAllowedGroupsParameter(allowedGroups) { const jsonParameter = new JsonParameter(); jsonParameter.in("query").name("includes"); jsonParameter.schema(JsonSchema.from({ type: "array", items: { type: "string", enum: [...allowedGroups] } })); this.addParameter(-1, jsonParameter); return this; } parameters(parameters) { super.set("parameters", parameters); return this; } addParameter(index, parameter) { if (index === -1) { index = this.get("parameters").length; } this.get("parameters")[index] = parameter; } consumes(consumes) { super.set("consumes", consumes); return this; } produces(produces) { super.set("produces", produces); return this; } addProduce(produce) { const produces = uniq([].concat(this.get("produces"), produce)).filter(Boolean); this.set("produces", produces); } addOperationPath(method, path) { const operationPath = new JsonMethodPath(method, path); this.operationPaths.set(String(method) + String(path), operationPath); return operationPath; } getAllowedOperationPath(allowedVerbs) { if (!allowedVerbs) { return [...this.operationPaths.values()]; } return [...this.operationPaths.values()].filter(({ method }) => method && allowedVerbs.includes(method.toUpperCase())); } }