diff --git a/opapi/package.json b/opapi/package.json index 03d63e231..db3a51b09 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -1,6 +1,6 @@ { "name": "@bpinternal/opapi", - "version": "0.18.0", + "version": "1.0.0", "description": "Opapi is a highly opinionated library to generate server, client and documentation from OpenAPI specification using typescript.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/opapi/readme.md b/opapi/readme.md index 56330507e..56c429579 100644 --- a/opapi/readme.md +++ b/opapi/readme.md @@ -10,7 +10,7 @@ Install the package and start creating your OpenAPI specification. See the examp import { OpenApi, schema } from '@bpinternal/opapi' import { z } from 'zod' -const api = OpenApi({ +const api = new OpenApi({ metadata: { title: 'Example API', // This is the title of the API description: 'Description of this api', // This is the description of the API @@ -21,7 +21,7 @@ const api = OpenApi({ // This is metadata to be used in the documentation section: { User: { - tilte: 'User', + title: 'User', description: 'User related endpoints', }, }, diff --git a/opapi/src/export-options.test.ts b/opapi/src/export-options.test.ts index 5a87380f3..537c2f742 100644 --- a/opapi/src/export-options.test.ts +++ b/opapi/src/export-options.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from 'vitest' -import { createState } from './state' import { z } from 'zod' import { applyExportOptions } from './export-options' import { addOperation } from './operation' +import { CreateStateProps, createState } from './state' -type AnyProps = Parameters[0] +type AnyProps = CreateStateProps function getApiState() { const metadata = { diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index 5041d47c9..00d89d310 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -7,22 +7,11 @@ import { generateServer, generateTypesBySection, } from './generator' -import { addOperation } from './operation' -import { - ApiError, - ComponentType, - createState, - getRef, - Metadata, - Operation, - Options, - Parameter, - State, - Security, -} from './state' +import { ComponentType, Operation, Options, State, CreateStateProps, getRef, createState } from './state' import { exportStateAsTypescript, ExportStateAsTypescriptOptions } from './generators/ts-state' import { generateHandler } from './handler-generator' import { applyExportOptions, ExportStateOptions } from './export-options' +import { addOperation } from './operation' export { Operation, Parameter } from './state' type AnatineSchemaObject = NonNullable[1]> @@ -36,25 +25,67 @@ export const schema = ( return extendApi(copy, schemaObject) } -export type OpenApi< - SchemaName extends string = string, - DefaultParameterName extends string = string, - SectionName extends string = string, -> = ReturnType> - -// TODO: ensure type inference comes from field 'sections' not 'schemas' -export type OpenApiProps = { - metadata: Metadata - // adds default parameters to all operations - defaultParameters?: Record> - // adds the openapi schemas - schemas?: Record - // adds the openapi tags - sections?: Record - // add the openapi errors - errors?: readonly ApiError[] - // add security schemes - security?: Security[] +export class OpenApi { + private _state: State + + constructor(props: CreateStateProps, options: Partial = {}) { + this._state = createState(props, options) + } + + static fromState( + state: State, + ) { + const openapi = new OpenApi({ metadata: state.metadata }) + openapi._state = state + return openapi + } + + getModelRef(name: SchemaName): OpenApiZodAny { + return getRef(this._state, ComponentType.SCHEMAS, name) + } + + addOperation(operation: Operation) { + addOperation(this._state, operation) + } + + exportClient(dir: string, options: GenerateClientOptions & ExportStateOptions) { + if (options.generator === 'openapi-generator') { + return generateClientWithOpenapiGenerator( + applyExportOptions(this._state, options), + dir, + options.endpoint, + options.postProcessors, + ) + } + if (options.generator === 'opapi') { + return generateClientWithOpapi(applyExportOptions(this._state, options), dir) + } + throw new Error('Unknown generator') + } + + exportTypesBySection(dir = '.', options?: ExportStateOptions) { + return generateTypesBySection(applyExportOptions(this._state, options), dir) + } + + exportServer(dir = '.', useExpressTypes: boolean, options?: ExportStateOptions) { + return generateServer(applyExportOptions(this._state, options), dir, useExpressTypes) + } + + exportOpenapi(dir = '.', options?: ExportStateOptions) { + return generateOpenapi(applyExportOptions(this._state, options), dir) + } + + exportState(dir = '.', options?: ExportStateAsTypescriptOptions & ExportStateOptions) { + return exportStateAsTypescript(applyExportOptions(this._state, options), dir, options) + } + + exportErrors(dir = '.') { + return generateErrorsFile(this._state.errors ?? [], dir) + } + + exportHandler(dir = '.', options?: ExportStateOptions) { + return generateHandler(applyExportOptions(this._state, options), dir) + } } export type CodePostProcessor = (code: string) => Promise | string @@ -63,7 +94,7 @@ export type OpenApiPostProcessors = { apiCode: CodePostProcessor } -export type GenerateClientProps = ( +export type GenerateClientOptions = ( | { generator: 'openapi-generator' endpoint: string @@ -75,82 +106,6 @@ export type GenerateClientProps = ( ) & ExportStateOptions -function exportClient(state: State) { - function _exportClient(dir: string, props: GenerateClientProps): Promise - function _exportClient( - dir: string, - openapiGeneratorEndpoint: string, - props?: OpenApiPostProcessors & ExportStateOptions, - ): Promise - function _exportClient( - dir = '.', - propsOrEndpoint: GenerateClientProps | string, - postProcessorsAndStateOpts?: OpenApiPostProcessors & ExportStateOptions, - ) { - let options: GenerateClientProps - if (typeof propsOrEndpoint === 'string') { - options = { - generator: 'openapi-generator', - endpoint: propsOrEndpoint, - postProcessors: postProcessorsAndStateOpts, - } - } else { - options = propsOrEndpoint - } - - state = applyExportOptions(state, postProcessorsAndStateOpts) - - if (options.generator === 'openapi-generator') { - return generateClientWithOpenapiGenerator(state, dir, options.endpoint, options.postProcessors) - } - if (options.generator === 'opapi') { - return generateClientWithOpapi(state, dir) - } - throw new Error('Unknown generator') - } - return _exportClient -} - -const createOpapiFromState = < - SchemaName extends string, - DefaultParameterName extends string, - SectionName extends string, ->( - state: State, -) => { - return { - getModelRef: (name: SchemaName): OpenApiZodAny => getRef(state, ComponentType.SCHEMAS, name), - getState: () => state, - addOperation: ( - operationProps: Operation, - ) => addOperation(state, operationProps), - exportClient: exportClient(state), - exportTypesBySection: (dir = '.', opts?: ExportStateOptions) => - generateTypesBySection(applyExportOptions(state, opts), dir), - exportServer: (dir = '.', useExpressTypes: boolean, opts?: ExportStateOptions) => - generateServer(applyExportOptions(state, opts), dir, useExpressTypes), - exportOpenapi: (dir = '.', opts?: ExportStateOptions) => generateOpenapi(applyExportOptions(state, opts), dir), - exportState: (dir = '.', opts?: ExportStateAsTypescriptOptions & ExportStateOptions) => - exportStateAsTypescript(applyExportOptions(state, opts), dir, opts), - exportErrors: (dir = '.') => generateErrorsFile(state.errors ?? [], dir), - exportHandler: (dir = '.', opts?: ExportStateOptions) => generateHandler(applyExportOptions(state, opts), dir), - } -} - -export function OpenApi( - props: OpenApiProps, - opts: Partial = {}, -) { - const state = createState(props, opts) - return createOpapiFromState(state) -} - -export namespace OpenApi { - export const fromState = ( - state: State, - ) => createOpapiFromState(state as State) -} - export type SchemaOf> = O extends OpenApi ? Skema : never diff --git a/opapi/src/state.ts b/opapi/src/state.ts index a602e5c85..a1da47133 100644 --- a/opapi/src/state.ts +++ b/opapi/src/state.ts @@ -228,7 +228,11 @@ type BaseOperationProps< deprecated?: boolean } -type CreateStateProps = { +export type CreateStateProps< + SchemaName extends string, + DefaultParameterName extends string, + SectionName extends string, +> = { metadata: Metadata defaultParameters?: Record> schemas?: Record diff --git a/opapi/test/api.ts b/opapi/test/api.ts index 9c39f55de..d299a33e6 100644 --- a/opapi/test/api.ts +++ b/opapi/test/api.ts @@ -36,7 +36,7 @@ const nestedSchema = z.object({ }) export const getMockApi = () => { - const api = OpenApi({ + const api = new OpenApi({ errors: [ { description: 'Foo not found', diff --git a/opapi/test/client.test.ts b/opapi/test/client.test.ts index 809c924c7..f3f763b89 100644 --- a/opapi/test/client.test.ts +++ b/opapi/test/client.test.ts @@ -10,7 +10,10 @@ describe('client generator', () => { const api = getMockApi() - await api.exportClient(genClientFolder, 'https://api.openapi-generator.tech') + await api.exportClient(genClientFolder, { + generator: 'openapi-generator', + endpoint: 'https://api.openapi-generator.tech', + }) const files = getFiles(genClientFolder) diff --git a/opapi/test/state.test.ts b/opapi/test/state.test.ts index 82c214034..52b16bb4a 100644 --- a/opapi/test/state.test.ts +++ b/opapi/test/state.test.ts @@ -40,7 +40,7 @@ const expectedErrorMessage = 'allOf, anyOf and oneOf are not supported' describe('openapi generator with unions not allowed', () => { it('should not allow unions when creating api', async () => { expect(() => { - OpenApi({ + new OpenApi({ metadata, sections, schemas: { @@ -54,7 +54,7 @@ describe('openapi generator with unions not allowed', () => { }) it('should not allow unions in response when adding an operation', async () => { - const api = OpenApi({ metadata, sections }) + const api = new OpenApi({ metadata, sections }) expect(() => { api.addOperation({ name: 'getTree', @@ -77,7 +77,7 @@ describe('openapi generator with unions not allowed', () => { }) it('should not allow unions in request body when adding an operation', async () => { - const api = OpenApi({ metadata, sections }) + const api = new OpenApi({ metadata, sections }) expect(() => { api.addOperation({ name: 'createTree', @@ -100,7 +100,7 @@ describe('openapi generator with unions not allowed', () => { describe('openapi generator with unions allowed', () => { const opts = { allowUnions: true } as const it('should allow unions when creating api', async () => { - OpenApi( + new OpenApi( { metadata, sections, @@ -116,7 +116,7 @@ describe('openapi generator with unions allowed', () => { }) it('should allow unions in response when adding an operation', async () => { - const api = OpenApi({ metadata, sections }, opts) + const api = new OpenApi({ metadata, sections }, opts) api.addOperation({ name: 'getTree', description: 'Get a tree', @@ -137,7 +137,7 @@ describe('openapi generator with unions allowed', () => { }) it('should allow unions in request body when adding an operation', async () => { - const api = OpenApi({ metadata, sections }, opts) + const api = new OpenApi({ metadata, sections }, opts) api.addOperation({ name: 'createTree', description: 'Create a tree', @@ -157,7 +157,7 @@ describe('openapi generator with unions allowed', () => { describe('openapi state generator', () => { it('should export state', async () => { - const api = OpenApi( + const api = new OpenApi( { metadata, sections, @@ -184,7 +184,7 @@ describe('openapi state generator', () => { }) it('should export state without defaultParameters when ignoreDefaultParameters is set', async () => { - const api = OpenApi( + const api = new OpenApi( { security: ['BearerAuth'], metadata, @@ -248,7 +248,7 @@ describe('openapi state generator', () => { }) it('should export state defaultParameters', async () => { - const api = OpenApi( + const api = new OpenApi( { security: ['BearerAuth'], metadata,