diff --git a/biome.json b/biome.json index 985f158..961e9c5 100644 --- a/biome.json +++ b/biome.json @@ -10,8 +10,10 @@ "ignore": ["dist", "docs", "example"] }, "formatter": { - "enabled": true, - "indentStyle": "tab" + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 }, "organizeImports": { "enabled": true diff --git a/package.json b/package.json index 25c1d81..1a6f63a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "README.md" ], "scripts": { - "prepare": "husky install", + "prepare": "husky", "build": "rm -rf dist/ && tsup src/index.ts --format cjs,esm --dts --config tsconfig.json", "lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)", "test": "vitest run --root tests", diff --git a/src/adapters/hono.ts b/src/adapters/hono.ts index b991f8e..2f22993 100644 --- a/src/adapters/hono.ts +++ b/src/adapters/hono.ts @@ -2,47 +2,44 @@ import { OpenAPIHandler, type OpenAPIRouterType } from "../openapi"; import type { RouterOptions } from "../types"; export class HonoOpenAPIHandler extends OpenAPIHandler { - getRequest(args: any[]) { - return args[0].req.raw; - } + getRequest(args: any[]) { + return args[0].req.raw; + } - getUrlParams(args: any[]): Record { - return args[0].req.param(); - } + getUrlParams(args: any[]): Record { + return args[0].req.param(); + } } -export function fromHono( - router: M, - options?: RouterOptions, -): M & OpenAPIRouterType { - const openapiRouter = new HonoOpenAPIHandler(router, options); +export function fromHono(router: M, options?: RouterOptions): M & OpenAPIRouterType { + const openapiRouter = new HonoOpenAPIHandler(router, options); - return new Proxy(router, { - get: (target: any, prop: string, ...args: any[]) => { - const _result = openapiRouter.handleCommonProxy(target, prop, ...args); - if (_result !== undefined) { - return _result; - } + return new Proxy(router, { + get: (target: any, prop: string, ...args: any[]) => { + const _result = openapiRouter.handleCommonProxy(target, prop, ...args); + if (_result !== undefined) { + return _result; + } - return (route: string, ...handlers: any[]) => { - if (prop !== "fetch") { - if (handlers.length === 1 && handlers[0].isChanfana === true) { - handlers = openapiRouter.registerNestedRouter({ - method: prop, - path: route, - nestedRouter: handlers[0], - }); - } else if (openapiRouter.allowedMethods.includes(prop)) { - handlers = openapiRouter.registerRoute({ - method: prop, - path: route, - handlers: handlers, - }); - } - } + return (route: string, ...handlers: any[]) => { + if (prop !== "fetch") { + if (handlers.length === 1 && handlers[0].isChanfana === true) { + handlers = openapiRouter.registerNestedRouter({ + method: prop, + path: route, + nestedRouter: handlers[0], + }); + } else if (openapiRouter.allowedMethods.includes(prop)) { + handlers = openapiRouter.registerRoute({ + method: prop, + path: route, + handlers: handlers, + }); + } + } - return Reflect.get(target, prop, ...args)(route, ...handlers); - }; - }, - }); + return Reflect.get(target, prop, ...args)(route, ...handlers); + }; + }, + }); } diff --git a/src/adapters/ittyRouter.ts b/src/adapters/ittyRouter.ts index 7e77e33..71a5b09 100644 --- a/src/adapters/ittyRouter.ts +++ b/src/adapters/ittyRouter.ts @@ -2,47 +2,44 @@ import { OpenAPIHandler, type OpenAPIRouterType } from "../openapi"; import type { RouterOptions } from "../types"; export class IttyRouterOpenAPIHandler extends OpenAPIHandler { - getRequest(args: any[]) { - return args[0]; - } + getRequest(args: any[]) { + return args[0]; + } - getUrlParams(args: any[]): Record { - return args[0].params; - } + getUrlParams(args: any[]): Record { + return args[0].params; + } } -export function fromIttyRouter( - router: M, - options?: RouterOptions, -): M & OpenAPIRouterType { - const openapiRouter = new IttyRouterOpenAPIHandler(router, options); +export function fromIttyRouter(router: M, options?: RouterOptions): M & OpenAPIRouterType { + const openapiRouter = new IttyRouterOpenAPIHandler(router, options); - return new Proxy(router, { - get: (target: any, prop: string, ...args: any[]) => { - const _result = openapiRouter.handleCommonProxy(target, prop, ...args); - if (_result !== undefined) { - return _result; - } + return new Proxy(router, { + get: (target: any, prop: string, ...args: any[]) => { + const _result = openapiRouter.handleCommonProxy(target, prop, ...args); + if (_result !== undefined) { + return _result; + } - return (route: string, ...handlers: any[]) => { - if (prop !== "fetch") { - if (handlers.length === 1 && handlers[0].isChanfana === true) { - handlers = openapiRouter.registerNestedRouter({ - method: prop, - path: route, - nestedRouter: handlers[0], - }); - } else if (openapiRouter.allowedMethods.includes(prop)) { - handlers = openapiRouter.registerRoute({ - method: prop, - path: route, - handlers: handlers, - }); - } - } + return (route: string, ...handlers: any[]) => { + if (prop !== "fetch") { + if (handlers.length === 1 && handlers[0].isChanfana === true) { + handlers = openapiRouter.registerNestedRouter({ + method: prop, + path: route, + nestedRouter: handlers[0], + }); + } else if (openapiRouter.allowedMethods.includes(prop)) { + handlers = openapiRouter.registerRoute({ + method: prop, + path: route, + handlers: handlers, + }); + } + } - return Reflect.get(target, prop, ...args)(route, ...handlers); - }; - }, - }); + return Reflect.get(target, prop, ...args)(route, ...handlers); + }; + }, + }); } diff --git a/src/contentTypes.ts b/src/contentTypes.ts index 3d3969a..8616363 100644 --- a/src/contentTypes.ts +++ b/src/contentTypes.ts @@ -2,19 +2,19 @@ import { z } from "zod"; import { legacyTypeIntoZod } from "./zod/utils"; type JsonContent = { - content: { - "application/json": { - schema: z.ZodType; - }; - }; + content: { + "application/json": { + schema: z.ZodType; + }; + }; }; type InferSchemaType = T extends z.ZodType ? z.infer : T; export const contentJson = (schema: T): JsonContent> => ({ - content: { - "application/json": { - schema: schema instanceof z.ZodType ? schema : legacyTypeIntoZod(schema), - }, - }, + content: { + "application/json": { + schema: schema instanceof z.ZodType ? schema : legacyTypeIntoZod(schema), + }, + }, }); diff --git a/src/endpoints/create.ts b/src/endpoints/create.ts index 8adedff..1834006 100644 --- a/src/endpoints/create.ts +++ b/src/endpoints/create.ts @@ -1,94 +1,104 @@ +import type { AnyZodObject } from "zod"; import { contentJson } from "../contentTypes"; import { InputValidationException } from "../exceptions"; import { OpenAPIRoute } from "../route"; -import type { Meta, O } from "./types"; - -export class CreateEndpoint< - HandleArgs extends Array = Array, -> extends OpenAPIRoute { - get meta(): Meta { - throw new Error("get Meta not implemented"); - } - - defaultValues?: Record any>; // TODO: move this into model - - getSchema() { - const bodyParameters = this.meta.fields.omit( - (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), - ); - const pathParameters = this.meta.fields.pick( - (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), - ); - - return { - request: { - body: contentJson(bodyParameters), - params: pathParameters, - ...this.schema?.request, - }, - responses: { - "200": { - description: "Returns the created Object", - ...contentJson({ - success: Boolean, - result: this.meta.model.serializerObject, - }), - ...this.schema?.responses?.[200], - }, - ...InputValidationException.schema(), - ...this.schema?.responses, - }, - ...this.schema, - }; - } - - async getObject(): Promise> { - const data = await this.getValidatedData(); - - // @ts-ignore TODO: check this - const newData: any = { - ...(data.body as object), - }; - - for (const param of this.params.urlParams) { - newData[param] = (data.params as any)[param]; - } - - if (this.defaultValues) { - for (const [key, value] of Object.entries(this.defaultValues)) { - if (newData[key] === undefined) { - newData[key] = value(); - } - } - } - - return newData; - } - - async before(data: O): Promise> { - return data; - } - - async after(data: O): Promise> { - return data; - } - - async create(data: O): Promise> { - return data; - } - - async handle(...args: HandleArgs) { - let obj = await this.getObject(); - - obj = await this.before(obj); - - obj = await this.create(obj); - - obj = await this.after(obj); - - return { - success: true, - result: this.meta.model.serializer(obj as object), - }; - } +import { MetaGenerator, type Model, type O } from "./types"; + +export class CreateEndpoint = Array> extends OpenAPIRoute { + get fields(): AnyZodObject | undefined { + return undefined; + } + + get model(): Model { + throw new Error("get model not implemented"); + } + + get meta() { + return MetaGenerator({ + model: this.model, + fields: this.fields, + }); + } + + defaultValues?: Record any>; // TODO: move this into model + + getSchema() { + const bodyParameters = this.meta.fields.omit( + (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + const pathParameters = this.meta.fields.pick( + (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + + return { + request: { + body: contentJson(bodyParameters), + params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, + ...this.schema?.request, + }, + responses: { + "200": { + description: "Returns the created Object", + ...contentJson({ + success: Boolean, + result: this.meta.model.serializerObject, + }), + ...this.schema?.responses?.[200], + }, + ...InputValidationException.schema(), + ...this.schema?.responses, + }, + ...this.schema, + }; + } + + async getObject(): Promise> { + const data = await this.getValidatedData(); + + // @ts-ignore TODO: check this + const newData: any = { + ...(data.body as object), + }; + + for (const param of this.params.urlParams) { + newData[param] = (data.params as any)[param]; + } + + if (this.defaultValues) { + for (const [key, value] of Object.entries(this.defaultValues)) { + if (newData[key] === undefined) { + newData[key] = value(); + } + } + } + + return newData; + } + + async before(data: O): Promise> { + return data; + } + + async after(data: O): Promise> { + return data; + } + + async create(data: O): Promise> { + return data; + } + + async handle(...args: HandleArgs) { + let obj = await this.getObject(); + + obj = await this.before(obj); + + obj = await this.create(obj); + + obj = await this.after(obj); + + return { + success: true, + result: this.meta.model.serializer(obj as object), + }; + } } diff --git a/src/endpoints/delete.ts b/src/endpoints/delete.ts index 99c1b1f..33e6ac4 100644 --- a/src/endpoints/delete.ts +++ b/src/endpoints/delete.ts @@ -1,133 +1,123 @@ +import type { AnyZodObject } from "zod"; import { contentJson } from "../contentTypes"; import { NotFoundException } from "../exceptions"; import { OpenAPIRoute } from "../route"; -import type { FilterCondition, Filters, Meta, O } from "./types"; - -export class DeleteEndpoint< - HandleArgs extends Array = Array, -> extends OpenAPIRoute { - get meta(): Meta { - throw new Error("get Meta not implemented"); - } - - getSchema() { - const bodyParameters = this.meta.fields - .pick( - (this.meta.model.primaryKeys || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ) - .omit( - (this.params.urlParams || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ); - const pathParameters = this.meta.fields - .pick( - (this.meta.model.primaryKeys || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ) - .pick( - (this.params.urlParams || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ); - - return { - request: { - body: Object.keys(bodyParameters.shape).length - ? contentJson(bodyParameters) - : undefined, - params: pathParameters, - ...this.schema?.request, - }, - responses: { - "200": { - description: "Returns the Object if it was successfully deleted", - ...contentJson({ - success: Boolean, - result: this.meta.model.serializerObject, - }), - ...this.schema?.responses?.[200], - }, - ...NotFoundException.schema(), - ...this.schema?.responses, - }, - ...this.schema, - }; - } - - async getFilters(): Promise { - const data = await this.getValidatedData(); - - const filters: Array = []; - - for (const part of [data.params, data.body]) { - if (part) { - for (const [key, value] of Object.entries(part)) { - filters.push({ - field: key, - operator: "EQ", - value: value as string, - }); - } - } - } - - return { - filters, - }; - } - - async before( - oldObj: O, - filters: Filters, - ): Promise { - return filters; - } - - async after(data: O): Promise> { - return data; - } - - async delete( - oldObj: O, - filters: Filters, - ): Promise | null> { - return null; - } - - async getObject(filters: Filters): Promise | null> { - return null; - } - - async handle(...args: HandleArgs) { - let filters = await this.getFilters(); - - const oldObj = await this.getObject(filters); - - if (oldObj === null) { - throw new NotFoundException(); - } - - filters = await this.before(oldObj, filters); - - let obj = await this.delete(oldObj, filters); - - if (obj === null) { - throw new NotFoundException(); - } - - obj = await this.after(obj); - - return { - success: true, - result: this.meta.model.serializer(obj), - }; - } +import { + type FilterCondition, + type Filters, + type Meta, + MetaGenerator, + type MetaInput, + type Model, + type O, +} from "./types"; + +export class DeleteEndpoint = Array> extends OpenAPIRoute { + get fields(): AnyZodObject | undefined { + return undefined; + } + + get model(): Model { + throw new Error("get model not implemented"); + } + + get meta() { + return MetaGenerator({ + model: this.model, + fields: this.fields, + }); + } + + getSchema() { + const bodyParameters = this.meta.fields + .pick((this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {})) + .omit((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})); + const pathParameters = this.meta.fields + .pick((this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {})) + .pick((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})); + + return { + request: { + body: Object.keys(bodyParameters.shape).length ? contentJson(bodyParameters) : undefined, + params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, + ...this.schema?.request, + }, + responses: { + "200": { + description: "Returns the Object if it was successfully deleted", + ...contentJson({ + success: Boolean, + result: this.meta.model.serializerObject, + }), + ...this.schema?.responses?.[200], + }, + ...NotFoundException.schema(), + ...this.schema?.responses, + }, + ...this.schema, + }; + } + + async getFilters(): Promise { + const data = await this.getValidatedData(); + + const filters: Array = []; + + for (const part of [data.params, data.body]) { + if (part) { + for (const [key, value] of Object.entries(part)) { + filters.push({ + field: key, + operator: "EQ", + value: value as string, + }); + } + } + } + + return { + filters, + }; + } + + async before(oldObj: O, filters: Filters): Promise { + return filters; + } + + async after(data: O): Promise> { + return data; + } + + async delete(oldObj: O, filters: Filters): Promise | null> { + return null; + } + + async getObject(filters: Filters): Promise | null> { + return null; + } + + async handle(...args: HandleArgs) { + let filters = await this.getFilters(); + + const oldObj = await this.getObject(filters); + + if (oldObj === null) { + throw new NotFoundException(); + } + + filters = await this.before(oldObj, filters); + + let obj = await this.delete(oldObj, filters); + + if (obj === null) { + throw new NotFoundException(); + } + + obj = await this.after(obj); + + return { + success: true, + result: this.meta.model.serializer(obj), + }; + } } diff --git a/src/endpoints/fetch.ts b/src/endpoints/fetch.ts index 52e04ab..78d8307 100644 --- a/src/endpoints/fetch.ts +++ b/src/endpoints/fetch.ts @@ -1,106 +1,118 @@ +import type { AnyZodObject } from "zod"; import { contentJson } from "../contentTypes"; import { NotFoundException } from "../exceptions"; import { OpenAPIRoute } from "../route"; -import type { FilterCondition, ListFilters, Meta, O } from "./types"; - -export class FetchEndpoint< - HandleArgs extends Array = Array, -> extends OpenAPIRoute { - get meta(): Meta { - throw new Error("get Meta not implemented"); - } - - getSchema() { - if ( - this.meta.model.primaryKeys.sort().toString() !== - this.params.urlParams.sort().toString() - ) { - throw Error( - `Model primaryKeys differ from urlParameters on: ${this.params.route}: ${JSON.stringify(this.meta.model.primaryKeys)} !== ${JSON.stringify(this.params.urlParams)}`, - ); - } - - //const queryParameters = this.model.omit((this.primaryKey || []).reduce((a, v) => ({ ...a, [v]: true }), {})); - const pathParameters = this.meta.fields.pick( - (this.meta.model.primaryKeys || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ); - - return { - request: { - //query: queryParameters, - params: pathParameters, - ...this.schema?.request, - }, - responses: { - "200": { - description: "Returns a single object if found", - ...contentJson({ - success: Boolean, - result: this.meta.model.serializerObject, - }), - ...this.schema?.responses?.[200], - }, - ...NotFoundException.schema(), - ...this.schema?.responses, - }, - ...this.schema, - }; - } - - async getFilters(): Promise { - const data = await this.getValidatedData(); - - const filters: Array = []; - - for (const part of [data.params, data.query]) { - if (part) { - for (const [key, value] of Object.entries(part)) { - filters.push({ - field: key, - operator: "EQ", - value: value as string, - }); - } - } - } - - return { - filters: filters, - options: {}, // TODO: make a new type for this - }; - } - - async before(filters: ListFilters): Promise { - return filters; - } - - async after(data: O): Promise> { - return data; - } - - async fetch(filters: ListFilters): Promise | null> { - return null; - } - - async handle(...args: HandleArgs) { - let filters = await this.getFilters(); - - filters = await this.before(filters); - - let obj = await this.fetch(filters); - - if (!obj) { - throw new NotFoundException(); - } - - obj = await this.after(obj); - - return { - success: true, - result: this.meta.model.serializer(obj), - }; - } +import { + type FilterCondition, + type ListFilters, + type Meta, + MetaGenerator, + type MetaInput, + type Model, + type O, +} from "./types"; + +export class FetchEndpoint = Array> extends OpenAPIRoute { + get fields(): AnyZodObject | undefined { + return undefined; + } + + get model(): Model { + throw new Error("get model not implemented"); + } + + get meta() { + return MetaGenerator({ + model: this.model, + fields: this.fields, + }); + } + + getSchema() { + if (this.meta.model.primaryKeys.sort().toString() !== this.params.urlParams.sort().toString()) { + throw Error( + `Model primaryKeys differ from urlParameters on: ${this.params.route}: ${JSON.stringify(this.meta.model.primaryKeys)} !== ${JSON.stringify(this.params.urlParams)}`, + ); + } + + //const queryParameters = this.model.omit((this.primaryKey || []).reduce((a, v) => ({ ...a, [v]: true }), {})); + const pathParameters = this.meta.fields.pick( + (this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + + return { + request: { + //query: queryParameters, + params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, + ...this.schema?.request, + }, + responses: { + "200": { + description: "Returns a single object if found", + ...contentJson({ + success: Boolean, + result: this.meta.model.serializerObject, + }), + ...this.schema?.responses?.[200], + }, + ...NotFoundException.schema(), + ...this.schema?.responses, + }, + ...this.schema, + }; + } + + async getFilters(): Promise { + const data = await this.getValidatedData(); + + const filters: Array = []; + + for (const part of [data.params, data.query]) { + if (part) { + for (const [key, value] of Object.entries(part)) { + filters.push({ + field: key, + operator: "EQ", + value: value as string, + }); + } + } + } + + return { + filters: filters, + options: {}, // TODO: make a new type for this + }; + } + + async before(filters: ListFilters): Promise { + return filters; + } + + async after(data: O): Promise> { + return data; + } + + async fetch(filters: ListFilters): Promise | null> { + return null; + } + + async handle(...args: HandleArgs) { + let filters = await this.getFilters(); + + filters = await this.before(filters); + + let obj = await this.fetch(filters); + + if (!obj) { + throw new NotFoundException(); + } + + obj = await this.after(obj); + + return { + success: true, + result: this.meta.model.serializer(obj), + }; + } } diff --git a/src/endpoints/list.ts b/src/endpoints/list.ts index e160d83..3ca437f 100644 --- a/src/endpoints/list.ts +++ b/src/endpoints/list.ts @@ -2,164 +2,164 @@ import { type AnyZodObject, z } from "zod"; import { contentJson } from "../contentTypes"; import { Enumeration, Str } from "../parameters"; import { OpenAPIRoute } from "../route"; -import type { - FilterCondition, - ListFilters, - ListResult, - Meta, - O, +import { + type FilterCondition, + type ListFilters, + type ListResult, + type Meta, + MetaGenerator, + type MetaInput, + type Model, + type O, } from "./types"; -export class ListEndpoint< - HandleArgs extends Array = Array, -> extends OpenAPIRoute { - get meta(): Meta { - throw new Error("get Meta not implemented"); - } - - filterFields?: Array; - searchFields?: Array; - searchFieldName = "search"; - optionFields = ["page", "per_page", "order_by", "order_by_direction"]; - - getSchema() { - const parsedQueryParameters = this.meta.fields - .pick( - (this.filterFields || []).reduce((a, v) => ({ ...a, [v]: true }), {}), - ) - .omit( - (this.params.urlParams || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ).shape; - const pathParameters = this.meta.fields.pick( - (this.params.urlParams || this.meta.model.primaryKeys || []).reduce( - (a, v) => ({ ...a, [v]: true }), - {}, - ), - ); - - for (const [key, value] of Object.entries(parsedQueryParameters)) { - // @ts-ignore TODO: check this - parsedQueryParameters[key] = (value as AnyZodObject).optional(); - } - - if (this.searchFields) { - // @ts-ignore TODO: check this - parsedQueryParameters[this.searchFieldName] = z - .string() - .optional() - .openapi({ - description: `Search by ${this.searchFields.join(", ")}`, - }); - } - - const queryParameters = z - .object({ - page: z.number().int().min(1).optional().default(1), - per_page: z.number().int().min(1).max(100).optional().default(20), - order_by: Str({ - description: "Order By Column Name", - required: false, - }), - order_by_direction: Enumeration({ - default: "asc", - values: ["asc", "desc"], - description: "Order By Direction", - required: false, - }), - }) - .extend(parsedQueryParameters); - - return { - request: { - params: pathParameters, - query: queryParameters, - ...this.schema?.request, - }, - responses: { - "200": { - description: "List objects", - ...contentJson({ - success: Boolean, - result: [this.meta.model.serializerObject], - }), - ...this.schema?.responses?.[200], - }, - ...this.schema?.responses, - }, - ...this.schema, - }; - } - - async getFilters(): Promise { - const data = await this.getValidatedData(); - - const filters: Array = []; - const options: Record = {}; // TODO: fix this type - - for (const part of [data.params, data.query]) { - if (part) { - for (const [key, value] of Object.entries(part)) { - if (this.searchFields && key === this.searchFieldName) { - filters.push({ - field: key, - operator: "LIKE", - value: value as string, - }); - } else if (this.optionFields.includes(key)) { - options[key] = value as string; - } else { - filters.push({ - field: key, - operator: "EQ", - value: value as string, - }); - } - } - } - } - - return { - options, - filters, - }; - } - - async before(filters: ListFilters): Promise { - return filters; - } - - async after( - data: ListResult>, - ): Promise>> { - return data; - } - - async list(filters: ListFilters): Promise>> { - return { - result: [], - }; - } - - async handle(...args: HandleArgs) { - let filters = await this.getFilters(); - - filters = await this.before(filters); - - let objs = await this.list(filters); - - objs = await this.after(objs); - - objs = { - ...objs, - result: objs.result.map(this.meta.model.serializer), - }; - - return { - success: true, - ...objs, - }; - } +export class ListEndpoint = Array> extends OpenAPIRoute { + get fields(): AnyZodObject | undefined { + return undefined; + } + + get model(): Model { + throw new Error("get model not implemented"); + } + + get meta() { + return MetaGenerator({ + model: this.model, + fields: this.fields, + }); + } + + filterFields?: Array; + searchFields?: Array; + searchFieldName = "search"; + optionFields = ["page", "per_page", "order_by", "order_by_direction"]; + + getSchema() { + const parsedQueryParameters = this.meta.fields + .pick((this.filterFields || []).reduce((a, v) => ({ ...a, [v]: true }), {})) + .omit((this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {})).shape; + const pathParameters = this.meta.fields.pick( + (this.params.urlParams || this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + + for (const [key, value] of Object.entries(parsedQueryParameters)) { + // @ts-ignore TODO: check this + parsedQueryParameters[key] = (value as AnyZodObject).optional(); + } + + if (this.searchFields) { + // @ts-ignore TODO: check this + parsedQueryParameters[this.searchFieldName] = z + .string() + .optional() + .openapi({ + description: `Search by ${this.searchFields.join(", ")}`, + }); + } + + const queryParameters = z + .object({ + page: z.number().int().min(1).optional().default(1), + per_page: z.number().int().min(1).max(100).optional().default(20), + order_by: Str({ + description: "Order By Column Name", + required: false, + }), + order_by_direction: Enumeration({ + default: "asc", + values: ["asc", "desc"], + description: "Order By Direction", + required: false, + }), + }) + .extend(parsedQueryParameters); + + return { + request: { + params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, + query: queryParameters, + ...this.schema?.request, + }, + responses: { + "200": { + description: "List objects", + ...contentJson({ + success: Boolean, + result: [this.meta.model.serializerObject], + }), + ...this.schema?.responses?.[200], + }, + ...this.schema?.responses, + }, + ...this.schema, + }; + } + + async getFilters(): Promise { + const data = await this.getValidatedData(); + + const filters: Array = []; + const options: Record = {}; // TODO: fix this type + + for (const part of [data.params, data.query]) { + if (part) { + for (const [key, value] of Object.entries(part)) { + if (this.searchFields && key === this.searchFieldName) { + filters.push({ + field: key, + operator: "LIKE", + value: value as string, + }); + } else if (this.optionFields.includes(key)) { + options[key] = value as string; + } else { + filters.push({ + field: key, + operator: "EQ", + value: value as string, + }); + } + } + } + } + + return { + options, + filters, + }; + } + + async before(filters: ListFilters): Promise { + return filters; + } + + async after(data: ListResult>): Promise>> { + return data; + } + + async list(filters: ListFilters): Promise>> { + return { + result: [], + }; + } + + async handle(...args: HandleArgs) { + let filters = await this.getFilters(); + + filters = await this.before(filters); + + let objs = await this.list(filters); + + objs = await this.after(objs); + + objs = { + ...objs, + result: objs.result.map(this.meta.model.serializer), + }; + + return { + success: true, + ...objs, + }; + } } diff --git a/src/endpoints/orms/workers-qb/create.ts b/src/endpoints/orms/workers-qb/create.ts index 464c94a..6350aff 100644 --- a/src/endpoints/orms/workers-qb/create.ts +++ b/src/endpoints/orms/workers-qb/create.ts @@ -2,45 +2,37 @@ import { ApiException, InputValidationException } from "../../../exceptions"; import { CreateEndpoint } from "../../create"; import type { O } from "../../types"; -export class QBCreateEndpoint< - HandleArgs extends Array = Array, -> extends CreateEndpoint { - qb: any; // D1QB - logger?: any; +export class QBCreateEndpoint = Array> extends CreateEndpoint { + qb: any; // D1QB + logger?: any; - async create(data: O): Promise> { - let inserted; - try { - inserted = await this.qb - .insert({ - tableName: this.meta.model.tableName, - data: data as any, - returning: "*", - }) - .execute(); - } catch (e: any) { - if (this.logger) - this.logger.error( - `Caught exception while trying to create ${this.meta.model.tableName}: ${e.message}`, - ); - if (e.message.includes("UNIQUE constraint failed")) { - if ( - e.message.includes(this.meta.model.tableName) && - e.message.includes(this.meta.model.primaryKeys[0]) - ) { - throw new InputValidationException( - `An object with this ${this.meta.model.primaryKeys[0]} already exists`, - ["body", this.meta.model.primaryKeys[0]], - ); - } - } + async create(data: O): Promise> { + let inserted; + try { + inserted = await this.qb + .insert({ + tableName: this.meta.model.tableName, + data: data as any, + returning: "*", + }) + .execute(); + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to create ${this.meta.model.tableName}: ${e.message}`); + if (e.message.includes("UNIQUE constraint failed")) { + if (e.message.includes(this.meta.model.tableName) && e.message.includes(this.meta.model.primaryKeys[0])) { + throw new InputValidationException(`An object with this ${this.meta.model.primaryKeys[0]} already exists`, [ + "body", + this.meta.model.primaryKeys[0], + ]); + } + } - throw new ApiException(e.message); - } + throw new ApiException(e.message); + } - if (this.logger) - this.logger.log(`Successfully created ${this.meta.model.tableName}`); + if (this.logger) this.logger.log(`Successfully created ${this.meta.model.tableName}`); - return inserted.results; - } + return inserted.results; + } } diff --git a/src/endpoints/orms/workers-qb/delete.ts b/src/endpoints/orms/workers-qb/delete.ts index 2fdf040..aedab2b 100644 --- a/src/endpoints/orms/workers-qb/delete.ts +++ b/src/endpoints/orms/workers-qb/delete.ts @@ -2,81 +2,73 @@ import { ApiException } from "../../../exceptions"; import { DeleteEndpoint } from "../../delete"; import type { Filters, O } from "../../types"; -export class QBDeleteEndpoint< - HandleArgs extends Array = Array, -> extends DeleteEndpoint { - qb: any; // D1QB - logger?: any; +export class QBDeleteEndpoint = Array> extends DeleteEndpoint { + qb: any; // D1QB + logger?: any; - getSafeFilters(filters: Filters) { - const conditions: string[] = []; - const conditionsParams: string[] = []; + getSafeFilters(filters: Filters) { + const conditions: string[] = []; + const conditionsParams: string[] = []; - for (const f of filters.filters) { - if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } + for (const f of filters.filters) { + if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } - return { conditions, conditionsParams }; - } + return { conditions, conditionsParams }; + } - async getObject(filters: Filters): Promise | null> { - const safeFilters = this.getSafeFilters(filters); - const oldObj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - }) - .execute(); + async getObject(filters: Filters): Promise | null> { + const safeFilters = this.getSafeFilters(filters); + const oldObj = await this.qb + .fetchOne({ + tableName: this.meta.model.tableName, + fields: "*", + where: { + conditions: safeFilters.conditions, + params: safeFilters.conditionsParams, + }, + }) + .execute(); - if (!oldObj.results) { - return null; - } + if (!oldObj.results) { + return null; + } - return oldObj.results; - } + return oldObj.results; + } - async delete( - oldObj: O, - filters: Filters, - ): Promise | null> { - const safeFilters = this.getSafeFilters(filters); + async delete(oldObj: O, filters: Filters): Promise | null> { + const safeFilters = this.getSafeFilters(filters); - let result; - try { - result = await this.qb - .delete({ - tableName: this.meta.model.tableName, - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - returning: "*", - }) - .execute(); - } catch (e: any) { - if (this.logger) - this.logger.error( - `Caught exception while trying to delete ${this.meta.model.tableName}: ${e.message}`, - ); - throw new ApiException(e.message); - } + let result; + try { + result = await this.qb + .delete({ + tableName: this.meta.model.tableName, + where: { + conditions: safeFilters.conditions, + params: safeFilters.conditionsParams, + }, + returning: "*", + }) + .execute(); + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to delete ${this.meta.model.tableName}: ${e.message}`); + throw new ApiException(e.message); + } - if (result.changes === 0) { - return null; - } + if (result.changes === 0) { + return null; + } - if (this.logger) - this.logger.log(`Successfully deleted ${this.meta.model.tableName}`); + if (this.logger) this.logger.log(`Successfully deleted ${this.meta.model.tableName}`); - return oldObj; - } + return oldObj; + } } diff --git a/src/endpoints/orms/workers-qb/fetch.ts b/src/endpoints/orms/workers-qb/fetch.ts index 1ca5bc9..7eaffc4 100644 --- a/src/endpoints/orms/workers-qb/fetch.ts +++ b/src/endpoints/orms/workers-qb/fetch.ts @@ -1,28 +1,26 @@ import { FetchEndpoint } from "../../fetch"; import type { ListFilters, O } from "../../types"; -export class QBFetchEndpoint< - HandleArgs extends Array = Array, -> extends FetchEndpoint { - qb: any; // D1QB - logger?: any; +export class QBFetchEndpoint = Array> extends FetchEndpoint { + qb: any; // D1QB + logger?: any; - async fetch(filters: ListFilters): Promise | null> { - const obj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: filters.filters.map((obj) => `${obj.field} = ?`), // TODO: implement operator - params: filters.filters.map((obj) => obj.value), - }, - }) - .execute(); + async fetch(filters: ListFilters): Promise | null> { + const obj = await this.qb + .fetchOne({ + tableName: this.meta.model.tableName, + fields: "*", + where: { + conditions: filters.filters.map((obj) => `${obj.field} = ?`), // TODO: implement operator + params: filters.filters.map((obj) => obj.value), + }, + }) + .execute(); - if (!obj.results) { - return null; - } + if (!obj.results) { + return null; + } - return obj.results; - } + return obj.results; + } } diff --git a/src/endpoints/orms/workers-qb/list.ts b/src/endpoints/orms/workers-qb/list.ts index f2fd626..9a6bf63 100644 --- a/src/endpoints/orms/workers-qb/list.ts +++ b/src/endpoints/orms/workers-qb/list.ts @@ -2,82 +2,75 @@ import { ApiException } from "../../../exceptions"; import { ListEndpoint } from "../../list"; import type { ListFilters, ListResult, O } from "../../types"; -export class QBListEndpoint< - HandleArgs extends Array = Array, -> extends ListEndpoint { - qb: any; // D1QB - logger?: any; +export class QBListEndpoint = Array> extends ListEndpoint { + qb: any; // D1QB + logger?: any; - async list( - filters: ListFilters, - ): Promise> & { result_info: object }> { - const offset = - (filters.options.per_page || 20) * (filters.options.page || 0) - - (filters.options.per_page || 20); - const limit = filters.options.per_page; + async list(filters: ListFilters): Promise> & { result_info: object }> { + const offset = (filters.options.per_page || 20) * (filters.options.page || 0) - (filters.options.per_page || 20); + const limit = filters.options.per_page; - const conditions: string[] = []; - const conditionsParams: string[] = []; + const conditions: string[] = []; + const conditionsParams: string[] = []; - for (const f of filters.filters) { - if (this.searchFields && f.field === this.searchFieldName) { - const searchCondition = this.searchFields - .map((obj) => { - return `UPPER(${obj}) like UPPER(?${conditionsParams.length + 1})`; - }) - .join(" or "); - conditions.push(`(${searchCondition})`); - conditionsParams.push(`%${f.value}%`); - } else if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } + for (const f of filters.filters) { + if (this.searchFields && f.field === this.searchFieldName) { + const searchCondition = this.searchFields + .map((obj) => { + return `UPPER(${obj}) like UPPER(?${conditionsParams.length + 1})`; + }) + .join(" or "); + conditions.push(`(${searchCondition})`); + conditionsParams.push(`%${f.value}%`); + } else if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } - let where = null; - if (conditions.length > 0) { - where = { - conditions: conditions, - params: conditionsParams, - }; - } + let where = null; + if (conditions.length > 0) { + where = { + conditions: conditions, + params: conditionsParams, + }; + } - const results = await this.qb - .fetchAll({ - tableName: this.meta.model.tableName, - fields: "*", - where: where, - limit: limit, - offset: offset, - orderBy: filters.options.order_by - ? { - [filters.options.order_by]: - filters.options.order_by_direction || "ASC", - } - : { - [this.meta.model.primaryKeys[0] as string]: "ASC", - }, - }) - .execute(); + const results = await this.qb + .fetchAll({ + tableName: this.meta.model.tableName, + fields: "*", + where: where, + limit: limit, + offset: offset, + orderBy: filters.options.order_by + ? { + [filters.options.order_by]: filters.options.order_by_direction || "ASC", + } + : { + [this.meta.model.primaryKeys[0] as string]: "ASC", + }, + }) + .execute(); - return { - result: results.results, - result_info: { - count: results.results.length, - page: filters.options.page, - per_page: filters.options.per_page, - total_count: ( - await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "count(*) as total", - where: where, - }) - .execute() - ).results.total, - }, - }; - } + return { + result: results.results, + result_info: { + count: results.results.length, + page: filters.options.page, + per_page: filters.options.per_page, + total_count: ( + await this.qb + .fetchOne({ + tableName: this.meta.model.tableName, + fields: "count(*) as total", + where: where, + }) + .execute() + ).results.total, + }, + }; + } } diff --git a/src/endpoints/orms/workers-qb/update.ts b/src/endpoints/orms/workers-qb/update.ts index e490b18..0ccff4e 100644 --- a/src/endpoints/orms/workers-qb/update.ts +++ b/src/endpoints/orms/workers-qb/update.ts @@ -2,85 +2,77 @@ import { ApiException } from "../../../exceptions"; import type { O, UpdateFilters } from "../../types"; import { UpdateEndpoint } from "../../update"; -export class QBUpdateEndpoint< - HandleArgs extends Array = Array, -> extends UpdateEndpoint { - qb: any; // D1QB - logger?: any; +export class QBUpdateEndpoint = Array> extends UpdateEndpoint { + qb: any; // D1QB + logger?: any; - getSafeFilters(filters: UpdateFilters) { - // Filters should only apply to primary keys - const safeFilters = filters.filters.filter((f) => { - return this.meta.model.primaryKeys.includes(f.field); - }); + getSafeFilters(filters: UpdateFilters) { + // Filters should only apply to primary keys + const safeFilters = filters.filters.filter((f) => { + return this.meta.model.primaryKeys.includes(f.field); + }); - const conditions: string[] = []; - const conditionsParams: string[] = []; + const conditions: string[] = []; + const conditionsParams: string[] = []; - for (const f of safeFilters) { - if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } + for (const f of safeFilters) { + if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } - return { conditions, conditionsParams }; - } + return { conditions, conditionsParams }; + } - async getObject(filters: UpdateFilters): Promise { - const safeFilters = this.getSafeFilters(filters); - const oldObj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - }) - .execute(); + async getObject(filters: UpdateFilters): Promise { + const safeFilters = this.getSafeFilters(filters); + const oldObj = await this.qb + .fetchOne({ + tableName: this.meta.model.tableName, + fields: "*", + where: { + conditions: safeFilters.conditions, + params: safeFilters.conditionsParams, + }, + }) + .execute(); - if (!oldObj.results) { - return null; - } + if (!oldObj.results) { + return null; + } - return oldObj.results; - } + return oldObj.results; + } - async update( - oldObj: O, - filters: UpdateFilters, - ): Promise> { - const safeFilters = this.getSafeFilters(filters); + async update(oldObj: O, filters: UpdateFilters): Promise> { + const safeFilters = this.getSafeFilters(filters); - let result; - try { - result = ( - await this.qb - .update({ - tableName: this.meta.model.tableName, - data: filters.updatedData, - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - returning: "*", - }) - .execute() - ).results[0]; - } catch (e: any) { - if (this.logger) - this.logger.error( - `Caught exception while trying to update ${this.meta.model.tableName}: ${e.message}`, - ); - throw new ApiException(e.message); - } + let result; + try { + result = ( + await this.qb + .update({ + tableName: this.meta.model.tableName, + data: filters.updatedData, + where: { + conditions: safeFilters.conditions, + params: safeFilters.conditionsParams, + }, + returning: "*", + }) + .execute() + ).results[0]; + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to update ${this.meta.model.tableName}: ${e.message}`); + throw new ApiException(e.message); + } - if (this.logger) - this.logger.log(`Successfully updated ${this.meta.model.tableName}`); + if (this.logger) this.logger.log(`Successfully updated ${this.meta.model.tableName}`); - return result; - } + return result; + } } diff --git a/src/endpoints/types.ts b/src/endpoints/types.ts index 1334f44..9cf0ec7 100644 --- a/src/endpoints/types.ts +++ b/src/endpoints/types.ts @@ -1,60 +1,64 @@ import type { AnyZodObject, z } from "zod"; -import type { SetOptional } from "../types"; +import type { SetRequired } from "../types"; export type FilterCondition = { - field: string; - operator: string; - value: string | number | boolean | null; + field: string; + operator: string; + value: string | number | boolean | null; }; export type ListFilters = { - filters: Array; - options: { - page?: number; - per_page?: number; - order_by?: string; - order_by_direction?: "asc" | "desc"; - }; + filters: Array; + options: { + page?: number; + per_page?: number; + order_by?: string; + order_by_direction?: "asc" | "desc"; + }; }; export type Filters = { - filters: Array; + filters: Array; }; export type UpdateFilters = { - filters: Array; - updatedData: Record; + filters: Array; + updatedData: Record; }; export type Model = { - tableName: string; - object: AnyZodObject; - primaryKeys: Array; - serializer: (obj: object) => object; - serializerObject: AnyZodObject; + tableName: string; + schema: AnyZodObject; + primaryKeys: Array; + serializer?: (obj: object) => object; + serializerObject?: AnyZodObject; +}; + +export type ModelComplete = SetRequired; + +export type MetaInput = { + model: Model; + fields?: AnyZodObject; }; export type Meta = { - model: Model; - fields: AnyZodObject; + model: ModelComplete; + fields: AnyZodObject; }; -export type O = z.infer; +export type O = z.infer; export type ListResult = { - result: Array; -}; - -export function M(params: { - model: SetOptional; - fields?: AnyZodObject; -}): Meta { - return { - fields: params.fields ?? params.model.object, - model: { - serializer: (obj) => obj, - serializerObject: params.model.object, - ...params.model, - }, - }; + result: Array; +}; + +export function MetaGenerator(meta: MetaInput) { + return { + fields: meta.fields ?? meta.model.schema, + model: { + serializer: (obj: any): any => obj, + serializerObject: meta.model.schema, + ...meta.model, + }, + }; } diff --git a/src/endpoints/update.ts b/src/endpoints/update.ts index 5b924c3..ec8fd0c 100644 --- a/src/endpoints/update.ts +++ b/src/endpoints/update.ts @@ -1,114 +1,126 @@ +import type { AnyZodObject } from "zod"; import { contentJson } from "../contentTypes"; import { InputValidationException, NotFoundException } from "../exceptions"; import { OpenAPIRoute } from "../route"; -import type { FilterCondition, Meta, O, UpdateFilters } from "./types"; - -export class UpdateEndpoint< - HandleArgs extends Array = Array, -> extends OpenAPIRoute { - get meta(): Meta { - throw new Error("get Meta not implemented"); - } - - getSchema() { - const bodyParameters = this.meta.fields.omit( - (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), - ); - const pathParameters = this.meta.model.object.pick( - (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), - ); - - return { - request: { - body: contentJson(bodyParameters), - params: pathParameters, - ...this.schema?.request, - }, - responses: { - "200": { - description: "Returns the updated Object", - ...contentJson({ - success: Boolean, - result: this.meta.model.serializerObject, - }), - ...this.schema?.responses?.[200], - }, - ...InputValidationException.schema(), - ...NotFoundException.schema(), - ...this.schema?.responses, - }, - ...this.schema, - }; - } - - async getFilters(): Promise { - const data = await this.getValidatedData(); - - const filters: Array = []; - const updatedData: Record = {}; // TODO: fix this type - - for (const part of [data.params, data.body]) { - if (part) { - for (const [key, value] of Object.entries(part)) { - if ((this.meta.model.primaryKeys || []).includes(key)) { - filters.push({ - field: key, - operator: "EQ", - value: value as string, - }); - } else { - updatedData[key] = value as string; - } - } - } - } - - return { - filters, - updatedData, - }; - } - - async before( - oldObj: O, - filters: UpdateFilters, - ): Promise { - return filters; - } - - async after(data: O): Promise> { - return data; - } - - async getObject(filters: UpdateFilters): Promise | null> { - return null; - } - - async update( - oldObj: O, - filters: UpdateFilters, - ): Promise> { - return oldObj; - } - - async handle(...args: HandleArgs) { - let filters = await this.getFilters(); - - const oldObj = await this.getObject(filters); - - if (oldObj === null) { - throw new NotFoundException(); - } - - filters = await this.before(oldObj, filters); - - let obj = await this.update(oldObj, filters); - - obj = await this.after(obj); - - return { - success: true, - result: this.meta.model.serializer(obj), - }; - } +import { + type FilterCondition, + type Meta, + MetaGenerator, + type MetaInput, + type Model, + type O, + type UpdateFilters, +} from "./types"; + +export class UpdateEndpoint = Array> extends OpenAPIRoute { + get fields(): AnyZodObject | undefined { + return undefined; + } + + get model(): Model { + throw new Error("get model not implemented"); + } + + get meta() { + return MetaGenerator({ + model: this.model, + fields: this.fields, + }); + } + + getSchema() { + const bodyParameters = this.meta.fields.omit( + (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + const pathParameters = this.meta.model.schema.pick( + (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), + ); + + return { + request: { + body: contentJson(bodyParameters), + params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, + ...this.schema?.request, + }, + responses: { + "200": { + description: "Returns the updated Object", + ...contentJson({ + success: Boolean, + result: this.meta.model.serializerObject, + }), + ...this.schema?.responses?.[200], + }, + ...InputValidationException.schema(), + ...NotFoundException.schema(), + ...this.schema?.responses, + }, + ...this.schema, + }; + } + + async getFilters(): Promise { + const data = await this.getValidatedData(); + + const filters: Array = []; + const updatedData: Record = {}; // TODO: fix this type + + for (const part of [data.params, data.body]) { + if (part) { + for (const [key, value] of Object.entries(part)) { + if ((this.meta.model.primaryKeys || []).includes(key)) { + filters.push({ + field: key, + operator: "EQ", + value: value as string, + }); + } else { + updatedData[key] = value as string; + } + } + } + } + + return { + filters, + updatedData, + }; + } + + async before(oldObj: O, filters: UpdateFilters): Promise { + return filters; + } + + async after(data: O): Promise> { + return data; + } + + async getObject(filters: UpdateFilters): Promise | null> { + return null; + } + + async update(oldObj: O, filters: UpdateFilters): Promise> { + return oldObj; + } + + async handle(...args: HandleArgs) { + let filters = await this.getFilters(); + + const oldObj = await this.getObject(filters); + + if (oldObj === null) { + throw new NotFoundException(); + } + + filters = await this.before(oldObj, filters); + + let obj = await this.update(oldObj, filters); + + obj = await this.after(obj); + + return { + success: true, + result: this.meta.model.serializer(obj), + }; + } } diff --git a/src/exceptions.ts b/src/exceptions.ts index 44879a9..31fef0c 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -1,106 +1,104 @@ import { contentJson } from "./contentTypes"; export class ApiException extends Error { - isVisible = false; - message: string; - default_message = "Internal Error"; - status = 500; - code = 7000; - includesPath = false; + isVisible = false; + message: string; + default_message = "Internal Error"; + status = 500; + code = 7000; + includesPath = false; - constructor(message = "") { - super(message); - this.message = message; - } + constructor(message = "") { + super(message); + this.message = message; + } - buildResponse() { - return [ - { - code: this.code, - message: this.isVisible - ? this.message || this.default_message - : "Internal Error", - }, - ]; - } + buildResponse() { + return [ + { + code: this.code, + message: this.isVisible ? this.message || this.default_message : "Internal Error", + }, + ]; + } - static schema() { - const inst = new this(); - const innerError = { - code: inst.code, - message: inst.default_message, - }; + static schema() { + const inst = new this(); + const innerError = { + code: inst.code, + message: inst.default_message, + }; - if (inst.includesPath === true) { - // @ts-ignore - innerError.path = ["body", "fieldName"]; - } + if (inst.includesPath === true) { + // @ts-ignore + innerError.path = ["body", "fieldName"]; + } - return { - [inst.status]: { - description: inst.default_message, - ...contentJson({ - success: false, - errors: [innerError], - }), - }, - }; - } + return { + [inst.status]: { + description: inst.default_message, + ...contentJson({ + success: false, + errors: [innerError], + }), + }, + }; + } } export class InputValidationException extends ApiException { - isVisible = true; - default_message = "Input Validation Error"; - status = 400; - code = 7001; - path = null; - includesPath = true; + isVisible = true; + default_message = "Input Validation Error"; + status = 400; + code = 7001; + path = null; + includesPath = true; - constructor(message?: string, path?: any) { - super(message); - this.path = path; - } + constructor(message?: string, path?: any) { + super(message); + this.path = path; + } - buildResponse() { - return [ - { - code: this.code, - message: this.isVisible ? this.message : "Internal Error", - path: this.path, - }, - ]; - } + buildResponse() { + return [ + { + code: this.code, + message: this.isVisible ? this.message : "Internal Error", + path: this.path, + }, + ]; + } } export class MultiException extends Error { - isVisible = true; - errors: Array; - status = 400; + isVisible = true; + errors: Array; + status = 400; - constructor(errors: Array) { - super("Multiple Exceptions"); - this.errors = errors; + constructor(errors: Array) { + super("Multiple Exceptions"); + this.errors = errors; - // Because the API can only return 1 status code, always return the highest - for (const err of errors) { - if (err.status > this.status) { - this.status = err.status; - } + // Because the API can only return 1 status code, always return the highest + for (const err of errors) { + if (err.status > this.status) { + this.status = err.status; + } - if (!err.isVisible && this.isVisible) { - this.isVisible = false; - } - } - } + if (!err.isVisible && this.isVisible) { + this.isVisible = false; + } + } + } - buildResponse() { - return this.errors.map((err) => err.buildResponse()[0]); - } + buildResponse() { + return this.errors.map((err) => err.buildResponse()[0]); + } } export class NotFoundException extends ApiException { - isVisible = true; - default_message = "Not Found"; - status = 404; - code = 7002; + isVisible = true; + default_message = "Not Found"; + status = 404; + code = 7002; } diff --git a/src/openapi.ts b/src/openapi.ts index e36ade9..edda031 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -1,7 +1,4 @@ -import { - OpenApiGeneratorV3, - OpenApiGeneratorV31, -} from "@asteasolutions/zod-to-openapi"; +import { OpenApiGeneratorV3, OpenApiGeneratorV31 } from "@asteasolutions/zod-to-openapi"; import yaml from "js-yaml"; import { z } from "zod"; import type { OpenAPIRoute } from "./route"; @@ -10,264 +7,237 @@ import { getReDocUI, getSwaggerUI } from "./ui"; import { OpenAPIRegistryMerger } from "./zod/registry"; export type OpenAPIRouterType = { - original: M; - options: RouterOptions; - registry: OpenAPIRegistryMerger; - - delete(path: string, endpoint: typeof OpenAPIRoute): M; - delete(path: string, router: M): M; - get(path: string, endpoint: typeof OpenAPIRoute): M; - get(path: string, router: M): M; - head(path: string, endpoint: typeof OpenAPIRoute): M; - head(path: string, router: M): M; - patch(path: string, endpoint: typeof OpenAPIRoute): M; - patch(path: string, router: M): M; - post(path: string, endpoint: typeof OpenAPIRoute): M; - post(path: string, router: M): M; - put(path: string, endpoint: typeof OpenAPIRoute): M; - put(path: string, router: M): M; - all(path: string, endpoint: typeof OpenAPIRoute): M; - all(path: string, router: M): M; + original: M; + options: RouterOptions; + registry: OpenAPIRegistryMerger; + + delete(path: string, endpoint: typeof OpenAPIRoute): M; + delete(path: string, router: M): M; + get(path: string, endpoint: typeof OpenAPIRoute): M; + get(path: string, router: M): M; + head(path: string, endpoint: typeof OpenAPIRoute): M; + head(path: string, router: M): M; + patch(path: string, endpoint: typeof OpenAPIRoute): M; + patch(path: string, router: M): M; + post(path: string, endpoint: typeof OpenAPIRoute): M; + post(path: string, router: M): M; + put(path: string, endpoint: typeof OpenAPIRoute): M; + put(path: string, router: M): M; + all(path: string, endpoint: typeof OpenAPIRoute): M; + all(path: string, router: M): M; }; export class OpenAPIHandler { - router: any; - options: RouterOptions; - registry: OpenAPIRegistryMerger; - - allowedMethods = ["get", "head", "post", "put", "delete", "patch"]; - - constructor(router: any, options?: RouterOptions) { - this.router = router; - this.options = options || {}; - this.registry = new OpenAPIRegistryMerger(); - - this.createDocsRoutes(); - } - - createDocsRoutes() { - if (this.options?.docs_url !== null && this.options?.openapi_url !== null) { - this.router.get(this.options?.docs_url || "/docs", () => { - return new Response( - getSwaggerUI( - (this.options?.base || "") + - (this.options?.openapi_url || "/openapi.json"), - ), - { - headers: { - "content-type": "text/html; charset=UTF-8", - }, - status: 200, - }, - ); - }); - } - - if ( - this.options?.redoc_url !== null && - this.options?.openapi_url !== null - ) { - this.router.get(this.options?.redoc_url || "/redocs", () => { - return new Response( - getReDocUI( - (this.options?.base || "") + - (this.options?.openapi_url || "/openapi.json"), - ), - { - headers: { - "content-type": "text/html; charset=UTF-8", - }, - status: 200, - }, - ); - }); - } - - if (this.options?.openapi_url !== null) { - this.router.get(this.options?.openapi_url || "/openapi.json", () => { - return new Response(JSON.stringify(this.getGeneratedSchema()), { - headers: { - "content-type": "application/json;charset=UTF-8", - }, - status: 200, - }); - }); - - this.router.get( - (this.options?.openapi_url || "/openapi.json").replace( - ".json", - ".yaml", - ), - () => { - return new Response(yaml.dump(this.getGeneratedSchema()), { - headers: { - "content-type": "text/yaml;charset=UTF-8", - }, - status: 200, - }); - }, - ); - } - } - - getGeneratedSchema() { - let openapiGenerator: any = OpenApiGeneratorV31; - if (this.options?.openapiVersion === "3") - openapiGenerator = OpenApiGeneratorV3; - - const generator = new openapiGenerator(this.registry.definitions); - - return generator.generateDocument({ - openapi: this.options?.openapiVersion === "3" ? "3.0.3" : "3.1.0", - info: { - version: this.options?.schema?.info?.version || "1.0.0", - title: this.options?.schema?.info?.title || "OpenAPI", - ...this.options?.schema?.info, - }, - ...this.options?.schema, - }); - } - - registerNestedRouter(params: { - method: string; - path: string; - nestedRouter: any; - }) { - this.registry.merge(params.nestedRouter.registry); - - return [params.nestedRouter.fetch]; - } - - parseRoute(path: string): string { - return ((this.options.base || "") + path) - .replaceAll(/\/+(\/|$)/g, "$1") // strip double & trailing splash - .replaceAll(/:(\w+)/g, "{$1}"); // convert parameters into openapi compliant - } - - registerRoute(params: { method: string; path: string; handlers: any[] }) { - const parsedRoute = this.parseRoute(params.path); - - const parsedParams = ((this.options.base || "") + params.path).match( - /:(\w+)/g, - ); - let urlParams: string[] = []; - if (parsedParams) { - urlParams = parsedParams.map((obj) => obj.replace(":", "")); - } - - // @ts-ignore - let schema: OpenAPIRouteSchema = undefined; - // @ts-ignore - let operationId: string = undefined; - - for (const handler of params.handlers) { - if (handler.name) { - operationId = `${params.method}_${handler.name}`; - } - - if (handler.isRoute === true) { - schema = new handler({ - route: parsedRoute, - urlParams: urlParams, - }).getSchemaZod(); - break; - } - } - - if (operationId === undefined) { - operationId = `${params.method}_${parsedRoute.replaceAll("/", "_")}`; - } - - if (schema === undefined) { - // No schema for this route, try to guest the parameters - - // @ts-ignore - schema = { - operationId: operationId, - responses: { - 200: { - description: "Successful response.", - }, - }, - }; - - if (urlParams.length > 0) { - schema.request = { - params: z.object( - urlParams.reduce( - (obj, item) => - Object.assign(obj, { - [item]: z.string(), - }), - {}, - ), - ), - }; - } - } else { - // Schema was provided in the endpoint - if (!schema.operationId) { - if ( - this.options?.generateOperationIds === false && - !schema.operationId - ) { - throw new Error(`Route ${params.path} don't have operationId set!`); - } - - schema.operationId = operationId; - } - } - - this.registry.registerPath({ - ...schema, - // @ts-ignore - method: params.method, - path: parsedRoute, - }); - - return params.handlers.map((handler: any) => { - if (handler.isRoute) { - return (...params: any[]) => - new handler({ - router: this, - route: parsedRoute, - urlParams: urlParams, - // raiseUnknownParameters: openapiConfig.raiseUnknownParameters, TODO - }).execute(...params); - } - - return handler; - }); - } - - handleCommonProxy(target: any, prop: string, ...args: any[]) { - // This is a hack to allow older versions of wrangler to use this library - // https://github.com/cloudflare/workers-sdk/issues/5420 - if (prop === "middleware") { - return []; - } - - if (prop === "isChanfana") { - return true; - } - if (prop === "original") { - return this.router; - } - if (prop === "schema") { - return this.getGeneratedSchema(); - } - if (prop === "registry") { - return this.registry; - } - - return undefined; - } - - getRequest(args: any[]) { - throw new Error("getRequest not implemented"); - } - - getUrlParams(args: any[]): Record { - throw new Error("getUrlParams not implemented"); - } + router: any; + options: RouterOptions; + registry: OpenAPIRegistryMerger; + + allowedMethods = ["get", "head", "post", "put", "delete", "patch"]; + + constructor(router: any, options?: RouterOptions) { + this.router = router; + this.options = options || {}; + this.registry = new OpenAPIRegistryMerger(); + + this.createDocsRoutes(); + } + + createDocsRoutes() { + if (this.options?.docs_url !== null && this.options?.openapi_url !== null) { + this.router.get(this.options?.docs_url || "/docs", () => { + return new Response(getSwaggerUI((this.options?.base || "") + (this.options?.openapi_url || "/openapi.json")), { + headers: { + "content-type": "text/html; charset=UTF-8", + }, + status: 200, + }); + }); + } + + if (this.options?.redoc_url !== null && this.options?.openapi_url !== null) { + this.router.get(this.options?.redoc_url || "/redocs", () => { + return new Response(getReDocUI((this.options?.base || "") + (this.options?.openapi_url || "/openapi.json")), { + headers: { + "content-type": "text/html; charset=UTF-8", + }, + status: 200, + }); + }); + } + + if (this.options?.openapi_url !== null) { + this.router.get(this.options?.openapi_url || "/openapi.json", () => { + return new Response(JSON.stringify(this.getGeneratedSchema()), { + headers: { + "content-type": "application/json;charset=UTF-8", + }, + status: 200, + }); + }); + + this.router.get((this.options?.openapi_url || "/openapi.json").replace(".json", ".yaml"), () => { + return new Response(yaml.dump(this.getGeneratedSchema()), { + headers: { + "content-type": "text/yaml;charset=UTF-8", + }, + status: 200, + }); + }); + } + } + + getGeneratedSchema() { + let openapiGenerator: any = OpenApiGeneratorV31; + if (this.options?.openapiVersion === "3") openapiGenerator = OpenApiGeneratorV3; + + const generator = new openapiGenerator(this.registry.definitions); + + return generator.generateDocument({ + openapi: this.options?.openapiVersion === "3" ? "3.0.3" : "3.1.0", + info: { + version: this.options?.schema?.info?.version || "1.0.0", + title: this.options?.schema?.info?.title || "OpenAPI", + ...this.options?.schema?.info, + }, + ...this.options?.schema, + }); + } + + registerNestedRouter(params: { + method: string; + path: string; + nestedRouter: any; + }) { + this.registry.merge(params.nestedRouter.registry); + + return [params.nestedRouter.fetch]; + } + + parseRoute(path: string): string { + return ((this.options.base || "") + path) + .replaceAll(/\/+(\/|$)/g, "$1") // strip double & trailing splash + .replaceAll(/:(\w+)/g, "{$1}"); // convert parameters into openapi compliant + } + + registerRoute(params: { method: string; path: string; handlers: any[] }) { + const parsedRoute = this.parseRoute(params.path); + + const parsedParams = ((this.options.base || "") + params.path).match(/:(\w+)/g); + let urlParams: string[] = []; + if (parsedParams) { + urlParams = parsedParams.map((obj) => obj.replace(":", "")); + } + + // @ts-ignore + let schema: OpenAPIRouteSchema = undefined; + // @ts-ignore + let operationId: string = undefined; + + for (const handler of params.handlers) { + if (handler.name) { + operationId = `${params.method}_${handler.name}`; + } + + if (handler.isRoute === true) { + schema = new handler({ + route: parsedRoute, + urlParams: urlParams, + }).getSchemaZod(); + break; + } + } + + if (operationId === undefined) { + operationId = `${params.method}_${parsedRoute.replaceAll("/", "_")}`; + } + + if (schema === undefined) { + // No schema for this route, try to guest the parameters + + // @ts-ignore + schema = { + operationId: operationId, + responses: { + 200: { + description: "Successful response.", + }, + }, + }; + + if (urlParams.length > 0) { + schema.request = { + params: z.object( + urlParams.reduce( + (obj, item) => + Object.assign(obj, { + [item]: z.string(), + }), + {}, + ), + ), + }; + } + } else { + // Schema was provided in the endpoint + if (!schema.operationId) { + if (this.options?.generateOperationIds === false && !schema.operationId) { + throw new Error(`Route ${params.path} don't have operationId set!`); + } + + schema.operationId = operationId; + } + } + + this.registry.registerPath({ + ...schema, + // @ts-ignore + method: params.method, + path: parsedRoute, + }); + + return params.handlers.map((handler: any) => { + if (handler.isRoute) { + return (...params: any[]) => + new handler({ + router: this, + route: parsedRoute, + urlParams: urlParams, + // raiseUnknownParameters: openapiConfig.raiseUnknownParameters, TODO + }).execute(...params); + } + + return handler; + }); + } + + handleCommonProxy(target: any, prop: string, ...args: any[]) { + // This is a hack to allow older versions of wrangler to use this library + // https://github.com/cloudflare/workers-sdk/issues/5420 + if (prop === "middleware") { + return []; + } + + if (prop === "isChanfana") { + return true; + } + if (prop === "original") { + return this.router; + } + if (prop === "schema") { + return this.getGeneratedSchema(); + } + if (prop === "registry") { + return this.registry; + } + + return undefined; + } + + getRequest(args: any[]) { + throw new Error("getRequest not implemented"); + } + + getUrlParams(args: any[]): Record { + throw new Error("getUrlParams not implemented"); + } } diff --git a/src/parameters.ts b/src/parameters.ts index 229dded..1372234 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -1,245 +1,207 @@ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; -import type { - EnumerationParameterType, - ParameterType, - RegexParameterType, - RouteParameter, -} from "./types"; +import type { EnumerationParameterType, ParameterType, RegexParameterType, RouteParameter } from "./types"; import { isSpecificZodType, legacyTypeIntoZod } from "./zod/utils"; extendZodWithOpenApi(z); export function convertParams(field: any, params: any): M { - params = params || {}; - if (params.required === false) - // @ts-ignore - field = field.optional(); + params = params || {}; + if (params.required === false) + // @ts-ignore + field = field.optional(); - if (params.description) field = field.describe(params.description); + if (params.description) field = field.describe(params.description); - if (params.default) - // @ts-ignore - field = field.default(params.default); + if (params.default) + // @ts-ignore + field = field.default(params.default); - if (params.example) { - field = field.openapi({ example: params.example }); - } + if (params.example) { + field = field.openapi({ example: params.example }); + } - if (params.format) { - field = field.openapi({ format: params.format }); - } + if (params.format) { + field = field.openapi({ format: params.format }); + } - return field; + return field; } export function Arr(innerType: any, params?: ParameterType): z.ZodArray { - return convertParams(legacyTypeIntoZod(innerType).array(), params); + return convertParams(legacyTypeIntoZod(innerType).array(), params); } export function Obj(fields: object, params?: ParameterType): z.ZodObject { - const parsed: Record = {}; - for (const [key, value] of Object.entries(fields)) { - parsed[key] = legacyTypeIntoZod(value); - } + const parsed: Record = {}; + for (const [key, value] of Object.entries(fields)) { + parsed[key] = legacyTypeIntoZod(value); + } - return convertParams(z.object(parsed), params); + return convertParams(z.object(parsed), params); } export function Num(params?: ParameterType): z.ZodNumber { - return convertParams(z.number(), params).openapi({ - type: "number", - }); + return convertParams(z.number(), params).openapi({ + type: "number", + }); } export function Int(params?: ParameterType): z.ZodNumber { - return convertParams(z.number().int(), params).openapi({ - type: "integer", - }); + return convertParams(z.number().int(), params).openapi({ + type: "integer", + }); } export function Str(params?: ParameterType): z.ZodString { - return convertParams(z.string(), params); + return convertParams(z.string(), params); } export function DateTime(params?: ParameterType): z.ZodString { - return convertParams( - z.string().datetime({ - message: "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ", - }), - params, - ); + return convertParams( + z.string().datetime({ + message: "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ", + }), + params, + ); } export function Regex(params: RegexParameterType): z.ZodString { - return convertParams( - // @ts-ignore - z - .string() - .regex(params.pattern, params.patternError || "Invalid"), - params, - ); + return convertParams( + // @ts-ignore + z + .string() + .regex(params.pattern, params.patternError || "Invalid"), + params, + ); } export function Email(params?: ParameterType): z.ZodString { - return convertParams(z.string().email(), params); + return convertParams(z.string().email(), params); } export function Uuid(params?: ParameterType): z.ZodString { - return convertParams(z.string().uuid(), params); + return convertParams(z.string().uuid(), params); } export function Hostname(params?: ParameterType): z.ZodString { - return convertParams( - z - .string() - .regex( - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, - ), - params, - ); + return convertParams( + z + .string() + .regex( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, + ), + params, + ); } export function Ipv4(params?: ParameterType): z.ZodString { - return convertParams(z.string().ip({ version: "v4" }), params); + return convertParams(z.string().ip({ version: "v4" }), params); } export function Ipv6(params?: ParameterType): z.ZodString { - return convertParams(z.string().ip({ version: "v6" }), params); + return convertParams(z.string().ip({ version: "v6" }), params); } export function Ip(params?: ParameterType): z.ZodString { - return convertParams(z.string().ip(), params); + return convertParams(z.string().ip(), params); } export function DateOnly(params?: ParameterType): z.ZodString { - return convertParams(z.date(), params); + return convertParams(z.date(), params); } export function Bool(params?: ParameterType): z.ZodBoolean { - return convertParams(z.boolean(), params).openapi({ - type: "boolean", - }); + return convertParams(z.boolean(), params).openapi({ + type: "boolean", + }); } export function Enumeration(params: EnumerationParameterType): z.ZodEnum { - let { values } = params; - const originalValues = { ...values }; + let { values } = params; + const originalValues = { ...values }; - if (Array.isArray(values)) - values = Object.fromEntries(values.map((x) => [x, x])); + if (Array.isArray(values)) values = Object.fromEntries(values.map((x) => [x, x])); - const originalKeys: [string, ...string[]] = Object.keys(values) as [ - string, - ...string[], - ]; + const originalKeys: [string, ...string[]] = Object.keys(values) as [string, ...string[]]; - if (params.enumCaseSensitive === false) { - values = Object.keys(values).reduce((accumulator, key) => { - // @ts-ignore - accumulator[key.toLowerCase()] = values[key]; - return accumulator; - }, {}); - } + if (params.enumCaseSensitive === false) { + values = Object.keys(values).reduce((accumulator, key) => { + // @ts-ignore + accumulator[key.toLowerCase()] = values[key]; + return accumulator; + }, {}); + } - const keys: [string, ...string[]] = Object.keys(values) as [ - string, - ...string[], - ]; + const keys: [string, ...string[]] = Object.keys(values) as [string, ...string[]]; - let field; - if ([undefined, true].includes(params.enumCaseSensitive)) { - field = z.enum(keys); - } else { - field = z - .preprocess((val) => String(val).toLowerCase(), z.enum(keys)) - .openapi({ enum: originalKeys }); - } + let field; + if ([undefined, true].includes(params.enumCaseSensitive)) { + field = z.enum(keys); + } else { + field = z.preprocess((val) => String(val).toLowerCase(), z.enum(keys)).openapi({ enum: originalKeys }); + } - field = field.transform((val) => values[val]); + field = field.transform((val) => values[val]); - const result = convertParams>(field, params); + const result = convertParams>(field, params); - // Keep retro compatibility - //@ts-ignore - result.values = originalValues; + // Keep retro compatibility + //@ts-ignore + result.values = originalValues; - return result; + return result; } // This should only be used for query, params, headers and cookies -export function coerceInputs( - data: Record, - schema?: RouteParameter, -): Record | null { - // For older node versions, searchParams is just an object without the size property - if ( - data.size === 0 || - (data.size === undefined && - typeof data === "object" && - Object.keys(data).length === 0) - ) { - return null; - } - - const params: Record = {}; - const entries = data.entries ? data.entries() : Object.entries(data); - for (let [key, value] of entries) { - // Query, path and headers can be empty strings, that should equal to null as nothing was provided - if (value === "") { - // @ts-ignore - value = null; - } - - if (params[key] === undefined) { - params[key] = value; - } else if (!Array.isArray(params[key])) { - params[key] = [params[key], value]; - } else { - params[key].push(value); - } - - let innerType; - if ( - schema && - (schema as z.AnyZodObject).shape && - (schema as z.AnyZodObject).shape[key] - ) { - innerType = (schema as z.AnyZodObject).shape[key]; - } else if (schema) { - // Fallback for Zod effects - innerType = schema; - } - - // Soft transform query strings into arrays - if (innerType) { - if ( - isSpecificZodType(innerType, "ZodArray") && - !Array.isArray(params[key]) - ) { - params[key] = [params[key]]; - } else if (isSpecificZodType(innerType, "ZodBoolean")) { - const _val = (params[key] as string).toLowerCase().trim(); - if (_val === "true" || _val === "false") { - params[key] = _val === "true"; - } - } else if ( - isSpecificZodType(innerType, "ZodNumber") || - innerType instanceof z.ZodNumber - ) { - params[key] = Number.parseFloat(params[key]); - } else if ( - isSpecificZodType(innerType, "ZodBigInt") || - innerType instanceof z.ZodBigInt - ) { - params[key] = Number.parseInt(params[key]); - } else if ( - isSpecificZodType(innerType, "ZodDate") || - innerType instanceof z.ZodDate - ) { - params[key] = new Date(params[key]); - } - } - } - - return params; +export function coerceInputs(data: Record, schema?: RouteParameter): Record | null { + // For older node versions, searchParams is just an object without the size property + if (data.size === 0 || (data.size === undefined && typeof data === "object" && Object.keys(data).length === 0)) { + return null; + } + + const params: Record = {}; + const entries = data.entries ? data.entries() : Object.entries(data); + for (let [key, value] of entries) { + // Query, path and headers can be empty strings, that should equal to null as nothing was provided + if (value === "") { + // @ts-ignore + value = null; + } + + if (params[key] === undefined) { + params[key] = value; + } else if (!Array.isArray(params[key])) { + params[key] = [params[key], value]; + } else { + params[key].push(value); + } + + let innerType; + if (schema && (schema as z.AnyZodObject).shape && (schema as z.AnyZodObject).shape[key]) { + innerType = (schema as z.AnyZodObject).shape[key]; + } else if (schema) { + // Fallback for Zod effects + innerType = schema; + } + + // Soft transform query strings into arrays + if (innerType) { + if (isSpecificZodType(innerType, "ZodArray") && !Array.isArray(params[key])) { + params[key] = [params[key]]; + } else if (isSpecificZodType(innerType, "ZodBoolean")) { + const _val = (params[key] as string).toLowerCase().trim(); + if (_val === "true" || _val === "false") { + params[key] = _val === "true"; + } + } else if (isSpecificZodType(innerType, "ZodNumber") || innerType instanceof z.ZodNumber) { + params[key] = Number.parseFloat(params[key]); + } else if (isSpecificZodType(innerType, "ZodBigInt") || innerType instanceof z.ZodBigInt) { + params[key] = Number.parseInt(params[key]); + } else if (isSpecificZodType(innerType, "ZodDate") || innerType instanceof z.ZodDate) { + params[key] = new Date(params[key]); + } + } + } + + return params; } diff --git a/src/route.ts b/src/route.ts index 7ef6eeb..ce3303c 100644 --- a/src/route.ts +++ b/src/route.ts @@ -1,183 +1,168 @@ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; import { type AnyZodObject, z } from "zod"; -import { - type ApiException, - InputValidationException, - MultiException, -} from "./exceptions"; +import { type ApiException, InputValidationException, MultiException } from "./exceptions"; import { coerceInputs } from "./parameters"; import type { OpenAPIRouteSchema, RouteOptions, ValidatedData } from "./types"; import { jsonResp } from "./utils"; extendZodWithOpenApi(z); export class OpenAPIRoute = any> { - handle( - ...args: any[] - ): Response | Promise | object | Promise { - throw new Error("Method not implemented."); - } - - static isRoute = true; - - args: HandleArgs; // Args the execute() was called with - validatedData: any = undefined; // this acts as a cache, in case the users calls the validate method twice - params: RouteOptions; - schema: OpenAPIRouteSchema = {}; - - constructor(params: RouteOptions) { - this.params = params; - this.args = [] as any; - } - - async getValidatedData(): Promise> { - const request = this.params.router.getRequest(this.args); - - if (this.validatedData !== undefined) return this.validatedData; - - const data = await this.validateRequest(request); - - this.validatedData = data; - return data; - } - - getSchema(): OpenAPIRouteSchema { - // Use this function to overwrite schema properties - return this.schema; - } - - getSchemaZod(): OpenAPIRouteSchema { - // Deep copy - const schema = { ...this.getSchema() }; - - if (!schema.responses) { - // No response was provided in the schema, default to a blank one - schema.responses = { - "200": { - description: "Successful response", - content: { - "application/json": { - schema: {}, - }, - }, - }, - }; - } - - // @ts-ignore - return schema; - } - - handleValidationError(errors: z.ZodIssue[]): Response { - return jsonResp( - { - errors: errors, - success: false, - result: {}, - }, - { - status: 400, - }, - ); - - // In the future, errors will be handled as exceptions - // Errors caught here are always validation errors - // const updatedError: Array = errors.map((err) => { - // // @ts-ignore - // if ((err as ApiException).buildResponse) { - // // Error is already an internal exception - // return err; - // } - // return new InputValidationException(err.message, err.path); - // }); - // - // throw new MultiException(updatedError as Array); - } - - async execute(...args: HandleArgs) { - this.validatedData = undefined; - this.args = args; - - let resp; - try { - resp = await this.handle(...args); - } catch (e) { - if (e instanceof z.ZodError) { - return this.handleValidationError(e.errors); - } - - throw e; - } - - if (!(resp instanceof Response) && typeof resp === "object") { - return jsonResp(resp); - } - - return resp; - } - - async validateRequest(request: Request) { - const schema: OpenAPIRouteSchema = this.getSchemaZod(); - const unvalidatedData: any = {}; - - const rawSchema: any = {}; - if (schema.request?.params) { - rawSchema.params = schema.request?.params; - unvalidatedData.params = coerceInputs( - this.params.router.getUrlParams(this.args), - schema.request?.params, - ); - } - if (schema.request?.query) { - rawSchema.query = schema.request?.query; - unvalidatedData.query = {}; - } - if (schema.request?.headers) { - rawSchema.headers = schema.request?.headers; - unvalidatedData.headers = {}; - } - - const { searchParams } = new URL(request.url); - const queryParams = coerceInputs(searchParams, schema.request?.query); - if (queryParams !== null) unvalidatedData.query = queryParams; - - if (schema.request?.headers) { - const tmpHeaders: Record = {}; - - // @ts-ignore - for (const header of Object.keys(schema.request?.headers.shape)) { - tmpHeaders[header] = request.headers.get(header); - } - - unvalidatedData.headers = coerceInputs( - tmpHeaders, - schema.request?.headers as AnyZodObject, - ); - } - - if ( - request.method.toLowerCase() !== "get" && - schema.request?.body && - schema.request?.body.content["application/json"] && - schema.request?.body.content["application/json"].schema - ) { - rawSchema.body = schema.request.body.content["application/json"].schema; - - try { - unvalidatedData.body = await request.json(); - } catch (e) { - unvalidatedData.body = {}; - } - } - - let validationSchema: any = z.object(rawSchema); - - if ( - this.params?.raiseUnknownParameters === undefined || - this.params?.raiseUnknownParameters === true - ) { - validationSchema = validationSchema.strict(); - } - - return await validationSchema.parseAsync(unvalidatedData); - } + handle(...args: any[]): Response | Promise | object | Promise { + throw new Error("Method not implemented."); + } + + static isRoute = true; + + args: HandleArgs; // Args the execute() was called with + validatedData: any = undefined; // this acts as a cache, in case the users calls the validate method twice + params: RouteOptions; + schema: OpenAPIRouteSchema = {}; + + constructor(params: RouteOptions) { + this.params = params; + this.args = [] as any; + } + + async getValidatedData(): Promise> { + const request = this.params.router.getRequest(this.args); + + if (this.validatedData !== undefined) return this.validatedData; + + const data = await this.validateRequest(request); + + this.validatedData = data; + return data; + } + + getSchema(): OpenAPIRouteSchema { + // Use this function to overwrite schema properties + return this.schema; + } + + getSchemaZod(): OpenAPIRouteSchema { + // Deep copy + const schema = { ...this.getSchema() }; + + if (!schema.responses) { + // No response was provided in the schema, default to a blank one + schema.responses = { + "200": { + description: "Successful response", + content: { + "application/json": { + schema: {}, + }, + }, + }, + }; + } + + // @ts-ignore + return schema; + } + + handleValidationError(errors: z.ZodIssue[]): Response { + return jsonResp( + { + errors: errors, + success: false, + result: {}, + }, + { + status: 400, + }, + ); + + // In the future, errors will be handled as exceptions + // Errors caught here are always validation errors + // const updatedError: Array = errors.map((err) => { + // // @ts-ignore + // if ((err as ApiException).buildResponse) { + // // Error is already an internal exception + // return err; + // } + // return new InputValidationException(err.message, err.path); + // }); + // + // throw new MultiException(updatedError as Array); + } + + async execute(...args: HandleArgs) { + this.validatedData = undefined; + this.args = args; + + let resp; + try { + resp = await this.handle(...args); + } catch (e) { + if (e instanceof z.ZodError) { + return this.handleValidationError(e.errors); + } + + throw e; + } + + if (!(resp instanceof Response) && typeof resp === "object") { + return jsonResp(resp); + } + + return resp; + } + + async validateRequest(request: Request) { + const schema: OpenAPIRouteSchema = this.getSchemaZod(); + const unvalidatedData: any = {}; + + const rawSchema: any = {}; + if (schema.request?.params) { + rawSchema.params = schema.request?.params; + unvalidatedData.params = coerceInputs(this.params.router.getUrlParams(this.args), schema.request?.params); + } + if (schema.request?.query) { + rawSchema.query = schema.request?.query; + unvalidatedData.query = {}; + } + if (schema.request?.headers) { + rawSchema.headers = schema.request?.headers; + unvalidatedData.headers = {}; + } + + const { searchParams } = new URL(request.url); + const queryParams = coerceInputs(searchParams, schema.request?.query); + if (queryParams !== null) unvalidatedData.query = queryParams; + + if (schema.request?.headers) { + const tmpHeaders: Record = {}; + + // @ts-ignore + for (const header of Object.keys(schema.request?.headers.shape)) { + tmpHeaders[header] = request.headers.get(header); + } + + unvalidatedData.headers = coerceInputs(tmpHeaders, schema.request?.headers as AnyZodObject); + } + + if ( + request.method.toLowerCase() !== "get" && + schema.request?.body && + schema.request?.body.content["application/json"] && + schema.request?.body.content["application/json"].schema + ) { + rawSchema.body = schema.request.body.content["application/json"].schema; + + try { + unvalidatedData.body = await request.json(); + } catch (e) { + unvalidatedData.body = {}; + } + } + + let validationSchema: any = z.object(rawSchema); + + if (this.params?.raiseUnknownParameters === undefined || this.params?.raiseUnknownParameters === true) { + validationSchema = validationSchema.strict(); + } + + return await validationSchema.parseAsync(unvalidatedData); + } } diff --git a/src/types.ts b/src/types.ts index 8472997..17106e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,198 +1,156 @@ -import type { - RouteConfig, - ZodMediaTypeObject, -} from "@asteasolutions/zod-to-openapi"; -import type { - HeadersObject as HeadersObject30, - LinksObject as LinksObject30, - OpenAPIObject, -} from "openapi3-ts/oas30"; -import type { - HeadersObject as HeadersObject31, - LinksObject as LinksObject31, -} from "openapi3-ts/oas31"; +import type { RouteConfig, ZodMediaTypeObject } from "@asteasolutions/zod-to-openapi"; +import type { HeadersObject as HeadersObject30, LinksObject as LinksObject30, OpenAPIObject } from "openapi3-ts/oas30"; +import type { HeadersObject as HeadersObject31, LinksObject as LinksObject31 } from "openapi3-ts/oas31"; import type { AnyZodObject, ZodEffects, ZodType, z } from "zod"; export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; -export type IsEqual = (() => G extends A ? 1 : 2) extends < - G, ->() => G extends B ? 1 : 2 - ? true - : false; +export type IsEqual = (() => G extends A ? 1 : 2) extends () => G extends B ? 1 : 2 ? true : false; type Filter = IsEqual extends true - ? never - : KeyType extends ExcludeType - ? never - : KeyType; + ? never + : KeyType extends ExcludeType + ? never + : KeyType; type ExceptOptions = { - requireExactProps?: boolean; + requireExactProps?: boolean; }; export type Except< - ObjectType, - KeysType extends keyof ObjectType, - Options extends ExceptOptions = { requireExactProps: false }, + ObjectType, + KeysType extends keyof ObjectType, + Options extends ExceptOptions = { requireExactProps: false }, > = { - [KeyType in keyof ObjectType as Filter< - KeyType, - KeysType - >]: ObjectType[KeyType]; -} & (Options["requireExactProps"] extends true - ? Partial> - : {}); + [KeyType in keyof ObjectType as Filter]: ObjectType[KeyType]; +} & (Options["requireExactProps"] extends true ? Partial> : {}); export type SetOptional = Simplify< - // Pick just the keys that are readonly from the base type. - Except & - // Pick the keys that should be mutable from the base type and make them mutable. - Partial> + // Pick just the keys that are readonly from the base type. + Except & + // Pick the keys that should be mutable from the base type and make them mutable. + Partial> >; +export type SetRequired = BaseType extends unknown + ? Simplify< + // Pick just the keys that are optional from the base type. + Except & + // Pick the keys that should be required from the base type and make them required. + Required> + > + : never; + // The following types are copied from @asteasolutions/zod-to-openapi as they are not exported -export type OpenAPIObjectConfig = Omit< - OpenAPIObject, - "paths" | "components" | "webhooks" ->; -export type OpenAPIObjectConfigV31 = Omit< - OpenAPIObject, - "paths" | "components" | "webhooks" ->; +export type OpenAPIObjectConfig = Omit; +export type OpenAPIObjectConfigV31 = Omit; type HeadersObject = HeadersObject30 | HeadersObject31; type LinksObject = LinksObject30 | LinksObject31; -export type ZodMediaType = - | "application/json" - | "text/html" - | "text/plain" - | "application/xml" - | (string & {}); -export type ZodContentObject = Partial< - Record ->; +export type ZodMediaType = "application/json" | "text/html" | "text/plain" | "application/xml" | (string & {}); +export type ZodContentObject = Partial>; export interface ZodRequestBody { - description?: string; - content: ZodContentObject; - required?: boolean; + description?: string; + content: ZodContentObject; + required?: boolean; } export interface ResponseConfig { - description: string; - headers?: AnyZodObject | HeadersObject; - links?: LinksObject; - content?: ZodContentObject; + description: string; + headers?: AnyZodObject | HeadersObject; + links?: LinksObject; + content?: ZodContentObject; } -export type RouteParameter = - | AnyZodObject - | ZodEffects - | undefined; +export type RouteParameter = AnyZodObject | ZodEffects | undefined; export interface RouterOptions { - base?: string; - schema?: Partial; - docs_url?: string | null; - redoc_url?: string | null; - openapi_url?: string | null; - raiseUnknownParameters?: boolean; - generateOperationIds?: boolean; - openapiVersion?: "3" | "3.1"; + base?: string; + schema?: Partial; + docs_url?: string | null; + redoc_url?: string | null; + openapi_url?: string | null; + raiseUnknownParameters?: boolean; + generateOperationIds?: boolean; + openapiVersion?: "3" | "3.1"; } export interface RouteOptions { - router: any; - raiseUnknownParameters: boolean; - route: string; - urlParams: Array; + router: any; + raiseUnknownParameters: boolean; + route: string; + urlParams: Array; } export interface ParameterType { - default?: string | number | boolean; - description?: string; - example?: string | number | boolean; - required?: boolean; - deprecated?: boolean; + default?: string | number | boolean; + description?: string; + example?: string | number | boolean; + required?: boolean; + deprecated?: boolean; } export interface StringParameterType extends ParameterType { - format?: string; + format?: string; } export interface EnumerationParameterType extends StringParameterType { - values: Record; - enumCaseSensitive?: boolean; + values: Record; + enumCaseSensitive?: boolean; } export interface RegexParameterType extends StringParameterType { - pattern: RegExp; - patternError?: string; + pattern: RegExp; + patternError?: string; } export type RequestTypes = { - body?: ZodRequestBody; - params?: AnyZodObject; - query?: AnyZodObject; - cookies?: AnyZodObject; - headers?: AnyZodObject | ZodType[]; + body?: ZodRequestBody; + params?: AnyZodObject; + query?: AnyZodObject; + cookies?: AnyZodObject; + headers?: AnyZodObject | ZodType[]; }; // Changes over the original RouteConfig: // - Make responses optional (a default one is generated) // - Removes method and path (its inject on boot) export type OpenAPIRouteSchema = Simplify< - Omit & { - request?: RequestTypes; - responses?: { - [statusCode: string]: ResponseConfig; - }; - } + Omit & { + request?: RequestTypes; + responses?: { + [statusCode: string]: ResponseConfig; + }; + } >; export type ValidatedData = S extends OpenAPIRouteSchema - ? { - query: GetRequest extends NonNullable> - ? GetOutput, "query"> - : undefined; - params: GetRequest extends NonNullable> - ? GetOutput, "params"> - : undefined; - headers: GetRequest extends NonNullable> - ? GetOutput, "headers"> - : undefined; - body: GetRequest extends NonNullable> - ? GetBody, "body">> - : undefined; - } - : { - query: undefined; - params: undefined; - headers: undefined; - body: undefined; - }; + ? { + query: GetRequest extends NonNullable> ? GetOutput, "query"> : undefined; + params: GetRequest extends NonNullable> ? GetOutput, "params"> : undefined; + headers: GetRequest extends NonNullable> ? GetOutput, "headers"> : undefined; + body: GetRequest extends NonNullable> ? GetBody, "body">> : undefined; + } + : { + query: undefined; + params: undefined; + headers: undefined; + body: undefined; + }; type GetRequest = T["request"]; -type GetOutput< - T extends object | undefined, - P extends keyof T, -> = T extends NonNullable - ? T[P] extends AnyZodObject - ? z.output - : undefined - : undefined; +type GetOutput = T extends NonNullable + ? T[P] extends AnyZodObject + ? z.output + : undefined + : undefined; -type GetPartBody< - T extends RequestTypes, - P extends keyof T, -> = T[P] extends ZodRequestBody ? T[P] : undefined; +type GetPartBody = T[P] extends ZodRequestBody ? T[P] : undefined; type GetBody = T extends NonNullable - ? T["content"]["application/json"] extends NonNullable< - T["content"]["application/json"] - > - ? T["content"]["application/json"]["schema"] extends z.ZodTypeAny - ? z.output - : undefined - : undefined - : undefined; + ? T["content"]["application/json"] extends NonNullable + ? T["content"]["application/json"]["schema"] extends z.ZodTypeAny + ? z.output + : undefined + : undefined + : undefined; diff --git a/src/ui.ts b/src/ui.ts index f05152b..9c86129 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,6 +1,6 @@ export function getSwaggerUI(schemaUrl: string): string { - schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); // strip double & trailing splash - return ` + schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); // strip double & trailing splash + return ` @@ -31,8 +31,8 @@ export function getSwaggerUI(schemaUrl: string): string { } export function getReDocUI(schemaUrl: string): string { - schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); // strip double & trailing splash - return ` + schemaUrl = schemaUrl.replace(/\/+(\/|$)/g, "$1"); // strip double & trailing splash + return ` ReDocUI diff --git a/src/utils.ts b/src/utils.ts index d9b42f0..1c7f824 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,10 @@ export function jsonResp(data: any, params?: object): Response { - return new Response(JSON.stringify(data), { - headers: { - "content-type": "application/json;charset=UTF-8", - }, - // @ts-ignore - status: params?.status ? params.status : 200, - ...params, - }); + return new Response(JSON.stringify(data), { + headers: { + "content-type": "application/json;charset=UTF-8", + }, + // @ts-ignore + status: params?.status ? params.status : 200, + ...params, + }); } diff --git a/src/zod/registry.ts b/src/zod/registry.ts index 40fdba7..27667ec 100644 --- a/src/zod/registry.ts +++ b/src/zod/registry.ts @@ -2,13 +2,13 @@ import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; // @ts-ignore export class OpenAPIRegistryMerger extends OpenAPIRegistry { - public _definitions: object[] = []; + public _definitions: object[] = []; - merge(registry: OpenAPIRegistryMerger): void { - if (!registry || !registry._definitions) return; + merge(registry: OpenAPIRegistryMerger): void { + if (!registry || !registry._definitions) return; - for (const definition of registry._definitions) { - this._definitions.push({ ...definition }); - } - } + for (const definition of registry._definitions) { + this._definitions.push({ ...definition }); + } + } } diff --git a/src/zod/utils.ts b/src/zod/utils.ts index d69a465..f1b414b 100644 --- a/src/zod/utils.ts +++ b/src/zod/utils.ts @@ -1,85 +1,77 @@ import type { z } from "zod"; -import { - Arr, - Bool, - DateTime, - Num, - Obj, - Str, - convertParams, -} from "../parameters"; +import { Arr, Bool, DateTime, Num, Obj, Str, convertParams } from "../parameters"; export function isAnyZodType(schema: object): schema is z.ZodType { - // @ts-ignore - return schema._def !== undefined; + // @ts-ignore + return schema._def !== undefined; } export function isSpecificZodType(field: any, typeName: string): boolean { - return ( - field._def.typeName === typeName || - field._def.innerType?._def.typeName === typeName || - field._def.schema?._def.innerType?._def.typeName === typeName || - field.unwrap?.()._def.typeName === typeName || - field.unwrap?.().unwrap?.()._def.typeName === typeName || - field._def.innerType?._def?.innerType?._def?.typeName === typeName - ); + return ( + field._def.typeName === typeName || + field._def.innerType?._def.typeName === typeName || + field._def.schema?._def.innerType?._def.typeName === typeName || + field.unwrap?.()._def.typeName === typeName || + field.unwrap?.().unwrap?.()._def.typeName === typeName || + field._def.innerType?._def?.innerType?._def?.typeName === typeName + ); } export function legacyTypeIntoZod(type: any, params?: any): z.ZodType { - params = params || {}; + params = params || {}; - if (type === null) { - return Str({ required: false, ...params }); - } + if (type === null) { + return Str({ required: false, ...params }); + } - if (isAnyZodType(type)) { - if (params) { - return convertParams(type, params); - } + if (isAnyZodType(type)) { + if (params) { + return convertParams(type, params); + } - return type; - } + return type; + } - if (type === String) { - return Str(params); - } + if (type === String) { + return Str(params); + } - if (typeof type === "string") { - return Str({ example: type }); - } + if (typeof type === "string") { + return Str({ example: type }); + } - if (type === Number) { - return Num(params); - } + if (type === Number) { + return Num(params); + } - if (typeof type === "number") { - return Num({ example: type }); - } + if (typeof type === "number") { + return Num({ example: type }); + } - if (type === Boolean) { - return Bool(params); - } + if (type === Boolean) { + return Bool(params); + } - if (typeof type === "boolean") { - return Bool({ example: type }); - } + if (typeof type === "boolean") { + return Bool({ example: type }); + } - if (type === Date) { - return DateTime(params); - } + if (type === Date) { + return DateTime(params); + } - if (Array.isArray(type)) { - if (type.length === 0) { - throw new Error("Arr must have a type"); - } + if (Array.isArray(type)) { + if (type.length === 0) { + throw new Error("Arr must have a type"); + } - return Arr(type[0], params); - } + return Arr(type[0], params); + } - if (typeof type === "object") { - return Obj(type, params); - } + if (typeof type === "object") { + return Obj(type, params); + } - // Legacy support - return type(params); + // Legacy support + return type(params); } diff --git a/tests/bindings.d.ts b/tests/bindings.d.ts index 45e77da..83409cd 100644 --- a/tests/bindings.d.ts +++ b/tests/bindings.d.ts @@ -1,7 +1,7 @@ export type Env = { - test: string; + test: string; }; declare module "cloudflare:test" { - interface ProvidedEnv extends Env {} + interface ProvidedEnv extends Env {} } diff --git a/tests/index.ts b/tests/index.ts index 5892ab0..6092077 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,7 +1,7 @@ import type { Env } from "./bindings"; export default { - async fetch(request: Request, env: Env) { - return new Response("test"); - }, + async fetch(request: Request, env: Env) { + return new Response("test"); + }, }; diff --git a/tests/integration/nested-routers.test.ts b/tests/integration/nested-routers.test.ts index 5c416d8..1a59ef1 100644 --- a/tests/integration/nested-routers.test.ts +++ b/tests/integration/nested-routers.test.ts @@ -7,93 +7,87 @@ import { jsonResp } from "../../src/utils"; import { buildRequest } from "../utils"; const innerRouter = fromIttyRouter(AutoRouter({ base: "/api/v1" }), { - base: "/api/v1", + base: "/api/v1", }); class ToDoGet extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "Get a single ToDo", - request: { - params: z.object({ - id: z.number(), - }), - }, - responses: { - "200": { - description: "example", - content: { - "application/json": { - schema: { - todo: { - lorem: String, - ipsum: String, - }, - }, - }, - }, - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "Get a single ToDo", + request: { + params: z.object({ + id: z.number(), + }), + }, + responses: { + "200": { + description: "example", + content: { + "application/json": { + schema: { + todo: { + lorem: String, + ipsum: String, + }, + }, + }, + }, + }, + }, + }; - async handle(request: Request, env: any, context: any) { - return { - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }; - } + async handle(request: Request, env: any, context: any) { + return { + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }; + } } innerRouter.get("/todo/:id", ToDoGet); innerRouter.all("*", () => jsonResp({ message: "Not Found" }, { status: 404 })); const router = fromIttyRouter(AutoRouter(), { - schema: { - info: { - title: "Radar Worker API", - version: "1.0", - }, - }, + schema: { + info: { + title: "Radar Worker API", + version: "1.0", + }, + }, }); router.all("/api/v1/*", innerRouter); router.all("*", () => new Response("Not Found.", { status: 404 })); describe("innerRouter", () => { - it("simpleSuccessfulCall", async () => { - const request = await router.fetch( - buildRequest({ method: "GET", path: "/api/v1/todo/1" }), - ); - const resp = await request.json(); + it("simpleSuccessfulCall", async () => { + const request = await router.fetch(buildRequest({ method: "GET", path: "/api/v1/todo/1" })); + const resp = await request.json(); - expect(request.status).toEqual(200); - expect(resp).toEqual({ - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }); - }); + expect(request.status).toEqual(200); + expect(resp).toEqual({ + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }); + }); - it("innerCatchAll", async () => { - const request = await router.fetch( - buildRequest({ method: "GET", path: "/api/v1/asd" }), - ); - const resp = await request.json(); + it("innerCatchAll", async () => { + const request = await router.fetch(buildRequest({ method: "GET", path: "/api/v1/asd" })); + const resp = await request.json(); - expect(request.status).toEqual(404); - expect(resp).toEqual({ message: "Not Found" }); - }); + expect(request.status).toEqual(404); + expect(resp).toEqual({ message: "Not Found" }); + }); - it("outerCatchAll", async () => { - const request = await router.fetch( - buildRequest({ method: "GET", path: "/asd" }), - ); - const resp = await request.text(); + it("outerCatchAll", async () => { + const request = await router.fetch(buildRequest({ method: "GET", path: "/asd" })); + const resp = await request.text(); - expect(request.status).toEqual(404); - expect(resp).toEqual("Not Found."); - }); + expect(request.status).toEqual(404); + expect(resp).toEqual("Not Found."); + }); }); diff --git a/tests/integration/openapi-schema.test.ts b/tests/integration/openapi-schema.test.ts index 9cd5552..5894432 100644 --- a/tests/integration/openapi-schema.test.ts +++ b/tests/integration/openapi-schema.test.ts @@ -5,34 +5,30 @@ import { ToDoGet, ToDoList, todoRouter } from "../router"; import { buildRequest, findError } from "../utils"; describe("openapi schema", () => { - it("custom content type", async () => { - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: "/openapi.json" }), - ); - const resp = await request.json(); - const respSchema = resp.paths["/contenttype"].get.responses[200]; + it("custom content type", async () => { + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: "/openapi.json" })); + const resp = await request.json(); + const respSchema = resp.paths["/contenttype"].get.responses[200]; - expect(respSchema.contentType).toBeUndefined(); - expect(respSchema.content).toEqual({ - "text/csv": { - schema: { - type: "string", - }, - }, - }); - }); + expect(respSchema.contentType).toBeUndefined(); + expect(respSchema.content).toEqual({ + "text/csv": { + schema: { + type: "string", + }, + }, + }); + }); - it("with base defined", async () => { - const router = fromIttyRouter(AutoRouter({ base: "/api" }), { - base: "/api", - }); - router.get("/todo", ToDoGet); + it("with base defined", async () => { + const router = fromIttyRouter(AutoRouter({ base: "/api" }), { + base: "/api", + }); + router.get("/todo", ToDoGet); - const request = await router.fetch( - buildRequest({ method: "GET", path: "/api/openapi.json" }), - ); - const resp = await request.json(); + const request = await router.fetch(buildRequest({ method: "GET", path: "/api/openapi.json" })); + const resp = await request.json(); - expect(Object.keys(resp.paths)[0]).toEqual("/api/todo"); - }); + expect(Object.keys(resp.paths)[0]).toEqual("/api/todo"); + }); }); diff --git a/tests/integration/openapi.test.disabled.ts b/tests/integration/openapi.test.disabled.ts index 4a7dfa7..217f294 100644 --- a/tests/integration/openapi.test.disabled.ts +++ b/tests/integration/openapi.test.disabled.ts @@ -3,8 +3,8 @@ import vitestOpenAPI from "vitest-openapi"; import { todoRouter } from "../router"; describe("openapiValidation", () => { - it("loadSpec", async () => { - console.log(todoRouter.schema); - vitestOpenAPI(todoRouter.schema); - }); + it("loadSpec", async () => { + console.log(todoRouter.schema); + vitestOpenAPI(todoRouter.schema); + }); }); diff --git a/tests/integration/parameters.test.ts b/tests/integration/parameters.test.ts index ce1c535..8b5781c 100644 --- a/tests/integration/parameters.test.ts +++ b/tests/integration/parameters.test.ts @@ -3,603 +3,493 @@ import { ToDoList, todoRouter } from "../router"; import { buildRequest, findError } from "../utils"; describe("queryParametersValidation", () => { - it("requiredFields", async () => { - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: "/todos" }), - ); - const resp = await request.json(); - - // minus 1, because 1 parameter is optional - expect(resp.errors.length).toEqual( - // @ts-ignore - Object.keys(new ToDoList({}).schema.request.query.shape).length - 1, - ); - - // sanity check some parameters - expect(findError(resp.errors, "p_number")).toEqual("Required"); - expect(findError(resp.errors, "p_boolean")).toEqual("Required"); - }); - - it("checkNumberInvalid", async () => { - const qs = "?p_number=asd"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_number")).toEqual( - "Expected number, received nan", - ); - }); - - it("checkNumberValidFloat", async () => { - const qs = "?p_number=12.3"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_number")).toBeUndefined(); - }); - - it("checkNumberValidInteger", async () => { - const qs = "?p_number=12"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_number")).toBeUndefined(); - }); - - it("checkStringValid", async () => { - const qs = "?p_string=asd21_sa"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_string")).toBeUndefined(); - }); - - it("checkStringInvalidEmpty", async () => { - const qs = "?p_string="; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_string")).toEqual( - "Expected string, received null", - ); - }); - - it("checkBooleanInvalid", async () => { - const qs = "?p_boolean=asd"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_boolean")).toEqual( - "Expected boolean, received string", - ); - }); - - it("checkBooleanValid", async () => { - const qs = "?p_boolean=false"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_boolean")).toBeUndefined(); - }); - - it("checkBooleanValidCaseInsensitive", async () => { - const qs = "?p_boolean=TrUe"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_boolean")).toBeUndefined(); - }); - - it("checkEnumerationSensitiveInvalid", async () => { - const qs = "?p_enumeration=sfDase"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration")).toEqual( - "Invalid enum value. Expected 'json' | 'csv', received 'sfDase'", - ); - }); - - it("checkEnumerationSensitiveInvalidCase", async () => { - const qs = "?p_enumeration=Csv"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration")).toEqual( - "Invalid enum value. Expected 'json' | 'csv', received 'Csv'", - ); - }); - - it("checkEnumerationSensitiveValid", async () => { - const qs = "?p_enumeration=csv"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration")).toBeUndefined(); - }); - - it("checkEnumerationInsensitiveInvalid", async () => { - const qs = "?p_enumeration_insensitive=sfDase"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration_insensitive")).toEqual( - "Invalid enum value. Expected 'json' | 'csv', received 'sfdase'", - ); - }); - - it("checkEnumerationInsensitiveValidCase", async () => { - const qs = "?p_enumeration_insensitive=Csv"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration_insensitive")).toBeUndefined(); - }); - - it("checkEnumerationInsensitiveValid", async () => { - const qs = "?p_enumeration_insensitive=csv"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_enumeration_insensitive")).toBeUndefined(); - }); - - it("checkDatetimeInvalid", async () => { - const qs = "?p_datetime=2023-13-01"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_datetime")).toEqual( - "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ", - ); - }); - - it("checkDatetimeInvalid2", async () => { - const qs = "?p_datetime=sdfg"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_datetime")).toEqual( - "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ", - ); - }); - - it("checkDatetimeInvalid3", async () => { - const qs = "?p_datetime=2022-09-15T00:00:00+01Z"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_datetime")).toEqual( - "Must be in the following format: YYYY-mm-ddTHH:MM:ssZ", - ); - }); - - it("checkDatetimeValid", async () => { - const qs = "?p_datetime=2022-09-15T00:00:01Z"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_datetime")).toBeUndefined(); - }); - - it("checkDatetimeValid2", async () => { - const qs = "?p_datetime=2022-09-15T00:00:00Z"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_datetime")).toBeUndefined(); - }); - - it("checkDateInvalid", async () => { - const qs = "?p_dateonly=2022-13-15"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_dateonly")).toEqual("Invalid date"); - }); - - it("checkDateInvalid3", async () => { - const qs = "?p_dateonly=2022-09-15T00:0f0:00.0Z"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_dateonly")).toEqual("Invalid date"); - }); - - it("checkDateValid", async () => { - const qs = "?p_dateonly=2022-09-15"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_dateonly")).toBeUndefined(); - }); - - it("checkRegexInvalid", async () => { - const qs = "?p_regex=123765"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_regex")).toBeTruthy(); - }); - - it("checkRegexValid", async () => { - const qs = "?p_regex=%2B919367788755"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_regex")).toBeUndefined(); - }); - - it("checkEmailInvalid", async () => { - const qs = "?p_email=asfdgsdf"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); - }); - - it("checkEmailInvalid2", async () => { - const qs = "?p_email=asfdgsdf@gmail"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); - }); - - it("checkEmailInvalid3", async () => { - const qs = "?p_email=@gmail.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); - }); - - it("checkEmailValid", async () => { - const qs = "?p_email=sdfg@gmail.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_email")).toBeUndefined(); - }); - - it("checkUuidInvalid", async () => { - const qs = "?p_uuid=f31f890-044b-11ee-be56-0242ac120002"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_uuid")).toEqual("Invalid uuid"); - }); - - it("checkUuidInvalid2", async () => { - const qs = "?p_uuid=asdf-sdfg-dsfg-sfdg"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_uuid")).toEqual("Invalid uuid"); - }); - - it("checkUuidValid", async () => { - const qs = "?p_uuid=f31f8b90-044b-11ee-be56-0242ac120002"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_uuid")).toBeUndefined(); - }); - - it("checkUuidValid2", async () => { - const qs = "?p_uuid=f5f26194-0b07-45a4-9a85-94d3db01e7a5"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_uuid")).toBeUndefined(); - }); - - it("checkHostnameInvalid", async () => { - const qs = "?p_hostname=.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_hostname")).toEqual("Invalid"); - }); - - it("checkHostnameValid", async () => { - const qs = "?p_hostname=cloudflare.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_hostname")).toBeUndefined(); - }); - - it("checkHostnameValid2", async () => { - const qs = "?p_hostname=radar.cloudflare.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_hostname")).toBeUndefined(); - }); - - it("checkIpv4Invalid", async () => { - const qs = "?p_ipv4=asdfrre.wer.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv4")).toEqual("Invalid ip"); - }); - - it("checkIpv4Invalid2", async () => { - const qs = "?p_ipv4=2001:0db8:85a3:0000:0000:8a2e:0370:7334"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv4")).toEqual("Invalid ip"); - }); - - it("checkIpv4Valid", async () => { - const qs = "?p_ipv4=1.1.1.1"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv4")).toBeUndefined(); - }); - - it("checkIpv6Invalid", async () => { - const qs = "?p_ipv6=asdfrre.wer.com"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv6")).toEqual("Invalid ip"); - }); - - it("checkIpv6Invalid2", async () => { - const qs = "?p_ipv6=1.1.1.1"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv6")).toEqual("Invalid ip"); - }); - - it("checkIpv6Valid", async () => { - const qs = "?p_ipv6=2001:0db8:85a3:0000:0000:8a2e:0370:7336"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_ipv6")).toBeUndefined(); - }); - - it("checkDateArrayInvalid", async () => { - const qs = "?p_array_dates=asadasd"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_array_dates")).toEqual("Invalid date"); - }); - - it("checkDateArrayValid", async () => { - const qs = "?p_array_dates=2023-01-01"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_array_dates")).toBeUndefined(); - }); - - it("checkDateArrayValid2", async () => { - const qs = "?p_array_dates=2023-01-01&p_array_dates=2023-01-02"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_array_dates")).toBeUndefined(); - }); - - it("checkOptionalMissing", async () => { - const qs = "?"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_optional")).toBeUndefined(); - }); - - it("checkOptionalInvalid", async () => { - const qs = "?p_optional=asfdasd"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_optional")).toEqual( - "Expected number, received nan", - ); - }); - - it("checkOptionalValid", async () => { - const qs = "?p_optional=32"; - const request = await todoRouter.fetch( - buildRequest({ method: "GET", path: `/todos${qs}` }), - ); - const resp = await request.json(); - - expect(findError(resp.errors, "p_optional")).toBeUndefined(); - }); + it("requiredFields", async () => { + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: "/todos" })); + const resp = await request.json(); + + // minus 1, because 1 parameter is optional + expect(resp.errors.length).toEqual( + // @ts-ignore + Object.keys(new ToDoList({}).schema.request.query.shape).length - 1, + ); + + // sanity check some parameters + expect(findError(resp.errors, "p_number")).toEqual("Required"); + expect(findError(resp.errors, "p_boolean")).toEqual("Required"); + }); + + it("checkNumberInvalid", async () => { + const qs = "?p_number=asd"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_number")).toEqual("Expected number, received nan"); + }); + + it("checkNumberValidFloat", async () => { + const qs = "?p_number=12.3"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_number")).toBeUndefined(); + }); + + it("checkNumberValidInteger", async () => { + const qs = "?p_number=12"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_number")).toBeUndefined(); + }); + + it("checkStringValid", async () => { + const qs = "?p_string=asd21_sa"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_string")).toBeUndefined(); + }); + + it("checkStringInvalidEmpty", async () => { + const qs = "?p_string="; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_string")).toEqual("Expected string, received null"); + }); + + it("checkBooleanInvalid", async () => { + const qs = "?p_boolean=asd"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_boolean")).toEqual("Expected boolean, received string"); + }); + + it("checkBooleanValid", async () => { + const qs = "?p_boolean=false"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_boolean")).toBeUndefined(); + }); + + it("checkBooleanValidCaseInsensitive", async () => { + const qs = "?p_boolean=TrUe"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_boolean")).toBeUndefined(); + }); + + it("checkEnumerationSensitiveInvalid", async () => { + const qs = "?p_enumeration=sfDase"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration")).toEqual( + "Invalid enum value. Expected 'json' | 'csv', received 'sfDase'", + ); + }); + + it("checkEnumerationSensitiveInvalidCase", async () => { + const qs = "?p_enumeration=Csv"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration")).toEqual( + "Invalid enum value. Expected 'json' | 'csv', received 'Csv'", + ); + }); + + it("checkEnumerationSensitiveValid", async () => { + const qs = "?p_enumeration=csv"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration")).toBeUndefined(); + }); + + it("checkEnumerationInsensitiveInvalid", async () => { + const qs = "?p_enumeration_insensitive=sfDase"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration_insensitive")).toEqual( + "Invalid enum value. Expected 'json' | 'csv', received 'sfdase'", + ); + }); + + it("checkEnumerationInsensitiveValidCase", async () => { + const qs = "?p_enumeration_insensitive=Csv"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration_insensitive")).toBeUndefined(); + }); + + it("checkEnumerationInsensitiveValid", async () => { + const qs = "?p_enumeration_insensitive=csv"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_enumeration_insensitive")).toBeUndefined(); + }); + + it("checkDatetimeInvalid", async () => { + const qs = "?p_datetime=2023-13-01"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_datetime")).toEqual("Must be in the following format: YYYY-mm-ddTHH:MM:ssZ"); + }); + + it("checkDatetimeInvalid2", async () => { + const qs = "?p_datetime=sdfg"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_datetime")).toEqual("Must be in the following format: YYYY-mm-ddTHH:MM:ssZ"); + }); + + it("checkDatetimeInvalid3", async () => { + const qs = "?p_datetime=2022-09-15T00:00:00+01Z"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_datetime")).toEqual("Must be in the following format: YYYY-mm-ddTHH:MM:ssZ"); + }); + + it("checkDatetimeValid", async () => { + const qs = "?p_datetime=2022-09-15T00:00:01Z"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_datetime")).toBeUndefined(); + }); + + it("checkDatetimeValid2", async () => { + const qs = "?p_datetime=2022-09-15T00:00:00Z"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_datetime")).toBeUndefined(); + }); + + it("checkDateInvalid", async () => { + const qs = "?p_dateonly=2022-13-15"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_dateonly")).toEqual("Invalid date"); + }); + + it("checkDateInvalid3", async () => { + const qs = "?p_dateonly=2022-09-15T00:0f0:00.0Z"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_dateonly")).toEqual("Invalid date"); + }); + + it("checkDateValid", async () => { + const qs = "?p_dateonly=2022-09-15"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_dateonly")).toBeUndefined(); + }); + + it("checkRegexInvalid", async () => { + const qs = "?p_regex=123765"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_regex")).toBeTruthy(); + }); + + it("checkRegexValid", async () => { + const qs = "?p_regex=%2B919367788755"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_regex")).toBeUndefined(); + }); + + it("checkEmailInvalid", async () => { + const qs = "?p_email=asfdgsdf"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); + }); + + it("checkEmailInvalid2", async () => { + const qs = "?p_email=asfdgsdf@gmail"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); + }); + + it("checkEmailInvalid3", async () => { + const qs = "?p_email=@gmail.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_email")).toEqual("Invalid email"); + }); + + it("checkEmailValid", async () => { + const qs = "?p_email=sdfg@gmail.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_email")).toBeUndefined(); + }); + + it("checkUuidInvalid", async () => { + const qs = "?p_uuid=f31f890-044b-11ee-be56-0242ac120002"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_uuid")).toEqual("Invalid uuid"); + }); + + it("checkUuidInvalid2", async () => { + const qs = "?p_uuid=asdf-sdfg-dsfg-sfdg"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_uuid")).toEqual("Invalid uuid"); + }); + + it("checkUuidValid", async () => { + const qs = "?p_uuid=f31f8b90-044b-11ee-be56-0242ac120002"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_uuid")).toBeUndefined(); + }); + + it("checkUuidValid2", async () => { + const qs = "?p_uuid=f5f26194-0b07-45a4-9a85-94d3db01e7a5"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_uuid")).toBeUndefined(); + }); + + it("checkHostnameInvalid", async () => { + const qs = "?p_hostname=.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_hostname")).toEqual("Invalid"); + }); + + it("checkHostnameValid", async () => { + const qs = "?p_hostname=cloudflare.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_hostname")).toBeUndefined(); + }); + + it("checkHostnameValid2", async () => { + const qs = "?p_hostname=radar.cloudflare.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_hostname")).toBeUndefined(); + }); + + it("checkIpv4Invalid", async () => { + const qs = "?p_ipv4=asdfrre.wer.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv4")).toEqual("Invalid ip"); + }); + + it("checkIpv4Invalid2", async () => { + const qs = "?p_ipv4=2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv4")).toEqual("Invalid ip"); + }); + + it("checkIpv4Valid", async () => { + const qs = "?p_ipv4=1.1.1.1"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv4")).toBeUndefined(); + }); + + it("checkIpv6Invalid", async () => { + const qs = "?p_ipv6=asdfrre.wer.com"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv6")).toEqual("Invalid ip"); + }); + + it("checkIpv6Invalid2", async () => { + const qs = "?p_ipv6=1.1.1.1"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv6")).toEqual("Invalid ip"); + }); + + it("checkIpv6Valid", async () => { + const qs = "?p_ipv6=2001:0db8:85a3:0000:0000:8a2e:0370:7336"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_ipv6")).toBeUndefined(); + }); + + it("checkDateArrayInvalid", async () => { + const qs = "?p_array_dates=asadasd"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_array_dates")).toEqual("Invalid date"); + }); + + it("checkDateArrayValid", async () => { + const qs = "?p_array_dates=2023-01-01"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_array_dates")).toBeUndefined(); + }); + + it("checkDateArrayValid2", async () => { + const qs = "?p_array_dates=2023-01-01&p_array_dates=2023-01-02"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_array_dates")).toBeUndefined(); + }); + + it("checkOptionalMissing", async () => { + const qs = "?"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_optional")).toBeUndefined(); + }); + + it("checkOptionalInvalid", async () => { + const qs = "?p_optional=asfdasd"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_optional")).toEqual("Expected number, received nan"); + }); + + it("checkOptionalValid", async () => { + const qs = "?p_optional=32"; + const request = await todoRouter.fetch(buildRequest({ method: "GET", path: `/todos${qs}` })); + const resp = await request.json(); + + expect(findError(resp.errors, "p_optional")).toBeUndefined(); + }); }); describe("bodyParametersValidation", () => { - it("requiredFieldTitle", async () => { - const request = await todoRouter.fetch( - buildRequest({ - method: "POST", - path: "/todos", - json: () => { - return {}; - }, - }), - ); - const resp = await request.json(); - - expect(request.status).toEqual(400); - - // the current body implementation only validates 1 field at time - expect(findError(resp.errors, "title")).toEqual("Required"); - }); - - it("requiredFieldTipe", async () => { - const request = await todoRouter.fetch( - buildRequest({ - method: "POST", - path: "/todos", - json: () => { - return { - title: "my todo", - }; - }, - }), - ); - const resp = await request.json(); - - expect(request.status).toEqual(400); - - // the current body implementation only validates 1 field at time - expect(findError(resp.errors, "type")).toEqual("Required"); - }); - - it("validRequest", async () => { - const request = await todoRouter.fetch( - buildRequest({ - method: "POST", - path: "/todos", - json: () => { - return { - title: "my todo", - type: "nextWeek", - }; - }, - }), - {}, - {}, - ); - const resp = await request.json(); - - expect(request.status).toEqual(200); - - expect(resp).toEqual({ - todo: { title: "my todo", type: "nextWeek" }, - }); - }); - - it("validRequestWithOptionalParameters", async () => { - const request = await todoRouter.fetch( - buildRequest({ - method: "POST", - path: "/todos", - json: () => { - return { - title: "my todo", - description: "this will be done", - type: "nextWeek", - }; - }, - }), - {}, - {}, - ); - const resp = await request.json(); - - //expect(request.status).toEqual(200) - - expect(resp).toEqual({ - todo: { - title: "my todo", - description: "this will be done", - type: "nextWeek", - }, - }); - }); + it("requiredFieldTitle", async () => { + const request = await todoRouter.fetch( + buildRequest({ + method: "POST", + path: "/todos", + json: () => { + return {}; + }, + }), + ); + const resp = await request.json(); + + expect(request.status).toEqual(400); + + // the current body implementation only validates 1 field at time + expect(findError(resp.errors, "title")).toEqual("Required"); + }); + + it("requiredFieldTipe", async () => { + const request = await todoRouter.fetch( + buildRequest({ + method: "POST", + path: "/todos", + json: () => { + return { + title: "my todo", + }; + }, + }), + ); + const resp = await request.json(); + + expect(request.status).toEqual(400); + + // the current body implementation only validates 1 field at time + expect(findError(resp.errors, "type")).toEqual("Required"); + }); + + it("validRequest", async () => { + const request = await todoRouter.fetch( + buildRequest({ + method: "POST", + path: "/todos", + json: () => { + return { + title: "my todo", + type: "nextWeek", + }; + }, + }), + {}, + {}, + ); + const resp = await request.json(); + + expect(request.status).toEqual(200); + + expect(resp).toEqual({ + todo: { title: "my todo", type: "nextWeek" }, + }); + }); + + it("validRequestWithOptionalParameters", async () => { + const request = await todoRouter.fetch( + buildRequest({ + method: "POST", + path: "/todos", + json: () => { + return { + title: "my todo", + description: "this will be done", + type: "nextWeek", + }; + }, + }), + {}, + {}, + ); + const resp = await request.json(); + + //expect(request.status).toEqual(200) + + expect(resp).toEqual({ + todo: { + title: "my todo", + description: "this will be done", + type: "nextWeek", + }, + }); + }); }); diff --git a/tests/integration/router-options.test.ts b/tests/integration/router-options.test.ts index c434aa8..fc94630 100644 --- a/tests/integration/router-options.test.ts +++ b/tests/integration/router-options.test.ts @@ -5,108 +5,98 @@ import { OpenAPIRoute } from "../../src/route"; import { buildRequest } from "../utils"; class EndpointWithoutOperationId extends OpenAPIRoute { - schema = { - summary: "Get a single ToDo", - responses: {}, - }; - - async handle(request: Request, env: any, context: any) { - return { - msg: "EndpointWithoutOperationId", - }; - } + schema = { + summary: "Get a single ToDo", + responses: {}, + }; + + async handle(request: Request, env: any, context: any) { + return { + msg: "EndpointWithoutOperationId", + }; + } } class EndpointWithOperationId extends OpenAPIRoute { - schema = { - responses: {}, - operationId: "get_my_todo", - summary: "Get a single ToDo", - }; - - async handle(request: Request, env: any, context: any) { - return { - msg: "EndpointWithOperationId", - }; - } + schema = { + responses: {}, + operationId: "get_my_todo", + summary: "Get a single ToDo", + }; + + async handle(request: Request, env: any, context: any) { + return { + msg: "EndpointWithOperationId", + }; + } } describe("routerOptions", () => { - it("generate operation ids false", async () => { - const t = () => { - const router = fromIttyRouter(AutoRouter(), { - generateOperationIds: false, - }); - router.get("/todo", EndpointWithoutOperationId); - }; - - expect(t).toThrow("Route /todo don't have operationId set!"); - }); - - it("generate operation ids true and unset", async () => { - const routerTrue = fromIttyRouter(AutoRouter(), { - generateOperationIds: true, - }); - routerTrue.get("/todo", EndpointWithoutOperationId); - - if (routerTrue.schema.paths?.["/todo"]?.get) { - expect(routerTrue.schema.paths["/todo"].get.operationId).toEqual( - "get_EndpointWithoutOperationId", - ); - } else { - throw new Error("/todo not found in schema"); - } - - const routerUnset = fromIttyRouter(AutoRouter()); - routerUnset.get("/todo", EndpointWithoutOperationId); - - if (routerUnset.schema.paths?.["/todo"]?.get) { - expect(routerUnset.schema.paths["/todo"].get.operationId).toEqual( - "get_EndpointWithoutOperationId", - ); - } else { - throw new Error("/todo not found in schema"); - } - }); - - it("generate operation ids true on endpoint with operation id", async () => { - const router = fromIttyRouter(AutoRouter(), { - generateOperationIds: true, - }); - router.get("/todo", EndpointWithOperationId); - - if (router.schema.paths?.["/todo"]?.get) { - expect(router.schema.paths["/todo"].get.operationId).toEqual( - "get_my_todo", - ); - } else { - throw new Error("/todo not found in schema"); - } - }); - - it("with base empty", async () => { - const router = fromIttyRouter(AutoRouter()); - router.get("/todo", EndpointWithOperationId); - - const request = await router.fetch( - buildRequest({ method: "GET", path: "/todo" }), - ); - const resp = await request.json(); - - expect(resp.msg).toEqual("EndpointWithOperationId"); - }); - - it("with base defined", async () => { - const router = fromIttyRouter(AutoRouter({ base: "/api" }), { - base: "/api", - }); - router.get("/todo", EndpointWithOperationId); - - const request = await router.fetch( - buildRequest({ method: "GET", path: "/api/todo" }), - ); - const resp = await request.json(); - - expect(resp.msg).toEqual("EndpointWithOperationId"); - }); + it("generate operation ids false", async () => { + const t = () => { + const router = fromIttyRouter(AutoRouter(), { + generateOperationIds: false, + }); + router.get("/todo", EndpointWithoutOperationId); + }; + + expect(t).toThrow("Route /todo don't have operationId set!"); + }); + + it("generate operation ids true and unset", async () => { + const routerTrue = fromIttyRouter(AutoRouter(), { + generateOperationIds: true, + }); + routerTrue.get("/todo", EndpointWithoutOperationId); + + if (routerTrue.schema.paths?.["/todo"]?.get) { + expect(routerTrue.schema.paths["/todo"].get.operationId).toEqual("get_EndpointWithoutOperationId"); + } else { + throw new Error("/todo not found in schema"); + } + + const routerUnset = fromIttyRouter(AutoRouter()); + routerUnset.get("/todo", EndpointWithoutOperationId); + + if (routerUnset.schema.paths?.["/todo"]?.get) { + expect(routerUnset.schema.paths["/todo"].get.operationId).toEqual("get_EndpointWithoutOperationId"); + } else { + throw new Error("/todo not found in schema"); + } + }); + + it("generate operation ids true on endpoint with operation id", async () => { + const router = fromIttyRouter(AutoRouter(), { + generateOperationIds: true, + }); + router.get("/todo", EndpointWithOperationId); + + if (router.schema.paths?.["/todo"]?.get) { + expect(router.schema.paths["/todo"].get.operationId).toEqual("get_my_todo"); + } else { + throw new Error("/todo not found in schema"); + } + }); + + it("with base empty", async () => { + const router = fromIttyRouter(AutoRouter()); + router.get("/todo", EndpointWithOperationId); + + const request = await router.fetch(buildRequest({ method: "GET", path: "/todo" })); + const resp = await request.json(); + + expect(resp.msg).toEqual("EndpointWithOperationId"); + }); + + it("with base defined", async () => { + const router = fromIttyRouter(AutoRouter({ base: "/api" }), { + base: "/api", + }); + router.get("/todo", EndpointWithOperationId); + + const request = await router.fetch(buildRequest({ method: "GET", path: "/api/todo" })); + const resp = await request.json(); + + expect(resp.msg).toEqual("EndpointWithOperationId"); + }); }); diff --git a/tests/integration/zod.ts b/tests/integration/zod.ts index f4abfa8..546b474 100644 --- a/tests/integration/zod.ts +++ b/tests/integration/zod.ts @@ -8,71 +8,69 @@ import { buildRequest } from "../utils"; const zodRouter = fromIttyRouter(AutoRouter()); class ToDoGet extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "Get a single ToDo", - request: { - params: z.object({ - id: z.number(), - }), - body: { - content: { - "application/json": { - schema: z.object({ - title: z.string(), - description: z.string(), //.optional(), - type: z.nativeEnum({ - nextWeek: "nextWeek", - nextMonth: "nextMonth", - }), - }), - }, - }, - }, - }, - responses: { - "200": { - description: "example", - content: { - "application/json": { - schema: { - todo: { - lorem: String, - ipsum: String, - }, - }, - }, - }, - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "Get a single ToDo", + request: { + params: z.object({ + id: z.number(), + }), + body: { + content: { + "application/json": { + schema: z.object({ + title: z.string(), + description: z.string(), //.optional(), + type: z.nativeEnum({ + nextWeek: "nextWeek", + nextMonth: "nextMonth", + }), + }), + }, + }, + }, + }, + responses: { + "200": { + description: "example", + content: { + "application/json": { + schema: { + todo: { + lorem: String, + ipsum: String, + }, + }, + }, + }, + }, + }, + }; - async handle(request: Request, env: any, context: any) { - return { - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }; - } + async handle(request: Request, env: any, context: any) { + return { + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }; + } } zodRouter.put("/todo/:id", ToDoGet); describe("zod validations", () => { - it("simpleSuccessfulCall", async () => { - const request = await zodRouter.fetch( - buildRequest({ method: "PUT", path: "/todo/1" }), - ); + it("simpleSuccessfulCall", async () => { + const request = await zodRouter.fetch(buildRequest({ method: "PUT", path: "/todo/1" })); - const resp = await request.json(); + const resp = await request.json(); - expect(request.status).toEqual(200); - expect(resp).toEqual({ - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }); - }); + expect(request.status).toEqual(200); + expect(resp).toEqual({ + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }); + }); }); diff --git a/tests/router.ts b/tests/router.ts index a083449..aa53907 100644 --- a/tests/router.ts +++ b/tests/router.ts @@ -4,262 +4,261 @@ import { OpenAPIRoute, extendZodWithOpenApi } from "../src"; import { fromIttyRouter } from "../src/adapters/ittyRouter"; import { contentJson } from "../src/contentTypes"; import { - Bool, - DateOnly, - DateTime, - Email, - Enumeration, - Hostname, - Int, - Ipv4, - Ipv6, - Num, - Regex, - Str, - Uuid, + Bool, + DateOnly, + DateTime, + Email, + Enumeration, + Hostname, + Int, + Ipv4, + Ipv6, + Num, + Regex, + Str, + Uuid, } from "../src/parameters"; extendZodWithOpenApi(z); export class ToDoList extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "List all ToDos", - request: { - query: z.object({ - p_array_dates: z.string().date().array(), - p_number: z.number(), - p_string: z.string(), - p_boolean: z.boolean(), - p_int: Int(), - p_num: Num(), - p_str: Str(), - p_bool: Bool(), - p_enumeration: Enumeration({ - values: { - json: "ENUM_JSON", - csv: "ENUM_CSV", - }, - }), - p_enumeration_insensitive: Enumeration({ - values: { - json: "json", - csv: "csv", - }, - enumCaseSensitive: false, - }), - p_datetime: DateTime(), - p_dateonly: DateOnly(), - p_regex: Regex({ - pattern: - /^[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}$/, - }), - p_email: Email(), - p_uuid: Uuid(), - p_hostname: Hostname(), - p_ipv4: Ipv4(), - p_ipv6: Ipv6(), - p_optional: z.number().optional(), - }), - }, - responses: { - "200": { - description: "example", - ...contentJson({ - params: {}, - results: ["lorem"], - }), - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "List all ToDos", + request: { + query: z.object({ + p_array_dates: z.string().date().array(), + p_number: z.number(), + p_string: z.string(), + p_boolean: z.boolean(), + p_int: Int(), + p_num: Num(), + p_str: Str(), + p_bool: Bool(), + p_enumeration: Enumeration({ + values: { + json: "ENUM_JSON", + csv: "ENUM_CSV", + }, + }), + p_enumeration_insensitive: Enumeration({ + values: { + json: "json", + csv: "csv", + }, + enumCaseSensitive: false, + }), + p_datetime: DateTime(), + p_dateonly: DateOnly(), + p_regex: Regex({ + pattern: /^[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}$/, + }), + p_email: Email(), + p_uuid: Uuid(), + p_hostname: Hostname(), + p_ipv4: Ipv4(), + p_ipv6: Ipv6(), + p_optional: z.number().optional(), + }), + }, + responses: { + "200": { + description: "example", + ...contentJson({ + params: {}, + results: ["lorem"], + }), + }, + }, + }; - async handle(request: Request, env: any, context: any) { - const data = await this.getValidatedData(); + async handle(request: Request, env: any, context: any) { + const data = await this.getValidatedData(); - return { - params: data, - results: ["lorem", "ipsum"], - }; - } + return { + params: data, + results: ["lorem", "ipsum"], + }; + } } export class ToDoGet extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "Get a single ToDo", - request: { - params: z.object({ - id: Num(), - }), - }, - responses: { - "200": { - description: "Successful Response", - content: { - "application/json": { - schema: z.object({ - todo: z.object({ - lorem: z.string(), - ipsum: Str(), - }), - }), - }, - }, - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "Get a single ToDo", + request: { + params: z.object({ + id: Num(), + }), + }, + responses: { + "200": { + description: "Successful Response", + content: { + "application/json": { + schema: z.object({ + todo: z.object({ + lorem: z.string(), + ipsum: Str(), + }), + }), + }, + }, + }, + }, + }; - async handle(request: Request, env: any, context: any) { - return { - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }; - } + async handle(request: Request, env: any, context: any) { + return { + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }; + } } export class ContentTypeGet extends OpenAPIRoute { - schema = { - responses: { - "200": { - description: "Successful Response", - content: { - "text/csv": { - schema: z.string(), - }, - }, - }, - }, - }; + schema = { + responses: { + "200": { + description: "Successful Response", + content: { + "text/csv": { + schema: z.string(), + }, + }, + }, + }, + }; - async handle(request: Request, env: any, context: any) { - return { - todo: { - lorem: "lorem", - ipsum: "ipsum", - }, - }; - } + async handle(request: Request, env: any, context: any) { + return { + todo: { + lorem: "lorem", + ipsum: "ipsum", + }, + }; + } } export class ToDoCreate extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "Create a new ToDo", - request: { - body: { - content: { - "application/json": { - schema: z.object({ - title: Str(), - description: Str({ required: false }), - type: Enumeration({ - values: { - nextWeek: "nextWeek", - nextMonth: "nextMonth", - }, - }), - }), - }, - }, - }, - }, - responses: { - "200": { - description: "example", - content: { - "application/json": { - schema: z.object({ - todo: z.object({ - title: Str(), - description: Str(), - type: Str(), - }), - }), - }, - }, - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "Create a new ToDo", + request: { + body: { + content: { + "application/json": { + schema: z.object({ + title: Str(), + description: Str({ required: false }), + type: Enumeration({ + values: { + nextWeek: "nextWeek", + nextMonth: "nextMonth", + }, + }), + }), + }, + }, + }, + }, + responses: { + "200": { + description: "example", + content: { + "application/json": { + schema: z.object({ + todo: z.object({ + title: Str(), + description: Str(), + type: Str(), + }), + }), + }, + }, + }, + }, + }; - async handle(request: Request, env: any, context: any) { - const data = await this.getValidatedData(); + async handle(request: Request, env: any, context: any) { + const data = await this.getValidatedData(); - return { - todo: data.body, - }; - } + return { + todo: data.body, + }; + } } const query = z.object({ - p_int: Int(), - p_num: Num(), - p_str: Str(), - p_arrstr: z.array(Str()), - p_bool: Bool(), - p_enumeration: Enumeration({ - values: { - json: "ENUM_JSON", - csv: "ENUM_CSV", - }, - }), - p_enumeration_insensitive: Enumeration({ - values: { - json: "json", - csv: "csv", - }, - enumCaseSensitive: false, - }), - p_datetime: DateTime(), - p_regex: Regex({ - pattern: /^[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}$/, - }), - p_email: Email(), - p_uuid: Uuid(), + p_int: Int(), + p_num: Num(), + p_str: Str(), + p_arrstr: z.array(Str()), + p_bool: Bool(), + p_enumeration: Enumeration({ + values: { + json: "ENUM_JSON", + csv: "ENUM_CSV", + }, + }), + p_enumeration_insensitive: Enumeration({ + values: { + json: "json", + csv: "csv", + }, + enumCaseSensitive: false, + }), + p_datetime: DateTime(), + p_regex: Regex({ + pattern: /^[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}$/, + }), + p_email: Email(), + p_uuid: Uuid(), - p_ipv4: Ipv4(), - p_ipv6: Ipv6(), - p_optional: Int({ - required: false, - }), + p_ipv4: Ipv4(), + p_ipv6: Ipv6(), + p_optional: Int({ + required: false, + }), }); export class ToDoCreateTyped extends OpenAPIRoute { - schema = { - tags: ["ToDo"], - summary: "List all ToDos", - request: { - query: query, - headers: z.object({ - p_hostname: Hostname(), - }), - body: { - content: { - "application/json": { - schema: z.object({ - title: z.string(), - description: z.string().optional(), - type: z.enum(["nextWeek", "nextMoth"]), - }), - }, - }, - }, - }, - responses: { - "200": { - description: "example", - ...contentJson({ - params: {}, - results: ["lorem"], - }), - }, - }, - }; + schema = { + tags: ["ToDo"], + summary: "List all ToDos", + request: { + query: query, + headers: z.object({ + p_hostname: Hostname(), + }), + body: { + content: { + "application/json": { + schema: z.object({ + title: z.string(), + description: z.string().optional(), + type: z.enum(["nextWeek", "nextMoth"]), + }), + }, + }, + }, + }, + responses: { + "200": { + description: "example", + ...contentJson({ + params: {}, + results: ["lorem"], + }), + }, + }, + }; - async handle(request: Request, env: any, context: any) { - return {}; - } + async handle(request: Request, env: any, context: any) { + return {}; + } } export const todoRouter = fromIttyRouter(AutoRouter(), { openapiVersion: "3" }); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 80f6091..73f4178 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "moduleResolution": "bundler", - "types": ["@cloudflare/vitest-pool-workers"] - }, - "include": ["./**/*.ts", "./bindings.d.ts"] + "extends": "../tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler", + "types": ["@cloudflare/vitest-pool-workers"] + }, + "include": ["./**/*.ts", "./bindings.d.ts"] } diff --git a/tests/utils.ts b/tests/utils.ts index 8d63aca..4305ee9 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,14 +1,14 @@ export const buildRequest = ({ method = "GET", path = "/", ...other }) => ({ - method: method.toUpperCase(), - path, - url: `https://example.com${path}`, - ...other, + method: method.toUpperCase(), + path, + url: `https://example.com${path}`, + ...other, }); export function findError(errors: any, field: any) { - for (const error of errors) { - if (error.path.includes(field)) { - return error.message; - } - } + for (const error of errors) { + if (error.path.includes(field)) { + return error.message; + } + } } diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts index 4f94cd4..eb366b8 100644 --- a/tests/vitest.config.ts +++ b/tests/vitest.config.ts @@ -1,13 +1,13 @@ import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { - configPath: "./wrangler.toml", - }, - }, - }, - }, + test: { + poolOptions: { + workers: { + wrangler: { + configPath: "./wrangler.toml", + }, + }, + }, + }, });