Skip to content
2 changes: 1 addition & 1 deletion opapi/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions opapi/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
},
},
Expand Down
4 changes: 2 additions & 2 deletions opapi/src/export-options.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof createState>[0]
type AnyProps = CreateStateProps<string, string, string>

function getApiState() {
const metadata = {
Expand Down
173 changes: 64 additions & 109 deletions opapi/src/opapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Parameters<typeof extendApi>[1]>
Expand All @@ -36,25 +25,67 @@ export const schema = <T extends OpenApiZodAny>(
return extendApi(copy, schemaObject)
}

export type OpenApi<
SchemaName extends string = string,
DefaultParameterName extends string = string,
SectionName extends string = string,
> = ReturnType<typeof createOpapiFromState<SchemaName, DefaultParameterName, SectionName>>

// TODO: ensure type inference comes from field 'sections' not 'schemas'
export type OpenApiProps<SchemaName extends string, DefaultParameterName extends string, SectionName extends string> = {
metadata: Metadata
// adds default parameters to all operations
defaultParameters?: Record<DefaultParameterName, Parameter<'zod-schema'>>
// adds the openapi schemas
schemas?: Record<SchemaName, { schema: OpenApiZodAny; section: SectionName }>
// adds the openapi tags
sections?: Record<SectionName, { title: string; description: string }>
// add the openapi errors
errors?: readonly ApiError[]
// add security schemes
security?: Security[]
export class OpenApi<SchemaName extends string, DefaultParameterName extends string, SectionName extends string> {
private _state: State<SchemaName, DefaultParameterName, SectionName>

constructor(props: CreateStateProps<SchemaName, DefaultParameterName, SectionName>, options: Partial<Options> = {}) {
this._state = createState(props, options)
}

static fromState<SchemaName extends string, DefaultParameterName extends string, SectionName extends string>(
state: State<SchemaName, DefaultParameterName, SectionName>,
) {
const openapi = new OpenApi({ metadata: state.metadata })
openapi._state = state
return openapi
}

getModelRef(name: SchemaName): OpenApiZodAny {
return getRef(this._state, ComponentType.SCHEMAS, name)
}

addOperation<Path extends string>(operation: Operation<DefaultParameterName, SectionName, Path, 'zod-schema'>) {
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> | string
Expand All @@ -63,7 +94,7 @@ export type OpenApiPostProcessors = {
apiCode: CodePostProcessor
}

export type GenerateClientProps = (
export type GenerateClientOptions = (
| {
generator: 'openapi-generator'
endpoint: string
Expand All @@ -75,82 +106,6 @@ export type GenerateClientProps = (
) &
ExportStateOptions

function exportClient(state: State<string, string, string>) {
function _exportClient(dir: string, props: GenerateClientProps): Promise<void>
function _exportClient(
dir: string,
openapiGeneratorEndpoint: string,
props?: OpenApiPostProcessors & ExportStateOptions,
): Promise<void>
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<SchemaName, DefaultParameterName, SectionName>,
) => {
return {
getModelRef: (name: SchemaName): OpenApiZodAny => getRef(state, ComponentType.SCHEMAS, name),
getState: () => state,
addOperation: <Path extends string>(
operationProps: Operation<DefaultParameterName, SectionName, Path, 'zod-schema'>,
) => 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<SchemaName extends string, DefaultParameterName extends string, SectionName extends string>(
props: OpenApiProps<SchemaName, DefaultParameterName, SectionName>,
opts: Partial<Options> = {},
) {
const state = createState(props, opts)
return createOpapiFromState(state)
}

export namespace OpenApi {
export const fromState = <SchemaName extends string, DefaultParameterName extends string, SectionName extends string>(
state: State<SchemaName, DefaultParameterName, SectionName>,
) => createOpapiFromState(state as State<SchemaName, DefaultParameterName, SectionName>)
}

export type SchemaOf<O extends OpenApi<any, any, any>> =
O extends OpenApi<infer Skema, infer _Param, infer _Sexion> ? Skema : never

Expand Down
6 changes: 5 additions & 1 deletion opapi/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,11 @@ type BaseOperationProps<
deprecated?: boolean
}

type CreateStateProps<SchemaName extends string, DefaultParameterName extends string, SectionName extends string> = {
export type CreateStateProps<
SchemaName extends string,
DefaultParameterName extends string,
SectionName extends string,
> = {
metadata: Metadata
defaultParameters?: Record<DefaultParameterName, Parameter<'zod-schema'>>
schemas?: Record<SchemaName, { schema: OpenApiZodAny; section: SectionName }>
Expand Down
2 changes: 1 addition & 1 deletion opapi/test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const nestedSchema = z.object({
})

export const getMockApi = () => {
const api = OpenApi({
const api = new OpenApi({
errors: [
{
description: 'Foo not found',
Expand Down
5 changes: 4 additions & 1 deletion opapi/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 9 additions & 9 deletions opapi/test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -248,7 +248,7 @@ describe('openapi state generator', () => {
})

it('should export state defaultParameters', async () => {
const api = OpenApi(
const api = new OpenApi(
{
security: ['BearerAuth'],
metadata,
Expand Down