Feature: custom operators for querying (#48)
* feat: add some custom operators for querying * feat: add some custom operators for querying * test: fix cases * improve custom operator function * improve filter field component Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
0754c5979a
commit
b2fe087fc2
@ -20,9 +20,12 @@ import update2 from './actions/update2';
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(__dirname, '')
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_');
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
return key
|
||||
}
|
||||
|
||||
@ -77,6 +80,10 @@ resourcer.define({
|
||||
name: 'users',
|
||||
actions: actions.common,
|
||||
});
|
||||
resourcer.define({
|
||||
name: 'profiles',
|
||||
actions: actions.common,
|
||||
});
|
||||
resourcer.define({
|
||||
type: 'hasOne',
|
||||
name: 'users.profile',
|
||||
@ -128,7 +135,7 @@ export async function initDatabase() {
|
||||
const options = requireModule(file);
|
||||
database.table(typeof options === 'function' ? options(database) : {
|
||||
...options,
|
||||
tableName: `${options.tableName}_${key}`
|
||||
tableName: `${key}_${options.tableName}`
|
||||
});
|
||||
});
|
||||
await database.sync({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Op } from 'sequelize';
|
||||
import { literal, Op } from 'sequelize';
|
||||
|
||||
import { initDatabase, agent } from './index';
|
||||
|
||||
@ -23,11 +23,14 @@ describe('list', () => {
|
||||
beforeEach(async () => {
|
||||
const User = db.getModel('users');
|
||||
await User.bulkCreate([
|
||||
{ name: 'a', ...timestamps },
|
||||
{ name: 'b', ...timestamps },
|
||||
{ name: 'a', ...timestamps, nicknames: ['aa', 'aaa'] },
|
||||
{ name: 'b', ...timestamps, nicknames: [] },
|
||||
{ name: 'c', ...timestamps }
|
||||
]);
|
||||
const users = await User.findAll();
|
||||
users[0].updateSingleAssociation('profile', { city: '1101', interest: [1] });
|
||||
users[1].updateSingleAssociation('profile', { city: '3710', interest: [1, 2] });
|
||||
users[2].updateSingleAssociation('profile', { city: '5301', interest: [] });
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
await Post.bulkCreate(Array(25).fill(null).map((_, index) => ({
|
||||
@ -140,7 +143,184 @@ describe('list', () => {
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows[0].id).toBe(2);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('custom ops', () => {
|
||||
it('$null', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const expected = await Post.findAll({
|
||||
where: {
|
||||
published_at: null
|
||||
}
|
||||
});
|
||||
const response = await agent.get('/posts?filter[published_at.$null]=');
|
||||
expect(response.body.count).toBe(expected.length);
|
||||
});
|
||||
|
||||
describe('$anyOf', () => {
|
||||
describe('single', () => {
|
||||
// TODO(question): 是否应该用 in/notIn 来处理单项?
|
||||
// 或者单项存值也使用 JSON 类型也可以。
|
||||
it.skip('$anyOf', async () => {
|
||||
// const Profile = db.getModel('profiles');
|
||||
// const profiles = await Profile.findAll();
|
||||
const response = await agent.get('/profiles?filter[city.$anyOf]=Beijing,Weihai');
|
||||
console.log(response.body);
|
||||
// expect(response.body.count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple', () => {
|
||||
it('$anyOf for 1 element in definition', async () => {
|
||||
const User = db.getModel('users');
|
||||
const expected = await User.findOne({
|
||||
where: {
|
||||
nicknames: { [Op.contains]: 'aa' }
|
||||
}
|
||||
});
|
||||
const response = await agent.get('/users?filter[nicknames.$anyOf][]=aa');
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows[0].name).toBe(expected.name);
|
||||
});
|
||||
|
||||
it('$anyOf for all elements in definition', async () => {
|
||||
const User = db.getModel('users');
|
||||
const expected = await User.findOne({
|
||||
where: {
|
||||
nicknames: { [Op.or]: [
|
||||
{ [Op.contains]: 'aaa' },
|
||||
{ [Op.contains]: 'aa' }
|
||||
] }
|
||||
}
|
||||
});
|
||||
const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,aa');
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows[0].name).toBe(expected.name);
|
||||
});
|
||||
|
||||
it('$anyOf for some element not in definition', async () => {
|
||||
const User = db.getModel('users');
|
||||
const expected = await User.findOne({
|
||||
where: {
|
||||
nicknames: { [Op.or]: [{ [Op.contains]: ['aaa'] }, { [Op.contains]: ['a'] }] }
|
||||
}
|
||||
});
|
||||
const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,a');
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows[0].name).toBe(expected.name);
|
||||
});
|
||||
|
||||
it('$anyOf for no element', async () => {
|
||||
const User = db.getModel('users');
|
||||
const expected = await User.findAll();
|
||||
const response = await agent.get('/users?filter={"nicknames.$anyOf":[]}');
|
||||
expect(response.body.count).toBe(expected.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('$allOf', () => {
|
||||
it('$allOf for no element', async () => {
|
||||
const response = await agent.get('/users?filter={"nicknames.$allOf":[]}');
|
||||
expect(response.body.count).toBe(3);
|
||||
});
|
||||
|
||||
it('$allOf for different element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa');
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
|
||||
it('$allOf for less element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$allOf][]=aa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows).toEqual([
|
||||
{ name: 'a', nicknames: ['aa', 'aaa'] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('$allOf for same element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$allOf]=aa,aaa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(1);
|
||||
expect(response.body.rows).toEqual([
|
||||
{ name: 'a', nicknames: ['aa', 'aaa'] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('$allOf for more element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa,aaa');
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(bug): 需要 toWhere 重构和操作符函数修改
|
||||
describe.skip('$noneOf', () => {
|
||||
it('$noneOf for no element', async () => {
|
||||
const response = await agent.get('/users?filter={"nicknames.$noneOf":[]}');
|
||||
expect(response.body.count).toBe(3);
|
||||
});
|
||||
|
||||
it('$noneOf for different element', async () => {
|
||||
const User = db.getModel('users');
|
||||
const users = await User.findAll({
|
||||
where: {
|
||||
[Op.not]: {
|
||||
// 不使用 or 包装两个同一个 col 的条件会被转化成 and,与官方文档不符
|
||||
// WHERE NOT ("users"."nicknames" @> '"aa"' AND "users"."nicknames" @> '"a"')
|
||||
[Op.or]: [
|
||||
{ nicknames: { [Op.contains]: 'aa' } },
|
||||
{ nicknames: { [Op.contains]: 'a' } },
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(users);
|
||||
// const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa');
|
||||
// expect(response.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('$noneOf for less element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$noneOf][]=aa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('$noneOf for same element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$noneOf]=aa,aaa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('$noneOf for more element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa,aaa');
|
||||
expect(response.body.count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('$match', () => {
|
||||
it('$match for no element', async () => {
|
||||
const response = await agent.get('/users?filter={"nicknames.$match":[]}');
|
||||
expect(response.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('$match for different element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$match]=a,aa');
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
|
||||
it('$match for less element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$match][]=aa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
|
||||
it('$match for same element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$match]=aa,aaa&fields=name,nicknames');
|
||||
expect(response.body.count).toBe(1);
|
||||
});
|
||||
|
||||
it('$match for more element', async () => {
|
||||
const response = await agent.get('/users?filter[nicknames.$match]=a,aa,aaa');
|
||||
expect(response.body.count).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('page', () => {
|
||||
@ -277,7 +457,8 @@ describe('list', () => {
|
||||
it('appends fields', async () => {
|
||||
const response = await agent.get('/posts?fields[only]=title&fields[appends]=user.name&filter[title]=title0');
|
||||
expect(response.body.rows[0].user.name).toEqual('a');
|
||||
expect(response.body.rows).toEqual([{ title: 'title0', user: { id: 1, name: 'a', ...timestampsStrings } }]);
|
||||
expect(response.body.rows).toEqual([{
|
||||
title: 'title0', user: { id: 1, nicknames: ['aa', 'aaa'], name: 'a', ...timestampsStrings } }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -349,8 +530,8 @@ describe('list', () => {
|
||||
// TODO(bug)
|
||||
it.skip('get posts of user with comments', async () => {
|
||||
const response = await agent
|
||||
.get(`/users/1/posts?fields=comments.content,user.name&filter[comments.status]=draft&sort=-content&page=1&perPage=2`);
|
||||
|
||||
.get(`/users/1/posts?fields=comments.content,user.name&filter[comments.status]=draft&sort=-comments.content&page=1&perPage=2`);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
count: 1,
|
||||
page: 1,
|
||||
@ -366,22 +547,55 @@ describe('list', () => {
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('count field in hasMany', async () => {
|
||||
try {
|
||||
const response = await agent
|
||||
.get(`/users/1?fields=name,posts_count`);
|
||||
console.log(response.body);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
it('count field in hasMany', async () => {
|
||||
try {
|
||||
const response = await agent
|
||||
.get(`/users/1/posts?fields=title,comments_count`);
|
||||
console.log(response.body);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('belongsToMany', () => {
|
||||
beforeEach(async () => {
|
||||
const Tag = db.getModel('tags');
|
||||
const tags = await Tag.bulkCreate([
|
||||
{name: 'tag1', status: 'published'},
|
||||
{name: 'tag2', status: 'draft'},
|
||||
{name: 'tag3', status: 'published'},
|
||||
{name: 'tag4', status: 'draft'},
|
||||
{name: 'tag5', status: 'published'},
|
||||
{name: 'tag6', status: 'draft'},
|
||||
{name: 'tag7', status: 'published'},
|
||||
{name: 'tag8', status: 'published'},
|
||||
{name: 'tag9', status: 'draft'},
|
||||
{name: 'tag10', status: 'published'},
|
||||
]);
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create();
|
||||
await post.updateAssociations({
|
||||
tags: [
|
||||
{name: 'tag1', status: 'published'},
|
||||
{name: 'tag2', status: 'draft'},
|
||||
{name: 'tag3', status: 'published'},
|
||||
{name: 'tag4', status: 'draft'},
|
||||
{name: 'tag5', status: 'published'},
|
||||
{name: 'tag6', status: 'draft'},
|
||||
{name: 'tag7', status: 'published'},
|
||||
],
|
||||
const [post1, post2] = await Post.bulkCreate([{}, {}]);
|
||||
await post1.updateAssociations({
|
||||
tags: [1,2,3,4,5,6,7]
|
||||
});
|
||||
await post2.updateAssociations({
|
||||
tags: [2,5,8]
|
||||
});
|
||||
const User = db.getModel('users');
|
||||
const user = await User.create();
|
||||
await user.updateAssociations({
|
||||
posts: [post1]
|
||||
});
|
||||
});
|
||||
|
||||
@ -396,6 +610,13 @@ describe('list', () => {
|
||||
page: 2,
|
||||
per_page: 2
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(bug): SQL 报错
|
||||
it.skip('list2', async () => {
|
||||
const response = await agent
|
||||
.get(`/users/1/posts?fields=tags`);
|
||||
console.log(response.body);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,9 +4,33 @@ export default {
|
||||
name: 'profiles',
|
||||
tableName: 'actions__profiles',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'email',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'city',
|
||||
dataSource: [
|
||||
{ value: '1101', title: 'Beijing' },
|
||||
{ value: '3710', title: 'Weihai' },
|
||||
{ value: '5301', title: 'Kunming' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'jsonb',
|
||||
name: 'interest',
|
||||
defaultValue: [],
|
||||
multiple: true,
|
||||
dataSource: [
|
||||
{ value: 1, title: 'running' },
|
||||
{ value: 2, title: 'climbing' },
|
||||
{ value: 3, title: 'fishing' },
|
||||
]
|
||||
}
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -9,8 +9,17 @@ export default {
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasone',
|
||||
type: 'jsonb',
|
||||
name: 'nicknames',
|
||||
defaultValue: []
|
||||
},
|
||||
{
|
||||
type: 'hasOne',
|
||||
name: 'profile',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'posts'
|
||||
}
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -259,19 +259,14 @@ export async function create(ctx: Context, next: Next) {
|
||||
throw new Error(`${associatedName} associated model invalid`);
|
||||
}
|
||||
const { create } = resourceField.getAccessors();
|
||||
// @ts-ignore
|
||||
model = await associated[create](values, options);
|
||||
await model.updateAssociations(values, options);
|
||||
ctx.body = model;
|
||||
} else {
|
||||
const ResourceModel = ctx.db.getModel(resourceName);
|
||||
// @ts-ignore
|
||||
model = await ResourceModel.create(values, options);
|
||||
// @ts-ignore
|
||||
await model.updateAssociations(values, options);
|
||||
ctx.body = model;
|
||||
}
|
||||
await model.updateAssociations(values, options);
|
||||
await transaction.commit();
|
||||
ctx.body = model;
|
||||
await next();
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Select, Input, Space, Form, InputNumber, DatePicker } from 'antd';
|
||||
import { Button, Select, Input, Space, Form, InputNumber, DatePicker, TimePicker, Radio } from 'antd';
|
||||
import { PlusCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import useDynamicList from './useDynamicList';
|
||||
import { connect } from '@formily/react-schema-renderer'
|
||||
import { mapStyledProps } from '../shared'
|
||||
import get from 'lodash/get';
|
||||
import moment from 'moment';
|
||||
import './style.less';
|
||||
|
||||
export function FilterGroup(props: any) {
|
||||
const { showDeleteButton = false, fields = [], onDelete, onChange, onAdd, dataSource = {} } = props;
|
||||
const { showDeleteButton = true, fields = [], onDelete, onChange, onAdd, dataSource = {} } = props;
|
||||
const { list, getKey, push, remove, replace } = useDynamicList<any>(dataSource.list || [
|
||||
{
|
||||
type: 'item',
|
||||
@ -47,7 +49,7 @@ export function FilterGroup(props: any) {
|
||||
{<Component
|
||||
fields={fields}
|
||||
dataSource={item}
|
||||
showDeleteButton={list.length > 1}
|
||||
// showDeleteButton={list.length > 1}
|
||||
onChange={(value) => {
|
||||
replace(index, value);
|
||||
const newList = [...list];
|
||||
@ -101,7 +103,7 @@ export function FilterGroup(props: any) {
|
||||
>
|
||||
<PlusCircleOutlined /> 添加条件组
|
||||
</Button>
|
||||
{showDeleteButton && <Button style={{padding: 0, position: 'absolute', top: 0, right: 0, width: 32}} type={'link'} onClick={(e) => {
|
||||
{showDeleteButton && <Button className={'filter-remove-link filter-group'} style={{padding: 0, position: 'absolute', top: 0, right: 0, width: 32}} type={'link'} onClick={(e) => {
|
||||
onDelete && onDelete(e);
|
||||
}}>
|
||||
<CloseCircleOutlined />
|
||||
@ -126,59 +128,82 @@ interface FilterItemProps {
|
||||
|
||||
const OP_MAP = {
|
||||
string: [
|
||||
{label: '等于', value: 'eq'},
|
||||
{label: '不等于', value: 'neq'},
|
||||
{label: '包含', value: 'cont'},
|
||||
{label: '不包含', value: 'ncont'},
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '包含', value: '$includes'},
|
||||
{label: '不包含', value: '$notIncludes'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
],
|
||||
number: [
|
||||
{label: '等于', value: 'eq'},
|
||||
{label: '不等于', value: 'neq'},
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '大于', value: 'gt'},
|
||||
{label: '大于等于', value: 'gte'},
|
||||
{label: '小于', value: 'lt'},
|
||||
{label: '小于等于', value: 'lte'},
|
||||
{label: '介于', value: 'between'},
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
],
|
||||
file: [
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
{label: '存在', value: 'id.gt'},
|
||||
{label: '不存在', value: 'id.$null'},
|
||||
],
|
||||
boolean: [
|
||||
{label: '等于', value: 'eq'},
|
||||
{label: '是', value: '$isTruly', selected: true},
|
||||
{label: '否', value: '$isFalsy'},
|
||||
],
|
||||
choices: [
|
||||
{label: '等于', value: 'eq'},
|
||||
{label: '不等于', value: 'neq'},
|
||||
{label: '包含', value: 'cont'},
|
||||
{label: '不包含', value: 'ncont'},
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
select: [
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '包含', value: '$anyOf'},
|
||||
{label: '不包含', value: '$noneOf'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
],
|
||||
multipleSelect: [
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'ne'},
|
||||
{label: '包含', value: '$anyOf'},
|
||||
{label: '不包含', value: '$noneOf'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
],
|
||||
datetime: [
|
||||
{label: '等于', value: 'eq'},
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'neq'},
|
||||
{label: '大于', value: 'gt'},
|
||||
{label: '大于等于', value: 'gte'},
|
||||
{label: '小于', value: 'lt'},
|
||||
{label: '小于等于', value: 'lte'},
|
||||
{label: '介于', value: 'between'},
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
{label: '是今天', value: 'now'},
|
||||
{label: '在今天之前', value: 'before_today'},
|
||||
{label: '在今天之后', value: 'after_today'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
// {label: '是今天', value: 'now'},
|
||||
// {label: '在今天之前', value: 'before_today'},
|
||||
// {label: '在今天之后', value: 'after_today'},
|
||||
],
|
||||
linkTo: [
|
||||
{label: '包含', value: 'cont'},
|
||||
{label: '不包含', value: 'ncont'},
|
||||
{label: '非空', value: 'notnull'},
|
||||
{label: '为空', value: 'null'},
|
||||
time: [
|
||||
{label: '等于', value: 'eq', selected: true},
|
||||
{label: '不等于', value: 'neq'},
|
||||
{label: '大于', value: 'gt'},
|
||||
{label: '大于等于', value: 'gte'},
|
||||
{label: '小于', value: 'lt'},
|
||||
{label: '小于等于', value: 'lte'},
|
||||
{label: '介于', value: 'between'},
|
||||
{label: '非空', value: '$notNull'},
|
||||
{label: '为空', value: '$null'},
|
||||
// {label: '是今天', value: 'now'},
|
||||
// {label: '在今天之前', value: 'before_today'},
|
||||
// {label: '在今天之后', value: 'after_today'},
|
||||
],
|
||||
// linkTo: [
|
||||
// {label: '包含', value: 'cont'},
|
||||
// {label: '不包含', value: 'ncont'},
|
||||
// {label: '非空', value: '$notNull'},
|
||||
// {label: '为空', value: '$null'},
|
||||
// ],
|
||||
};
|
||||
|
||||
const op = {
|
||||
@ -188,6 +213,15 @@ const op = {
|
||||
percent: OP_MAP.number,
|
||||
datetime: OP_MAP.datetime,
|
||||
date: OP_MAP.datetime,
|
||||
time: OP_MAP.time,
|
||||
checkbox: OP_MAP.boolean,
|
||||
boolean: OP_MAP.boolean,
|
||||
select: OP_MAP.select,
|
||||
multipleSelect: OP_MAP.select,
|
||||
checkboxes: OP_MAP.select,
|
||||
radio: OP_MAP.select,
|
||||
upload: OP_MAP.file,
|
||||
attachment: OP_MAP.file,
|
||||
};
|
||||
|
||||
const StringInput = (props) => {
|
||||
@ -203,41 +237,101 @@ const controls = {
|
||||
string: StringInput,
|
||||
textarea: StringInput,
|
||||
number: InputNumber,
|
||||
// datetime: DatePicker,
|
||||
date: (props) => {
|
||||
const { value, onChange, ...restProps } = props;
|
||||
const m = moment(value, 'YYYY-MM-DD HH:mm:ss');
|
||||
return (
|
||||
<DatePicker value={m.isValid() ? m : null} onChange={(value) => {
|
||||
onChange(value ? value.format('YYYY-MM-DD HH:mm:ss') : null)
|
||||
console.log(value.format('YYYY-MM-DD HH:mm:ss'));
|
||||
}}/>
|
||||
);
|
||||
},
|
||||
percent: InputNumber,
|
||||
boolean: BooleanControl,
|
||||
checkbox: BooleanControl,
|
||||
select: OptionControl,
|
||||
radio: OptionControl,
|
||||
checkboxes: OptionControl,
|
||||
multipleSelect: OptionControl,
|
||||
time: TimePicker,
|
||||
date: DateControl,
|
||||
};
|
||||
|
||||
function DateControl(props: any) {
|
||||
const { value, onChange, ...restProps } = props;
|
||||
const m = moment(value, 'YYYY-MM-DD HH:mm:ss');
|
||||
return (
|
||||
<DatePicker value={m.isValid() ? m : null} onChange={(value) => {
|
||||
onChange(value ? value.format('YYYY-MM-DD HH:mm:ss') : null)
|
||||
console.log(value.format('YYYY-MM-DD HH:mm:ss'));
|
||||
}}/>
|
||||
);
|
||||
}
|
||||
|
||||
function TimeControl(props: any) {
|
||||
return
|
||||
}
|
||||
|
||||
function OptionControl(props) {
|
||||
const { multiple = true, op, options, value, onChange, ...restProps } = props;
|
||||
let mode: any = 'multiple';
|
||||
if (!multiple && ['eq', 'ne'].indexOf(op) !== -1) {
|
||||
mode = undefined;
|
||||
}
|
||||
return (
|
||||
<Select style={{ minWidth: 120 }} mode={mode} value={value} onChange={(value) => {
|
||||
onChange(value);
|
||||
}} options={options}>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function BooleanControl(props) {
|
||||
const { value, onChange, ...restProps } = props;
|
||||
return (
|
||||
<Radio.Group value={value} onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}>
|
||||
<Radio value={true}>是</Radio>
|
||||
<Radio value={false}>否</Radio>
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function NullControl(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function FilterItem(props: FilterItemProps) {
|
||||
const { index, fields = [], showDeleteButton = false, onDelete, onChange, dataSource = {} } = props;
|
||||
const { index, fields = [], showDeleteButton = true, onDelete, onChange } = props;
|
||||
const [type, setType] = useState('string');
|
||||
const [field, setField] = useState<any>({});
|
||||
const [dataSource, setDataSource] = useState(props.dataSource||{});
|
||||
useEffect(() => {
|
||||
const field = fields.find(field => field.name === dataSource.column);
|
||||
const field = fields.find(field => field.name === props.dataSource.column);
|
||||
if (field) {
|
||||
setField(field);
|
||||
setType(field.component.type);
|
||||
// console.log(dataSource);
|
||||
}
|
||||
setDataSource({...props.dataSource});
|
||||
// if (['boolean', 'checkbox'].indexOf(type) !== -1) {
|
||||
// onChange({...dataSource, op: undefined});
|
||||
// }
|
||||
}, [
|
||||
dataSource,
|
||||
props.dataSource, type,
|
||||
]);
|
||||
console.log({type});
|
||||
const ValueControl = controls[type]||controls.string;
|
||||
let ValueControl = controls[type]||controls.string;
|
||||
if (['$null', '$notNull', '$isTruly', '$isFalsy'].indexOf(dataSource.op) !== -1) {
|
||||
ValueControl = NullControl;
|
||||
}
|
||||
if (['boolean', 'checkbox'].indexOf(type) !== -1) {
|
||||
ValueControl = NullControl;
|
||||
}
|
||||
// let multiple = true;
|
||||
// if ()
|
||||
const opOptions = op[type]||op.string;
|
||||
console.log({field, dataSource, type, ValueControl});
|
||||
return (
|
||||
<Space>
|
||||
<Select value={dataSource.column}
|
||||
onChange={(value) => {
|
||||
const field = fields.find(field => field.name === value);
|
||||
if (field) {
|
||||
setType(field.interface);
|
||||
setType(field.component.type);
|
||||
}
|
||||
onChange({...dataSource, column: value});
|
||||
onChange({...dataSource, column: value, op: get(op, [field.component.type, 0, 'value']), value: undefined});
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
placeholder={'选择字段'}>
|
||||
@ -249,18 +343,20 @@ export function FilterItem(props: FilterItemProps) {
|
||||
onChange={(value) => {
|
||||
onChange({...dataSource, op: value});
|
||||
}}
|
||||
defaultValue={get(opOptions, [0, 'value'])}
|
||||
options={opOptions}
|
||||
>
|
||||
{(op[type]||op.string).map(option => (
|
||||
{/* {(op[type]||op.string).map(option => (
|
||||
<Select.Option value={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
))} */}
|
||||
</Select>
|
||||
<ValueControl value={dataSource.value} onChange={(value) => {
|
||||
<ValueControl multiple={type === 'checkboxes' || !!field.multiple} op={dataSource.op} options={field.dataSource} value={dataSource.value} onChange={(value) => {
|
||||
onChange({...dataSource, value: value});
|
||||
}}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
{showDeleteButton && (
|
||||
<Button type={'link'} style={{padding: 0}} onClick={(e) => {
|
||||
<Button className={'filter-remove-link filter-item'} type={'link'} style={{padding: 0}} onClick={(e) => {
|
||||
onDelete && onDelete(e);
|
||||
}}><CloseCircleOutlined /></Button>
|
||||
)}
|
||||
@ -270,12 +366,18 @@ export function FilterItem(props: FilterItemProps) {
|
||||
|
||||
function toFilter(values: any) {
|
||||
let filter: any;
|
||||
const { type, andor = 'and', list = [], column, op, value } = values;
|
||||
let { type, andor = 'and', list = [], column, op, value } = values;
|
||||
if (type === 'group') {
|
||||
filter = {
|
||||
[andor]: list.map(value => toFilter(value)).filter(Boolean)
|
||||
}
|
||||
} else if (type === 'item' && column && op) {
|
||||
if (['id.$null', 'id.$notNull', '$null', '$notNull', '$isTruly', '$isFalsy'].indexOf(op) !== -1) {
|
||||
value = true;
|
||||
}
|
||||
// if (op === 'id.gt') {
|
||||
// value = 0;
|
||||
// }
|
||||
filter = {
|
||||
[`${column}`]: {[op]: value},
|
||||
}
|
||||
@ -314,7 +416,7 @@ export const Filter = connect({
|
||||
};
|
||||
const { value, onChange, ...restProps } = props;
|
||||
console.log('valuevaluevaluevaluevaluevalue', value);
|
||||
return <FilterGroup dataSource={value ? toValues(value) : dataSource} onChange={(values) => {
|
||||
return <FilterGroup showDeleteButton={false} dataSource={value ? toValues(value) : dataSource} onChange={(values) => {
|
||||
console.log(values);
|
||||
onChange(toFilter(values));
|
||||
}} {...restProps}/>
|
||||
|
@ -0,0 +1,3 @@
|
||||
.filter-remove-link {
|
||||
color: inherit;
|
||||
}
|
@ -1222,13 +1222,6 @@ describe('belongsToMany', () => {
|
||||
});
|
||||
expect(await post.countTags()).toBe(1);
|
||||
});
|
||||
// TODO(question)
|
||||
it.skip('update with primaryKey (defined targetKey)', async () => {
|
||||
await post.updateAssociations({
|
||||
tags: tag2.id,
|
||||
});
|
||||
expect(await post.countTags()).toBe(1);
|
||||
});
|
||||
it('update with model', async () => {
|
||||
await post.updateAssociations({
|
||||
tags: [tag1, tag2],
|
||||
|
62
packages/database/src/__tests__/op.test.ts
Normal file
62
packages/database/src/__tests__/op.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { getDatabase } from '.';
|
||||
import Database, { Field } from '../';
|
||||
import Table from '../table';
|
||||
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = getDatabase();
|
||||
db.table({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'jsonb',
|
||||
name: 'arr',
|
||||
defaultValue: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('op', () => {
|
||||
it('test', async () => {
|
||||
const Test = db.getModel('tests');
|
||||
await Test.bulkCreate([
|
||||
{
|
||||
arr: ['aa', 'bb'],
|
||||
},
|
||||
{
|
||||
arr: ['bb', 'dd'],
|
||||
},
|
||||
{
|
||||
arr: ['cc', 'bb'],
|
||||
},
|
||||
{
|
||||
arr: ['dd'],
|
||||
}
|
||||
]);
|
||||
const options = Test.parseApiJson({
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
'arr.$anyOf': ['bb'],
|
||||
},
|
||||
{
|
||||
'arr.$noneOf': ['aa', 'cc'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const test = await Test.findOne(options);
|
||||
expect(test.get('arr')).toEqual(['bb', 'dd']);
|
||||
});
|
||||
});
|
@ -140,6 +140,119 @@ describe('utils.toWhere', () => {
|
||||
id: { [Op.between]: ['2020-11-01T00:00:00.000Z', '2020-12-01T00:00:00.000Z'] }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$null', () => {
|
||||
expect(toWhere({
|
||||
'id.$null': true
|
||||
})).toEqual({
|
||||
id: { [Op.is]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$null', () => {
|
||||
expect(toWhere({
|
||||
'id.$null': false
|
||||
})).toEqual({
|
||||
id: { [Op.is]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$null', () => {
|
||||
expect(toWhere({
|
||||
'id.$null': null
|
||||
})).toEqual({
|
||||
id: { [Op.is]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notNull', () => {
|
||||
expect(toWhere({
|
||||
'id.$notNull': true
|
||||
})).toEqual({
|
||||
id: { [Op.not]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notNull', () => {
|
||||
expect(toWhere({
|
||||
'id.$notNull': false
|
||||
})).toEqual({
|
||||
id: { [Op.not]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notNull', () => {
|
||||
expect(toWhere({
|
||||
'id.$notNull': null
|
||||
})).toEqual({
|
||||
id: { [Op.not]: null }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$includes', () => {
|
||||
expect(toWhere({
|
||||
'string.$includes': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.iLike]: '%a%' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notIncludes', () => {
|
||||
expect(toWhere({
|
||||
'string.$notIncludes': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.notILike]: '%a%' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$startsWith', () => {
|
||||
expect(toWhere({
|
||||
'string.$startsWith': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.iLike]: 'a%' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notStartsWith', () => {
|
||||
expect(toWhere({
|
||||
'string.$notStartsWith': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.notILike]: 'a%' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$endsWith', () => {
|
||||
expect(toWhere({
|
||||
'string.$endsWith': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.iLike]: '%a' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$notEndsWith', () => {
|
||||
expect(toWhere({
|
||||
'string.$notEndsWith': 'a'
|
||||
})).toEqual({
|
||||
string: { [Op.notILike]: '%a' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Op.$anyOf', () => {
|
||||
expect(toWhere({
|
||||
'array.$anyOf': ['a', 'b']
|
||||
})).toEqual({
|
||||
array: { [Op.or]: [{ [Op.contains]: 'a' }, { [Op.contains]: 'b' }] }
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(bug)
|
||||
it.skip('Op.$noneOf', () => {
|
||||
expect(toWhere({
|
||||
'array.$noneOf': ['a', 'b']
|
||||
})).toEqual({
|
||||
array: { [Op.not]: [{ [Op.contains]: 'a' }, { [Op.contains]: 'b' }] }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('group by logical operator', () => {
|
||||
@ -283,6 +396,37 @@ describe('utils.toWhere', () => {
|
||||
return expect(where);
|
||||
}
|
||||
|
||||
it('logical and other comparation', () => {
|
||||
toWhereExpect({
|
||||
or: [
|
||||
{ a: 1 },
|
||||
{ b: { gt: 2 } },
|
||||
{ and: [
|
||||
{
|
||||
'c.and': [
|
||||
{gt: 3, lt: 6}
|
||||
],
|
||||
},
|
||||
] },
|
||||
],
|
||||
})
|
||||
.toEqual({
|
||||
[Op.or]: [
|
||||
{ a: 1 },
|
||||
{ b: { [Op.gt]: 2 } },
|
||||
{[Op.and]: [
|
||||
{
|
||||
c: {
|
||||
[Op.and]: [
|
||||
{ [Op.gt]: 3, [Op.lt]: 6 }
|
||||
]
|
||||
},
|
||||
}
|
||||
]},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('with included association where', () => {
|
||||
toWhereExpect({
|
||||
col1: 'val1',
|
||||
@ -295,6 +439,7 @@ describe('utils.toWhere', () => {
|
||||
},
|
||||
user: {
|
||||
col1: 12,
|
||||
'col2.lt': 2,
|
||||
},
|
||||
},
|
||||
'posts.col3.ilike': 'aa',
|
||||
@ -313,7 +458,10 @@ describe('utils.toWhere', () => {
|
||||
},
|
||||
$__include: {
|
||||
user: {
|
||||
col1: 12
|
||||
col1: 12,
|
||||
col2: {
|
||||
[Op.lt]: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -248,6 +248,7 @@ export abstract class Model extends SequelizeModel {
|
||||
associations: this.associations,
|
||||
dialect: this.sequelize.getDialect(),
|
||||
ctx: context,
|
||||
database: this.database,
|
||||
});
|
||||
if (page || perPage) {
|
||||
data.limit = perPage === -1 ? MAX_LIMIT : Math.min(perPage || DEFAULT_LIMIT, MAX_LIMIT);
|
||||
|
86
packages/database/src/op.ts
Normal file
86
packages/database/src/op.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Op, Utils, Sequelize } from 'sequelize';
|
||||
|
||||
function toArray(value: any): any[] {
|
||||
if (value == null) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
const op = new Map<string, typeof Op | Function>();
|
||||
|
||||
// Sequelize 内置
|
||||
for (const key in Op) {
|
||||
op.set(key, Op[key]);
|
||||
const val = Utils.underscoredIf(key, true);
|
||||
op.set(val, Op[key]);
|
||||
op.set(val.replace(/_/g, ''), Op[key]);
|
||||
}
|
||||
|
||||
// 通用
|
||||
|
||||
// 是否为空:数据库意义的 null
|
||||
op.set('$null', () => ({ [Op.is]: null }));
|
||||
op.set('$notNull', () => ({ [Op.not]: null }));
|
||||
|
||||
op.set('$isTruly', () => ({
|
||||
[Op.eq]: true,
|
||||
}));
|
||||
op.set('$isFalsy', () => ({
|
||||
[Op.or]: [
|
||||
{
|
||||
[Op.eq]: false,
|
||||
},
|
||||
{
|
||||
[Op.is]: null,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// 字符串
|
||||
|
||||
// 包含:指对应字段的值包含某个子串
|
||||
op.set('$includes', (value: string) => ({ [Op.iLike]: `%${value}%` }));
|
||||
// 不包含:指对应字段的值不包含某个子串(慎用:性能问题)
|
||||
op.set('$notIncludes', (value: string) => ({ [Op.notILike]: `%${value}%` }));
|
||||
// 以之起始
|
||||
op.set('$startsWith', (value: string) => ({ [Op.iLike]: `${value}%` }));
|
||||
// 不以之起始
|
||||
op.set('$notStartsWith', (value: string) => ({ [Op.notILike]: `${value}%` }));
|
||||
// 以之结束
|
||||
op.set('$endsWith', (value: string) => ({ [Op.iLike]: `%${value}` }));
|
||||
// 不以之结束
|
||||
op.set('$notEndsWith', (value: string) => ({ [Op.notILike]: `%${value}` }));
|
||||
|
||||
// 多选(JSON)类型
|
||||
|
||||
// 包含组中任意值(命名来源:`Array.prototype.some`)
|
||||
op.set('$anyOf', (values: any[]) => ({
|
||||
[Op.or]: toArray(values).map(value => ({ [Op.contains]: value }))
|
||||
}));
|
||||
// 包含组中所有值
|
||||
op.set('$allOf', (values: any) => ({ [Op.contains]: toArray(values) }));
|
||||
// TODO(bug): 不包含组中任意值
|
||||
op.set('$noneOf', (values: any[], options) => {
|
||||
if (!values) {
|
||||
return Sequelize.literal('');
|
||||
}
|
||||
values = Array.isArray(values) ? values : [values];
|
||||
const { field, fieldPath } = options;
|
||||
const column = fieldPath.split('.').map(name => `"${name}"`).join('.');
|
||||
const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR ');
|
||||
console.log(sql);
|
||||
return Sequelize.literal(`not (${sql})`);
|
||||
});
|
||||
// 与组中值匹配
|
||||
op.set('$match', (values: any[]) => {
|
||||
const array = toArray(values);
|
||||
return {
|
||||
[Op.contains]: array,
|
||||
[Op.contained]: array
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default op;
|
@ -1,21 +1,16 @@
|
||||
import { Op, Utils, Sequelize } from 'sequelize';
|
||||
import { Utils, Sequelize, Op } from 'sequelize';
|
||||
import Model, { ModelCtor } from './model';
|
||||
import _ from 'lodash';
|
||||
|
||||
const op = new Map();
|
||||
|
||||
for (const key in Op) {
|
||||
op.set(key, Op[key]);
|
||||
const val = Utils.underscoredIf(key, true);
|
||||
op.set(val, Op[key]);
|
||||
op.set(val.replace(/_/g, ''), Op[key]);
|
||||
}
|
||||
import op from './op';
|
||||
import Database from './database';
|
||||
|
||||
interface ToWhereContext {
|
||||
model?: ModelCtor<Model> | Model | typeof Model;
|
||||
associations?: any;
|
||||
dialect?: string;
|
||||
database?: Database;
|
||||
ctx?: any;
|
||||
prefix?: any;
|
||||
}
|
||||
|
||||
export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||
@ -25,18 +20,20 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||
if (Array.isArray(options)) {
|
||||
return options.map((item) => toWhere(item, context));
|
||||
}
|
||||
const { model, associations = {}, ctx, dialect } = context;
|
||||
const { prefix, model, associations = {}, ctx, dialect } = context;
|
||||
const items = {};
|
||||
// 先处理「点号」的问题
|
||||
for (const key in options) {
|
||||
_.set(items, key, options[key]);
|
||||
}
|
||||
const values = {};
|
||||
let values = {};
|
||||
for (const key in items) {
|
||||
const childPreifx = prefix ? `${prefix}.${key}` : key;
|
||||
if (associations[key]) {
|
||||
values['$__include'] = values['$__include'] || {}
|
||||
values['$__include'][key] = toWhere(items[key], {
|
||||
...context,
|
||||
prefix: childPreifx,
|
||||
model: associations[key].target,
|
||||
associations: associations[key].target.associations,
|
||||
});
|
||||
@ -52,7 +49,42 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||
}
|
||||
else {
|
||||
// TODO: to fix same op key as field name
|
||||
values[op.has(key) ? op.get(key) : key] = toWhere(items[key], context);
|
||||
const opKey = op.get(key);
|
||||
let k;
|
||||
switch (typeof opKey) {
|
||||
case 'function':
|
||||
const name = model ? model.options.name.plural : '';
|
||||
const result = opKey(items[key], { model, fieldPath: name ? `${name}.${prefix}` : prefix });
|
||||
if (result.constructor.name === 'Literal') {
|
||||
values['$__literals'] = values['$__literals'] || [];
|
||||
values['$__literals'].push(result);
|
||||
} else {
|
||||
Object.assign(values, result);
|
||||
}
|
||||
// console.log(result.constructor.name === 'Literal');
|
||||
continue;
|
||||
case 'undefined':
|
||||
k = key;
|
||||
break;
|
||||
default:
|
||||
k = opKey;
|
||||
break;
|
||||
}
|
||||
values[k] = toWhere(items[key], {
|
||||
...context,
|
||||
prefix: op.has(key) ? prefix : childPreifx,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (values['$__literals']) {
|
||||
const $__literals = _.cloneDeep(values['$__literals']);
|
||||
delete values['$__literals'];
|
||||
console.log(Object.keys(values));
|
||||
return {
|
||||
[Op.and]: [
|
||||
...$__literals,
|
||||
values,
|
||||
],
|
||||
}
|
||||
}
|
||||
return values;
|
||||
@ -63,6 +95,7 @@ interface ToIncludeContext {
|
||||
sourceAlias?: string;
|
||||
associations?: any;
|
||||
dialect?: string;
|
||||
database?: Database;
|
||||
ctx?: any
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,9 @@ export function parseQuery(input: string): any {
|
||||
// 自带 query 处理的不太给力,需要用 qs 转一下
|
||||
const query = qs.parse(input, {
|
||||
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
||||
strictNullHandling: true
|
||||
strictNullHandling: true,
|
||||
// 逗号分隔转换为数组
|
||||
comma: true
|
||||
});
|
||||
// filter 支持 json string
|
||||
if (typeof query.filter === 'string') {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import qs from 'qs';
|
||||
import compose from 'koa-compose';
|
||||
import { pathToRegexp } from 'path-to-regexp';
|
||||
import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, ResourcerContext } from '@nocobase/resourcer';
|
||||
import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext } from '@nocobase/resourcer';
|
||||
import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database';
|
||||
|
||||
interface MiddlewareOptions extends KoaMiddlewareOptions {
|
||||
@ -95,14 +95,7 @@ export function middleware(options: MiddlewareOptions = {}) {
|
||||
ctx.action = resourcer.getAction(resourceName, params.actionName).clone();
|
||||
ctx.action.setContext(ctx);
|
||||
// 自带 query 处理的不太给力,需要用 qs 转一下
|
||||
const query = qs.parse(ctx.request.querystring, {
|
||||
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
||||
strictNullHandling: true
|
||||
});
|
||||
// filter 支持 json string
|
||||
if (typeof query.filter === 'string') {
|
||||
query.filter = JSON.parse(query.filter);
|
||||
}
|
||||
const query = parseQuery(ctx.request.querystring);
|
||||
// 兼容 ctx.params 的处理,之后的版本里会去掉
|
||||
ctx[paramsKey] = {
|
||||
table: params.resourceName,
|
||||
|
Loading…
Reference in New Issue
Block a user