Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add ability to define securitySchemes under components #25

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand All @@ -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",
Expand Down
70 changes: 70 additions & 0 deletions spec/securitySchemas.spec.ts
Original file line number Diff line number Diff line change
@@ -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",
}});
})
})
16 changes: 15 additions & 1 deletion src/openapi-generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ReferenceObject,
SecuritySchemeObject,
SchemaObject,
ParameterObject,
RequestBodyObject,
Expand Down Expand Up @@ -62,6 +63,7 @@ export class OpenAPIGenerator {
private schemaRefs: Record<string, SchemaObject> = {};
private paramRefs: Record<string, ParameterObject> = {};
private pathRefs: Record<string, Record<string, PathObject>> = {};
private securitySchemaRefs: Record<string, SecuritySchemeObject> = {};

constructor(private definitions: OpenAPIDefinitions[]) {
this.sortDefinitions();
Expand All @@ -73,6 +75,7 @@ export class OpenAPIGenerator {
return {
...config,
components: {
securitySchemes: this.securitySchemaRefs,
schemas: this.schemaRefs,
parameters: this.paramRefs,
},
Expand All @@ -85,6 +88,7 @@ export class OpenAPIGenerator {

return {
components: {
securitySchemes: this.securitySchemaRefs,
schemas: this.schemaRefs,
parameters: this.paramRefs,
},
Expand All @@ -93,6 +97,7 @@ export class OpenAPIGenerator {

private sortDefinitions() {
const generationOrder: OpenAPIDefinitions['type'][] = [
'securitySchema',
'schema',
'parameter',
'route',
Expand All @@ -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);
}
Expand All @@ -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');
}

Expand Down Expand Up @@ -345,6 +354,11 @@ export class OpenAPIGenerator {
: simpleSchema;
}

private generateSecuritySchema(name: string, schema: SecuritySchemeObject): SecuritySchemeObject {
this.securitySchemaRefs[name] = schema;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see you're aiming to provide a pure object definition for the components.securitySchemes property, right? If that is the case I see no reason for putting this through the registry and binding it into the generation logic. I'll try and showcase a different approach.

// Note I think this would look better in a class/factory but a function is easier to shwocase. We can just export this as
// some sort of helper function from `zod-to-openapi`.
function generateSecuritySchemes<T extends Record<string, SecuritySchemeObject>>(securitySchemes: T) {
  return {
    securitySchemes,
    addSecurityScheme: (key: keyof T, val: string[] = []) => ({[key]: val}),
  }
}


  const generator = new OpenAPIGenerator(rootRegistry.definitions);

  const Security = generateSecuritySchemes({bearerAuth: {
    type: 'http',
    scheme: 'bearer',
    bearerFormat: 'JWT',
  }})

  registry.registerPath({
    path: '/units',
    method: 'get',
    security: [
      Security.addSecurityScheme('bearerAuth'),
    ],
    responses: {
      200: {
        mediaType: 'application/json',
        schema: Unit.array().openapi({description: 'Array of all units'})
      }
    }
  })

  generator.generateDocument({
    // the generic stuff
    components: {
      securitySchemes: Security.securitySchemes
    }
  });

In order to make this work we would only need to make sure the config is being merged correctly in generateDocument. I feel like it is not wrong to provide support for manually added components (if someone wants to do that).

@viktor-ku @georgyangelov what do you think. I'd also love to here any possible benefits of getting it through the registry, since I wrote this on the fly and haven't tested particular cases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's certainly reasonable, however I think that registry could serve a bit more than just a zod schema handler. The registry just sounds like a convenient way to just add stuff to it like security schemas and it would just know what to do with it.

Maybe it's better to rename it from registry.registerSecuritySchema to like registry.addSecuritySchema implying that there is nothing to generate, just add to the registry?

And then I don't believe it's necessary to have additional boilerplate around generateDocument, I believe the registry is therefore capable of handling exactly that: knowing when to add something to the components and then doing it.

return schema
}

private generateSchemaDefinition(zodSchema: ZodSchema<any>): SchemaObject {
const metadata = this.getMetadata(zodSchema);
const refId = metadata?.refId;
Expand Down
18 changes: 17 additions & 1 deletion src/openapi-registry.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -23,6 +24,7 @@ export interface RouteConfig extends OperationObject {

export type OpenAPIDefinitions =
| { type: 'schema'; schema: ZodSchema<any> }
| { type: 'securitySchema'; name: string, schema: SecuritySchemeObject }
| { type: 'parameter'; schema: ZodSchema<any> }
| { type: 'route'; route: RouteConfig };

Expand Down Expand Up @@ -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}
*/
Expand Down