UNPKG

@avonjs/avonjs

Version:

A fluent Node.js API generator.

851 lines (850 loc) 35.4 kB
"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; };