OpenAPI - Controllers
Controllers provide an entry point for all openapi entries. When Psychic builds openapi files for your application, it starts by scanning all of your controllers and finding all openapi decorators. It then computes the schema for those endpoints, and then spits that schema out into the corresponding json files.
To leverage the OpenAPI decorator in your controllers, simply import it and use it to decorate the methods you wish to expose, like so:
import { OpenAPI } from '@rvoh/psychic'
export default class PlacesController extends PsychicController {
@OpenAPI(Place, {
many: true,
status: 200,
serializerKey: 'summary',
})
public async index() {
this.ok(await this.currentHost.associationQuery('places').all())
}
}
By default, Psychic will match this endpoint to the corresponding route entry in your conf/routes.ts
file. If it does not find a matching route, it will raise an exception, so be sure to add a matching entry pointing to that method on your controller, i.e.
// conf/routes.ts
export default function routes(r: PsychicRouter) {
r.resources('places', { only: ['index'] })
// OR
r.get('/places', PlacesController, 'index')
}
Implicit serialization
If a serializable class is provided as the first argument to the @OpenAPI
decorator, it will automatically read all related attribute definitions on the corresponding serializer, formulating an object shape that it injects into the openapi document.
export default class PlacesController extends PsychicController {
// by providing Place here, we are telling OpenAPI to locate the default
// serializer attached to the Place model and serialize its attribute shapes
// into an openapi document, which will become the 200 response shape for
// this endpoint
@OpenAPI(Place, {
status: 200,
})
public async show() {
this.ok(
await this.currentHost
.associationQuery('places')
.findOrFail(this.castParam('id', 'bigint'))
)
}
}
In addition to providing a model class as the first argument, you can also specify a serializer directly, which takes away the implicit decision making that Psychic does to decide on a serializer for you.
@OpenAPI(PlaceSerializer, {
status: 200,
})
serializerKey
You can provide a serializerKey
option, which will inform the decorator to use a specific serializer attached to your model, like so:
// models/Place.ts
export default class Place extends ApplicationModel {
public get table() {
return 'places' as const
}
public get serializers(): DreamSerializers<Place> {
return { default: 'PlaceSerializer', summary: 'PlaceSummarySerializer' }
}
// ...
}
// controllers/V1/Host/PlacesController
export default class V1HostPlacesController extends PsychicController {
// by specifying the "summary" serializerKey, we are telling OpenAPI
// to use the `PlaceSummarySerializer`.
@OpenAPI(Place, {
serializerKey: 'summary',
status: 200,
})
public async index() {
this.ok(
await this.currentHost
.associationQuery('places')
.preloadFor('summary')
.all()
)
}
}
many
Use the many
option when you are providing a serializable class, and you want the endpoint to render an array of them. This can be done like so:
export default class AdminPlacesController extends PsychicController {
@OpenAPI(Place, {
many: true,
status: 200,
})
public async index() {
this.ok(await this.currentHost.associationQuery('places').limit(100).all())
}
}
nullable
As expected, the nullable
option enables you to specify that the default response can also be null.
export default class AdminPlacesController extends PsychicController {
@OpenAPI(Place, {
nullable: true,
status: 200,
})
public async show() {
this.ok(await this.currentHost.associationQuery('places').first())
}
}
status
By providing a status
option to the OpenAPI decorator, you can specify which HTTP status code will be returned if the request succeeds.
export default class PostsController extends PsychicController {
@OpenAPI({
status: 204,
})
public async helloWorld() {
this.noContent()
}
}
requestBody
Using the requestBody
option, we can inform OpenAPI what the shape of our incoming request will be, using standard OpenAPI notation.
export default class PostsController extends PsychicController {
@OpenAPI({
status: 204,
requestBody: {
type: 'object',
required: ['searchTerm'],
properties: {
searchTerm: {
type: 'string',
description: 'A search term provided by the user',
},
},
},
})
public async helloWorld() {
this.noContent()
}
}
Request bodies will by default be inferred from the serializable argument, if it is provided and is pointing to a dream model, like so:
export default class UsersController extends PsychicController {
@OpenAPI(User, {
status: 204,
})
public async create() {
await User.create(this.paramsFor(User))
this.noContent()
}
}
for
For more granular specification, you can take advantage of the for
, only
, and including
options, which enable you to explicitly provide certain params for a model class.
export default class UsersController extends PsychicController {
@OpenAPI({
status: 204,
requestBody: {
for: User,
only: ['email'],
},
})
public async create() {
await User.create(this.paramsFor(User, { only: ['email'] }))
this.noContent()
}
}
If the for
argument is left off, the allowed fields will fall back to the base model provided to the decorator, i.e.
export default class UsersController extends PsychicController {
@OpenAPI(User, {
status: 204,
requestBody: {
only: ['email'],
},
})
public async create() {
await User.create(this.paramsFor(User, { only: ['email'] }))
this.noContent()
}
}
Tapping into the including
arg, we can explicitly provide params that would otherwise be excluded (i.e. foreign keys, primary keys, and timestamp fields).
export default class PetsController extends PsychicController {
@OpenAPI(Pet, {
status: 204,
requestBody: {
only: ['species'],
including: ['userId'],
},
})
public async create() {
await Pet.create(
this.paramsFor(Pet, { only: ['species'], including: ['userId'] })
)
this.noContent()
}
}
headers
Provide the headers
option to specify which headers are expected for this endpoint.
export default class PostsController extends PsychicController {
@OpenAPI({
headers: {
Authorization: { description: 'Bearer token', required: true },
},
})
public async helloWorld() {
this.noContent()
}
}
responses
Use the responses
option to specify the payload shape for specific response statuses, like so:
export default class PostsController extends PsychicController {
@OpenAPI({
status: 204,
responses: {
400: {
type: 'object',
properties: {
errors: 'string[]',
},
},
},
})
public async helloWorld() {
if (something) {
this.badRequest({ errors: ['error 1', 'error 2'] })
}
this.noContent()
}
}
query
The query
option enables you to specify the shape of custom query params for your endpoint. By default, all query params are automatically assumed to be strings, but you can customize their shape if desired, using the schema
suboption, as shown below
export default class PostsController extends PsychicController {
@OpenAPI({
query: {
searchTerm: {
required: false,
},
searchTerms: {
required: false,
schema: {
type: 'string[]',
},
},
},
})
public async helloWorld() {
this.noContent()
}
}
defaultResponse
Use the defaultResponse
option to specify attributes for the default response, such as description:
export default class PostsController extends PsychicController {
@OpenAPI({
defaultResponse: {
description: 'my description',
},
})
public async helloWorld() {
this.noContent()
}
}
security
Use the security
option to provide security entries for openapi. The shape provided here is identical to the shape required by openapi itself, which is an array of objects, where the keys are the names, and the values are arrays of scopes required (or a blank array if there are no scopes).
export default class PostsController extends PsychicController {
@OpenAPI({
security: [
{
bearerToken: ['read', 'write'],
},
{
httpBasicAuth: [],
},
],
})
public async helloWorld() {
this.noContent()
}
}
omitDefaultHeaders
The omitDefaultHeaders
option enables you to exclude all default-provided headers (which are configured in conf/app.ts
).
export default class PostsController extends PsychicController {
@OpenAPI({
omitDefaultHeaders: true,
})
public async helloWorld() {
this.noContent()
}
}
omitDefaultResponses
Similar to the omitDefaultHeaders option, the omitDefaultResponses
option enables you to exclude all default-provided responses (which are also configured in conf/app.ts
).
export default class PostsController extends PsychicController {
@OpenAPI({
omitDefaultResponses: true,
})
public async helloWorld() {
this.noContent()
}
}
pathParams
export default class PostsController extends PsychicController {
@OpenAPI({
pathParams: {
id: {
description: 'the id of the post',
},
},
})
public async show() {
this.ok(
await this.currentHost
.associationQuery('places')
.findOrFail(this.castParam('id', 'bigint'))
)
}
}