-
Notifications
You must be signed in to change notification settings - Fork 481
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
Model-Level Description #2671
Comments
Implemented such functionality on my own, but I would be happy to see one delivered out-of-box by library. /**
* Experimental method to add API model functionality to the OpenAPI document.
*
* @param {OpenAPIObject} document - The OpenAPI document to add model functionality to.
* @return {void}
*/
function experimentalAddApiModelFunctionality(document: OpenAPIObject): void {
const modelMetadataStore = getMetadataStore();
if (document.components) {
for (const definitionKey in document.components.schemas) {
const metatype = modelMetadataStore[definitionKey];
if (metatype) {
if (metatype.name) {
document.components.schemas[metatype.name] = document.components.schemas[definitionKey];
delete document.components.schemas[definitionKey];
}
if (metatype.description) {
(
document.components.schemas[metatype.name ?? definitionKey] as any
).description = metatype.description;
}
}
}
}
// Experimental
const updateSchema = (schema: any) => {
if (schema && schema.$ref) {
const refArray = schema.$ref.split('/');
const originalName = refArray.pop();
const metatype = modelMetadataStore[originalName];
if (metatype && metatype.name) {
refArray.push(metatype.name);
schema.$ref = refArray.join('/');
}
}
};
// Update Swagger Paths
for (const pathKey in document.paths) {
for (const methodKey in document.paths[pathKey]) {
const operation = document.paths[pathKey][methodKey];
if (operation && operation.parameters) {
for (const param of operation.parameters) {
updateSchema(param.schema); // references under parameters can be updated
}
}
if (operation && operation.requestBody && operation.requestBody.content) {
for (const mediaTypeKey in operation.requestBody.content) {
const schema = operation.requestBody.content[mediaTypeKey].schema;
updateSchema(schema); // references under request bodies can be updated
}
}
if (operation && operation.responses) {
for (const responseKey in operation.responses) {
const contentType = operation.responses[responseKey]?.content;
for (const mediaTypeKey in contentType) {
const schema = contentType[mediaTypeKey].schema;
updateSchema(schema); // references under responses can be updated.
}
}
}
}
}
} import {SetMetadata} from '@nestjs/common';
const modelMetadataStore = {};
export const ApiModel = ({
name,
description,
}: { name?: string; description?: string } = {}): ClassDecorator => {
return (target: Function) => {
SetMetadata('API_MODEL_METADATA', {
name,
description,
})(target);
modelMetadataStore[target.name] = {
name,
description,
};
};
};
export function getMetadataStore() {
return modelMetadataStore;
} |
@keinsell where do you call the |
@joeskeen I've a dedicated function which I run at the bootstrapping of application. export async function buildSwaggerDocumentation(app : INestApplication) : Promise<void>
{
const logger = new Logger( 'doc:swagger' )
const swaggerConfig = new DocumentBuilder()
.setTitle( __config.get( 'SERVICE_NAME' ) )
.setDescription( __config.get( 'SERVICE_DESCRIPTION' ) )
.setVersion( '1.0' )
.addTag( 'api' )
.addBearerAuth( {
name : 'Bearer Token',
type : 'http',
scheme : 'bearer',
bearerFormat : 'JWT',
description : 'JWT Authorization header using the Bearer scheme. Example: "Authorization: Bearer <token>"',
} )
.build()
logger.verbose( `Swagger documentation base built for ${__config.get( 'SERVICE_NAME' )} service.` )
const document = SwaggerModule.createDocument( app, swaggerConfig,
new DocumentBuilder()
// https://stackoverflow.com/questions/59376717/global-headers-for-all-controllers-nestjs-swagger
// .addGlobalParameters( { in : 'header', required :
// true, name : 'x-request-id', description : 'A unique
// identifier assigned to the request. Clients can include this
// header' + ' to trace and correlate requests across different
// services and systems.', schema : {type : 'string'},
// deprecated : true, example : nanoid( 128 ), } )
.build() as any,
)
logger.verbose( `Swagger documentation document built for ${__config.get( 'SERVICE_NAME' )} service.` )
experimentalAddApiModelFunctionality( document )
//SwaggerModule.setup(ApplicationConfiguration.openapiDocumentationPath, app, document, {
// explorer: true,
// customCss: new SwaggerTheme('v3').getBuffer("flattop"), // OR newspaper
//});
//app.use("/api", apiReference({
// spec: {
// content: document,
// },
//}))
const documentationObjectPath = `${process.cwd()}/src/common/modules/documentation/swagger/public/api/openapi3.json`
const formattedDocument = await prettier.format( JSON.stringify( document ), {
parser : 'json-stringify',
tabWidth : 2,
} )
// Save Swagger Documentation to File
fs.writeFileSync( documentationObjectPath, formattedDocument )
logger.verbose( `Swagger documentation was snapshot into ${tildify( documentationObjectPath )}` )
} export async function bootstrap()
{
// Bootstrap application
const app = await NestFactory.create( Container, {
abortOnError : false,
autoFlushLogs : true,
bufferLogs : true,
snapshot : isDevelopment(),
} )
// Implement logger used for bootstrapping and notifying about application state
const logger = new Logger( 'Bootstrap' )
await executePrismaRelatedProcesses()
// Enable graceful shutdown hooks
app.enableShutdownHooks()
// Build swagger documentation
await buildSwaggerDocumentation( app )
buildCompodocDocumentation()
// ... |
@keinsell Thanks for sharing! I went a different route to accomplish the same thing, but for my use case I needed to add custom metadata for both the DTO class and properties to make the schema output include nonstandard properties used by https://github.com/json-editor/json-editor. Here's what I came up with. I noticed that there is a built-in decorator called const DECORATOR_API_EXTENSION = 'swagger/apiExtension';
const DECORATOR_API_MODEL_PROPERTIES_ARRAY = 'swagger/apiModelPropertiesArray';
const DECORATOR_API_MODEL_PROPERTIES = 'swagger/apiModelProperties';
const METADATA_FACTORY_NAME = '_OPENAPI_METADATA_FACTORY';
/* this function taken from the '@nestjs/swagger' source at lib/decorators/api-extension.decorator.ts */
export function ApiExtension(extensionKey: string, extensionProperties: any) {
/*
We do NOT care about using 'x-' prefixes, as this is going to be used to be compatible with
@json-editor/json-editor, which does not use 'x-' prefixes.
*/
// if (!extensionKey.startsWith('x-')) {
// throw new Error(
// 'Extension key is not prefixed. Please ensure you prefix it with `x-`.',
// );
// }
const extensionObject = {
[extensionKey]: { ...extensionProperties },
};
return createMixedDecorator(extensionObject);
}
export function ApiExtensions(extensionProperties: Record<string, any>) {
const extensionObject = {
...extensionProperties,
};
return createMixedDecorator(extensionObject);
}
export function createMixedDecorator<T = any>(
metadata: T,
overrideExisting = true,
): MethodDecorator & ClassDecorator & PropertyDecorator {
return (
target: object,
propertyKey?: string,
descriptor?: TypedPropertyDescriptor<any>,
): any => {
const isMethod = !!descriptor;
const isProperty = !isMethod && !!propertyKey;
const isClass = !isMethod && !isProperty;
if (isMethod) {
const metadataKey = DECORATOR_API_EXTENSION;
let targetMetadata: any;
if (Array.isArray(metadata)) {
const existingMetadata =
Reflect.getMetadata(metadataKey, descriptor.value) || [];
targetMetadata = overrideExisting
? metadata
: [...existingMetadata, ...metadata];
} else {
const existingMetadata =
Reflect.getMetadata(metadataKey, descriptor.value) || {};
targetMetadata = overrideExisting
? metadata
: { ...existingMetadata, ...metadata };
}
Reflect.defineMetadata(metadataKey, targetMetadata, descriptor.value);
return descriptor;
} else if (isClass) {
const metadataKey = DECORATOR_API_EXTENSION;
Reflect.defineMetadata(metadataKey, metadata, target);
return target;
} else if (isProperty) {
const metadataKey = DECORATOR_API_MODEL_PROPERTIES;
const properties =
Reflect.getMetadata(DECORATOR_API_MODEL_PROPERTIES_ARRAY, target) || [];
const key = `:${propertyKey}`;
if (!properties.includes(key)) {
Reflect.defineMetadata(
DECORATOR_API_MODEL_PROPERTIES_ARRAY,
[...properties, key],
target,
);
}
const existingMetadata = Reflect.getMetadata(
metadataKey,
target,
propertyKey,
);
if (existingMetadata) {
const newMetadata = withoutUndefinedProperties(metadata);
const metadataToSave = overrideExisting
? {
...existingMetadata,
...newMetadata,
}
: {
...newMetadata,
...existingMetadata,
};
Reflect.defineMetadata(
metadataKey,
metadataToSave,
target,
propertyKey,
);
} else {
const type =
target?.constructor?.[METADATA_FACTORY_NAME]?.()[propertyKey]?.type ??
Reflect.getMetadata('design:type', target, propertyKey);
Reflect.defineMetadata(
metadataKey,
{
type,
...withoutUndefinedProperties(metadata),
},
target,
propertyKey,
);
}
}
};
}
export function withoutUndefinedProperties<T extends Record<string, any>>(
object: T,
): Partial<T> {
return Object.keys(object)
.filter((key) => object[key] !== undefined)
.reduce((acc, key) => {
acc[key] = object[key];
return acc;
}, {});
} Usage is as follows: @ApiExtensions({
name: MyDto.name,
description: 'A collection of data.',
headerTemplate: '<%= self.name %> data',
additionalProperties: false,
})
export class MyDto {
@ApiProperty({
description:
'The globally-unique identifier of this DTO',
type: String,
required: false,
example: 'ielnvidjem934j',
})
@ApiExtension('options', { hidden: true })
id?: string;
...
} I'm thinking it would be beneficial to have this library allow for non- |
Fixed here #2427 Would you like to create a PR that allows setting other attributes as well? |
I created a PR that adds support for a description: #3198 |
Let's track this here #3198 |
Is there an existing issue that is already proposing this?
Is your feature request related to a problem? Please describe it
During writing documentation for some OpenAPI Specification I've noticed there are missing features related to model documentation, in terms of complex applications it would be helpful for end-users to explain for what specific model is responsible and how to properly build one. OpenAPI 3.0 itself have support for description of models sadly Nest.js do not.
Additional case is no feature for renaming models, if I have class
RegisterAccountDto
it's a little frustrating I cannot removeDto
suffix for user as this is irrelevant information for him.Describe the solution you'd like
Teachability, documentation, adoption, migration strategy
No response
What is the motivation / use case for changing the behavior?
User Experience and Documentation of complex/large-scale applications
The text was updated successfully, but these errors were encountered: