Handle.js

Handle,一个基于 koa 和 sequelize 的中间库,让你只专注于接口逻辑。
API Documentation
安装
npm i @ntbl/handle --save
Usage
import Handle from '@ntbl/handle'
import { Article } from '../models/db'
const article = new Handle(Article)
const find = article.findAll()
router.get('/article/find', find)
加载器
加载器让 sequelize 模型文件的导入和 Handle 实例化合二为一。
const Article sequelize.import(__dirname + './article')
const article = new Handle(Article)
const article = Handle.load(sequelize, __dirname + './article')
另外,还支持批量加载,一劳永逸。
const db = Handle.loadAll(sequelize, __dirname, {
rule: '/**/!(index|_)*.js',
})
实例方法
Handle 拥有大部分 sequelize 模型实例上的方法,分为两类。
第一类,统称为快捷方法。调用后直接生成一个 async 函数(接口函数),可以直接挂载至路由,无须编写一行代码。。
- GET: findOne, findAll, findById, findOrCreate, findAndCountAll, findCreateFind, count, max, min, sum
- POST: create, bulkCreate, update, destroy, increment, decrement
router.get('/article/find', article.findAll())
第二类,统称为过程方法,调用后仅返回数据,配合实例的 process
方法进一步处理。
rawFindOne, rawFindAll, rawFindById, rawFindOrCreate, rawFindAndCountAll, rawFindCreateFind, rawCount, rawMax, rawMin, rawSum,rawCreate, rawBulkCreate, rawUpdate, rawDestroy, rawIncrement, rawDecrement
const find = artcile.process(async function (d) {
const userData = await user
.where('username', 'password')
.rawFindOne()
const result = await this
.where(['id', userData.id])
.rawFindAll()
return result.filter(e => e.type === 'recommend')
})
router.get('/article/find', find)
修改默认的请求方法
你可能注意到了快捷方法已被固定了请求方法,我们可以通过以下方式修改。
Handle.defaults.proxy.findAll.method = 'post'
article.options.proxy = {
findAll: {
method: 'post'
}
}
article
.method('post')
.findAll()
注意,三者的优先级:方法 > 实例 > 整个应用,前者会覆盖掉后者。
工具方法
handle.js 内置了一个工具集,封装了一些常用的接口逻辑,帮助你快速编写复杂的接口,让你充分利用封装所带来的优良特性。
接口参数
where 工具方法帮助你更加灵活地处理接口参数逻辑,它提供六种 wehre 子句的便捷写法。
article
.where('uid')
.findAll()
article
.where('uid', 'id')
.findAll()
article
.where(['id', 1])
.findAll()
article
.where(['id', '@aid'])
.findAll()
article
.where(['!id', '!uid'])
.findAll()
article
.where('id >')
.findAll()
某些 Op 语法需要特殊的动态值,为此,Handle 增加了传入数组的第二个元素对函数写法的支持。
article
.where(['title #like', d => `%${d.title}%`])
.findAll()
where 支持的所有 Op 便捷写法。
let opTag = {
'>': 'gt',
'>=': 'gte',
'<': 'lt',
'<=': 'lte',
'!=': 'ne',
'=': 'and',
'#and': 'and',
'#or': 'or',
'#gt': 'gt',
'#gte': 'gte',
'#lt': 'lt',
'#lte': 'lte',
'#ne': 'ne',
'#eq': 'eq',
'#not': 'not',
'#between': 'between',
'#notBetween': 'notBetween',
'#in': 'in',
'#notIn': 'notIn',
'#like': 'like',
'#notLike': 'notLike',
'#iLike': 'iLike',
'#regexp': 'regexp',
'#iRegexp': 'iRegexp',
'#notIRegexp': 'notIRegexp',
'#overlap': 'overlap',
'#contains': 'contains',
'#contained': 'contained',
'#any': 'any',
'#col': 'col',
}
另外,以上的六种便捷写法互相之间可以组合,但你需要了解一些约束:
- 别名语法(
@
) 只能用于数组中的第二个元素上
- 可选值语法(
!
)不能用于数组中的第二个元素上
- 除了默认值,所有的命名必须是一个合法的标识符
- 在一个位置里写了多 Op 标识,只会应用第一个,不同位置的相同的 Op 标识后面会覆盖前面
我们能写出更强大的接口参数。
full: db.article
.where('uid')
.where('!id')
.where(['!title #like', d => `%${d.title}%`])
.findAll()
模糊查询
fuzzyQuery
(模糊查询)、fuzzyQueryLeft
(左模糊查询)、fuzzyQueryRight
(右模糊查询)可以帮你快速生成一个模糊查询。
article
.fuzzyQuery('title')
.findAll()
article
.fuzzyQuery('!title')
.findAll()
分页
pagination
帮助你快速生成一个分页逻辑。
article
.pagination(10)
.findAll()
排序
order
帮助你简单生成一个排序。
article
.order(['createdAt', 'DESC'])
.findAll()
关联
include
帮助你添加关联(关联的更多知识,请参考 sequelize 官方文档)
article
.include(User, Comment)
.findAll()
数据处理
remove
帮助你移除 request data 中的指定字段。
set
则允许你修改。
article
.remove('status')
.update()
article
set('status', 'fall')
.update()
条件分支
it
类似 if 语句,可以让你的接口逻辑出现分支,这对于一些有细微差别的接口很重要,你可以通过 it 把它们合并成一个接口。除此之外,它还可以做某些特定查询的开关。
it(condition, f1, [f2])
- string/function condition: 用于 request data 的条件
- array/function f1 测试成功时执行
- array/function f2 测试失败时执行
其语法为:
it(条件, 条件成立时执行, 条件不成立时执行)
it('comment', f1, f2)
it(d => d.count > 2, f1, f2)
it('comment', f1, [f1, f2, f3])
it('comment', [f1, f2, f3])
article
.it('comment', include(Comment))
not
是 it 的反向版本。
not(条件, 条件不成立时执行, 条件成立时不执行)
more
类似 switch 语句,同时可分支多个条件。
article
itField('sort', {
'name': f1,
'age': [f2, f3],
'height': f4
})
.findAll()
Scope
链式调用看起来简洁大方,但是却缺乏良好的复用性。当你有一组相同或类似的接口逻辑,你可以使用独立的函数版本再次封装后调用 scope
方法添加。
const Scopes = Handle.Scopes
const {where, pagination, fuzzyQuery, include, order, it, merge} = Scopes
function nb () {
return merge(
where('uid'),
where('!id'),
fuzzyQuery('!title'),
order(['createdAt', 'DESC']),
pagination(10),
)
}
article
.scope(nb)
.findAll()
scope
方法合并的选项对象仅在第一次被使用的方法上有效。如果,想要让所有当前实例的模型方法都共享某些工具方法 ,可以在实例上通过 defaultScope
添加。
自定义
你也可以扩展自定义的工具方法,你需要在实例化之前,添加你的自定义工具放在在 Handle.Scope
中即可
Handle.Scope.myUtil = function (d) {
return {
where: {
uid: d.uid
}
}
}
通过提供一个偏函数指定默认值
Handle.Scope.myUtil = function (defaultValue) {
return function (data) {
return {
}
}
}
然后你就可以在全局使用自定义的工具函数 myUtil
。
article
.myUtil()
.findAll()
Process
process
的是为一个接口需要多表操作并且对返回数据进一步处理的情况提供,也是实现更为复杂的接口的一个台阶。
这里,我们先分解了一个分页工具函数。
articleStar.process(async function (d) {
const {count = 15, page = 0, uid} = d
const res = await this.rawFindAll({
include: [
{
model: Article,
include: [User]
}
],
where: { uid },
limit: count,
offset: page * count
})
return res && res.map(d => d.article)
})
然后,再看看多表操作。
const find = artcile.process(async function (d) {
const userData = await user
.where('username', 'password')
.rawFindOne()
const result = await this
.where(['id', userData.id])
.rawFindAll()
return result.filter(e => e.type === 'recommend')
})
process 默认为 get 请求,Handle 支持 6 种 http 标准请求方法(get/head/put/delete/post/options)
articleStar.process('post', async function (d) {})
事务
transaction
是通过 process
简单的对 sequelize 原生事务的封装。在使用上,和 process
完全一致。
articleStar.transaction(async function (d) {
return
}),
钩子
Handle
在选项对象里提供了三个全局钩子 before
、after
, data
。
每个快捷方法都会执行这些钩子,而过程方法则会忽略这些钩子,process
会在调用回调前执行 before
调用回调后执行 after
和 data
。
new Handle(model, {
before (data, ctx, next) {
}
after (result, ctx, next) {
}
data (err, result, ctx, next) {
}
})
另外,每个实例方法上都有 before
和 after
函数,可以注册仅在实例上执行的钩子,帮助我们完成一些有用的处理。
我们可以通过 before
钩子校验前端发过来的数据。
article
.before(function (data) {
const {title} = data
if (!title) {
throw new Error('文章标题不能为空')
}
if (title.length < 1 || title.length > 25) {
throw new Error('文章标题不小于 2 个字符且不大于 25 个字符')
}
return data
})
.create()
也可以通过 after
钩子过滤数据。
article
.after(function (data) {
return data.length
})
.where('uid')
.findAll()
请注意,实例的 before
钩子先于全局 before
钩子执行,而实例的 after
钩子晚于全局 after
钩子执行
原生数据
Handle 会很聪明的生成 sequelize 方法的参数,一般情况下,我们无须关心。但是对于,increment,decrement 或一些特殊情况,你想要使用指定的数据,而不是 Handle 帮你处理后的 Request Data(前端发送到后端的数据),可以通过 raw
方法设置原生数据。
article
.raw('hot')
.increment('id')
但是,你需要了解,Request Data 仍然会用于各种场景下,比如 Scope 和 where 工具函数的解析,只是在最后合成 sequelize 方法的参数时,Request Data 被替换成了 原生数据,也就意味着,在钩子或者其他地方修改 Request Data 不会应用到数据库访问中。通过这一点,你可以使用类似 mock 的库批量向数据库添加数据。(并在未来可能会支持 mocK 的数据模拟)
一句话
一句话,如果你在工具函数中找不到可以帮你解决问题的函数时,我强烈建议你把相关代码封装成一个自定义的 scope 再使用,其一是你会有个优雅的代码结构和可读的命名,其二,当在其他地方复用时你必须再重新写一遍。如果你的 scope 足够通用时,你可以提交到 handle.js 中,为更多的人提供便利。
如果你不使用 pull requests 或 Issues,也可以通过以下方式联系到我: