diff --git a/package-lock.json b/package-lock.json index ee32628..9a4a561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@asteasolutions/zod-to-openapi": "^6.4.0", - "itty-router": "4.0.26", "js-yaml": "^4.1.0", "openapi3-ts": "^4.1.2", "zod": "^3.21.4" @@ -31,6 +30,7 @@ "eslint-config-prettier": "^8.8.0", "husky": "^7.0.2", "isomorphic-fetch": "^3.0.0", + "itty-router": "^5.0.17", "jest": "^29.0.0", "jest-openapi": "^0.14.2", "pinst": "^2.1.6", @@ -4180,9 +4180,10 @@ } }, "node_modules/itty-router": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-4.0.26.tgz", - "integrity": "sha512-GBcmhxQRvIQ7fuzXPlvfeQcN6O7VGhctzdG9WHIcEcaRYH4FvDpMxuSWRIAxQrIM+nxtfxK0/bR78ATvb4LC5Q==" + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.17.tgz", + "integrity": "sha512-ZHnPI0OOyTTLuNp2FdciejYaK4Wl3ZV3O0yEm8njOGggh/k/ek3BL7X2I5YsCOfc5vLhIJgj3Z4pUtLs6k9Ucg==", + "dev": true }, "node_modules/jest": { "version": "29.0.0", @@ -10702,9 +10703,10 @@ } }, "itty-router": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-4.0.26.tgz", - "integrity": "sha512-GBcmhxQRvIQ7fuzXPlvfeQcN6O7VGhctzdG9WHIcEcaRYH4FvDpMxuSWRIAxQrIM+nxtfxK0/bR78ATvb4LC5Q==" + "version": "5.0.17", + "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.17.tgz", + "integrity": "sha512-ZHnPI0OOyTTLuNp2FdciejYaK4Wl3ZV3O0yEm8njOGggh/k/ek3BL7X2I5YsCOfc5vLhIJgj3Z4pUtLs6k9Ucg==", + "dev": true }, "jest": { "version": "29.0.0", diff --git a/package.json b/package.json index 7448119..46b1a26 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "eslint-config-prettier": "^8.8.0", "husky": "^7.0.2", "isomorphic-fetch": "^3.0.0", + "itty-router": "^5.0.17", "jest": "^29.0.0", "jest-openapi": "^0.14.2", "pinst": "^2.1.6", @@ -93,7 +94,6 @@ }, "dependencies": { "@asteasolutions/zod-to-openapi": "^6.4.0", - "itty-router": "4.0.26", "js-yaml": "^4.1.0", "openapi3-ts": "^4.1.2", "zod": "^3.21.4" diff --git a/src/openapi.ts b/src/openapi.ts index d3e8fe0..cf3827b 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -1,12 +1,5 @@ import { getReDocUI, getSwaggerUI } from './ui' -import { - IRequest, - RouteHandler, - Router, - RouterType, - UniversalRoute, -} from 'itty-router' -import { APIType, AuthType, RouterOptions, SchemaVersion } from './types' +import { RouterOptions } from './types' import { OpenApiGeneratorV3, OpenApiGeneratorV31, @@ -15,283 +8,271 @@ import { import { OpenAPIRegistryMerger } from './zod/registry' import { z } from 'zod' import { OpenAPIObject } from 'openapi3-ts/oas31' -import { OpenAPIRoute } from './route' import yaml from 'js-yaml' -export type Route = < - RequestType = IRequest, - Args extends any[] = any[], - RT = OpenAPIRouterType, ->( - path: string, - ...handlers: (RouteHandler | OpenAPIRouterType | any)[] // TODO: fix this any to be instance of OpenAPIRoute -) => RT - -export type OpenAPIRouterType = { - original: RouterType - schema: OpenAPIObject +export type OpenAPIRouterType = { + original: M + options: RouterOptions + registry: OpenAPIRegistryMerger +} + +export class OpenAPIHandler { + router: any + options: RouterOptions registry: OpenAPIRegistryMerger -} & RouterType - -// helper function to detect equality in types (used to detect custom Request on router) -type Equal = - (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 - ? true - : false - -export function OpenAPIRouter< - RequestType = IRequest, - Args extends any[] = any[], - RouteType = Equal extends true - ? Route - : UniversalRoute, ->(options?: RouterOptions): OpenAPIRouterType { - const registry: OpenAPIRegistryMerger = new OpenAPIRegistryMerger() - - const getGeneratedSchema = (): OpenAPIObject => { + + 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(): OpenAPIObject { let openapiGenerator: any = OpenApiGeneratorV31 - if (options?.openapiVersion === '3') openapiGenerator = OpenApiGeneratorV3 + if (this.options?.openapiVersion === '3') + openapiGenerator = OpenApiGeneratorV3 - const generator = new openapiGenerator(registry.definitions) + const generator = new openapiGenerator(this.registry.definitions) return generator.generateDocument({ - openapi: options?.openapiVersion === '3' ? '3.0.3' : '3.1.0', + openapi: this.options?.openapiVersion === '3' ? '3.0.3' : '3.1.0', info: { - version: options?.schema?.info?.version || '1.0.0', - title: options?.schema?.info?.title || 'OpenAPI', - ...options?.schema?.info, + version: this.options?.schema?.info?.version || '1.0.0', + title: this.options?.schema?.info?.title || 'OpenAPI', + ...this.options?.schema?.info, }, - ...options?.schema, + ...this.options?.schema, }) } - const routerToUse = options?.baseRouter || Router + registerNestedRouter(params: { + method: string + path: string + nestedRouter: any + }) { + this.registry.merge(params.nestedRouter.registry) - const router = routerToUse({ base: options?.base, routes: options?.routes }) + return [params.nestedRouter.fetch] + } - const routerProxy: OpenAPIRouterType = new Proxy(router, { - // @ts-expect-error (we're adding an expected prop "path" to the get) - get: (target: any, prop: string, receiver: object, path: string) => { - if (prop === 'original') { - return router - } - if (prop === 'schema') { - return getGeneratedSchema() + 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) + + // @ts-ignore + let schema: RouteConfig = undefined + // @ts-ignore + let operationId: string = undefined + + for (const handler of params.handlers) { + if (handler.name) { + operationId = `${params.method}_${handler.name}` } - if (prop === 'registry') { - return registry + + if (handler.getSchemaZod) { + schema = handler.getSchemaZod() + break } + } - return ( - route: string, - ...handlers: RouteHandler[] & - (typeof OpenAPIRoute)[] & - OpenAPIRouterType[] - ) => { - if (prop !== 'handle') { - if (handlers.length === 1 && handlers[0].registry) { - const nestedRouter = handlers[0] + if (operationId === undefined) { + operationId = `${params.method}_${parsedRoute.replaceAll('/', '_')}` + } - // Merge inner router definitions into outer router - registry.merge(nestedRouter.registry) - } else if (prop !== 'all') { - const parsedRoute = ((options?.base || '') + route) - .replaceAll(/\/+(\/|$)/g, '$1') // strip double & trailing splash - .replaceAll(/:(\w+)/g, '{$1}') // convert parameters into openapi compliant - - // @ts-ignore - let schema: RouteConfig = undefined - // @ts-ignore - let operationId: string = undefined - - for (const handler of handlers) { - if (handler.name) { - operationId = `${prop.toString()}_${handler.name}` - } - - if (handler.getSchemaZod) { - schema = handler.getSchemaZod() - break - } - } - - if (operationId === undefined) { - operationId = `${prop.toString()}_${route.replaceAll('/', '_')}` - } - - if (schema === undefined) { - // No schema for this route, try to guest the parameters - - // @ts-ignore - schema = { - operationId: operationId, - responses: { - 200: { - description: 'Object with user data.', - }, - }, - } - - const params = ((options?.base || '') + route).match(/:(\w+)/g) - if (params) { - schema.request = { - // TODO: make sure this works - params: z.object( - params.reduce( - // matched parameters start with ':' so replace the first occurrence with nothing - (obj, item) => - Object.assign(obj, { - [item.replace(':', '')]: z.string(), - }), - {} - ) - ), - } - } - } else { - // Schema was provided in the endpoint - if (!schema.operationId) { - if ( - options?.generateOperationIds === false && - !schema.operationId - ) { - throw new Error(`Route ${route} don't have operationId set!`) - } - - schema.operationId = operationId - } - } - - registry.registerPath({ - ...schema, - // @ts-ignore - method: prop.toString(), - path: parsedRoute, - }) - } - } + if (schema === undefined) { + // No schema for this route, try to guest the parameters - // console.log(`${prop.toString()}`) - // @ts-ignore - // console.log(Reflect.routes) - return Reflect.get( - target, - prop, - receiver - // path - )( - route, - ...handlers.map((handler: any) => { - if (handler.handle) { - // Nested router - return handler.handle - } - - if (handler.isRoute) { - // console.log(handler) - return (...params: any[]) => - new handler({ - // raiseUnknownParameters: openapiConfig.raiseUnknownParameters, TODO - skipValidation: options?.skipValidation, - }).execute(...params) - } - - // console.log(handler()) - return handler - }) - ) + // @ts-ignore + schema = { + operationId: operationId, + responses: { + 200: { + description: 'Object with user data.', + }, + }, } - }, - }) - if (options?.docs_url !== null && options?.openapi_url !== null) { - router.get(options?.docs_url || '/docs', () => { - return new Response( - getSwaggerUI( - (options?.base || '') + (options?.openapi_url || '/openapi.json') - ), - { - headers: { - 'content-type': 'text/html; charset=UTF-8', - }, - status: 200, - } + const parsedParams = ((this.options.base || '') + params.path).match( + /:(\w+)/g ) - }) - } - - if (options?.redoc_url !== null && options?.openapi_url !== null) { - router.get(options?.redoc_url || '/redocs', () => { - return new Response( - getReDocUI( - (options?.base || '') + (options?.openapi_url || '/openapi.json') - ), - { - headers: { - 'content-type': 'text/html; charset=UTF-8', - }, - status: 200, + if (parsedParams) { + schema.request = { + // TODO: make sure this works + params: z.object( + parsedParams.reduce( + // matched parameters start with ':' so replace the first occurrence with nothing + (obj, item) => + Object.assign(obj, { + [item.replace(':', '')]: 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!`) } - ) - }) - } - if (options?.openapi_url !== null) { - router.get(options?.openapi_url || '/openapi.json', () => { - return new Response(JSON.stringify(getGeneratedSchema()), { - headers: { - 'content-type': 'application/json;charset=UTF-8', - }, - status: 200, - }) + schema.operationId = operationId + } + } + + this.registry.registerPath({ + ...schema, + // @ts-ignore + method: params.method, + path: parsedRoute, }) - router.get( - (options?.openapi_url || '/openapi.json').replace('.json', '.yaml'), - () => { - return new Response(yaml.dump(getGeneratedSchema()), { - headers: { - 'content-type': 'text/yaml;charset=UTF-8', - }, - status: 200, - }) + return params.handlers.map((handler: any) => { + if (handler.isRoute) { + // console.log(handler) + return (...params: any[]) => + new handler({ + // raiseUnknownParameters: openapiConfig.raiseUnknownParameters, TODO + skipValidation: this.options?.skipValidation, + }).execute(...params) } - ) + + // console.log(handler()) + return handler + }) } - if (options?.aiPlugin && options?.openapi_url !== null) { - router.get('/.well-known/ai-plugin.json', (request: IRequest) => { - const schemaApi = { - type: APIType.OPENAPI, - has_user_authentication: false, - url: options?.openapi_url || '/openapi.json', - ...options?.aiPlugin?.api, - } + 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 === 'original') { + return this.router + } + if (prop === 'schema') { + return this.getGeneratedSchema() + } + if (prop === 'registry') { + return this.registry + } + + return undefined + } +} + +export class IttyRouterOpenAPIHandler extends OpenAPIHandler {} - // Check if schema path is relative - if (!schemaApi.url.startsWith('http')) { - // dynamically add the host - schemaApi.url = `https://${request.headers.get('host')}${schemaApi.url}` +export function fromIttyRouter( + router: M, + options?: RouterOptions +): M & OpenAPIRouterType & any { + 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 Response( - JSON.stringify({ - schema_version: SchemaVersion.V1, - auth: { - type: AuthType.NONE, - }, - ...options?.aiPlugin, - api: schemaApi, - }), - { - headers: { - 'content-type': 'application/json;charset=UTF-8', - }, - status: 200, + return (route: string, ...handlers: any[]) => { + if (prop !== 'fetch') { + if (handlers.length === 1 && handlers[0].registry) { + handlers = openapiRouter.registerNestedRouter({ + method: prop, + path: route, + nestedRouter: handlers[0], + }) + } else if (prop !== 'all') { + handlers = openapiRouter.registerRoute({ + method: prop, + path: route, + handlers: handlers, + }) + } } - ) - }) - } - return routerProxy + return Reflect.get(target, prop, ...args)(route, ...handlers) + } + }, + }) } diff --git a/src/types.ts b/src/types.ts index 316e766..c7ee951 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { RouteEntry } from 'itty-router' +import { IRequest, RouteEntry } from 'itty-router' import { z, AnyZodObject, ZodType } from 'zod' import { ResponseConfig, @@ -11,20 +11,19 @@ import { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/op import { HeadersObject as HeadersObject30 } from 'openapi3-ts/dist/model/openapi30' // @ts-ignore import { HeadersObject as HeadersObject31 } from 'openapi3-ts/dist/model/openapi31' +import { OpenAPIRoute } from './route' +import { OpenAPIRouterType } from './openapi' export interface RouterOptions { base?: string - routes?: RouteEntry[] schema?: Partial docs_url?: string redoc_url?: string openapi_url?: string - aiPlugin?: AIPlugin raiseUnknownParameters?: boolean generateOperationIds?: boolean openapiVersion?: '3' | '3.1' skipValidation?: boolean - baseRouter?: any } export declare type RouteParameter = { @@ -117,56 +116,6 @@ export interface RouteValidated { errors?: Record } -export enum SchemaVersion { - V1 = 'v1', -} - -export enum AuthType { - NONE = 'none', - OAUTH = 'oauth', - SERVICE_HTTP = 'service_http', - USER_HTTP = 'user_http', -} - -export enum APIType { - OPENAPI = 'openapi', -} - -export interface AIPlugin { - schema_version?: SchemaVersion | string - name_for_model: string - name_for_human: string - description_for_model: string - description_for_human: string - auth?: Auth - api?: API - logo_url: string - contact_email: string - legal_info_url: string - is_dev?: boolean -} - -export interface API { - type: APIType | string - url: string - has_user_authentication: boolean -} - -export interface Auth { - type: AuthType | string - authorization_type?: string - authorization_url?: string - client_url?: string - scope?: string - authorization_content_type?: string - verification_tokens?: VerificationTokens - instructions?: string -} - -export interface VerificationTokens { - openai: string -} - export type LegacyParameter = Z & (new (params?: ParameterType) => Z) diff --git a/tests/integration/nested-routers.test.ts b/tests/integration/nested-routers.test.ts index 0368175..9d73519 100644 --- a/tests/integration/nested-routers.test.ts +++ b/tests/integration/nested-routers.test.ts @@ -2,11 +2,15 @@ import 'isomorphic-fetch' import { OpenAPIRoute } from '../../src/route' import { Path } from '../../src/parameters' -import { OpenAPIRouter } from '../../src/openapi' +import { fromIttyRouter } from '../../src/openapi' import { buildRequest } from '../utils' import { jsonResp } from '../../src/utils' +import { AutoRouter } from 'itty-router' + +const innerRouter = fromIttyRouter(AutoRouter({ base: '/api/v1' }), { + base: '/api/v1', +}) -const innerRouter = OpenAPIRouter({ base: '/api/v1' }) class ToDoGet extends OpenAPIRoute { static schema = { tags: ['ToDo'], @@ -40,7 +44,7 @@ class ToDoGet extends OpenAPIRoute { innerRouter.get('/todo/:id', ToDoGet) innerRouter.all('*', () => jsonResp({ message: 'Not Found' }, { status: 404 })) -const router = OpenAPIRouter({ +const router = fromIttyRouter(AutoRouter(), { schema: { info: { title: 'Radar Worker API', @@ -54,7 +58,7 @@ router.all('*', () => new Response('Not Found.', { status: 404 })) describe('innerRouter', () => { it('simpleSuccessfulCall', async () => { - const request = await router.handle( + const request = await router.fetch( buildRequest({ method: 'GET', path: `/api/v1/todo/1` }) ) const resp = await request.json() @@ -69,7 +73,7 @@ describe('innerRouter', () => { }) it('innerCatchAll', async () => { - const request = await router.handle( + const request = await router.fetch( buildRequest({ method: 'GET', path: `/api/v1/asd` }) ) const resp = await request.json() @@ -79,7 +83,7 @@ describe('innerRouter', () => { }) it('outerCatchAll', async () => { - const request = await router.handle( + const request = await router.fetch( buildRequest({ method: 'GET', path: `/asd` }) ) const resp = await request.text() diff --git a/tests/integration/openapi-schema.test.ts b/tests/integration/openapi-schema.test.ts index 7e2442c..7a67f93 100644 --- a/tests/integration/openapi-schema.test.ts +++ b/tests/integration/openapi-schema.test.ts @@ -1,10 +1,12 @@ import 'isomorphic-fetch' import { buildRequest, findError } from '../utils' -import { ToDoList, todoRouter } from '../router' +import { ToDoGet, ToDoList, todoRouter } from '../router' +import { fromIttyRouter } from '../../src' +import { AutoRouter } from 'itty-router' describe('openapi schema', () => { test('custom content type', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: '/openapi.json' }) ) const resp = await request.json() @@ -19,4 +21,18 @@ describe('openapi schema', () => { }, }) }) + + 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() + + expect(Object.keys(resp.paths)[0]).toEqual('/api/todo') + }) }) diff --git a/tests/integration/parameters.test.ts b/tests/integration/parameters.test.ts index d24a6ba..2a02756 100644 --- a/tests/integration/parameters.test.ts +++ b/tests/integration/parameters.test.ts @@ -4,7 +4,7 @@ import { ToDoList, todoRouter } from '../router' describe('queryParametersValidation', () => { test('requiredFields', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: '/todos' }) ) const resp = await request.json() @@ -23,7 +23,7 @@ describe('queryParametersValidation', () => { test('checkNumberInvalid', async () => { const qs = '?p_number=asd' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -35,7 +35,7 @@ describe('queryParametersValidation', () => { test('checkNumberValidFloat', async () => { const qs = '?p_number=12.3' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -45,7 +45,7 @@ describe('queryParametersValidation', () => { test('checkNumberValidInteger', async () => { const qs = '?p_number=12' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -55,7 +55,7 @@ describe('queryParametersValidation', () => { test('checkStringValid', async () => { const qs = '?p_string=asd21_sa' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -65,7 +65,7 @@ describe('queryParametersValidation', () => { test('checkStringInvalidEmpty', async () => { const qs = '?p_string=' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -77,7 +77,7 @@ describe('queryParametersValidation', () => { test('checkBooleanInvalid', async () => { const qs = '?p_boolean=asd' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -89,7 +89,7 @@ describe('queryParametersValidation', () => { test('checkBooleanValid', async () => { const qs = '?p_boolean=false' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -99,7 +99,7 @@ describe('queryParametersValidation', () => { test('checkBooleanValidCaseInsensitive', async () => { const qs = '?p_boolean=TrUe' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -109,7 +109,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationSensitiveInvalid', async () => { const qs = '?p_enumeration=sfDase' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -121,7 +121,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationSensitiveInvalidCase', async () => { const qs = '?p_enumeration=Csv' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -133,7 +133,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationSensitiveValid', async () => { const qs = '?p_enumeration=csv' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -143,7 +143,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationInsensitiveInvalid', async () => { const qs = '?p_enumeration_insensitive=sfDase' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -155,7 +155,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationInsensitiveValidCase', async () => { const qs = '?p_enumeration_insensitive=Csv' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -165,7 +165,7 @@ describe('queryParametersValidation', () => { test('checkEnumerationInsensitiveValid', async () => { const qs = '?p_enumeration_insensitive=csv' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -175,7 +175,7 @@ describe('queryParametersValidation', () => { test('checkDatetimeInvalid', async () => { const qs = '?p_datetime=2023-13-01' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -187,7 +187,7 @@ describe('queryParametersValidation', () => { test('checkDatetimeInvalid2', async () => { const qs = '?p_datetime=sdfg' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -199,7 +199,7 @@ describe('queryParametersValidation', () => { test('checkDatetimeInvalid3', async () => { const qs = '?p_datetime=2022-09-15T00:00:00+01Z' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -211,7 +211,7 @@ describe('queryParametersValidation', () => { test('checkDatetimeValid', async () => { const qs = '?p_datetime=2022-09-15T00:00:01Z' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -221,7 +221,7 @@ describe('queryParametersValidation', () => { test('checkDatetimeValid2', async () => { const qs = '?p_datetime=2022-09-15T00:00:00Z' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -231,7 +231,7 @@ describe('queryParametersValidation', () => { test('checkDateInvalid', async () => { const qs = '?p_dateonly=2022-13-15' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -241,7 +241,7 @@ describe('queryParametersValidation', () => { test('checkDateInvalid3', async () => { const qs = '?p_dateonly=2022-09-15T00:0f0:00.0Z' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -251,7 +251,7 @@ describe('queryParametersValidation', () => { test('checkDateValid', async () => { const qs = '?p_dateonly=2022-09-15' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -261,7 +261,7 @@ describe('queryParametersValidation', () => { test('checkRegexInvalid', async () => { const qs = '?p_regex=123765' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -271,7 +271,7 @@ describe('queryParametersValidation', () => { test('checkRegexValid', async () => { const qs = '?p_regex=%2B919367788755' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -281,7 +281,7 @@ describe('queryParametersValidation', () => { test('checkEmailInvalid', async () => { const qs = '?p_email=asfdgsdf' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -291,7 +291,7 @@ describe('queryParametersValidation', () => { test('checkEmailInvalid2', async () => { const qs = '?p_email=asfdgsdf@gmail' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -301,7 +301,7 @@ describe('queryParametersValidation', () => { test('checkEmailInvalid3', async () => { const qs = '?p_email=@gmail.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -311,7 +311,7 @@ describe('queryParametersValidation', () => { test('checkEmailValid', async () => { const qs = '?p_email=sdfg@gmail.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -321,7 +321,7 @@ describe('queryParametersValidation', () => { test('checkUuidInvalid', async () => { const qs = '?p_uuid=f31f890-044b-11ee-be56-0242ac120002' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -331,7 +331,7 @@ describe('queryParametersValidation', () => { test('checkUuidInvalid2', async () => { const qs = '?p_uuid=asdf-sdfg-dsfg-sfdg' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -341,7 +341,7 @@ describe('queryParametersValidation', () => { test('checkUuidValid', async () => { const qs = '?p_uuid=f31f8b90-044b-11ee-be56-0242ac120002' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -351,7 +351,7 @@ describe('queryParametersValidation', () => { test('checkUuidValid2', async () => { const qs = '?p_uuid=f5f26194-0b07-45a4-9a85-94d3db01e7a5' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -361,7 +361,7 @@ describe('queryParametersValidation', () => { test('checkHostnameInvalid', async () => { const qs = '?p_hostname=.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -371,7 +371,7 @@ describe('queryParametersValidation', () => { test('checkHostnameValid', async () => { const qs = '?p_hostname=cloudflare.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -381,7 +381,7 @@ describe('queryParametersValidation', () => { test('checkHostnameValid2', async () => { const qs = '?p_hostname=radar.cloudflare.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -391,7 +391,7 @@ describe('queryParametersValidation', () => { test('checkIpv4Invalid', async () => { const qs = '?p_ipv4=asdfrre.wer.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -401,7 +401,7 @@ describe('queryParametersValidation', () => { test('checkIpv4Invalid2', async () => { const qs = '?p_ipv4=2001:0db8:85a3:0000:0000:8a2e:0370:7334' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -411,7 +411,7 @@ describe('queryParametersValidation', () => { test('checkIpv4Valid', async () => { const qs = '?p_ipv4=1.1.1.1' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -421,7 +421,7 @@ describe('queryParametersValidation', () => { test('checkIpv6Invalid', async () => { const qs = '?p_ipv6=asdfrre.wer.com' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -431,7 +431,7 @@ describe('queryParametersValidation', () => { test('checkIpv6Invalid2', async () => { const qs = '?p_ipv6=1.1.1.1' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -441,7 +441,7 @@ describe('queryParametersValidation', () => { test('checkIpv6Valid', async () => { const qs = '?p_ipv6=2001:0db8:85a3:0000:0000:8a2e:0370:7336' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -451,7 +451,7 @@ describe('queryParametersValidation', () => { test('checkOptionalMissing', async () => { const qs = '?' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -461,7 +461,7 @@ describe('queryParametersValidation', () => { test('checkOptionalInvalid', async () => { const qs = '?p_optional=asfdasd' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -473,7 +473,7 @@ describe('queryParametersValidation', () => { test('checkOptionalValid', async () => { const qs = '?p_optional=32' - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'GET', path: `/todos${qs}` }) ) const resp = await request.json() @@ -484,7 +484,7 @@ describe('queryParametersValidation', () => { describe('bodyParametersValidation', () => { test('requiredFieldTitle', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'POST', path: '/todos', @@ -502,7 +502,7 @@ describe('bodyParametersValidation', () => { }) test('requiredFieldTipe', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'POST', path: '/todos', @@ -522,7 +522,7 @@ describe('bodyParametersValidation', () => { }) test('validRequest', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'POST', path: '/todos', @@ -546,7 +546,7 @@ describe('bodyParametersValidation', () => { }) test('validRequestWithOptionalParameters', async () => { - const request = await todoRouter.handle( + const request = await todoRouter.fetch( buildRequest({ method: 'POST', path: '/todos', diff --git a/tests/integration/router-options.test.ts b/tests/integration/router-options.test.ts index 0190b6a..b31619c 100644 --- a/tests/integration/router-options.test.ts +++ b/tests/integration/router-options.test.ts @@ -1,9 +1,9 @@ import 'isomorphic-fetch' import { OpenAPIRoute } from '../../src/route' -import { OpenAPIRouter } from '../../src/openapi' import { buildRequest } from '../utils' -import { OpenAPIRouteSchema } from '../../src' +import { fromIttyRouter, OpenAPIRouteSchema } from '../../src' +import { AutoRouter } from 'itty-router' class EndpointWithoutOperationId extends OpenAPIRoute { static schema = { @@ -35,7 +35,7 @@ class EndpointWithOperationId extends OpenAPIRoute { describe('routerOptions', () => { it('generate operation ids false', async () => { const t = () => { - const router = OpenAPIRouter({ + const router = fromIttyRouter(AutoRouter(), { generateOperationIds: false, }) router.get('/todo', EndpointWithoutOperationId) @@ -45,7 +45,7 @@ describe('routerOptions', () => { }) it('generate operation ids true and unset', async () => { - const routerTrue = OpenAPIRouter({ + const routerTrue = fromIttyRouter(AutoRouter(), { generateOperationIds: true, }) routerTrue.get('/todo', EndpointWithoutOperationId) @@ -62,7 +62,7 @@ describe('routerOptions', () => { throw new Error('/todo not found in schema') } - const routerUnset = OpenAPIRouter() + const routerUnset = fromIttyRouter(AutoRouter()) routerUnset.get('/todo', EndpointWithoutOperationId) if ( @@ -79,7 +79,7 @@ describe('routerOptions', () => { }) it('generate operation ids true on endpoint with operation id', async () => { - const router = OpenAPIRouter({ + const router = fromIttyRouter(AutoRouter(), { generateOperationIds: true, }) router.get('/todo', EndpointWithOperationId) @@ -98,10 +98,10 @@ describe('routerOptions', () => { }) it('with base empty', async () => { - const router = OpenAPIRouter() + const router = fromIttyRouter(AutoRouter()) router.get('/todo', EndpointWithOperationId) - const request = await router.handle( + const request = await router.fetch( buildRequest({ method: 'GET', path: '/todo' }) ) const resp = await request.json() @@ -110,10 +110,12 @@ describe('routerOptions', () => { }) it('with base defined', async () => { - const router = OpenAPIRouter({ base: '/api' }) + const router = fromIttyRouter(AutoRouter({ base: '/api' }), { + base: '/api', + }) router.get('/todo', EndpointWithOperationId) - const request = await router.handle( + const request = await router.fetch( buildRequest({ method: 'GET', path: '/api/todo' }) ) const resp = await request.json() diff --git a/tests/integration/zod.ts b/tests/integration/zod.ts index 8fd26a4..292cbfe 100644 --- a/tests/integration/zod.ts +++ b/tests/integration/zod.ts @@ -2,11 +2,12 @@ import 'isomorphic-fetch' import { OpenAPIRoute } from '../../src/route' import { Path } from '../../src/parameters' -import { OpenAPIRouter } from '../../src/openapi' +import { fromIttyRouter } from '../../src/openapi' import { buildRequest } from '../utils' import { z } from 'zod' +import { AutoRouter } from 'itty-router' -const zodRouter = OpenAPIRouter() +const zodRouter = fromIttyRouter(AutoRouter()) class ToDoGet extends OpenAPIRoute { static schema = { @@ -50,7 +51,7 @@ zodRouter.put('/todo/:id', ToDoGet) describe('zod validations', () => { it('simpleSuccessfulCall', async () => { - const request = await zodRouter.handle( + const request = await zodRouter.fetch( buildRequest({ method: 'PUT', path: `/todo/1` }) ) diff --git a/tests/router.ts b/tests/router.ts index 81db03c..1ab920e 100644 --- a/tests/router.ts +++ b/tests/router.ts @@ -1,4 +1,4 @@ -import { OpenAPIRouter } from '../src/openapi' +import { fromIttyRouter } from '../src/openapi' import { OpenAPIRoute } from '../src/route' import { Bool, @@ -6,20 +6,21 @@ import { DateTime, Email, Enumeration, + Header, Hostname, Int, Ipv4, Ipv6, Num, + Path, Query, Regex, Str, Uuid, - Path, - Header, } from '../src/parameters' -import { OpenAPIRouteSchema, DataOf } from '../src' +import { DataOf, OpenAPIRouteSchema } from '../src' import { z } from 'zod' +import { AutoRouter } from 'itty-router' export class ToDoList extends OpenAPIRoute { static schema = { @@ -243,7 +244,7 @@ export class ToDoCreateTyped extends OpenAPIRoute { } } -export const todoRouter = OpenAPIRouter({ openapiVersion: '3' }) +export const todoRouter = fromIttyRouter(AutoRouter(), { openapiVersion: '3' }) todoRouter.get('/todos', ToDoList) todoRouter.get('/todos/:id', ToDoGet) todoRouter.post('/todos', ToDoCreate)