@avonjs/avonjs
Version:
A fluent Node.js API generator.
851 lines (850 loc) • 35.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const collect_js_1 = __importDefault(require("collect.js"));
const pluralize_1 = require("pluralize");
const FieldCollection_1 = __importDefault(require("../Collections/FieldCollection"));
const Contracts_1 = require("../Contracts");
const Filters_1 = require("../Filters");
const Orderings_1 = require("../Orderings");
const helpers_1 = require("../helpers");
exports.default = (Parent) => {
class ResourceSchema extends Parent {
/**
* Indicates resource is available to display in Swagger UI.
*/
availableForSwagger = true;
/**
* Indicates resource is available for `index` API.
*/
availableForIndex = true;
/**
* Indicates resource is available for `detail` API.
*/
availableForDetail = true;
/**
* Indicates resource is available for `create` API.
*/
availableForCreation = true;
/**
* Indicates resource is available for `update` API.
*/
availableForUpdate = true;
/**
* Indicates resource is available for `delete` API.
*/
availableForDelete = true;
/**
* Indicates resource is available for `force delete` API.
*/
availableForForceDelete = true;
/**
* Indicates resource is available for `restore` API.
*/
availableForRestore = true;
/**
* Indicates resource is available for `review` API.
*/
availableForReview = true;
/**
* Get the Open API json schema.
*/
schema(request) {
if (!this.availableForSwagger) {
return {};
}
const paths = this.apis(request);
return {
[paths.index]: {
...this.resourceIndexSchema(request),
...this.resourceStoreSchema(request),
},
[paths.detail]: {
...this.resourceDetailSchema(request),
...this.resourceUpdateSchema(request),
...this.resourceDeleteSchema(request),
},
[paths.lookup]: {
...this.resourceLookupSchema(request),
},
[paths.restore]: {
...this.resourceRestoreSchema(request),
},
[paths.review]: {
...this.resourceReviewSchema(request),
},
[paths.forceDelete]: {
...this.resourceForceDeleteSchema(request),
},
...this.actionsSchema(request),
...this.associationSchema(request),
};
}
resourceIndexSchema(request) {
if (this.availableForIndex) {
return {
get: {
tags: [this.uriKey()],
description: `Get list of available ${this.label()}`,
operationId: 'index',
parameters: [
...this.searchParameters(request),
...this.paginationParameters(request),
...this.softDeleteParameters(request),
...this.filteringParameters(request),
...this.orderingParameters(request),
],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
200: {
description: `Get list of available ${this.label()}`,
content: this.paginatedResponseSchema(this.resourceContentSchema(this.formatResponseFields(request, new FieldCollection_1.default(this.fieldsForIndex(request)).filterForIndex(request, this.resource)))),
},
},
},
};
}
}
resourceContentSchema(fields) {
return {
type: 'array',
items: {
type: 'object',
properties: {
metadata: this.resourceMetaDataSchema(),
authorization: {
type: 'object',
properties: {
authorizedToView: {
type: 'boolean',
default: true,
description: 'Determines user authorized to view the resource detail',
},
authorizedToUpdate: {
type: 'boolean',
default: true,
description: 'Determines user authorized to update the resource',
},
authorizedToDelete: {
type: 'boolean',
default: true,
description: 'Determines user authorized to delete the resource',
},
...(this.softDeletes()
? {
authorizedToForceDelete: {
type: 'boolean',
default: true,
description: 'Determines user authorized to force-delete the resource',
},
authorizedToRestore: {
type: 'boolean',
default: true,
description: 'Determines user authorized to restore the resource',
},
authorizedToReview: {
type: 'boolean',
default: true,
description: 'Determines user authorized to review soft deleted the resource',
},
}
: {}),
},
},
fields: {
type: 'object',
properties: fields,
},
},
},
};
}
/**
* Get the resource metadata schema.
*/
resourceMetaDataSchema() {
return {
type: 'object',
properties: {
softDeletes: {
type: 'boolean',
description: 'Indicates resource uses soft delete feature.',
},
softDeleted: {
type: 'boolean',
description: 'Indicates resource is deleted or not',
},
},
};
}
/**
* Get the searching parameters for index schema.
*/
searchParameters(request) {
return [
{
name: 'search',
in: 'query',
description: 'Enter value to search through records',
schema: {
type: 'string',
nullable: true,
},
},
];
}
/**
* Get pagination parameters for index schema.
*/
paginationParameters(request) {
return [
{
name: 'page',
in: 'query',
description: 'The pagination page',
example: 1,
schema: {
type: 'integer',
minimum: 1,
nullable: true,
},
},
{
name: 'perPage',
in: 'query',
description: 'Number of items per page',
example: this.perPageOptions()[0],
schema: {
type: 'number',
nullable: true,
enum: this.perPageOptions(),
},
},
];
}
/**
* Get soft delete resource parameters for schema.
*/
softDeleteParameters(request) {
return this.softDeletes() === false
? []
: [
{
name: 'trashed',
in: 'query',
description: 'Determine trashed items behavior',
example: Contracts_1.TrashedStatus.DEFAULT,
schema: {
type: 'string',
nullable: false,
enum: [
Contracts_1.TrashedStatus.WITH,
Contracts_1.TrashedStatus.ONLY,
Contracts_1.TrashedStatus.DEFAULT,
],
},
},
];
}
/**
* Get ordering parameters.
*/
orderingParameters(request) {
const orderings = (0, collect_js_1.default)(this.resolveOrderings(request));
this.availableFieldsOnIndexOrDetail(request)
.withOnlyOrderableFields()
.each((field) => {
const ordering = field.resolveOrdering(request);
if (ordering instanceof Orderings_1.Ordering) {
orderings.push(ordering);
}
});
return orderings
.unique((ordering) => ordering.key())
.all()
.flatMap((ordering) => ordering.serializeParameters(request));
}
/**
* Get filtering parameters.
*/
filteringParameters(request) {
const filters = (0, collect_js_1.default)(this.resolveFilters(request));
this.availableFieldsOnIndexOrDetail(request)
.withOnlyFilterableFields()
.each((field) => {
const filter = field.resolveFilter(request);
if (filter instanceof Filters_1.Filter) {
filters.push(filter);
}
});
return filters
.unique((filter) => filter.key())
.all()
.flatMap((filter) => filter.serializeParameters(request));
}
/**
* Get resource store schema.
*/
resourceStoreSchema(request) {
if (this.availableForCreation) {
const fields = new FieldCollection_1.default(this.fieldsForCreate(request))
.withoutUnfillableFields()
.onlyCreationFields(request);
const schema = {
type: 'object',
required: fields
.filter((field) => field.isRequiredForCreation(request))
.map((field) => field.attribute)
.all(),
properties: this.formatPayloadFields(request, fields),
};
return {
post: {
tags: [this.uriKey()],
description: 'Create new record for the given payload',
operationId: 'store',
requestBody: {
content: (0, collect_js_1.default)(this.accepts())
.mapWithKeys((content) => [content, { schema }])
.all(),
},
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
...this.validationResponses(),
201: {
description: `Get detail of stored ${this.label()}`,
content: this.singleResourceContent(request, {
id: { $ref: '#components/schemas/PrimaryKey' },
}),
},
},
},
};
}
}
/**
*
*/
resourceDetailSchema(request) {
if (this.availableForDetail) {
return {
get: {
tags: [this.uriKey()],
description: `Get detail of resource by the given ${this.label()} key`,
operationId: 'detail',
parameters: [...this.singleResourcePathParameters(request)],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
200: {
description: `Get detail of ${this.label()} for given id`,
content: this.singleResourceContent(request),
},
},
},
};
}
}
/**
*
*/
resourceLookupSchema(request) {
const lookups = this.availableFields(request).filter((field) => field.isLookupable());
if (this.availableForDetail && lookups.isNotEmpty()) {
return {
get: {
tags: [this.uriKey()],
description: `Get detail of resource by the alternative ${this.label()} key`,
operationId: 'lookup',
parameters: [
...this.singleResourcePathParameters(request),
{
name: 'field',
in: 'path',
required: true,
description: 'The resource alternative key name',
schema: {
type: 'string',
enum: lookups
.map((field) => field.attribute)
.values()
.toArray(),
},
},
],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
200: {
description: `Get detail of ${this.label()} for given lookup key`,
content: this.singleResourceContent(request),
},
},
},
};
}
}
/**
*
*/
resourceReviewSchema(request) {
if (this.availableForReview && Boolean(this.softDeletes())) {
return {
get: {
tags: [this.uriKey()],
description: `Get detail of resource by the given ${this.label()} key`,
operationId: 'review',
parameters: [...this.singleResourcePathParameters(request)],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
200: {
description: `Get detail of ${this.label()} for given id`,
content: {
...this.reviewResourceContent(request),
},
},
},
},
};
}
}
/**
*
*/
resourceUpdateSchema(request) {
if (this.availableForUpdate) {
const fields = new FieldCollection_1.default(this.fieldsForUpdate(request))
.withoutUnfillableFields()
.onlyUpdateFields(request, this.repository().model());
const schema = {
type: 'object',
required: fields
.filter((field) => field.isRequiredForUpdate(request))
.map((field) => field.attribute)
.all(),
properties: this.formatPayloadFields(request, fields),
};
return {
put: {
tags: [this.uriKey()],
description: 'Update resource by the given payload',
operationId: 'update',
parameters: [...this.singleResourcePathParameters(request)],
requestBody: {
content: (0, collect_js_1.default)(this.accepts())
.mapWithKeys((content) => [content, { schema }])
.all(),
},
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
...this.validationResponses(),
200: {
description: `Get detail of updated ${this.label()}`,
content: this.singleResourceContent(request, {
id: { $ref: '#components/schemas/PrimaryKey' },
}),
},
},
},
};
}
}
/**
*
*/
resourceDeleteSchema(request) {
if (this.availableForDelete) {
return {
delete: {
tags: [this.uriKey()],
description: `Delete ${this.label()} by the given id`,
operationId: 'delete',
parameters: [...this.singleResourcePathParameters(request)],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
204: { $ref: '#/components/responses/EmptyResponse' },
},
},
};
}
}
/**
*
*/
resourceForceDeleteSchema(request) {
if (this.availableForForceDelete && Boolean(this.softDeletes())) {
return {
delete: {
tags: [this.uriKey()],
description: `Delete ${this.label()} by the given id`,
operationId: 'forceDelete',
parameters: [...this.singleResourcePathParameters(request)],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
204: { $ref: '#/components/responses/EmptyResponse' },
},
},
};
}
}
/**
*
*/
resourceRestoreSchema(request) {
if (this.availableForRestore && Boolean(this.softDeletes())) {
return {
put: {
tags: [this.uriKey()],
description: `Restore deleted ${this.label()} by id`,
operationId: 'restore',
parameters: [...this.singleResourcePathParameters(request)],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
204: { $ref: '#/components/responses/EmptyResponse' },
},
},
};
}
}
/**
* Get the Open API json schema for relationship fields.
*/
actionsSchema(request) {
const actions = (0, collect_js_1.default)(this.resolveActions(request));
const paths = this.apis(request);
return actions
.mapWithKeys((action) => {
const fields = new FieldCollection_1.default(action.fields(request));
const schema = {
type: 'object',
required: fields.map((field) => field.attribute).all(),
properties: this.formatPayloadFields(request, fields),
};
return [
`${action.isInline() ? paths.detail : paths.index}/actions/${action.uriKey()}`,
{
[action.isDestructive() ? 'delete' : 'post']: {
tags: [this.uriKey()],
description: `Run the ${action.name()} on the given resources`,
operationId: `${this.uriKey()}-${action.uriKey()}`,
parameters: action.isInline()
? this.singleResourcePathParameters(request)
: this.actionQueryParameters(request, action),
requestBody: fields.isEmpty()
? undefined
: {
content: (0, collect_js_1.default)(action.accepts())
.mapWithKeys((content) => [content, { schema }])
.all(),
},
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
...this.validationResponses(),
...action.responseSchema(request),
},
},
},
];
})
.all();
}
/**
* Get the Open API json schema for relationship fields.
*/
associationSchema(request) {
const paths = this.apis(request);
return this.availableFieldsOnForms(request)
.withOnlyRelatableFields()
.withoutUnfillableFields()
.mapWithKeys((field) => {
const relatable = field.relatedResource;
return [
`${paths.index}/associable/${field.attribute}`,
{
get: {
tags: [this.uriKey()],
description: `Get list of related ${relatable.label()}`,
operationId: field.attribute,
parameters: [
{
name: 'page',
in: 'query',
description: 'The pagination page',
example: 1,
default: 1,
schema: {
type: 'integer',
minimum: 1,
nullable: true,
},
},
{
name: 'perPage',
in: 'query',
description: 'Number of items per page',
example: relatable.relatableSearchResults,
default: relatable.relatableSearchResults,
schema: {
type: 'number',
nullable: true,
enum: [relatable.relatableSearchResults],
},
},
...relatable.searchParameters(request),
...relatable.softDeleteParameters(request),
...(0, collect_js_1.default)(field.availableFilters(request))
.unique((filter) => filter.key())
.all()
.flatMap((filter) => filter.serializeParameters(request)),
...(0, collect_js_1.default)(field.availableOrderings(request))
.unique((order) => order.key())
.all()
.flatMap((order) => order.serializeParameters(request)),
],
responses: {
...this.authorizationResponses(),
...this.errorsResponses(),
200: {
description: `Get list of related ${relatable.label()}`,
content: this.paginatedResponseSchema(this.resourceContentSchema(relatable.formatResponseFields(request, new FieldCollection_1.default(relatable.fieldsForAssociation(request))
.filterForAssociation(request)
.withoutUnresolvableFields()
.withoutRelatableFields()))),
},
},
},
},
];
})
.all();
}
/**
* Get the single resource content schema.
*/
singleResourceContent(request, schema) {
return this.jsonResponseSchema({
type: 'object',
properties: {
metadata: this.resourceMetaDataSchema(),
authorization: {
type: 'object',
properties: {
authorizedToUpdate: {
type: 'boolean',
default: true,
description: 'Determines user authorized to update the resource',
},
authorizedToDelete: {
type: 'boolean',
default: true,
description: 'Determines user authorized to delete the resource',
},
...(this.softDeletes()
? {
authorizedToForceDelete: {
type: 'boolean',
default: true,
description: 'Determines user authorized to force-delete the resource',
},
}
: {}),
},
},
fields: {
type: 'object',
properties: schema ??
this.formatResponseFields(request, new FieldCollection_1.default(this.fieldsForDetail(request)).filterForDetail(request, this.resource)),
},
},
});
}
paginatedResponseSchema(data) {
return this.jsonResponseSchema(data, {
type: 'object',
properties: {
count: {
type: 'integer',
},
page: {
type: 'integer',
},
perPage: {
type: 'integer',
},
perPageOptions: {
type: 'array',
uniqueItems: true,
items: {
type: 'integer',
},
},
},
});
}
jsonResponseSchema(data, meta) {
return {
'application/json': {
schema: {
type: 'object',
properties: {
code: { type: 'number', default: 200 },
data,
meta: { type: 'object', ...meta },
},
},
},
};
}
/**
* Get the single resource content schema.
*/
reviewResourceContent(request) {
return this.jsonResponseSchema({
type: 'object',
properties: {
metadata: this.resourceMetaDataSchema(),
authorization: {
type: 'object',
properties: {
authorizedToForceDelete: {
type: 'boolean',
default: true,
description: 'Determines user authorized to force-delete the resource',
},
authorizedToRestore: {
type: 'boolean',
default: true,
description: 'Determines user authorized to restore the resource',
},
},
},
fields: {
type: 'object',
properties: this.formatResponseFields(request, new FieldCollection_1.default(this.fieldsForReview(request)).filterForReview(request, this.resource)),
},
},
});
}
/**
* Get the single resource path parameters.
*/
singleResourcePathParameters(request) {
return [
{
name: this.getRouteKeyName(),
in: 'path',
required: true,
description: 'The resource primary key',
example: 1,
schema: { $ref: '#components/schemas/PrimaryKey' },
},
];
}
/**
* Get the single resource path parameters.
*/
actionQueryParameters(request, action) {
return [
{
name: 'resources',
in: 'query',
description: 'Enter record id you want to run action on it',
required: !action.isStandalone(),
style: 'deepObject',
explode: true,
schema: {
type: 'array',
items: {
oneOf: [
{ type: 'number', nullable: false, minLength: 1 },
{ type: 'string', nullable: false },
],
},
nullable: false,
minItems: action.isStandalone() ? 0 : 1,
},
},
];
}
/**
* Get the API paths.
*/
apis(request) {
const basePath = request.getRequest().baseUrl;
const resourcePath = `/${basePath}/resources/${String(this.uriKey())}`.replace(/\/{2,}/g, '/');
return {
index: resourcePath,
detail: `${resourcePath}/{${this.getRouteKeyName()}}`,
lookup: `${resourcePath}/{${this.getRouteKeyName()}}/using/{field}`,
review: `${resourcePath}/{${this.getRouteKeyName()}}/review`,
restore: `${resourcePath}/{${this.getRouteKeyName()}}/restore`,
forceDelete: `${resourcePath}/{${this.getRouteKeyName()}}/force`,
action: `${resourcePath}/actions/{actionName}`,
association: `${resourcePath}/associable/{field}`,
};
}
/**
* Get route key name.
*/
getRouteKeyName() {
return 'resourceId';
}
/**
* Get the schema label.
*/
label() {
return (0, pluralize_1.plural)((0, helpers_1.slugify)(this.constructor.name, ' '));
}
/**
* Format the given schema for responses.
*/
formatResponseFields(request, fields) {
return new FieldCollection_1.default(fields)
.resolve(this.resource ?? this.repository().model())
.responseSchemas(request);
}
/**
* Format the given schema for responses.
*/
formatPayloadFields(request, fields) {
return fields
.resolve(this.resource ?? this.repository().model())
.payloadSchemas(request);
}
/**
* name
*/
authorizationResponses() {
return (0, helpers_1.authorizationResponses)();
}
/**
* name
*/
errorsResponses() {
return (0, helpers_1.errorsResponses)();
}
/**
* name
*/
validationResponses() {
return (0, helpers_1.validationResponses)();
}
/**
* Get the swagger-ui possible request body contents.
*/
accepts() {
return ['application/json', 'multipart/form-data'];
}
}
return ResourceSchema;
};