@loopback/openapi-v3
Version:
Decorators that annotate LoopBack artifacts with OpenAPI v3 metadata and utilities that transform LoopBack metadata to OpenAPI v3 specifications
506 lines (487 loc) • 16.7 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
import {FilterSchemaOptions, Model} from '@loopback/repository-json-schema';
import {getFilterSchemaFor, getWhereSchemaFor} from '../filter-schema';
import {resolveSchema} from '../generate-schema';
import {OAI3Keys} from '../keys';
import {
isSchemaObject,
ParameterLocation,
ParameterObject,
ReferenceObject,
SchemaObject,
} from '../types';
export const PARAMETER_INDEX = 'x-parameter-index';
/**
* Describe an input parameter of a Controller method.
*
* `@param` must be applied to parameters.
*
* @example
* ```ts
* class MyController {
* @get('/')
* list(
* @param(offsetSpec) offset?: number,
* @param(pageSizeSpec) pageSize?: number,
* ) {}
* }
* ```
*
* @param paramSpec - Parameter specification.
*/
export function param(paramSpec: ParameterObject) {
return function (target: object, member: string, index: number) {
paramSpec = {...paramSpec};
// Get the design time method parameter metadata
const methodSig = MetadataInspector.getDesignTypeForMethod(target, member);
const paramTypes = methodSig?.parameterTypes || [];
// Map design-time parameter type to the OpenAPI param type
const paramType = paramTypes[index];
if (paramType) {
if (
// generate schema if `paramSpec` doesn't have it
!paramSpec.schema ||
// generate schema if `paramSpec` has `schema` but without `type`
(isSchemaObject(paramSpec.schema) && !paramSpec.schema.type)
) {
// If content explicitly mentioned do not resolve schema
if (!paramSpec.content) {
// please note `resolveSchema` only adds `type` and `format` for `schema`
paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
}
}
}
if (
paramSpec.schema &&
isSchemaObject(paramSpec.schema) &&
paramSpec.schema.type === 'array'
) {
// The design-time type is `Object` for `any`
if (paramType != null && paramType !== Object && paramType !== Array) {
throw new Error(
`The parameter type is set to 'array' but the JavaScript type is ${paramType.name}`,
);
}
}
ParameterDecoratorFactory.createDecorator<ParameterObject>(
OAI3Keys.PARAMETERS_KEY,
paramSpec,
{decoratorName: '@param'},
)(target, member, index);
};
}
/**
* The `type` and `format` inferred by a common name of OpenAPI 3.0.0 data type
* reference link:
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types
*/
const builtinTypes: Record<string, ParamShortcutOptions> = {
string: {type: 'string'},
boolean: {type: 'boolean'},
number: {type: 'number'},
integer: {type: 'integer', format: 'int32'},
long: {type: 'integer', format: 'int64'},
float: {type: 'number', format: 'float'},
double: {type: 'number', format: 'double'},
byte: {type: 'string', format: 'byte'},
binary: {type: 'string', format: 'binary'},
date: {type: 'string', format: 'date'},
dateTime: {type: 'string', format: 'date-time'},
password: {type: 'string', format: 'password'},
};
/**
* Namespace for `@param.*` decorators
*/
export namespace param {
/**
* Query parameter decorator
*/
export const query = {
/**
* Define a parameter of "integer" type that's read from the query string.
* Usage: ` @param.query.string('paramName')`
*
* @param name - Parameter name.
*/
string: createParamShortcut('query', builtinTypes.string),
/**
* Define a parameter of "number" type that's read from the query string.
* Usage: ` @param.query.number('paramName')`
*
* @param name - Parameter name.
*/
number: createParamShortcut('query', builtinTypes.number),
/**
* Define a parameter of "boolean" type that's read from the query string.
* Usage: ` @param.query.boolean('paramName')`
*
* @param name - Parameter name.
*/
boolean: createParamShortcut('query', builtinTypes.boolean),
/**
* Define a parameter of "integer" type that's read from the query string.
* Usage: ` @param.query.integer('paramName')`
*
* @param name - Parameter name.
*/
integer: createParamShortcut('query', builtinTypes.integer),
/**
* Define a parameter of "long" type that's read from the query string.
* Usage: ` @param.query.long('paramName')`
*
* @param name - Parameter name.
*/
long: createParamShortcut('query', builtinTypes.long),
/**
* Define a parameter of "float" type that's read from the query string.
* Usage: ` @param.query.float('paramName')`
*
* @param name - Parameter name.
*/
float: createParamShortcut('query', builtinTypes.float),
/**
* Define a parameter of "double" type that's read from the query string.
* Usage: ` @param.query.double('paramName')`
*
* @param name - Parameter name.
*/
double: createParamShortcut('query', builtinTypes.double),
/**
* Define a parameter of "byte" type that's read from the query string.
* Usage: ` @param.query.byte('paramName')`
*
* @param name - Parameter name.
*/
byte: createParamShortcut('query', builtinTypes.byte),
/**
* Define a parameter of "binary" type that's read from the query string.
* Usage: ` @param.query.binary('paramName')`
*
* @param name - Parameter name.
*/
binary: createParamShortcut('query', builtinTypes.binary),
/**
* Define a parameter of "date" type that's read from the query string.
* Usage: ` @param.query.date('paramName')`
*
* @param name - Parameter name.
*/
date: createParamShortcut('query', builtinTypes.date),
/**
* Define a parameter of "dateTime" type that's read from the query string.
* Usage: ` @param.query.dateTime('paramName')`
*
* @param name - Parameter name.
*/
dateTime: createParamShortcut('query', builtinTypes.dateTime),
/**
* Define a parameter of "password" type that's read from the query string.
* Usage: ` @param.query.password('paramName')`
*
* @param name - Parameter name.
*/
password: createParamShortcut('query', builtinTypes.password),
/**
* Define a parameter accepting an object value encoded
* - as a JSON string, e.g. `filter={"where":{"id":1}}`); or
* - in multiple nested keys, e.g. `filter[where][id]=1`
*
* @param name - Parameter name
* @param schema - Optional OpenAPI Schema describing the object value.
*/
object: function (
name: string,
schema: SchemaObject | ReferenceObject = {
type: 'object',
additionalProperties: true,
},
spec?: Partial<ParameterObject>,
) {
schema = {
type: 'object',
...schema,
};
return param({
name,
in: 'query',
content: {
'application/json': {
schema,
},
},
...spec,
});
},
};
/**
* Header parameter decorator
*/
export const header = {
/**
* Define a parameter of "string" type that's read from a request header.
* Usage: ` @param.header.string('paramName')`
*
* @param name - Parameter name, it must match the header name
* (e.g. `Content-Type`).
*/
string: createParamShortcut('header', builtinTypes.string),
/**
* Define a parameter of "number" type that's read from a request header.
* Usage: ` @param.header.number('paramName')`
*
* @param name - Parameter name, it must match the header name
* (e.g. `Content-Length`).
*/
number: createParamShortcut('header', builtinTypes.number),
/**
* Define a parameter of "boolean" type that's read from a request header.
* Usage: ` @param.header.boolean('paramName')`
*
* @param name - Parameter name, it must match the header name
* (e.g. `DNT` or `X-Do-Not-Track`).
*/
boolean: createParamShortcut('header', builtinTypes.boolean),
/**
* Define a parameter of "integer" type that's read from a request header.
* Usage: ` @param.header.integer('paramName')`
*
* @param name - Parameter name, it must match the header name
* (e.g. `Content-Length`).
*/
integer: createParamShortcut('header', builtinTypes.integer),
/**
* Define a parameter of "long" type that's read from a request header.
* Usage: ` @param.header.long('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
long: createParamShortcut('header', builtinTypes.long),
/**
* Define a parameter of "float" type that's read from a request header.
* Usage: ` @param.header.float('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
float: createParamShortcut('header', builtinTypes.float),
/**
* Define a parameter of "double" type that's read from a request header.
* Usage: ` @param.header.double('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
double: createParamShortcut('header', builtinTypes.double),
/**
* Define a parameter of "byte" type that's read from a request header.
* Usage: ` @param.header.byte('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
byte: createParamShortcut('header', builtinTypes.byte),
/**
* Define a parameter of "binary" type that's read from a request header.
* Usage: ` @param.header.binary('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
binary: createParamShortcut('header', builtinTypes.binary),
/**
* Define a parameter of "date" type that's read from a request header.
* Usage: ` @param.header.date('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
date: createParamShortcut('header', builtinTypes.date),
/**
* Define a parameter of "dateTime" type that's read from a request header.
* Usage: ` @param.header.dateTime('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
dateTime: createParamShortcut('header', builtinTypes.dateTime),
/**
* Define a parameter of "password" type that's read from a request header.
* Usage: ` @param.header.password('paramName')`
*
* @param name - Parameter name, it must match the header name
*/
password: createParamShortcut('header', builtinTypes.password),
};
/**
* Path parameter decorator
*/
export const path = {
/**
* Define a parameter of "string" type that's read from request path.
* Usage: ` @param.path.string('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
string: createParamShortcut('path', builtinTypes.string),
/**
* Define a parameter of "number" type that's read from request path.
* Usage: ` @param.path.number('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
number: createParamShortcut('path', builtinTypes.number),
/**
* Define a parameter of "boolean" type that's read from request path.
* Usage: ` @param.path.boolean('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
boolean: createParamShortcut('path', builtinTypes.boolean),
/**
* Define a parameter of "integer" type that's read from request path.
* Usage: ` @param.path.integer('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
integer: createParamShortcut('path', builtinTypes.integer),
/**
* Define a parameter of "long" type that's read from request path.
* Usage: ` @param.path.long('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
long: createParamShortcut('path', builtinTypes.long),
/**
* Define a parameter of "float" type that's read from request path.
* Usage: ` @param.path.float('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
float: createParamShortcut('path', builtinTypes.float),
/**
* Define a parameter of "double" type that's read from request path.
* Usage: ` @param.path.double('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
double: createParamShortcut('path', builtinTypes.double),
/**
* Define a parameter of "byte" type that's read from request path.
* Usage: ` @param.path.byte('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
byte: createParamShortcut('path', builtinTypes.byte),
/**
* Define a parameter of "binary" type that's read from request path.
* Usage: ` @param.path.binary('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
binary: createParamShortcut('path', builtinTypes.binary),
/**
* Define a parameter of "date" type that's read from request path.
* Usage: ` @param.path.date('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
date: createParamShortcut('path', builtinTypes.date),
/**
* Define a parameter of "dateTime" type that's read from request path.
* Usage: ` @param.path.dateTime('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
dateTime: createParamShortcut('path', builtinTypes.dateTime),
/**
* Define a parameter of "password" type that's read from request path.
* Usage: ` @param.path.password('paramName')`
*
* @param name - Parameter name matching one of the placeholders in the path
*/
password: createParamShortcut('path', builtinTypes.password),
};
/**
* Define a parameter of `array` type.
*
* @example
* ```ts
* export class MyController {
* @get('/greet')
* greet(@param.array('names', 'query', {type: 'string'}) names: string[]): string {
* return `Hello, ${names}`;
* }
* }
* ```
*
* @param name - Parameter name
* @param source - Source of the parameter value
* @param itemSpec - Item type for the array or the full item object
*/
export const array = function (
name: string,
source: ParameterLocation,
itemSpec: SchemaObject | ReferenceObject,
) {
return param({
name,
in: source,
schema: {type: 'array', items: itemSpec},
});
};
/**
* Sugar decorator for `filter` query parameter
*
* @example
* ```ts
* async find(
* @param.filter(modelCtor)) filter?: Filter<T>,
* ): Promise<(T & Relations)[]> {
* // ...
* }
* ```
* @param modelCtor - Model class
* @param options - Options to customize the parameter name or filter schema
*
*/
export function filter(
modelCtor: typeof Model,
options?: string | (FilterSchemaOptions & {name?: string}),
) {
let name = 'filter';
if (typeof options === 'string') {
name = options;
options = {};
}
name = options?.name ?? name;
return param.query.object(name, getFilterSchemaFor(modelCtor, options));
}
/**
* Sugar decorator for `where` query parameter
*
* @example
* ```ts
* async count(
* @param.where(modelCtor)) where?: Where<T>,
* ): Promise<Count> {
* // ...
* }
* ```
* @param modelCtor - Model class
* @param name - Custom name for the parameter, default to `where`
*
*/
export function where(modelCtor: typeof Model, name = 'where') {
return param.query.object(name, getWhereSchemaFor(modelCtor));
}
}
interface ParamShortcutOptions {
type: SchemaObject['type'];
format?: string;
}
function createParamShortcut(
source: ParameterLocation,
options: ParamShortcutOptions,
) {
return (name: string, spec?: Partial<ParameterObject>) => {
return param({name, in: source, schema: {...options}, ...spec});
};
}