Skip to content
Merged
5 changes: 3 additions & 2 deletions opapi/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bpinternal/opapi",
"version": "0.17.1",
"version": "0.18.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 All @@ -20,7 +20,8 @@
"build": "tsup src/index.ts --dts --format cjs,esm --clean",
"check:type": "tsc --noEmit",
"check:format": "prettier --check .",
"fix:format": "prettier --write ."
"fix:format": "prettier --write .",
"check": "pnpm run check:type && pnpm run check:format"
},
"devDependencies": {
"@swc/core": "1.9.3",
Expand Down
137 changes: 137 additions & 0 deletions opapi/src/export-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, it, expect } from 'vitest'
import { createState } from './state'
import { z } from 'zod'
import { applyExportOptions } from './export-options'
import { addOperation } from './operation'

type AnyProps = Parameters<typeof createState>[0]

function getApiState() {
const metadata = {
title: 'Test API',
description: 'Test API',
server: 'http://localhost:3000',
version: '1.0.0',
prefix: '/v1',
} satisfies AnyProps['metadata']

const sections = {
trees: {
title: 'Trees',
description: 'Trees section',
},
} satisfies AnyProps['sections']

const leaf: z.ZodType = z.object({
type: z.literal('leaf'),
name: z.string(),
data: z.string(),
})
const node: z.ZodType = z.object({
type: z.literal('node'),
name: z.string(),
children: z.array(z.lazy(() => tree)),
})
const tree: z.ZodType = z.union([leaf, node])

const state = createState(
{
metadata,
sections,
defaultParameters: {
'x-tree': {
description: 'Tree id',
in: 'header',
type: 'string',
},
},
schemas: {
Tree: {
section: 'trees',
schema: tree,
},
},
security: ['BearerAuth'],
},
{ allowUnions: true },
)

addOperation(state, {
security: ['BearerAuth'],
name: 'getTree',
description: 'Get a tree',
method: 'get',
path: '/trees/{id}',
parameters: {
id: {
description: 'Tree id',
in: 'path',
type: 'string',
},
},
response: {
description: 'Tree information',
schema: tree,
},
})

return state
}

describe('Apply Export Options', () => {
it('defaultParameters should be removed when ignoreDefaultParameters is true', async () => {
const initState = getApiState()

expect(initState.operations['getTree']?.parameters!['x-tree']).toBeDefined()
expect(initState.operations['getTree']?.parameters!['id']).toBeDefined()

const updatedState = applyExportOptions(initState, {
ignoreDefaultParameters: true,
})

expect(updatedState.operations['getTree']?.parameters!['x-tree']).toBeUndefined()
expect(updatedState.operations['getTree']?.parameters!['id']).toBeDefined()
})

it('defaultParameters should not be removed when ignoreDefaultParameters is false', async () => {
const initState = getApiState()

expect(initState.operations['getTree']?.parameters!['x-tree']).toBeDefined()
expect(initState.operations['getTree']?.parameters!['id']).toBeDefined()

const updatedState = applyExportOptions(initState, {
ignoreDefaultParameters: false,
})

expect(updatedState.operations['getTree']?.parameters!['x-tree']).toBeDefined()
expect(updatedState.operations['getTree']?.parameters!['id']).toBeDefined()
})

it('security should not be removed when ignoreSecurity is false', async () => {
const initState = getApiState()

expect(initState.security).toBeDefined()
expect(initState.operations['getTree']?.security).toBeDefined()

const updatedState = applyExportOptions(initState, {
ignoreSecurity: false,
})

expect(updatedState.security).toBeDefined()
expect(updatedState.operations['getTree']?.security).toBeDefined()
})

it('security should be removed when ignoreSecurity is true', async () => {
const initState = getApiState()

expect(initState.security).toBeDefined()
expect(initState.operations['getTree']?.security).toBeDefined()

const updatedState = applyExportOptions(initState, {
ignoreSecurity: true,
})

expect(updatedState.security).toBeUndefined()
expect(updatedState.operations['getTree']?.security).toBeUndefined()
})
})
36 changes: 36 additions & 0 deletions opapi/src/export-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { State } from './state'

