Skip to content

Commit

Permalink
feat: allow passing raw schema definition #1682 #2117
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Oct 28, 2024
1 parent a248d0b commit 92df199
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 38 deletions.
29 changes: 28 additions & 1 deletion e2e/api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,23 @@
}
}
},
"rawDefinition": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "ErrorName"
},
"status": {
"type": "number",
"example": "400"
}
},
"required": [
"name",
"status"
]
},
"enum": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -1336,13 +1353,23 @@
"items": {
"type": "string"
}
},
"siblings": {
"type": "object",
"properties": {
"ids": {
"type": "number"
}
},
"required": [
"ids"
]
}
},
"required": [
"name",
"age",
"breed",
"_tags",
"createdAt",
"urls",
"_options",
Expand Down
16 changes: 16 additions & 0 deletions e2e/src/cats/classes/cat.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ export class Cat {
})
options?: Record<string, any>[];

@ApiProperty({
type: 'object',
properties: {
name: {
type: 'string',
example: 'ErrorName'
},
status: {
type: 'number',
example: '400'
}
},
required: ['name', 'status']
})
rawDefinition?: Record<string, any>;

@ApiProperty({
enum: LettersEnum
})
Expand Down
9 changes: 8 additions & 1 deletion e2e/validate-schema.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ describe('Validate OpenAPI schema', () => {
Cat: {
tags: {
description: 'Tags of the cat',
example: ['tag1', 'tag2']
example: ['tag1', 'tag2'],
required: false
},
siblings: {
required: false,
type: () => ({
ids: { required: true, type: () => Number }
})
}
}
}
Expand Down
32 changes: 19 additions & 13 deletions lib/decorators/api-property.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Type } from '@nestjs/common';
import { DECORATORS } from '../constants';
import { EnumSchemaAttributes } from '../interfaces/enum-schema-attributes.interface';
import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface';
import {
EnumAllowedTypes,
SchemaObjectMetadata
} from '../interfaces/schema-object-metadata.interface';
import { getEnumType, getEnumValues } from '../utils/enum.utils';
import { createPropertyDecorator, getTypeIsArrayTuple } from './helpers';

export interface ApiPropertyCommonOptions
extends Omit<SchemaObjectMetadata, 'name' | 'enum'> {
name?: string;
enum?: any[] | Record<string, any> | (() => any[] | Record<string, any>);
export type ApiPropertyCommonOptions = SchemaObjectMetadata & {
'x-enumNames'?: string[];
/**
* Lazy function returning the type for which the decorated property
Expand All @@ -20,7 +20,7 @@ export interface ApiPropertyCommonOptions
* @see [Swagger link objects](https://swagger.io/docs/specification/links/)
*/
link?: () => Type<unknown> | Function;
}
};

export type ApiPropertyOptions =
| ApiPropertyCommonOptions
Expand All @@ -29,8 +29,14 @@ export type ApiPropertyOptions =
enumSchema?: EnumSchemaAttributes;
});

const isEnumArray = (obj: ApiPropertyOptions): boolean =>
obj.isArray && !!obj.enum;
const isEnumArray = (
opts: ApiPropertyOptions
): opts is {
isArray: true;
enum: EnumAllowedTypes;
type: any;
items: any;
} => opts.isArray && 'enum' in opts;

export function ApiProperty(
options: ApiPropertyOptions = {}
Expand All @@ -47,7 +53,7 @@ export function createApiPropertyDecorator(
...options,
type,
isArray
};
} as ApiPropertyOptions;

if (isEnumArray(options)) {
options.type = 'array';
Expand All @@ -58,7 +64,7 @@ export function createApiPropertyDecorator(
enum: enumValues
};
delete options.enum;
} else if (options.enum) {
} else if ('enum' in options) {
const enumValues = getEnumValues(options.enum);

options.enum = enumValues;
Expand Down Expand Up @@ -88,17 +94,17 @@ export function ApiPropertyOptional(
return ApiProperty({
...options,
required: false
});
} as ApiPropertyOptions);
}

export function ApiResponseProperty(
options: Pick<
ApiPropertyOptions,
'type' | 'example' | 'format' | 'enum' | 'deprecated'
'type' | 'example' | 'format' | 'deprecated' | 'enum'
> = {}
): PropertyDecorator {
return ApiProperty({
readOnly: true,
...options
});
} as ApiPropertyOptions);
}
31 changes: 27 additions & 4 deletions lib/interfaces/schema-object-metadata.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,40 @@ import { Type } from '@nestjs/common';
import { EnumSchemaAttributes } from './enum-schema-attributes.interface';
import { SchemaObject } from './open-api-spec.interface';

export type EnumAllowedTypes =
| any[]
| Record<string, any>
| (() => any[] | Record<string, any>);

