@nodeswork/sbase
Version:
Basic REST api foundation from Nodeswork.
394 lines (392 loc) • 15 kB
JavaScript
"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