UNPKG

@nodeswork/sbase

Version:

Basic REST api foundation from Nodeswork.

394 lines (392 loc) 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require("underscore"); const utils_1 = require("@nodeswork/utils"); const object_path_1 = require("object-path"); const model = require("./model"); const validators = require("../koa/validators"); const _1 = require("./"); const params_1 = require("../koa/params"); exports.READONLY = 'READONLY'; exports.AUTOGEN = 'AUTOGEN'; class KoaMiddlewares extends model.DocumentModel { static createMiddleware(options) { const self = this.cast(); _.defaults(options, DEFAULT_COMMON_OPTIONS); async function create(ctx, next) { const opts = _.extend({}, options, ctx.overrides && ctx.overrides.options); const rModel = self; const omits = _.union(['_id'], opts.omits, self.schema.api.AUTOGEN); let doc = _.omit(ctx.request.body, omits); doc = _.extend(doc, ctx.overrides && ctx.overrides.doc); ctx[opts.target] = doc; if (opts.triggerNext) { await next(); } doc = ctx[opts.target]; let object = (await rModel.create(doc)); if (opts.project || opts.level) { object = await self.findById(object._id, opts.project, _.pick(opts, 'level', 'lean')); } ctx[opts.target] = object; if (opts.populate) { await rModel.populate(object, opts.populate); } if (!opts.noBody) { ctx.body = await opts.transform(object, ctx); } } Object.defineProperty(create, 'name', { value: `${self.modelName}#createMiddleware`, writable: false, }); return create; } /** * Returns Koa get middleware. * * Examples: * * 1. Load from ctx.params. This is the most common case where the url path * stores the model id. * * @Post('/articles/:articleId') * getArticle = models.Article.getMiddleware({ field: 'articleId' }); * * 2. Load from ctx.request. * * // When there is a dot in the field path, it will load from ctx.request. * @Middleware(models.Article.getMiddleware({ field: 'body.articleId' })); * * 3. No need to specify id. * // Pass a star. * @Middleware(models.Article.getMiddleware({ field: '*' })); * * @param options.field specifies which field to load the id key value. * @param options.idFieldName specifies the field name in query. * Default: '_id'. * @param options.target specifies which field under ctx to set the target. * Default: 'object'. * @param options.triggerNext specifies whether to trigger next middleware. * Default: false. * @param options.transform a map function before send to ctx.body. */ static getMiddleware(options) { const self = this.cast(); options = _.defaults({}, options, DEFAULT_GET_OPTIONS); const idFieldName = options.idFieldName; async function get(ctx, next) { const opts = _.extend({}, options, ctx.overrides && ctx.overrides.options); const query = (ctx.overrides && ctx.overrides.query) || {}; if (opts.field !== '*') { if (opts.field.indexOf('.') >= 0) { query[idFieldName] = object_path_1.withInheritedProps.get(ctx.request, opts.field); } else { query[idFieldName] = ctx.params[opts.field]; } if (query[idFieldName] == null) { throw new utils_1.NodesworkError('invalid value', { responseCode: 422, path: opts.field, idFieldName, }); } } if (Object.keys(query).length === 0) { throw new utils_1.NodesworkError('no query parameters', { responseCode: 422, path: opts.field, }); } const queryOption = _.pick(opts, 'level', 'lean'); let queryPromise = self.findOne(query, opts.project, queryOption); if (opts.populate) { queryPromise = queryPromise.populate(opts.populate); } const object = await queryPromise; ctx[opts.target] = object; if (!opts.nullable && object == null) { throw utils_1.NodesworkError.notFound(); } if (opts.triggerNext) { await next(); } if (!opts.noBody) { ctx.body = await opts.transform(ctx[opts.target], ctx); } } Object.defineProperty(get, 'name', { value: `${self.modelName}#getMiddleware`, writable: false, }); return get; } /** * Returns KOA find middleware. * * Examples: * * 1. Normal query. * * @Get('/articles') * find = models.Article.findMiddleware(); * * 2. Pagination. User query.size, query.page to calculate numbers to skip. * * @Get('/articles') * find = models.Article.findMiddleware({ * pagination: { * size: 50, // single page size * sizeChoices: [50, 100, 200], * // where to store the full IPaginationData<any>. * target: 'articlesWithPagination', * }, * }) * * @param options.pagination.size specifies max number of returning records. * @param options.pagination.sizeChoices * @param options.pagination.target specifies where to store the full data. * @param options.sort specifies the returning order. * @param options.level specifies the data level. * @param options.project specifies the projection. * @param options.populate specifies the populates. */ static findMiddleware(options = {}) { const self = this.cast(); _.defaults(options, DEFAULT_COMMON_OPTIONS); if (options.pagination) { _.defaults(options.pagination, DEFAULT_FIND_PAGINATION_OPTIONS); } const defaultPagination = { page: 0, size: options.pagination ? options.pagination.size : 0, }; const paginationParams = options.pagination && params_1.params({ 'query.page': [validators.toInt()], 'query.size': [ validators.toInt(), validators.isEnum(options.pagination.sizeChoices), ], }); async function find(ctx, next) { const opts = _.extend({}, options, ctx.overrides && ctx.overrides.options); const query = (ctx.overrides && ctx.overrides.query) || {}; const queryOption = _.pick(opts, 'sort', 'lean', 'level'); let pagination = null; if (opts.pagination) { await paginationParams(ctx, () => null); if (ctx.status === 422) { return; } pagination = ctx.request.query; _.defaults(pagination, defaultPagination); queryOption.skip = pagination.page * pagination.size; queryOption.limit = pagination.size; } if (ctx.overrides && ctx.overrides.sort) { queryOption.sort = ctx.overrides.sort; } let queryPromise = self.find(query, opts.project, queryOption); if (opts.populate) { queryPromise = queryPromise.populate(opts.populate); } const object = await queryPromise; ctx[opts.target] = object; const bodyTarget = pagination == null ? object : { pageSize: pagination.size, page: pagination.page, total: await self.find(query).countDocuments(), data: object, }; if (pagination && pagination.target) { ctx[pagination.target] = bodyTarget; } if (opts.triggerNext) { await next(); } if (!opts.noBody) { const objects = ctx[opts.target]; for (let i = 0; i < objects.length; i++) { objects[i] = await opts.transform(objects[i], ctx); } if (pagination && pagination.target) { bodyTarget.data = objects; } ctx.body = bodyTarget; } } Object.defineProperty(find, 'name', { value: `${self.modelName}#findMiddleware`, writable: false, }); return find; } static updateMiddleware(options) { const self = this.cast(); options = _.defaults({}, options, DEFAULT_UPDATE_OPTIONS); const idFieldName = options.idFieldName; async function update(ctx, next) { const opts = _.extend({}, options, ctx.overrides && ctx.overrides.options); const query = (ctx.overrides && ctx.overrides.query) || {}; if (opts.field !== '*') { if (opts.field.indexOf('.') >= 0) { query[idFieldName] = object_path_1.withInheritedProps.get(ctx.request, opts.field); } else { query[idFieldName] = ctx.params[opts.field]; } if (query[idFieldName] == null) { throw new utils_1.NodesworkError('invalid value', { responseCode: 422, path: opts.field, }); } } if (Object.keys(query).length === 0) { throw new utils_1.NodesworkError('no query parameters', { responseCode: 422, path: opts.field, }); } const queryOption = { new: true, fields: opts.project, level: opts.level, runValidators: true, context: 'query', lean: opts.lean, }; const omits = _.union(['_id'], [idFieldName], opts.omits, self.schema.api.READONLY, self.schema.api.AUTOGEN); const fOmits = _.filter(Object.keys(ctx.request.body), (k) => { return _.find(omits, (o) => o === k || k.startsWith(o + '.')) != null; }); let doc = _.omit(ctx.request.body, fOmits); doc = _.extend(doc, ctx.overrides && ctx.overrides.doc); const upDoc = { $set: doc, }; let updatePromise = self.findOneAndUpdate(query, upDoc, queryOption); if (opts.populate) { updatePromise = updatePromise.populate(opts.populate); } const object = await updatePromise; if (object == null) { throw utils_1.NodesworkError.notFound(); } ctx[opts.target] = object; if (opts.triggerNext) { await next(); } if (!opts.noBody) { ctx.body = await opts.transform(ctx[opts.target], ctx); } } Object.defineProperty(update, 'name', { value: `${self.modelName}#updateMiddleware`, writable: false, }); return update; } static deleteMiddleware(options) { const self = this.cast(); options = _.defaults({}, options, DEFAULT_DELETE_OPTIONS); const idFieldName = options.idFieldName; async function del(ctx, next) { const opts = _.extend({}, options, ctx.overrides && ctx.overrides.options); const query = (ctx.overrides && ctx.overrides.query) || {}; if (opts.field.indexOf('.') >= 0) { query[idFieldName] = object_path_1.withInheritedProps.get(ctx.request, opts.field); } else { query[idFieldName] = ctx.params[opts.field]; } const queryOption = {}; const queryPromise = self.findOne(query, undefined, queryOption); let object = await queryPromise; ctx[opts.target] = object; object = ctx[opts.target]; if (!opts.nullable && object == null) { throw new utils_1.NodesworkError('not found', { responseCode: 404, }); } if (object) { await object.remove(); } if (opts.triggerNext) { await next(); } ctx.status = 204; } Object.defineProperty(del, 'name', { value: `${self.modelName}#deleteMiddleware`, writable: false, }); return del; } } exports.KoaMiddlewares = KoaMiddlewares; const DEFAULT_COMMON_OPTIONS = { target: 'object', transform: _.identity, }; const DEFAULT_SINGLE_ITEM_OPTIONS = { idFieldName: '_id', nullable: false, }; const DEFAULT_GET_OPTIONS = _.defaults({}, DEFAULT_COMMON_OPTIONS, DEFAULT_SINGLE_ITEM_OPTIONS); const DEFAULT_UPDATE_OPTIONS = _.defaults({}, DEFAULT_COMMON_OPTIONS, DEFAULT_SINGLE_ITEM_OPTIONS); const DEFAULT_DELETE_OPTIONS = _.defaults({}, DEFAULT_COMMON_OPTIONS, DEFAULT_SINGLE_ITEM_OPTIONS); const DEFAULT_FIND_PAGINATION_OPTIONS = { size: 20, sizeChoices: [20, 50, 100, 200], }; KoaMiddlewares.Plugin({ fn: apiLevel, }); function apiLevel(schema, _options) { for (let s = schema; s; s = s.parentSchema) { if (s.api == null) { s.api = { READONLY: [], AUTOGEN: [], }; } } schema.eachPath((pathname, schemaType) => { if ([exports.READONLY, exports.AUTOGEN].indexOf(schemaType.options.api) < 0) { return; } for (let s = schema; s != null; s = s.parentSchema) { s.api[schemaType.options.api] = _.union(s.api[schemaType.options.api], [ pathname, ]); } }); } function Autogen(schema = {}) { return _1.Field(_.extend({}, schema, { api: exports.AUTOGEN, })); } exports.Autogen = Autogen; function Readonly(schema = {}) { return _1.Field(_.extend({}, schema, { api: exports.READONLY, })); } exports.Readonly = Readonly; function isPaginationData(data) { return (data && data.pageSize != null && data.page != null && data.total != null && data.data != null && _.isArray(data.data)); } exports.isPaginationData = isPaginationData; //# sourceMappingURL=koa.js.map