Comparing version 0.0.5 to 0.1.0
@@ -24,12 +24,11 @@ 'use strict'; | ||
* | ||
* alaska.start('blog').then(function(){ | ||
* console.log('blog started'); | ||
* alaska.launch('blog').then(function(){ | ||
* console.log('blog launched'); | ||
* }); | ||
* | ||
* let other = new alaska.Alaska(); | ||
* other.start('shop').then(function(){ | ||
* console.log('shop started); | ||
* other.launch('shop').then(function(){ | ||
* console.log('shop launched); | ||
* }); | ||
* ``` | ||
* @extends events.EventEmitter | ||
*/ | ||
@@ -41,3 +40,2 @@ class Alaska { | ||
* @constructor | ||
* @fires Alaska#create | ||
*/ | ||
@@ -47,3 +45,3 @@ | ||
/** | ||
* 允许资源所有者访问接口 | ||
* 允许已经认证的用户访问接口 | ||
*/ | ||
@@ -53,3 +51,4 @@ | ||
/** | ||
* 允许所有用户访问接口 | ||
* 接口关闭 | ||
* @type {number} | ||
*/ | ||
@@ -59,3 +58,3 @@ | ||
/** | ||
* HTTP状态码422 REST接口create/update数据验证失败 | ||
* HTTP状态码405 REST接口HTTP方法不允许 | ||
*/ | ||
@@ -65,3 +64,3 @@ | ||
/** | ||
* HTTP状态码404 REST接口show/list请求的资源不存在 | ||
* HTTP状态码403 REST已授权,但是没有权限 | ||
*/ | ||
@@ -71,3 +70,3 @@ | ||
/** | ||
* HTTP状态码401 REST接口未授权 | ||
* HTTP状态码400 REST接口create/update请求未识别,错误的请求 | ||
*/ | ||
@@ -77,9 +76,4 @@ | ||
/** | ||
* HTTP状态码204 REST接口remove成功 | ||
* HTTP状态码201 REST接口create/update成功 | ||
*/ | ||
/** | ||
* HTTP状态码200 REST接口show/list方法成功 | ||
*/ | ||
constructor() { | ||
@@ -99,5 +93,2 @@ this.OK = 200; | ||
this.OWNER = 3; | ||
this._loaded = false; | ||
this._routed = false; | ||
this._started = false; | ||
this._app = null; | ||
@@ -112,2 +103,3 @@ this._services = {}; | ||
this.Service = require('./service'); | ||
this.Field = require('./field'); | ||
this.defaultAlaska = defaultAlaska ? defaultAlaska : this; | ||
@@ -123,3 +115,3 @@ } | ||
/** | ||
* 允许已经认证的用户访问接口 | ||
* 允许资源所有者访问接口 | ||
*/ | ||
@@ -129,4 +121,3 @@ | ||
/** | ||
* 接口关闭 | ||
* @type {number} | ||
* 允许所有用户访问接口 | ||
*/ | ||
@@ -136,3 +127,3 @@ | ||
/** | ||
* HTTP状态码405 REST接口HTTP方法不允许 | ||
* HTTP状态码422 REST接口create/update数据验证失败 | ||
*/ | ||
@@ -142,3 +133,3 @@ | ||
/** | ||
* HTTP状态码403 REST已授权,但是没有权限 | ||
* HTTP状态码404 REST接口show/list请求的资源不存在 | ||
*/ | ||
@@ -148,3 +139,3 @@ | ||
/** | ||
* HTTP状态码400 REST接口create/update请求未识别,错误的请求 | ||
* HTTP状态码401 REST接口未授权 | ||
*/ | ||
@@ -154,4 +145,9 @@ | ||
/** | ||
* HTTP状态码201 REST接口create/update成功 | ||
* HTTP状态码204 REST接口remove成功 | ||
*/ | ||
/** | ||
* HTTP状态码200 REST接口show/list方法成功 | ||
*/ | ||
app() { | ||
@@ -191,3 +187,2 @@ if (!this._app) { | ||
* 注册新的Service | ||
* @fires Alaska#registerService | ||
* @param {Service} service Service对象 | ||
@@ -221,10 +216,2 @@ */ | ||
/** | ||
* 定义或者找回此Alaska实例下的Model | ||
* @param {string} name 模型名称 | ||
* @param {Object} [options] 模型定义 | ||
* @returns {Model|null} | ||
*/ | ||
model(name, options) {} | ||
/** | ||
* 获取当前主配置的数据库链接 | ||
@@ -238,6 +225,4 @@ * @returns {mongoose.Connection | null} | ||
/** | ||
* 启动Alaska实例 | ||
* @fires Alaska#start | ||
* [async]启动Alaska实例 | ||
* @param {string|Object} options 默认Service配置信息,此参数将传递给Service的初始化函数 | ||
* @returns {Promise} | ||
*/ | ||
@@ -256,3 +241,3 @@ launch(options) { | ||
yield _this._mainService.init(); | ||
yield _this._mainService.load(); | ||
yield _this._mainService.loadModels(); | ||
yield _this._mainService.route(); | ||
@@ -267,3 +252,3 @@ yield _this._mainService.launch(); | ||
* 输出Alaska实例JSON调试信息 | ||
* @returns {Object} | ||
* @returns {object} | ||
*/ | ||
@@ -275,2 +260,46 @@ toJSON() { | ||
} | ||
/** | ||
* 抛出严重错误,并输出调用栈 | ||
* @param message | ||
* @param code | ||
*/ | ||
panic(message, code) { | ||
let error = new defaultAlaska.PanicError(message); | ||
if (code) { | ||
error.code = code; | ||
} | ||
console.error('Panic ' + error.stack); | ||
throw error; | ||
} | ||
/** | ||
* 抛出普通异常 | ||
* @param {string|Error} message | ||
* @param {string|number} code | ||
*/ | ||
error(message, code) { | ||
let error = new defaultAlaska.NormalError(message); | ||
if (code) { | ||
error.code = code; | ||
} | ||
throw error; | ||
} | ||
/** | ||
* [async]执行一个异步任务,如果失败则抛出NormalError | ||
* @param {Promise} promise | ||
* @returns {*} | ||
*/ | ||
try(promise) { | ||
var _this2 = this; | ||
return _asyncToGenerator(function* () { | ||
try { | ||
return yield promise; | ||
} catch (error) { | ||
_this2.error(error); | ||
} | ||
})(); | ||
} | ||
} | ||
@@ -281,2 +310,24 @@ | ||
/** | ||
* 一般错误 | ||
* @class {NormalError} | ||
*/ | ||
defaultAlaska.NormalError = class NormalError extends Error { | ||
constructor(message, code) { | ||
super(message); | ||
this.code = code; | ||
} | ||
}; | ||
/** | ||
* 严重错误 | ||
* @class {PanicError} | ||
*/ | ||
defaultAlaska.PanicError = class PanicError extends Error { | ||
constructor(message, code) { | ||
super(message); | ||
this.code = code; | ||
} | ||
}; | ||
module.exports = defaultAlaska; |
@@ -15,5 +15,5 @@ 'use strict'; | ||
* REST接口默认控制器 | ||
* 本控制器默认关闭,开启默认REST接口,需要将PKG配置中的rest项设置为true,并且打开各个模型的设置 | ||
* 例如 `UserModel.options.rest=false` 将关闭UserModel模型所有的默认REST接口 | ||
* `UserModel.options.rest={list:1,show:1}` 将只打开list和show接口 | ||
* 本控制器默认关闭,开启默认REST接口,需要将Service配置中的rest项设置为true,并且打开各个模型的设置 | ||
* 例如 `User.rest=false` 将关闭User模型所有的默认REST接口 | ||
* `User.rest={list:1,show:1}` 将只打开list和show接口 | ||
* 不同的数值代表: | ||
@@ -20,0 +20,0 @@ * > list接口 1:允许匿名调用此接口 2:允许认证后的用户调用 3:只允许用户列出自己的资源 |
@@ -27,5 +27,8 @@ 'use strict'; | ||
* [APP] APP中间件列表 | ||
* @type {Array} | ||
* @type {array} | ||
*/ | ||
appMiddlewares: [], | ||
appMiddlewares: ['koa-logger', { | ||
name: 'koa-bodyparser', | ||
options: {} | ||
}], | ||
@@ -43,2 +46,14 @@ /** | ||
env: 'development', | ||
/** | ||
* [APP] session | ||
* @type {object|string} | ||
*/ | ||
session: { | ||
type: 'alaska-cache-lru', | ||
cookie: {}, | ||
store: { | ||
maxAge: 1000 * 60 * 60 | ||
} | ||
}, | ||
// | ||
@@ -50,3 +65,3 @@ // KOA settings | ||
* [KOA] 代理模式 | ||
* @type {Boolean} | ||
* @type {boolean} | ||
*/ | ||
@@ -65,8 +80,2 @@ proxy: false, | ||
/** | ||
* [Service] 别名列表 | ||
* @type {Array} | ||
*/ | ||
alias: [], | ||
/** | ||
* [Service] 域名,如果不指定,子Service将使用主Service的域名 | ||
@@ -99,8 +108,23 @@ * 例如 docs.google.com *.58.com | ||
* [Service] 控制器路由接受的HTTP方法列表 | ||
* @type {Array} | ||
* @type {array} | ||
*/ | ||
methods: ['GET', 'POST'], | ||
/** | ||
* [Service] 静态目录列表 | ||
* @type {Array|string|object} | ||
*/ | ||
statics: '', | ||
/** | ||
* [Service] 模板引擎 | ||
* @type {string} | ||
*/ | ||
render: 'swig', | ||
/** | ||
* [Service] 模板目录 | ||
* @type {string} | ||
*/ | ||
templates: 'templates', | ||
/** | ||
* [Service] 该Service依赖的子Service列表 | ||
* @type {Array} | ||
* @type {array} | ||
*/ | ||
@@ -110,3 +134,3 @@ services: [], | ||
* [Service] 该Service的路由中间件 | ||
* @type {Array} | ||
* @type {array} | ||
*/ | ||
@@ -122,4 +146,20 @@ middlewares: [], | ||
/** | ||
* [Service] 数据库collection前缀 | ||
* @type {Boolean|string} | ||
*/ | ||
dbPrefix: false, | ||
/** | ||
* [Service] 缓存设置或已经实例化的缓存驱动对象 | ||
* @type {Object|string} | ||
*/ | ||
cache: { | ||
type: 'alaska-cache-lru', | ||
prefix: false, | ||
store: { | ||
maxAge: 3600 * 24 * 1000 | ||
} | ||
}, | ||
/** | ||
* 是否开启控制器路由 | ||
* @type {Boolean} | ||
* @type {boolean} | ||
*/ | ||
@@ -129,6 +169,5 @@ controllers: true, | ||
* 是否开启rest api | ||
* @type {Boolean} | ||
* @type {boolean} | ||
*/ | ||
api: true | ||
}; |
434
lib/model.js
'use strict'; | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; } | ||
/** | ||
@@ -13,8 +15,30 @@ * @copyright Maichong Software Ltd. 2016 http://maichong.it | ||
const Schema = mongoose.Schema; | ||
const BaseField = require('./field'); | ||
const util = require('./util'); | ||
function panic() { | ||
throw new Error('Can not call the function after Model register.'); | ||
throw new Error('Can not call the function when Model has been registered.'); | ||
} | ||
class Model { | ||
/** | ||
* Data | ||
* @type {{pick: (function()), omit: (function())}} | ||
*/ | ||
const Data = { | ||
pick() { | ||
let data = _.pick.apply(_, [this].concat(Array.prototype.slice.call(arguments))) || {}; | ||
data.__proto__ = Data; | ||
return data; | ||
}, | ||
omit() { | ||
let data = _.omit.apply(_, [this].concat(Array.prototype.slice.call(arguments))) || {}; | ||
data.__proto__ = Data; | ||
return data; | ||
} | ||
}; | ||
/** | ||
* @class Model | ||
*/ | ||
class BaseModel { | ||
constructor() { | ||
@@ -27,2 +51,7 @@ throw new Error('Can not initialize a Model before register.'); | ||
/** | ||
* 注册前置钩子 | ||
* @param {string} action 动作名称,Init|Validate|Save|Remove | ||
* @param {function} fn | ||
*/ | ||
static pre(action, fn) { | ||
@@ -34,2 +63,7 @@ this._pre || (this._pre = {}); | ||
/** | ||
* 注册后置钩子 | ||
* @param {string} action 动作名称,Init|Validate|Save|Remove | ||
* @param {function} fn | ||
*/ | ||
static post(action, fn) { | ||
@@ -41,7 +75,33 @@ this._post || (this._post = {}); | ||
/** | ||
* 注册underscore方法 | ||
* @param {string} field 绑定的字段 | ||
* @param {string} name 方法名 | ||
* @param {Function} fn 方法 | ||
*/ | ||
static underscoreMethod(field, name, fn) { | ||
this._underscore || (this._underscore = {}); | ||
this._underscore[field] || (this._underscore[field] = {}); | ||
this._underscore[field][name] = fn; | ||
} | ||
/** | ||
* 注册 | ||
*/ | ||
static register() { | ||
let service = __service; | ||
let Model = this; | ||
/** | ||
* 模型所属服务 | ||
* @type {alaska.Service} | ||
*/ | ||
Model.service = service; | ||
/** | ||
* 标识模型对象 | ||
* @type {boolean} | ||
*/ | ||
Model.isModel = true; | ||
let MongooseModel; | ||
@@ -53,40 +113,93 @@ let name = Model.name; | ||
//整理字段列表 | ||
let fields = {}; | ||
for (let key in Model.fields) { | ||
let field = Model.fields[key]; | ||
let schema = Model.schema = new Schema({}, { | ||
collection: Model.collection || (Model.prefix || service.dbPrefix) + name.replace(/([a-z])([A-Z])/g, (a, b, c) => b + '_' + c).toLowerCase() | ||
}); | ||
function loadFieldConfig(fieldTypeName) { | ||
let config = service.config(true, fieldTypeName); | ||
if (!config) { | ||
return {}; | ||
} | ||
if (config.type && config.type != fieldTypeName) { | ||
let otherConfig = loadFieldConfig(config.type); | ||
return _.assign({}, config, otherConfig); | ||
} | ||
return _.clone(config); | ||
} | ||
//将Model字段注册到Mongoose.Schema中 | ||
for (let path in Model.fields) { | ||
let field = Model.fields[path]; | ||
if (typeof field === 'function') { | ||
Model.fields[path] = field = { type: field }; | ||
} | ||
if (!field.type) { | ||
throw new Error(Model.name + '.' + path + ' field type not specified'); | ||
} | ||
if (_.isArray(field.type) && field.type.length === 1) { | ||
// type : [ OtherModel ] | ||
let OtherModel = field.type[0]; | ||
field.type = 'relationship'; | ||
field.ref = OtherModel; | ||
field.multi = true; | ||
} | ||
if (field.type.isModel) { | ||
let OtherModel = field.type; | ||
field.type = 'relationship'; | ||
field.ref = OtherModel; | ||
} | ||
field.path = path; | ||
if (!field.type) { | ||
throw new Error('Field type is not specified. ' + name); | ||
} | ||
let FieldType; | ||
if (field.type === String) { | ||
FieldType = require('alaska-field-text'); | ||
} else if (field.type === Date) { | ||
FieldType = require('alaska-field-date'); | ||
} else if (field.type === Boolean) { | ||
FieldType = require('alaska-field-checkbox'); | ||
} else if (field.type === Number) { | ||
FieldType = require('alaska-field-number'); | ||
let AlaskaFieldType; | ||
if (typeof field.type === 'object' && field.type.plain) { | ||
AlaskaFieldType = field.type; | ||
} else { | ||
FieldType = field.type; | ||
let fieldTypeName; | ||
if (field.type === String) { | ||
fieldTypeName = 'alaska-field-text'; | ||
} else if (field.type === Date) { | ||
fieldTypeName = 'alaska-field-datetime'; | ||
} else if (field.type === Boolean) { | ||
fieldTypeName = 'alaska-field-checkbox'; | ||
} else if (field.type === Number) { | ||
fieldTypeName = 'alaska-field-number'; | ||
} else if (typeof field.type === 'string') { | ||
fieldTypeName = 'alaska-field-' + field.type; | ||
} else { | ||
throw new Error(`Unsupported field type for ${ Model.name }.${ path }`); | ||
} | ||
field.type = fieldTypeName; | ||
_.assign(field, loadFieldConfig(fieldTypeName)); | ||
AlaskaFieldType = require(field.type); | ||
} | ||
field.type = FieldType; | ||
let options = { | ||
type: FieldType.plain | ||
}; | ||
//将用户定义的选项传给Mongoose | ||
FieldType.update && FieldType.update(field, options); | ||
if (_.has(field, 'default')) { | ||
options.default = field.default; | ||
//console.log(AlaskaFieldType, field); | ||
field.type = AlaskaFieldType; | ||
field.label = field.label || path.toUpperCase(); | ||
if (AlaskaFieldType.initSchema) { | ||
AlaskaFieldType.initSchema(field, schema, Model); | ||
} else { | ||
BaseField.initSchema.call(AlaskaFieldType, field, schema, Model); | ||
} | ||
fields[key] = options; | ||
} | ||
let schema = Model.schema = new Schema(fields); | ||
let groups = {}; | ||
schema.virtual('_').get(function () { | ||
if (!this.__methods) { | ||
this.__methods = util.bindMethods(Model._underscore, this); | ||
} | ||
return this.__methods; | ||
}); | ||
_.defaults(Model, { | ||
title: 'title', | ||
userField: 'user', | ||
api: false, | ||
searchFields: '', | ||
defaultSort: '' | ||
defaultSort: '', | ||
defaultColumns: '', | ||
label: Model.name, | ||
groups: {} | ||
}); | ||
@@ -104,2 +217,13 @@ if (Model.api === 1) { | ||
//允许自动缓存 | ||
if (Model.cache) { | ||
//保存成功后更新缓存 | ||
Model.post('save', function () { | ||
Model.setCache(this); | ||
}); | ||
Model.post('remove', function () { | ||
Model.delCache(this); | ||
}); | ||
} | ||
Model._pre || (Model._pre = []); | ||
@@ -167,80 +291,224 @@ Model._post || (Model._post = []); | ||
Model.register = panic; | ||
Model.pre = panic; | ||
Model.post = panic; | ||
delete Model._pre; | ||
delete Model._post; | ||
['paginate', 'createCacheKey', 'getCache', 'setCache', 'delCache', 'castCache', 'castCacheArray', 'castModelArray'].forEach(key => { | ||
Model[key] = BaseModel[key]; | ||
}); | ||
//register | ||
let db = service.db(); | ||
MongooseModel = db.model(name, schema); | ||
/** | ||
* | ||
* @param Object options | ||
* @returns {mongoose.Query} | ||
* 原始Mongoose模型 | ||
* @type mongoose.Model | ||
*/ | ||
Model.paginate = function (options) { | ||
options = options || {}; | ||
let page = parseInt(options.page) || 1; | ||
let perPage = parseInt(options.perPage) || 10; | ||
let skip = (page - 1) * perPage; | ||
let search = options.search || ''; | ||
Model.MongooseModel = MongooseModel; | ||
Model.__proto__ = MongooseModel; | ||
Model.prototype.__proto__ = MongooseModel.prototype; | ||
//TODO search & filter | ||
let query = this.find(options.filters); | ||
/** | ||
* 返回格式化数据 | ||
* @returns {Data} | ||
*/ | ||
Model.prototype.data = function () { | ||
//TODO data() | ||
let doc = {}; | ||
for (let key in this.schema.tree) { | ||
if (key[0] == '_' || !Model.fields[key] || Model.fields[key].private) { | ||
continue; | ||
} | ||
if (this._[key] && this._[key].data) { | ||
doc[key] = this._[key].data(); | ||
} else { | ||
doc[key] = this.get(key); | ||
} | ||
} | ||
doc.id = this.id; | ||
doc.__proto__ = Data; | ||
return doc; | ||
}; | ||
} | ||
query.originalExec = query.exec; | ||
/** | ||
* 分页查询 | ||
* @param {object} options | ||
* @returns {mongoose.Query} | ||
*/ | ||
static paginate(options) { | ||
options = options || {}; | ||
let page = parseInt(options.page) || 1; | ||
let perPage = parseInt(options.perPage) || 10; | ||
let skip = (page - 1) * perPage; | ||
let search = options.search || ''; | ||
let results = { | ||
total: 0, | ||
page: page, | ||
perPage: perPage, | ||
previous: page <= 1 ? false : page - 1, | ||
next: false, | ||
results: [] | ||
}; | ||
//TODO search & filter | ||
let query = this.find(options.filters); | ||
query.exec = function (callback) { | ||
callback = callback || _.noop; | ||
query.originalExec = query.exec; | ||
return new Promise(function (resolve, reject) { | ||
query.exec = query.originalExec; | ||
query.count(function (error, total) { | ||
let results = { | ||
total: 0, | ||
page: page, | ||
perPage: perPage, | ||
previous: page <= 1 ? false : page - 1, | ||
next: false, | ||
results: [] | ||
}; | ||
query.exec = function (callback) { | ||
callback = callback || _.noop; | ||
return new Promise(function (resolve, reject) { | ||
query.exec = query.originalExec; | ||
query.count(function (error, total) { | ||
if (error) { | ||
return reject(error); | ||
} | ||
if (!total) { | ||
callback(null, results); | ||
resolve(results); | ||
return; | ||
} | ||
results.total = total; | ||
results.next = Math.ceil(total / perPage) > page ? page + 1 : false; | ||
query.find().limit(perPage).skip(skip).exec(function (error, res) { | ||
if (error) { | ||
return reject(error); | ||
} | ||
if (!total) { | ||
callback(null, results); | ||
resolve(results); | ||
return; | ||
} | ||
results.total = total; | ||
results.next = Math.ceil(total / perPage) > page ? page + 1 : false; | ||
query.find().limit(perPage).skip(skip).exec(function (error, res) { | ||
if (error) { | ||
return reject(error); | ||
} | ||
results.results = res; | ||
callback(null, results); | ||
resolve(results); | ||
}); | ||
results.results = res; | ||
callback(null, results); | ||
resolve(results); | ||
}); | ||
}); | ||
}; | ||
return query; | ||
}); | ||
}; | ||
//register | ||
return query; | ||
} | ||
let db = service.db(); | ||
MongooseModel = db.model(name, schema); | ||
/** | ||
* 依据记录ID,生成数据缓存所使用的cache key | ||
* @param {string} id | ||
* @returns {*} | ||
*/ | ||
static createCacheKey(id) { | ||
return `model_cache_${ this.service.name }.${ this.name }_${ id }`; | ||
} | ||
Model.__proto__ = MongooseModel; | ||
Model.prototype.__proto__ = MongooseModel.prototype; | ||
Model.prototype.data = function () { | ||
//TODO data() | ||
let doc = this; | ||
return doc.toObject(); | ||
}; | ||
/** | ||
* 获取某条记录的缓存,如果没有找到缓存数据,则查询数据库 | ||
* @param {string} id | ||
* @returns {Model} | ||
*/ | ||
static getCache(id) { | ||
var _this = this; | ||
return _asyncToGenerator(function* () { | ||
let cache; | ||
let cacheKey; | ||
if (_this.cache) { | ||
//模型允许自动缓存 | ||
cache = _this.service.cache(); | ||
cacheKey = _this.createCacheKey(id); | ||
let data = yield cache.get(cacheKey); | ||
if (data) { | ||
return _this.castCache(data); | ||
} | ||
} | ||
//没有找到缓存数据 | ||
let record = yield _this.findById(id); | ||
if (record && cache) { | ||
_this.setCache(record); | ||
} | ||
return record; | ||
})(); | ||
} | ||
/** | ||
* 设置模型缓存 | ||
* @param {Model} record | ||
*/ | ||
static setCache(record) { | ||
var _this2 = this; | ||
return _asyncToGenerator(function* () { | ||
let cacheKey = _this2.createCacheKey(record.id); | ||
let cache = _this2.service.cache(); | ||
yield cache.set(cacheKey, cache.noSerialization ? record : record.toObject(), _this2.cache); | ||
})(); | ||
} | ||
/** | ||
* 删除模型缓存 | ||
* @param {string} id | ||
*/ | ||
static delCache(id) { | ||
var _this3 = this; | ||
return _asyncToGenerator(function* () { | ||
let cacheKey = _this3.createCacheKey(id); | ||
let cache = _this3.service.cache(); | ||
yield cache.del(cacheKey); | ||
})(); | ||
} | ||
/** | ||
* 将object数据转为Model对象 | ||
* @param data | ||
* @returns {Model} | ||
*/ | ||
static castCache(data) { | ||
let cache = this.service.cache(); | ||
if (cache.noSerialization) { | ||
//缓存驱动不需要序列化 | ||
return data; | ||
} | ||
//缓存驱动需要序列化 | ||
let record = new this(null, null, true); | ||
record.init(data); | ||
return record; | ||
} | ||
/** | ||
* 将object数组转为Model对象数组 | ||
* @param {[Object]} array | ||
* @returns {[Model]} | ||
*/ | ||
static castCacheArray(array) { | ||
let cache = this.service.cache(); | ||
if (cache.noSerialization) { | ||
//缓存驱动不需要序列化 | ||
return array; | ||
} | ||
return _.map(array, data => this.castCache(data)); | ||
} | ||
/** | ||
* 将模型数组转为plain object数组 | ||
* @param {[Model]} array | ||
* @returns {array} | ||
*/ | ||
static castModelArray(array) { | ||
let cache = this.service.cache(); | ||
if (cache.noSerialization) { | ||
//缓存驱动不需要序列化 | ||
return array; | ||
} | ||
return _.map(array, record => record.toObject()); | ||
} | ||
} | ||
Model.fields = null; | ||
module.exports = Model; | ||
BaseModel.fields = null; | ||
BaseModel.cache = 0; | ||
BaseModel.prefix = ''; | ||
BaseModel.collection = ''; | ||
module.exports = BaseModel; |
@@ -11,6 +11,4 @@ 'use strict'; | ||
const assert = require('assert'); | ||
const _ = require('lodash'); | ||
const Router = require('koa-router'); | ||
const compose = require('koa-compose'); | ||
const collie = require('collie'); | ||
@@ -34,4 +32,28 @@ const util = require('./util'); | ||
*/ | ||
/** | ||
* 所依赖的子Service实例对象别名映射表 | ||
* @type {object} | ||
* @private | ||
*/ | ||
/** | ||
* 本Service的所有额外配置目录 | ||
* @type {[string]} | ||
* @private | ||
*/ | ||
/** | ||
* 数据库连接实例 | ||
* @type {mongoose.Connection} | ||
* @private | ||
*/ | ||
/** | ||
* 路由器 | ||
* @type {Router} | ||
* @private | ||
*/ | ||
constructor(options, alaska) { | ||
this._router = false; | ||
this._router = null; | ||
this._controllers = {}; | ||
@@ -42,2 +64,3 @@ this._apiControllers = {}; | ||
this._config = {}; | ||
this._configDirs = []; | ||
this._services = []; | ||
@@ -49,5 +72,11 @@ this._alias = {}; | ||
collie(this, 'init'); | ||
collie(this, 'load'); | ||
collie(this, 'route'); | ||
this.panic = alaska.panic; | ||
collie(this, 'init', require('./service/init')); | ||
collie(this, 'loadModels', require('./service/loadModels')); | ||
collie(this, 'route', require('./service/route')); | ||
collie(this, 'loadAppMiddlewares', require('./service/loadAppMiddlewares')); | ||
collie(this, 'loadMiddlewares', require('./service/loadMiddlewares')); | ||
collie(this, 'loadApi', require('./service/loadApi')); | ||
collie(this, 'loadControllers', require('./service/loadControllers')); | ||
collie(this, 'loadStatics', require('./service/loadStatics')); | ||
collie(this, 'launch'); | ||
@@ -64,6 +93,4 @@ collie(this, 'registerModel'); | ||
this.debug = require('debug')(this._options.id); | ||
this.debug = debug; | ||
this.debug('init'); | ||
if (!this._options.dir) { | ||
@@ -77,3 +104,2 @@ throw new Error('Service dir is not specified.'); | ||
} | ||
this.id = this._options.id; | ||
this.alaska = alaska; | ||
@@ -83,3 +109,3 @@ | ||
//载入配置 | ||
let configFilePath = this._options.dir + '/configs/' + this._options.configFile; | ||
let configFilePath = this._options.dir + '/config/' + this._options.configFile; | ||
let config = util.include(configFilePath); | ||
@@ -99,2 +125,50 @@ if (config) { | ||
/** | ||
* Service id | ||
* @returns {string} | ||
*/ | ||
/** | ||
* Model基类 | ||
* @type {Model} | ||
*/ | ||
/** | ||
* 所依赖的子Service实例对象列表 | ||
* @type {[Service]} | ||
* @private | ||
*/ | ||
/** | ||
* 本Service的配置项 | ||
* @type {object} | ||
* @private | ||
*/ | ||
/** | ||
* 本Service数据模型列表 | ||
* @type {object} | ||
* @private | ||
*/ | ||
get id() { | ||
return this._options.id; | ||
} | ||
/** | ||
* Service 目录 | ||
* @returns {string} | ||
*/ | ||
get dir() { | ||
return this._options.dir; | ||
} | ||
/** | ||
* 追加配置项 | ||
* @param {object} config | ||
*/ | ||
applyConfig(config) { | ||
this._config = _.assign({}, this._config, config); | ||
} | ||
/** | ||
* 判断当前Service是否是主Service | ||
@@ -122,338 +196,55 @@ * @returns Boolean | ||
/** | ||
* 初始化 | ||
* [async] 初始化 | ||
* @method init | ||
*/ | ||
init() { | ||
var _this = this; | ||
return _asyncToGenerator(function* () { | ||
debug('%s load', _this.id); | ||
_this.init = util.noop; | ||
/** | ||
* [async] 加载数据模型 | ||
* @method loadModels | ||
*/ | ||
let services = _this.config('services') || []; | ||
if (typeof services === 'string') { | ||
services = [services]; | ||
} | ||
for (let service of services) { | ||
let serviceId = service; | ||
let serviceAlias = ''; | ||
if (service.alias) { | ||
//如果Service配置有别名 | ||
serviceAlias = service.alias; | ||
serviceId = service.id; | ||
} | ||
assert(typeof serviceId === 'string', 'Sub service id should be string.'); | ||
let sub = _this.alaska.service(serviceId); | ||
_this._services.push(sub); | ||
assert(!_this._alias[serviceId], 'Service alias is exists.'); | ||
_this._alias[serviceId] = sub; | ||
if (serviceAlias) { | ||
assert(!_this._alias[serviceAlias], 'Service alias is exists.'); | ||
_this._alias[serviceAlias] = sub; | ||
} | ||
yield sub.init(); | ||
} | ||
})(); | ||
} | ||
/** | ||
* 加载 | ||
* [async]配置路由 | ||
* @method route | ||
*/ | ||
load() { | ||
var _this2 = this; | ||
return _asyncToGenerator(function* () { | ||
debug('%s load', _this2.id); | ||
_this2.load = util.noop; | ||
for (let service of _this2._services) { | ||
yield service.load(); | ||
} | ||
if (_this2.config('db') !== false) { | ||
global.__service = _this2; | ||
_this2._models = util.include(_this2._options.dir + '/models'); | ||
for (let name in _this2._models) { | ||
yield _this2.registerModel(_this2._models[name]); | ||
} | ||
} | ||
})(); | ||
} | ||
/** | ||
* 载入APP中间件 | ||
* @private | ||
* [async] 载入APP中间件 | ||
* @method loadAppMiddlewares | ||
*/ | ||
_loadAppMiddlewares() { | ||
this._loadAppMiddlewares = util.noop; | ||
let app = this.alaska.app(); | ||
let alaska = this.alaska; | ||
let service = this; | ||
app.use(function (ctx, next) { | ||
ctx.service = service; | ||
ctx.alaska = alaska; | ||
return next(); | ||
}); | ||
this.config('appMiddlewares', []).forEach(function (name) { | ||
if (typeof name === 'function') { | ||
//数组中直接就是一个中间件函数 | ||
app.use(name); | ||
return; | ||
} | ||
let options; | ||
if (typeof name === 'object') { | ||
options = name.options; | ||
name = name.name; | ||
} | ||
if (name.startsWith('.')) { | ||
//如果是一个文件路径 | ||
name = this._options.dir + '/' + name; | ||
} | ||
let middleware = require(name); | ||
app.use(middleware(options)); | ||
}); | ||
} | ||
/** | ||
* 载入Service中间件 | ||
* @private | ||
* [async] 载入Service中间件 | ||
* @method loadMiddlewares | ||
*/ | ||
_loadServiceMiddlewares() { | ||
let router = this.router(); | ||
//middlewares for service | ||
this.config('middlewares', []).forEach(function (item) { | ||
if (typeof item === 'string') { | ||
item = { | ||
name: item | ||
}; | ||
} | ||
let name = item.name; | ||
if (name.startsWith('.')) { | ||
name = this._options.dir + name; | ||
} | ||
let middleware = require(name); | ||
let path = item.path; | ||
if (!path) { | ||
router.use(path, middleware(item.options)); | ||
return; | ||
} | ||
let methods = item.methods || ['GET', 'POST']; | ||
if (methods === 'all') { | ||
router.all(path, middleware(item.options)); | ||
return; | ||
} | ||
if (typeof methods === 'string') { | ||
methods = [methods]; | ||
} | ||
router.register(path, methods, middleware(item.options)); | ||
}); | ||
} | ||
/** | ||
* 载入API接口控制器 | ||
* @private | ||
* [async] 载入API接口控制器 | ||
* @method loadApi | ||
*/ | ||
_loadApiControllers() { | ||
let alaska = this.alaska; | ||
let service = this; | ||
let router = this.router(); | ||
this._apiControllers = util.include(this._options.dir + '/api'); | ||
let defaultApiController = require('./api'); | ||
let bodyParser = require('koa-bodyparser')(); | ||
//TODO 优化性能 | ||
function restApi(action) { | ||
return function (ctx, next) { | ||
function onError(error) { | ||
console.error(service.id + ' API ' + error.stack); | ||
if (!ctx.body) { | ||
if (ctx.status === 404) { | ||
ctx.status = 500; | ||
} | ||
ctx.body = { | ||
error: error.message | ||
}; | ||
} | ||
} | ||
try { | ||
if (['show', 'update', 'remove'].indexOf(action) > -1) { | ||
if (!/^[a-f0-9]{24}$/.test(ctx.params.id)) { | ||
ctx.status = alaska.BAD_REQUEST; | ||
return; | ||
} | ||
} | ||
//console.log(ctx.params.model); | ||
//console.log(service); | ||
//console.log(service._models); | ||
let Model = service._models[ctx.params.model]; | ||
//console.log(Model); | ||
if (!Model) { | ||
//404 | ||
return; | ||
} | ||
let modelId = ctx.params.model.toLowerCase(); | ||
let middlewares = []; | ||
// api 目录下定义的中间件 | ||
if (service._apiControllers[modelId] && service._apiControllers[modelId][action]) { | ||
middlewares.push(service._apiControllers[modelId][action]); | ||
} | ||
// Model.api参数定义的中间件 | ||
if (Model.api && Model.api[action]) { | ||
middlewares.push(defaultApiController[action]); | ||
} | ||
//console.log(middlewares); | ||
if (!middlewares.length) { | ||
//404 | ||
return; | ||
} | ||
ctx.Model = Model; | ||
return compose(middlewares)(ctx).catch(onError); | ||
} catch (error) { | ||
onError(error); | ||
return; | ||
} | ||
}; | ||
} | ||
router.get('/api/:model/count', restApi('count')); | ||
router.get('/api/:model/:id', restApi('show')); | ||
router.get('/api/:model', restApi('list')); | ||
router.post('/api/:model', bodyParser, restApi('create')); | ||
router.put('/api/:model/:id', bodyParser, restApi('update')); | ||
router.del('/api/:model/:id', restApi('remove')); | ||
} | ||
/** | ||
* 载入控制器 | ||
* @private | ||
* [async] 载入控制器 | ||
* @method loadControllers | ||
*/ | ||
_loadControllers() { | ||
let router = this.router(); | ||
let service = this; | ||
this._controllers = util.include(this._options.dir + '/controllers', false); | ||
router.register('/:controller?/:action?', ['GET', 'HEAD', 'POST'], function (ctx, next) { | ||
let controller = ctx.params.controller || service.config('defaultController'); | ||
let action = ctx.params.action || service.config('defaultAction'); | ||
service.debug('route %s:%s', controller, action); | ||
if (service._controllers[controller] && service._controllers[controller][action] && action[0] !== '_') { | ||
return service._controllers[controller][action](ctx, next); | ||
} | ||
next(); | ||
}); | ||
} | ||
/** | ||
* 配置路由 | ||
* @fires Service#route | ||
* [async] 加载资源服务 | ||
* @method loadStatics | ||
*/ | ||
route() { | ||
var _this3 = this; | ||
return _asyncToGenerator(function* () { | ||
debug('route %s', _this3.id); | ||
_this3.route = util.noop; | ||
let app = _this3.alaska.app(); | ||
let router = _this3.router(); | ||
if (_this3.isMain()) { | ||
_this3._loadAppMiddlewares(); | ||
} | ||
//配置子Service路由 | ||
for (let sub of _this3._services) { | ||
yield sub.route(); | ||
} | ||
_this3._loadServiceMiddlewares(); | ||
//API 接口 | ||
if (_this3.config('api')) { | ||
_this3._loadApiControllers(); | ||
} | ||
//控制器 | ||
if (_this3.config('controllers')) { | ||
_this3._loadControllers(); | ||
} | ||
//路由表 | ||
let routes = router.routes(); | ||
//精确匹配域名 | ||
let exact = true; | ||
//Service域名 | ||
let domain = _this3.config('domain', ''); | ||
if (domain.startsWith('*.')) { | ||
domain = domain.substr(2); | ||
exact = false; | ||
} | ||
//如果是主域名,匹配失败后的跳转地址 | ||
let redirect; | ||
if (_this3.isMain()) { | ||
redirect = _this3.config('redirect', ''); | ||
} | ||
let service = _this3; | ||
app.use(function (ctx, next) { | ||
ctx.subdomain = ''; | ||
ctx.service = service; | ||
if (domain) { | ||
let hostname = ctx.hostname; | ||
if (exact) { | ||
//如果精确匹配域名 | ||
if (hostname !== domain) { | ||
redirect && ctx.redirect(redirect); | ||
return; | ||
} | ||
} else { | ||
//分析子域名 | ||
let index = hostname.lastIndexOf(domain); | ||
if (index === -1) { | ||
redirect && ctx.redirect(redirect); | ||
return; | ||
} | ||
ctx.subdomain = hostname.substring(0, index - 1); | ||
} | ||
} | ||
//domain not set | ||
let toJSON = ctx.toJSON; | ||
ctx.toJSON = function () { | ||
let json = toJSON.call(ctx); | ||
json.subdomain = ctx.subdomain; | ||
json.alaska = ctx.alaska.toJSON(); | ||
json.service = ctx.service.toJSON(); | ||
return json; | ||
}; | ||
return routes(ctx, next); | ||
}); | ||
})(); | ||
} | ||
/** | ||
* 启动Service | ||
* @fires Service#start | ||
* [async] 启动Service | ||
* @method launch | ||
*/ | ||
launch() { | ||
var _this4 = this; | ||
var _this = this; | ||
return _asyncToGenerator(function* () { | ||
debug('%s start', _this4.id); | ||
_this4.launch = util.noop; | ||
debug('%s launch', _this.id); | ||
_this.launch = util.noop; | ||
yield _this4.init(); | ||
yield _this4.load(); | ||
yield _this4.route(); | ||
yield _this.init(); | ||
yield _this.loadModels(); | ||
yield _this.route(); | ||
})(); | ||
@@ -464,4 +255,4 @@ } | ||
* 获取当前Service配置 | ||
* @param {Boolean} [mainAsDefault] 如果当前Service中不存在配置,则获取主Service的配置 | ||
* @param {String} path 配置名 | ||
* @param {boolean} [mainAsDefault] 如果当前Service中不存在配置,则获取主Service的配置 | ||
* @param {string} path 配置名 | ||
* @param {*} [defaultValue] 默认值 | ||
@@ -514,2 +305,34 @@ */ | ||
/** | ||
* 获取缓存驱动 | ||
* @returns {LruDriver|*} | ||
*/ | ||
cache() { | ||
if (!this._cache) { | ||
let options = this.config('cache'); | ||
if (_.isString(options)) { | ||
options = { type: options }; | ||
} | ||
if (options.isCacheDriver) { | ||
//已经实例化的缓存驱动 | ||
this._cache = options; | ||
} else { | ||
let Driver = require(options.type); | ||
this._cache = new Driver(options.store || {}); | ||
} | ||
} | ||
return this._cache; | ||
} | ||
/** | ||
* 获取模板引擎 | ||
* @returns {*} | ||
*/ | ||
engine() { | ||
if (!this._engine) { | ||
this._engine = require(this.config('render')); | ||
} | ||
return this._engine; | ||
} | ||
/** | ||
* 获取当前Service所依赖的子Service | ||
@@ -524,3 +347,3 @@ * @returns {Service} | ||
* 输出Service实例JSON调试信息 | ||
* @returns {Object} | ||
* @returns {object} | ||
*/ | ||
@@ -561,7 +384,12 @@ toJSON() { | ||
/** | ||
* 注册模型 | ||
* @param {Model} Model | ||
* @returns {Model} | ||
*/ | ||
registerModel(Model) { | ||
var _this5 = this; | ||
var _this2 = this; | ||
return _asyncToGenerator(function* () { | ||
global.__service = _this5; | ||
global.__service = _this2; | ||
Model.register(); | ||
@@ -568,0 +396,0 @@ return Model; |
@@ -10,2 +10,3 @@ 'use strict'; | ||
const fs = require('fs'); | ||
const _ = require('lodash'); | ||
@@ -42,3 +43,3 @@ /** | ||
* @param importDefault | ||
* @returns {Object} | ||
* @returns {object} | ||
*/ | ||
@@ -65,5 +66,33 @@ exports.include = function include(path) { | ||
/** | ||
* 判断某路径是否是隐藏的 | ||
* @param path | ||
* @returns {boolean} | ||
*/ | ||
exports.isHidden = function isHidden(path) { | ||
return (/[\\\/]\.\w/.test(path) | ||
); | ||
}; | ||
const resolved = Promise.resolve(); | ||
exports.noop = function noop() { | ||
return resolved; | ||
}; | ||
/** | ||
* 递归将obj上所有的方法绑定至scope | ||
* @param obj | ||
* @param scope | ||
* @returns {object} | ||
*/ | ||
exports.bindMethods = function bindMethods(obj, scope) { | ||
let bound = {}; | ||
for (let key in obj) { | ||
if (typeof obj[key] === 'function') { | ||
bound[key] = obj[key].bind(scope); | ||
} else if (_.isObject(obj[key])) { | ||
bound[key] = bindMethods(obj[key], scope); | ||
} | ||
} | ||
return bound; | ||
}; |
{ | ||
"name": "alaska", | ||
"version": "0.0.5", | ||
"version": "0.1.0", | ||
"description": "Modern web framework for Node.js", | ||
"main": "index.js", | ||
"main": "lib/alaska.js", | ||
"dependencies": { | ||
"collie": "^0.2.0", | ||
"alaska-cache-lru": "^0.1.3", | ||
"async-busboy": "0.0.4", | ||
"collie": "^0.2.1", | ||
"koa": "^2.0.0-alpha.3", | ||
@@ -13,11 +15,15 @@ "koa-bodyparser": "^3.0.0", | ||
"lodash": "^4.0.0", | ||
"mongoose": "^4.3.6" | ||
"mime": "^1.3.4", | ||
"mongoose": "^4.4.5", | ||
"mz": "^2.3.1", | ||
"string-random": "^0.1.0", | ||
"swig": "^1.4.2" | ||
}, | ||
"devDependencies": { | ||
"babel-plugin-syntax-async-functions": "^6.3.13", | ||
"babel-plugin-syntax-class-properties": "^6.3.13", | ||
"babel-plugin-transform-class-properties": "^6.4.0", | ||
"babel-plugin-transform-es2015-destructuring": "^6.4.0", | ||
"babel-plugin-transform-es2015-parameters": "^6.4.2", | ||
"babel-plugin-transform-object-rest-spread": "^6.3.13", | ||
"babel-plugin-syntax-async-functions": "^6.5.0", | ||
"babel-plugin-syntax-class-properties": "^6.5.0", | ||
"babel-plugin-transform-class-properties": "^6.6.0", | ||
"babel-plugin-transform-es2015-destructuring": "^6.6.5", | ||
"babel-plugin-transform-es2015-parameters": "^6.7.0", | ||
"babel-plugin-transform-object-rest-spread": "^6.6.5", | ||
"rimraf": "*" | ||
@@ -24,0 +30,0 @@ }, |
@@ -5,4 +5,2 @@ #[alaska](https://github.com/maichong/alaska) | ||
*Warning. This module is not stable yet.* | ||
## Contribute | ||
@@ -9,0 +7,0 @@ [Maichong Software](http://maichong.it) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
111922
34
13
2303
13
1
+ Addedalaska-cache-lru@^0.1.3
+ Addedasync-busboy@0.0.4
+ Addedmime@^1.3.4
+ Addedmz@^2.3.1
+ Addedstring-random@^0.1.0
+ Addedswig@^1.4.2
+ Addedalaska-cache-lru@0.1.3(transitive)
+ Addedamdefine@1.0.1(transitive)
+ Addedasync@0.2.10(transitive)
+ Addedasync-busboy@0.0.4(transitive)
+ Addedbusboy@0.2.14(transitive)
+ Addedcamelcase@1.2.1(transitive)
+ Addeddecamelize@1.2.0(transitive)
+ Addeddicer@0.2.5(transitive)
+ Addedhas-symbols@1.0.3(transitive)
+ Addedlru-cache@4.1.5(transitive)
+ Addedmime@1.6.0(transitive)
+ Addedminimist@0.0.10(transitive)
+ Addedmz@2.7.0(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedoptimist@0.6.1(transitive)
+ Addedpseudomap@1.0.2(transitive)
+ Addedreadable-stream@1.1.14(transitive)
+ Addedsource-map@0.1.34(transitive)
+ Addedstreamsearch@0.1.2(transitive)
+ Addedstring-random@0.1.3(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addedswig@1.4.2(transitive)
+ Addedthenify@3.3.1(transitive)
+ Addedthenify-all@1.6.0(transitive)
+ Addeduglify-js@2.4.24(transitive)
+ Addeduglify-to-browserify@1.0.2(transitive)
+ Addedwindow-size@0.1.0(transitive)
+ Addedwordwrap@0.0.20.0.3(transitive)
+ Addedyallist@2.1.2(transitive)
+ Addedyargs@3.5.4(transitive)
- Removedhas-symbols@1.1.0(transitive)
Updatedcollie@^0.2.1
Updatedmongoose@^4.4.5