diff --git a/package-lock.json b/package-lock.json index 06c0c5a..6d652af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "openapi3-ts": "^2.0.2" }, "devDependencies": { - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "^4.6.3", @@ -965,9 +965,9 @@ } }, "node_modules/@types/jest": { - "version": "27.4.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", - "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dev": true, "dependencies": { "jest-matcher-utils": "^27.0.0", @@ -4880,9 +4880,9 @@ } }, "@types/jest": { - "version": "27.4.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", - "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dev": true, "requires": { "jest-matcher-utils": "^27.0.0", diff --git a/package.json b/package.json index 9171c37..4c0c259 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.2.3", "description": "Builds OpenAPI schemas from Zod schemas", "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ "dist", "package.json", @@ -23,6 +24,7 @@ "homepage": "https://github.com/asteasolutions/zod-to-openapi", "scripts": { "build": "tsc -p tsconfig.build.json", + "dev": "npm run build -- --watch", "test": "jest", "prepublishOnly": "npm run build" }, @@ -33,7 +35,7 @@ "zod": "^3.14.0" }, "devDependencies": { - "@types/jest": "^27.4.1", + "@types/jest": "^27.5.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", "typescript": "^4.6.3", diff --git a/spec/securitySchemas.spec.ts b/spec/securitySchemas.spec.ts new file mode 100644 index 0000000..2ec0fca --- /dev/null +++ b/spec/securitySchemas.spec.ts @@ -0,0 +1,70 @@ +import { OpenAPIGenerator } from '../src/openapi-generator'; +import { OpenAPIRegistry } from '../src/openapi-registry'; +import { SecuritySchemeObject } from 'openapi3-ts'; +import { z } from 'zod'; +import {extendZodWithOpenApi} from "../src"; + +const testDocConfig = { + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'Swagger Petstore', + description: 'A sample API', + termsOfService: 'http://swagger.io/terms/', + license: { + name: 'Apache 2.0', + url: 'https://www.apache.org/licenses/LICENSE-2.0.html', + }, + }, + servers: [{ url: 'v1' }], +}; + +extendZodWithOpenApi(z); + +describe('securitySchemas', () => { + const registry = new OpenAPIRegistry(); + + const bearerAuth = registry.registerSecurityScheme('bearerAuth', { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }) + + const Unit = registry.register('UnitDto', z.object({ + id: z.string().uuid(), + name: z.string(), + })).openapi({description: 'unit'}) + + registry.registerPath({ + path: '/units', + method: 'get', + security: [ + bearerAuth.security(), + ], + responses: { + 200: { + mediaType: 'application/json', + schema: Unit.array().openapi({description: 'Array of all units'}) + } + } + }) + + const builder = new OpenAPIGenerator(registry.definitions) + const doc = builder.generateDocument(testDocConfig); + + it('should have security in /units', () => { + expect(doc.paths!['/units'].get.security).toStrictEqual([ + { + bearerAuth: [] + } + ]); + }) + + it('should have securitySchemes', () => { + expect(doc.components!.securitySchemes).toStrictEqual({bearerAuth: { + bearerFormat: "JWT", + scheme: "bearer", + type: "http", + }}); + }) +}) diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index f0b50df..546ef05 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -1,5 +1,6 @@ import { ReferenceObject, + SecuritySchemeObject, SchemaObject, ParameterObject, RequestBodyObject, @@ -62,6 +63,7 @@ export class OpenAPIGenerator { private schemaRefs: Record = {}; private paramRefs: Record = {}; private pathRefs: Record> = {}; + private securitySchemaRefs: Record = {}; constructor(private definitions: OpenAPIDefinitions[]) { this.sortDefinitions(); @@ -73,6 +75,7 @@ export class OpenAPIGenerator { return { ...config, components: { + securitySchemes: this.securitySchemaRefs, schemas: this.schemaRefs, parameters: this.paramRefs, }, @@ -85,6 +88,7 @@ export class OpenAPIGenerator { return { components: { + securitySchemes: this.securitySchemaRefs, schemas: this.schemaRefs, parameters: this.paramRefs, }, @@ -93,6 +97,7 @@ export class OpenAPIGenerator { private sortDefinitions() { const generationOrder: OpenAPIDefinitions['type'][] = [ + 'securitySchema', 'schema', 'parameter', 'route', @@ -110,7 +115,7 @@ export class OpenAPIGenerator { private generateSingle( definition: OpenAPIDefinitions - ): SchemaObject | ParameterObject | ReferenceObject { + ): SchemaObject | ParameterObject | ReferenceObject | SecuritySchemeObject { if (definition.type === 'parameter') { return this.generateParameterDefinition(definition.schema); } @@ -123,6 +128,10 @@ export class OpenAPIGenerator { return this.generateSingleRoute(definition.route); } + if (definition.type === 'securitySchema') { + return this.generateSecuritySchema(definition.name, definition.schema); + } + throw new ZodToOpenAPIError('Invalid definition type'); } @@ -345,6 +354,11 @@ export class OpenAPIGenerator { : simpleSchema; } + private generateSecuritySchema(name: string, schema: SecuritySchemeObject): SecuritySchemeObject { + this.securitySchemaRefs[name] = schema; + return schema + } + private generateSchemaDefinition(zodSchema: ZodSchema): SchemaObject { const metadata = this.getMetadata(zodSchema); const refId = metadata?.refId; diff --git a/src/openapi-registry.ts b/src/openapi-registry.ts index ab36cb2..e7e4252 100644 --- a/src/openapi-registry.ts +++ b/src/openapi-registry.ts @@ -1,5 +1,6 @@ -import { OperationObject } from 'openapi3-ts'; +import {OpenAPIObject, OperationObject} from 'openapi3-ts'; import type { ZodVoid, ZodObject, ZodSchema, ZodType } from 'zod'; +import { SecuritySchemeObject } from 'openapi3-ts'; type Method = 'get' | 'post' | 'put' | 'delete' | 'patch'; @@ -23,6 +24,7 @@ export interface RouteConfig extends OperationObject { export type OpenAPIDefinitions = | { type: 'schema'; schema: ZodSchema } + | { type: 'securitySchema'; name: string, schema: SecuritySchemeObject } | { type: 'parameter'; schema: ZodSchema } | { type: 'route'; route: RouteConfig }; @@ -53,6 +55,20 @@ export class OpenAPIRegistry { return schemaWithMetadata; } + registerSecurityScheme(name: string, schema: SecuritySchemeObject) { + this._definitions.push({ + type: 'securitySchema', + name, + schema, + }) + + return { + name, + schema: {...schema}, + security: (val: string[] = []) => ({[name]: val}), + } + } + /** * Registers a new parameter schema under /components/parameters/${name} */