
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
jbdap-engine
Advanced tools
此项目已经停止维护,但是可以继续正常使用。在 Node 版 JBDAP 升级的过程中我们意识到,虽然 knex 是一个很好的数据库操作适配器,但是使用一套引擎来适配多种数据库还是有其明显的缺点,更不利于我们针对不同数据库扩展其各自功能,所以我们后面会为每一个数据库提供单独的实现。 —— 2020.07.08
目前已经单独实现且可用的数据库 JBDAP 引擎如下:
JBDAP-Node-Engine 是 JBDAP 的 nodejs 版官方实现,简单来说就是一个关系型数据库访问组件,帮助开发人员快速实现数据库相关应用的搭建,数据处理模块代码量有望减少 70% 以上。
ORM 的主要目的是将数据操作代码 对象化、语义化、简洁化,ORM 很棒,它工作在 “术” 的层面,让我们的数据处理模块变得清晰易懂,不再杂乱无章。ORM 改变的是具体编码模式和质量,但是我们依然要为每个数据表去编写对应的数据模型类和数据实体类,里面充斥着大量雷同的模板化代码。
JBDAP 的设计则定位在 “道” 的层面,目的是将数据处理整个这一层的开发工作大限度 自动化、标准化、配置化,换言之,我们希望改变的是开发模式,以尽可能的砍掉那些雷同的数据对象处理类。
首先请允许我向 GraphQL 致敬!因为 GraphQL 是一个了不起的创举,它承载了优秀的、突破性的思维模式。与技术本身相比,思想才是解决方案的灵魂!
然后必须承认,JBDAP 的设计借鉴了 GraphQL 的思想,想要解决的问题也非常类似。然而两者的侧重点又有不同,想要达到的目标也不尽相同。我的主要出发点是让这种优秀的思想在实现上变得更加友好一些。下面是根据个人粗浅理解所列出的一些区别:
作为一个曾经学习过 GraphQL 的懒人,在对它的思想赞叹之余,对其实现方式上是有些不同观点的,可以看到认为 GraphQL 学起来费劲的人也有很多,这恰恰指向其在“语义化”方面的欠缺,注意“语义化”是为了让人可以“望文生义”,不用费尽心思去揣摩和记忆就能理解和应用,我认为如果能够降低使用者的心智成本,牺牲一些所谓的简洁度或者浪费一点字符空间是完全可以接受的。
总结一句:不敢说比 GraphQL 更优秀,但是更好用是一定的。
Request:
{
commands: [
{
name: 'allUsers',
type: 'list', // list 代表要获取数据列表
target: 'User' // 数据表名
}
]
}
等效 SQL:
select * from `User`
Response:
{
"code": 200,
"message": "ok",
"data": {
"allUsers": [
{
"id": 1,
"username": "user1",
"password": "password1",
"avatar": null,
"email": null,
"gender": "female",
"createdAt": "2019-02-28T13:27:05.150Z",
"updatedAt": "2019-02-28T13:27:05.150Z"
},
... // 更多数据省略
]
}
}
/**
* 说明:
* 如果没有符合条件的记录,data.allUsers 为 null
*/
Request:
{
commands: [
{
name: 'userInfo',
type: 'entity', // entity 代表要获取单个数据
target: 'User',
query: {
where: {
id: 1
}
}
}
]
}
等效 SQL:
select * from `User` where (`id` = 1)
Response:
{
"code": 200,
"message": "ok",
"data": {
"userInfo": {
"id": 1,
"username": "user1",
"password": "password1",
"avatar": null,
"email": null,
"gender": "female",
"createdAt": "2019-02-28T13:27:05.150Z",
"updatedAt": "2019-02-28T13:27:05.150Z"
}
}
}
/**
* 说明:
* 1、如果符合查询条件的结果有多个,那么只返回第一个
* 2、如果没有符合条件的,则 data.userInfo 为 null
*/
Request:
{
commands: [
{
name: 'allUsers',
type: 'list',
target: 'User',
fields: [
'id',
'username',
'avatar',
'updatedAt=>lastVisitedAt' // 别名返回
]
}
]
}
/**
* 说明:
* 1、list 查询和 entity 查询指定字段的方式完全一致
* 2、允许返回数据字段别名返回,如上面 'updatedAt=>lastVisitedAt'
* 将会把 updatedAt 改名为 lastVisitedAt 返回
*/
等效 SQL:
select `id`, `username`, `avatar`, `updatedAt` as `lastVisitedAt`
from `User`
Response:
{
"code": 200,
"message": "ok",
"data": {
"allUsers": [
{
"id": 1,
"username": "user1",
"avatar": null,
"lastVisitedAt": "2019-02-28T13:27:05.150Z" // 已经改名
},
... // 更多数据省略
]
}
}
Request:
{
commands: [
{
name: 'goodBlogs',
type: 'list',
target: 'Blog',
query: {
where: { // 这里是一个非常复杂的查询条件
'userId': 1,
'views#gte': 100,
$or: {
'title#like': 'blog%',
$and: {
'content#like': '%user%',
'createdAt#gte': '2019-02-28T13:27:05.162Z'
}
},
$not: {
'hearts#lte': 10,
'views#lte': 50
}
}
},
fields: 'id,title,content,views,hearts=>likes' // 别名返回
}
]
}
/**
* 说明:
* 1、属性的 key 用 # 隔开了 field 名称与运算符,支持的运算符有:
* 值比较:eq, ne, lte, lt, gte, gt
* 包含判断:in, notIn
* 字符串匹配:like, notLike, contains, notContain, startsWith, notStartWith, endsWith, notEndWith
* 区域判断:between, notBetween
* Null 值判断:isNull, isNotNull
* 分组运算符: and, or, not
* 2、组合使用可以实现任何多层级的复杂查询
*/
等效 SQL:
select `id`, `title`, `content`, `views`, `hearts` as `likes`
from `Blog`
where (
`userId` = 1
and `views` >= 100
and (
`title` like 'blog%'
or (
`content` like '%user%'
and `createdAt` >= '2019-02-28T13:27:05.162Z'
)
)
and (
not `hearts` <= 10 and not `views` <= 50
)
)
/**
* WOW,看这个 SQL 语句,很牛逼的样子
* 我打赌你一般用不到这么复杂的查询,但是 JBDAP-Node-Engine 确实允许你无限写下去
*/
Response:
略
Request:
{
commands: [
{
name: 'someUsers',
type: 'list',
target: 'User',
query: {
order: 'id#desc',
size: 2, // 每页条数
page: 3 // 返回第几页
},
fields: 'id,username,avatar'
}
]
}
/**
* 说明:
* query.page 为 3 意味着返回第 3 页,每页 query.size 条
*/
等效 SQL:
select `id`, `username`, `avatar` from `User`
order by `id` desc
limit 2 offset 4
Response:
{
"code": 200,
"message": "ok",
"data": {
"someUsers": [
{
"id": 8,
"username": "user8",
"avatar": null
},
{
"id": 7,
"username": "user7",
"avatar": null
}
]
}
}
Request:
{
commands: [
{
name: 'blogStat',
type: 'values', // 这里指明是 values 查询
target: 'Blog',
query: {
where: {
userId: 1
},
order: 'id#desc'
},
fields: [
'count#id=>totalBlogs', // 计数
'sum#hearts=>totalHearts', // 求和
'max#hearts=>maxViews', // 求最大
'avg#hearts=>avgHearts', // 求均值
'first#title=>latestTitle', // 第一条记录的指定字段
'pick#id=>blogIds', // 拣取指定字段拼为数组
'clone#id,title,content,hearts=>List' // 克隆每行数据的指定字段
]
}
]
}
/**
* 说明:
* 1、请注意 fields 的定义,count, sum, max, min, avg 的用法无需多言
* 2、最后三个比较特殊:
* first - 取第一条记录的单个指定字段,比如可以用来取当前最大 id
* pick - 将指定字段取出放入一个数组,比如可以取出 id 的数组用于 where 中的 in 查询
* clone - 克隆每条记录的指定字段,比如获得一个小的简略数据列表
* 3、对于后三个运算,建议不要在大量数据场景下使用
*/
等效 SQL:
select * from `Blog` where (`userId` = 1) order by `id` desc
Response:
{
"code": 200,
"message": "ok",
"data": {
"blogStat": {
"totalBlogs": 5, // 总共 5 篇博客
"totalHearts": 414, // 总计点赞 414
"maxViews": 122, // 单篇最大浏览量 122
"avgHearts": 82.8, // 平均点赞 82.8
"latestTitle": "blog99", // 最新一篇标题
"blogIds": [ // 所有博客 id 组成的数组
99,
98,
68,
66,
2
],
"List": [ // 克隆出原始数据的字段子集
{
"id": 99,
"title": "blog99",
"content": "blog content 99 from user 1",
"hearts": 122
},
{
"id": 98,
"title": "blog98",
"content": "blog content 98 from user 1",
"hearts": 49
},
... // 更多数据省略
]
}
}
}
Request:
{
commands: [
{
name: 'newBlogs',
type: 'create',
target: 'Blog',
data: [
{
userId: 17,
categoryId: 1,
title: 'new blog 17-1',
createdAt: 'JBDAP.fn.ISODate', // 服务器函数
updatedAt: 'JBDAP.fn.ISODate'
},
{
userId: 17,
categoryId: 1,
title: 'new blog 17-2',
createdAt: 'JBDAP.fn.ISODate',
updatedAt: 'JBDAP.fn.ISODate'
}
]
}
]
}
/**
* 说明:
* 1、给 data 传入一个数组可以批量创建数据
* 考虑到网络传输压力和服务器性能,不建议一次批量插入超过 500 条数据
* 且强烈建议使用事务(如何使用事务,这是后话)
* 2、这个例子我们使用了一个名为 JBDAP.fn.ISODate 的服务端函数
* 执行时会被替换成服务器时间的 ISO 格式,如 '2019-02-28T13:27:05.162Z'
* 这是目前唯一一个服务端函数
*/
等效 SQL:
insert into `Blog`
(`categoryId`, `createdAt`, `title`, `updatedAt`, `userId`)
select
1 as `categoryId`,
'2019-03-11T02:26:53.366Z' as `createdAt`,
'new blog 17-1' as `title`,
'2019-03-11T02:26:53.366Z' as `updatedAt`,
17 as `userId`
union all
select
1 as `categoryId`,
'2019-03-11T02:26:53.366Z' as `createdAt`,
'new blog 17-2' as `title`,
'2019-03-11T02:26:53.366Z' as `updatedAt`,
17 as `userId`
Response:
{
"code": 200,
"message": "ok",
"data": {
"newBlogs": {
"dbServer": "sqlite",
"return": [
104
]
}
}
}
/**
* 说明:
* return 是插入的最后一条记录 id 值
*/
Request:
{
commands: [
{
name: 'updateBlogs',
type: 'update', // 指明是 update 操作
target: 'Blog',
query: {
where: {
userId: 17,
'title#like': 'new blog 17-%'
}
},
data: {
content: 'new blog content for user i7',
views: 100,
hearts: 10
}
}
]
}
/**
* 这里我们将刚才插入的两条博文进行了更新
* data 里有的字段才会被更新
*/
等效 SQL:
update `Blog`
set
`content` = 'new blog content for user i7',
`views` = 100,
`hearts` = 10
where
(`userId` = 17 and `title` like 'new blog 17-%')
Response:
{
"code": 200,
"message": "ok",
"data": {
"updateBlogs": {
"dbServer": "sqlite",
"return": 2
}
}
}
/**
* 说明:
* 此处 return 值为受影响数据条数
*/
Request:
{
commands: [
{
name: 'delBlog',
type: 'delete', // 指明是 delete 操作
target: 'Blog',
query: {
where: {
id: 104
}
}
}
]
}
/**
* 这里我们将刚才插入的两条博文之一进行了删除
*/
等效 SQL:
delete from `Blog` where (`id` = 104)
Response:
{
"code": 200,
"message": "ok",
"data": {
"delBlog": {
"dbServer": "sqlite",
"return": 1
}
}
}
/**
* 说明:
* 此处 return 值为受影响数据条数
*/
Request:
{
commands: [
{
name: 'fakeNumbers',
type: 'increase', // 指明是 increase 原子自增操作
target: 'Blog',
query: {
where: {
userId: 17,
}
},
data: {
hearts: 10,
views: 100
}
}
]
}
/**
* 可以同时更新多个值,注意增加数字不能为负数
*/
等效 SQL:
update `Blog`
set
`hearts` = `hearts` + 10,
`views` = `views` + 100
where
(`userId` = 17)
Response:
{
"code": 200,
"message": "ok",
"data": {
"fakeNumbers": {
"dbServer": "sqlite",
"return": 5
}
}
}
/**
* 说明:
* 此处 return 值为受影响数据条数
*/
Request:
{
commands: [
{
name: 'delBlogs',
type: 'delete',
target: 'Blog',
query: {
where: {
userId: 17
}
}
},
{
name: 'delUser',
type: 'delete',
target: 'User',
query: {
where: {
id: 17
}
}
}
]
}
/**
* 执行多条指令的时候,会按照指令在 commands 数组中出现的先后顺序执行
*/
等效 SQL:
delete from `Blog` where (`userId` = 17)
delete from `User` where (`id` = 17)
Response:
{
"code": 200,
"message": "ok",
"data": {
"delBlogs": {
"dbServer": "sqlite",
"return": 5
},
"delUser": {
"dbServer": "sqlite",
"return": 1
}
}
}
Request:
{
commands: [
{
name: 'userInfo',
type: 'entity',
target: 'User',
query: {
where: {
id: 1
}
},
fields: [ // 对字段做一些精简
'id',
'username',
'avatar',
'updatedAt=>lastVisitedAt',
{
name: 'top5blogs',
type: 'list',
target: 'Blog',
query: {
where: {
userId: '$.id' // 这里 $ 指 userInfo
},
order: 'updatedAt#desc'
},
fields: [
'id',
'categoryId',
'title',
'content',
'views',
'hearts',
{
name: 'category',
type: 'entity',
target: 'Category',
query: {
where: {
id: '$.categoryId' // 这里 $ 指单个 blog
}
},
fields: 'id,name'
},
{
name: 'top5comments',
type: 'list',
target: 'Comment',
query: {
where: {
blogId: '$.id' // 这里 $ 指单个 blog
},
order: 'id#desc',
size: 5
},
fields: 'id,content,hearts'
}
]
}
]
}
]
}
/**
* 多层的级联填充,原理都是一样的,难度在于你要理解表达式中的 '$' 代表的是什么
*/
等效 SQL:
select `id`, `username`, `avatar`, `updatedAt` as `lastVisitedAt`
from `User`
where (`id` = 1)
select `id`, `categoryId`, `title`, `content`, `views`, `hearts`
from `Blog`
where (`userId` = 1)
order by `updatedAt` desc
select `id`, `name`
from `Category`
where (`id` = 2)
select `id`, `content`, `hearts`
from `Comment`
where (`blogId` = 2)
order by `id` desc
limit 5
...
Response:
{
"code": 200,
"message": "ok",
"data": {
"userInfo": {
"id": 1,
"username": "user1",
"avatar": null,
"lastVisitedAt": "2019-02-28T13:27:05.150Z",
"top5blogs": [
{
"id": 2,
"categoryId": 2,
"title": "blog2",
"content": "blog content 2 from user 1",
"views": 953,
"hearts": 94,
"category": {
"id": 2,
"name": "政治"
},
"top5comments": [
{
"id": 914,
"content": "comment 914 for blog 2",
"hearts": 27
},
{
"id": 888,
"content": "comment 888 for blog 2",
"hearts": 83
},
... // 省略 3 条 Comment 数据
]
},
... // 省略 4 条 Blog 数据
]
}
}
}
Request:
{
commands: [
{
name: 'userBlogs', // 这个指令的查询结果将被引用
type: 'values',
target: 'Blog',
query: {
where: {
userId: 1
}
},
fields: [
'pick#id=>ids'
]
},
{
name: 'top10comments',
type: 'list',
target: 'Comment',
query: {
where: {
'id#in': '/userBlogs.ids' // 引用 userBlogs 查询结果中的 ids 属性作为条件进行查询
},
order: 'id#desc', // 倒序取最新
size: 10 // 取前 10 条
}
}
]
}
/**
* 说明:
* 我们通过多指令的方式来实现将被引用的 userBlogs 指令查询
* 然后在 top10comments 的查询条件中通过 /userBlogs.ids 的方式获得对该数值的引用以实现查询
*/
等效 SQL:
select * from `Blog` where (`userId` = 1)
select *
from `Comment`
where
(`id` in (2, 66, 68, 98, 99))
order by `id` desc
limit 10
Response:
{
"code": 200,
"message": "ok",
"data": {
"userBlogs": {
"ids": [
2,
66,
68,
98,
99
]
},
"top10comments": [
{
"id": 99,
"blogId": 45,
"fromUserId": 2,
"replyTo": null,
"content": "comment 99 for blog 45",
"hearts": 66,
"createdAt": "2019-02-28T13:27:05.174Z",
"updatedAt": "2019-02-28T13:27:05.174Z"
},
... // 其余数据省略
]
}
}
/**
* 结果与预期完全一致
*/
Request:
{
commands: [
{
return: false, // renturn 设为 false,此查询结果将不返回
name: 'userBlogs',
type: 'values',
target: 'Blog',
query: {
where: {
userId: 1
}
},
fields: [
'pick#id=>ids'
]
},
...
]
}
Request:
{
commands: [
{
return: false, // 无需返回
name: 'userInfo',
type: 'entity',
target: 'User',
query: {
where: {
username: 'user100' // 用户名查重
}
},
fields: 'id'
},
{
name: 'newUser',
type: 'create',
target: 'User',
onlyIf: {
'/userInfo#isNull': true // 以 userInfo 查询结果是 null 为前提
},
data: {
username: 'user100',
password: 'password111',
gender: 'female',
createdAt: 'JBDAP.fn.ISODate',
updatedAt: 'JBDAP.fn.ISODate'
}
}
]
}
/**
* 说明:
* onlyIf 与 where 的定义方式和运算规则基本一致,支持分组运算,但是也有小的区别如下:
* 1、where 其下每一个键值对叫做一个查询条件,其键名中 # 的左边只能是 field 名称
* 2、onlyIf 其下的每一个键值对则叫做一个比较表达式,其键名中 # 的左边是可以进行赋值的表达式
* 3、两者运算符也有不同
* 没有 like 和 notLike
* 没有 between 和 notBetween
* 新增 matches 和 notMatch
* 新增 exists 和 notExist
* 新增 isUndefined 和 isNotUndefined
* 新增 isEmpty 和 isNotEmpty
*/
等效 SQL:
select `id` from `User` where (`username` = 'user100')
insert into `User`
(`createdAt`, `gender`, `password`, `updatedAt`, `username`)
values
('2019-03-11T13:31:25.194Z', 'female', 'password111', '2019-03-11T13:31:25.195Z', 'user100')
Response:
{
"code": 200,
"message": "ok",
"data": {
"newUser": {
"dbServer": "sqlite",
"return": [
18
]
}
}
}
/**
* 注意:
* 这是没有用户名冲突执行成功的结果,如果存在冲突的话,返回值 newUser 将会是 null
*/
Request:
{
commands: [
{
name: 'blogInfo',
type: 'entity',
target: 'Blog',
query: {
where: {
id: 1
}
},
after: {
name: 'updateViews',
type: 'increase',
target: 'Blog',
query: {
where: {
id: 1
}
},
data: 'views:1'
}
}
]
}
/**
* 查询 blog 详情的时候,顺道就把访问量加 1 的工作给做了。
* 注意:after 可以接受数组参数,也就是说主指令执行完成后可以执行一系列指令
* 与此同时,在 after 指令里面,继续使用 onlyIf 判断可以进一步加强对数据操作流程的掌控
*/
等效 SQL:
select * from `Blog` where (`id` = 1)
update `Blog` set `views` = `views` + 1 where (`id` = 1)
Response:
{
"code": 200,
"message": "ok",
"data": {
"blogInfo": {
"id": 1,
"userId": 4,
"categoryId": 1,
"title": "blog1",
"keywords": null,
"content": "blog content 1 from user 4",
"views": 753,
"hearts": 55,
"createdAt": "2019-02-28T13:27:05.162Z",
"updatedAt": "2019-02-28T13:27:05.162Z"
}
}
}
/**
* 我们关注的重心依然是 blog 详情本身
*/
Request
{
isTransaction: true, // 没错,你只要把 isTransaction 配置为 true 就可以了
commands: [
{
name: 'delBlogs',
type: 'delete',
target: 'Blog',
query: {
where: {
userId: 17
}
}
},
{
name: 'delUser',
type: 'delete',
target: 'User',
query: {
where: {
id: 17
}
}
}
]
}
Request:
{
"code": 0,
"message": "[CmdExecError]:解析或执行指令失败 <= [JBDAPCommandError]:处理指令 \"newUser\" 出错 <= [DBExecError]:操作数据出错 <= [Error]:insert into `User` (`gender`, `password`, `username`) values ('female', 'password111', 'just4test') - SQLITE_CONSTRAINT: UNIQUE constraint failed: User.username",
"data": null
}
把 message 单独拿出来整理一下格式是这样的
[CmdExecError]:解析或执行指令失败
<= [JBDAPCommandError]:处理指令 \"newUser\" 出错
<= [DBExecError]:操作数据出错
<= [Error]:
insert into `User`
(`gender`, `password`, `username`)
values
('female', 'password111', 'just4test')
- SQLITE_CONSTRAINT: UNIQUE constraint failed: User.username
Request:
{
needLogs: true, // 告知服务器需要返回执行日志
commands: [
{
name: 'userInfo',
type: 'entity',
target: 'User',
query: {
where: {
id: 1
}
},
fields: [
'*',
{
name: 'top5blogs',
type: 'list',
target: 'Blog',
query: {
where: {
userId: '$.id'
},
order: 'updatedAt#desc'
}
}
]
}
]
}
Response:
{
"code": 200,
"message": "ok",
"data": {
"userInfo": {
... // 省略
}
},
"logs": [
"- 开启 JBDAP 任务",
"- 检查接收到的 JSON 是否合法",
"* 用户身份校验",
"- 开始处理接收到的指令",
"- 非事务方式执行",
"$ 开始执行顶层指令 /userInfo - entity 类型",
" @ 开始执行级联指令 [top5blogs] - list 类型",
" @ 级联指令 [top5blogs] 执行完毕",
"$ 顶层指令 /userInfo 执行完毕",
"- 全部指令处理完成",
"- JBDAP 任务成功"
]
}
FAQs
The official JBDAP implementation written in nodejs.
We found that jbdap-engine demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.