interface SchemaObjectCommonMetadata
extends Omit<SchemaObject, 'type' | 'required'> {
type?: Type<unknown> | Function | [Function] | string | Record<string, any>;
extends Omit<SchemaObject, 'type' | 'required' | 'properties' | 'enum'> {
isArray?: boolean;
required?: boolean;
name?: string;
enum?: EnumAllowedTypes;
}

export type SchemaObjectMetadata =
| SchemaObjectCommonMetadata
| (SchemaObjectCommonMetadata & {
type?:
| Type<unknown>
| Function
| [Function]
| 'array'
| 'string'
| 'number'
| 'boolean'
| 'integer'
| 'null';
required?: boolean;
})
| ({
type?: Type<unknown> | Function | [Function] | Record<string, any>;
required?: boolean;
enumName: string;
enumSchema?: EnumSchemaAttributes;
} & SchemaObjectCommonMetadata)
| ({
type: 'object';
properties: Record<string, SchemaObjectMetadata>;
required?: string[];
} & SchemaObjectCommonMetadata);
69 changes: 51 additions & 18 deletions lib/services/schema-object-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,33 @@ export class SchemaObjectFactory {
}
const extensionProperties =
Reflect.getMetadata(DECORATORS.API_EXTENSION, type) || {};

const typeDefinition: SchemaObject = {
type: 'object',
properties: mapValues(keyBy(propertiesWithType, 'name'), (property) =>
omit(property, [
properties: mapValues(keyBy(propertiesWithType, 'name'), (property) => {
const keysToOmit = [
'name',
'isArray',
'required',
'enumName',
'enumSchema'
])
) as Record<string, SchemaObject | ReferenceObject>,
'enumSchema',
'selfRequired'
];

if ('required' in property && Array.isArray(property.required)) {
return omit(property, keysToOmit);
}

return omit(property, [...keysToOmit, 'required']);
}) as Record<string, SchemaObject | ReferenceObject>,
...extensionProperties
};

const typeDefinitionRequiredFields = propertiesWithType
.filter((property) => property.required != false)
.filter(
(property) =>
(property.required != false && !Array.isArray(property.required)) ||
('selfRequired' in property && property.selfRequired != false)
)
.map((property) => property.name);

if (typeDefinitionRequiredFields.length > 0) {
Expand Down Expand Up @@ -250,7 +262,11 @@ export class SchemaObjectFactory {
schemas: Record<string, SchemaObject>,
pendingSchemaRefs: string[],
metadata?: SchemaObjectMetadata
): SchemaObjectMetadata | ReferenceObject | ParameterObject {
):
| SchemaObjectMetadata
| ReferenceObject
| ParameterObject
| (SchemaObject & { selfRequired?: boolean }) {
if (!metadata) {
metadata =
omit(
Expand Down Expand Up @@ -335,7 +351,7 @@ export class SchemaObjectFactory {
key: string,
metadata: SchemaObjectMetadata,
schemas: Record<string, SchemaObject>
) {
): SchemaObjectMetadata {
if (!('enumName' in metadata) || !metadata.enumName) {
return {
...metadata,
Expand Down Expand Up @@ -383,7 +399,7 @@ export class SchemaObjectFactory {
pathsToOmit.push('type');
}

return omit(paramObject, pathsToOmit);
return omit(paramObject, pathsToOmit) as SchemaObjectMetadata;
}

createNotBuiltInTypeReference(
Expand Down Expand Up @@ -473,6 +489,8 @@ export class SchemaObjectFactory {
) {
const objLiteralKeys = Object.keys(literalObj);
const properties = {};
const required = [];

objLiteralKeys.forEach((key) => {
const propertyCompilerMetadata = literalObj[key];
if (isEnumArray<Record<string, any>>(propertyCompilerMetadata)) {
Expand All @@ -497,15 +515,22 @@ export class SchemaObjectFactory {
[],
propertyCompilerMetadata
);
const keysToRemove = ['isArray', 'name'];

if ('required' in propertyMetadata && propertyMetadata.required) {
required.push(key);
}
const keysToRemove = ['isArray', 'name', 'required'];
const validMetadataObject = omit(propertyMetadata, keysToRemove);
properties[key] = validMetadataObject;
});
return {

const schema = {
name: key,
type: 'object',
properties
properties,
required
};
return schema;
}

createFromNestedArray(
Expand Down Expand Up @@ -542,14 +567,22 @@ export class SchemaObjectFactory {
schemas: Record<string, SchemaObject>,
pendingSchemaRefs: string[],
nestedArrayType?: unknown
) {
):
| SchemaObjectMetadata
| ReferenceObject
| ParameterObject
| (SchemaObject & { selfRequired?: boolean }) {
const typeRef = nestedArrayType || metadata.type;
if (this.isObjectLiteral(typeRef as Record<string, any>)) {
return this.createFromObjectLiteral(
const schemaFromObjectLiteral = this.createFromObjectLiteral(
key,
typeRef as Record<string, any>,
schemas
);
return {
...schemaFromObjectLiteral,
selfRequired: metadata.required as boolean
};
}

if (isString(typeRef)) {
Expand Down Expand Up @@ -577,15 +610,15 @@ export class SchemaObjectFactory {
...metadata,
type: 'string',
name: metadata.name || key
};
} as SchemaObjectMetadata;
}
if (this.isBigInt(typeRef as Function)) {
return {
format: 'int64',
...metadata,
type: 'integer',
name: metadata.name || key
};
} as SchemaObjectMetadata;
}
if (!isBuiltInType(typeRef as Function)) {
return this.createNotBuiltInTypeReference(
Expand Down Expand Up @@ -639,7 +672,7 @@ export class SchemaObjectFactory {
...metadata,
name: metadata.name || key,
type: itemType
};
} as SchemaObjectMetadata;
}

private isArrayCtor(type: Type<unknown> | string): boolean {
Expand Down
1 change: 1 addition & 0 deletions lib/services/swagger-types-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ParamWithTypeMetadata } from './parameter-metadata-accessor';
type KeysToRemove =
| keyof ApiPropertyOptions
| '$ref'
| 'properties'
| 'enumName'
| 'enumSchema';

Expand Down
2 changes: 1 addition & 1 deletion test/services/schema-object-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ describe('SchemaObjectFactory', () => {
enum: [1, 2, 3],
enumName: 'MyEnum',
isArray: false
};
} as const;
const schemas = {};

schemaObjectFactory.createEnumSchemaType('field', metadata, schemas);
Expand Down

0 comments on commit 92df199

Please sign in to comment.