export type ExportStateOptions = Partial<{
ignoreDefaultParameters: boolean
ignoreSecurity: boolean
}>

export const applyExportOptions = <
SchemaName extends string,
DefaultParameterName extends string,
SectionName extends string,
>(
state: State<SchemaName, DefaultParameterName, SectionName>,
options?: ExportStateOptions,
) => {
if (options?.ignoreDefaultParameters && state.defaultParameters) {
const defaultParametersName = Object.keys(state.defaultParameters)
for (const operationId of Object.keys(state.operations)) {
if (!state.operations[operationId]?.parameters) {
continue
}
state.operations[operationId].parameters = Object.fromEntries(
Object.entries(state.operations[operationId].parameters).filter(
([parameterName]) => !defaultParametersName.includes(parameterName),
),
)
}
}
if (options?.ignoreSecurity) {
delete state.security
for (const operationId of Object.keys(state.operations)) {
delete state.operations[operationId]?.security
}
}
return state
}
41 changes: 29 additions & 12 deletions opapi/src/opapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './state'
import { exportStateAsTypescript, ExportStateAsTypescriptOptions } from './generators/ts-state'
import { generateHandler } from './handler-generator'
import { applyExportOptions, ExportStateOptions } from './export-options'
export { Operation, Parameter } from './state'

type AnatineSchemaObject = NonNullable<Parameters<typeof extendApi>[1]>
Expand Down Expand Up @@ -62,7 +63,7 @@ export type OpenApiPostProcessors = {
apiCode: CodePostProcessor
}

export type GenerateClientProps =
export type GenerateClientProps = (
| {
generator: 'openapi-generator'
endpoint: string
Expand All @@ -71,22 +72,34 @@ export type GenerateClientProps =
| {
generator: 'opapi'
}
) &
ExportStateOptions

function exportClient(state: State<string, string, string>) {
function _exportClient(dir: string, props: GenerateClientProps): Promise<void>
function _exportClient(
dir: string,
openapiGeneratorEndpoint: string,
postProcessors?: OpenApiPostProcessors,
props?: OpenApiPostProcessors & ExportStateOptions,
): Promise<void>
function _exportClient(dir: string, props: GenerateClientProps): Promise<void>
function _exportClient(dir = '.', props: GenerateClientProps | string, postProcessors?: OpenApiPostProcessors) {
function _exportClient(
dir = '.',
propsOrEndpoint: GenerateClientProps | string,
postProcessorsAndStateOpts?: OpenApiPostProcessors & ExportStateOptions,
) {
let options: GenerateClientProps
if (typeof props === 'string') {
options = { generator: 'openapi-generator', endpoint: props, postProcessors }
if (typeof propsOrEndpoint === 'string') {
options = {
generator: 'openapi-generator',
endpoint: propsOrEndpoint,
postProcessors: postProcessorsAndStateOpts,
}
} else {
options = props
options = propsOrEndpoint
}

state = applyExportOptions(state, postProcessorsAndStateOpts)

if (options.generator === 'openapi-generator') {
return generateClientWithOpenapiGenerator(state, dir, options.endpoint, options.postProcessors)
}
Expand All @@ -107,16 +120,20 @@ const createOpapiFromState = <
) => {
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 = '.') => generateTypesBySection(state, dir),
exportServer: (dir = '.', useExpressTypes: boolean) => generateServer(state, dir, useExpressTypes),
exportOpenapi: (dir = '.') => generateOpenapi(state, dir),
exportState: (dir = '.', opts?: ExportStateAsTypescriptOptions) => exportStateAsTypescript(state, dir, opts),
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 = '.') => generateHandler(state, dir),
exportHandler: (dir = '.', opts?: ExportStateOptions) => generateHandler(applyExportOptions(state, opts), dir),
}
}

Expand Down
Loading