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() {
|
function getTestKey() {
|
||||||
const { id } = require.main;
|
const { id } = require.main;
|
||||||
const key = id
|
const key = id
|
||||||
.replace(__dirname, '')
|
.replace(`${process.env.PWD}/packages`, '')
|
||||||
|
.replace(/src\/__tests__/g, '')
|
||||||
.replace('.test.ts', '')
|
.replace('.test.ts', '')
|
||||||
.replace(/[^\w]/g, '_');
|
.replace(/[^\w]/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '');
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +80,10 @@ resourcer.define({
|
|||||||
name: 'users',
|
name: 'users',
|
||||||
actions: actions.common,
|
actions: actions.common,
|
||||||
});
|
});
|
||||||
|
resourcer.define({
|
||||||
|
name: 'profiles',
|
||||||
|
actions: actions.common,
|
||||||
|
});
|
||||||
resourcer.define({
|
resourcer.define({
|
||||||
type: 'hasOne',
|
type: 'hasOne',
|
||||||
name: 'users.profile',
|
name: 'users.profile',
|
||||||
@ -128,7 +135,7 @@ export async function initDatabase() {
|
|||||||
const options = requireModule(file);
|
const options = requireModule(file);
|
||||||
database.table(typeof options === 'function' ? options(database) : {
|
database.table(typeof options === 'function' ? options(database) : {
|
||||||
...options,
|
...options,
|
||||||
tableName: `${options.tableName}_${key}`
|
tableName: `${key}_${options.tableName}`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await database.sync({
|
await database.sync({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Op } from 'sequelize';
|
import { literal, Op } from 'sequelize';
|
||||||
|
|
||||||
import { initDatabase, agent } from './index';
|
import { initDatabase, agent } from './index';
|
||||||
|
|
||||||
@ -23,11 +23,14 @@ describe('list', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const User = db.getModel('users');
|
const User = db.getModel('users');
|
||||||
await User.bulkCreate([
|
await User.bulkCreate([
|
||||||
{ name: 'a', ...timestamps },
|
{ name: 'a', ...timestamps, nicknames: ['aa', 'aaa'] },
|
||||||
{ name: 'b', ...timestamps },
|
{ name: 'b', ...timestamps, nicknames: [] },
|
||||||
{ name: 'c', ...timestamps }
|
{ name: 'c', ...timestamps }
|
||||||
]);
|
]);
|
||||||
const users = await User.findAll();
|
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');
|
const Post = db.getModel('posts');
|
||||||
await Post.bulkCreate(Array(25).fill(null).map((_, index) => ({
|
await Post.bulkCreate(Array(25).fill(null).map((_, index) => ({
|
||||||
@ -140,7 +143,184 @@ describe('list', () => {
|
|||||||
expect(response.body.count).toBe(1);
|
expect(response.body.count).toBe(1);
|
||||||
expect(response.body.rows[0].id).toBe(2);
|
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', () => {
|
describe('page', () => {
|
||||||
@ -277,7 +457,8 @@ describe('list', () => {
|
|||||||
it('appends fields', async () => {
|
it('appends fields', async () => {
|
||||||
const response = await agent.get('/posts?fields[only]=title&fields[appends]=user.name&filter[title]=title0');
|
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[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)
|
// TODO(bug)
|
||||||
it.skip('get posts of user with comments', async () => {
|
it.skip('get posts of user with comments', async () => {
|
||||||
const response = await agent
|
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({
|
expect(response.body).toEqual({
|
||||||
count: 1,
|
count: 1,
|
||||||
page: 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', () => {
|
describe('belongsToMany', () => {
|
||||||
beforeEach(async () => {
|
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 = db.getModel('posts');
|
||||||
const post = await Post.create();
|
const [post1, post2] = await Post.bulkCreate([{}, {}]);
|
||||||
await post.updateAssociations({
|
await post1.updateAssociations({
|
||||||
tags: [
|
tags: [1,2,3,4,5,6,7]
|
||||||
{name: 'tag1', status: 'published'},
|
});
|
||||||
{name: 'tag2', status: 'draft'},
|
await post2.updateAssociations({
|
||||||
{name: 'tag3', status: 'published'},
|
tags: [2,5,8]
|
||||||
{name: 'tag4', status: 'draft'},
|
});
|
||||||
{name: 'tag5', status: 'published'},
|
const User = db.getModel('users');
|
||||||
{name: 'tag6', status: 'draft'},
|
const user = await User.create();
|
||||||
{name: 'tag7', status: 'published'},
|
await user.updateAssociations({
|
||||||
],
|
posts: [post1]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -396,6 +610,13 @@ describe('list', () => {
|
|||||||
page: 2,
|
page: 2,
|
||||||
per_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',
|
name: 'profiles',
|
||||||
tableName: 'actions__profiles',
|
tableName: 'actions__profiles',
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'email',
|
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;
|
} as TableOptions;
|
||||||
|
@ -9,8 +9,17 @@ export default {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasone',
|
type: 'jsonb',
|
||||||
|
name: 'nicknames',
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasOne',
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'posts'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
} as TableOptions;
|
} as TableOptions;
|
||||||
|
@ -259,19 +259,14 @@ export async function create(ctx: Context, next: Next) {
|
|||||||
throw new Error(`${associatedName} associated model invalid`);
|
throw new Error(`${associatedName} associated model invalid`);
|
||||||
}
|
}
|
||||||
const { create } = resourceField.getAccessors();
|
const { create } = resourceField.getAccessors();
|
||||||
// @ts-ignore
|
|
||||||
model = await associated[create](values, options);
|
model = await associated[create](values, options);
|
||||||
await model.updateAssociations(values, options);
|
|
||||||
ctx.body = model;
|
|
||||||
} else {
|
} else {
|
||||||
const ResourceModel = ctx.db.getModel(resourceName);
|
const ResourceModel = ctx.db.getModel(resourceName);
|
||||||
// @ts-ignore
|
|
||||||
model = await ResourceModel.create(values, options);
|
model = await ResourceModel.create(values, options);
|
||||||
// @ts-ignore
|
|
||||||
await model.updateAssociations(values, options);
|
|
||||||
ctx.body = model;
|
|
||||||
}
|
}
|
||||||
|
await model.updateAssociations(values, options);
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
ctx.body = model;
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { PlusCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||||
import useDynamicList from './useDynamicList';
|
import useDynamicList from './useDynamicList';
|
||||||
import { connect } from '@formily/react-schema-renderer'
|
import { connect } from '@formily/react-schema-renderer'
|
||||||
import { mapStyledProps } from '../shared'
|
import { mapStyledProps } from '../shared'
|
||||||
|
import get from 'lodash/get';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
export function FilterGroup(props: any) {
|
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 || [
|
const { list, getKey, push, remove, replace } = useDynamicList<any>(dataSource.list || [
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@ -47,7 +49,7 @@ export function FilterGroup(props: any) {
|
|||||||
{<Component
|
{<Component
|
||||||
fields={fields}
|
fields={fields}
|
||||||
dataSource={item}
|
dataSource={item}
|
||||||
showDeleteButton={list.length > 1}
|
// showDeleteButton={list.length > 1}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
replace(index, value);
|
replace(index, value);
|
||||||
const newList = [...list];
|
const newList = [...list];
|
||||||
@ -101,7 +103,7 @@ export function FilterGroup(props: any) {
|
|||||||
>
|
>
|
||||||
<PlusCircleOutlined /> 添加条件组
|
<PlusCircleOutlined /> 添加条件组
|
||||||
</Button>
|
</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);
|
onDelete && onDelete(e);
|
||||||
}}>
|
}}>
|
||||||
<CloseCircleOutlined />
|
<CloseCircleOutlined />
|
||||||
@ -126,59 +128,82 @@ interface FilterItemProps {
|
|||||||
|
|
||||||
const OP_MAP = {
|
const OP_MAP = {
|
||||||
string: [
|
string: [
|
||||||
{label: '等于', value: 'eq'},
|
{label: '等于', value: 'eq', selected: true},
|
||||||
{label: '不等于', value: 'neq'},
|
{label: '不等于', value: 'ne'},
|
||||||
{label: '包含', value: 'cont'},
|
{label: '包含', value: '$includes'},
|
||||||
{label: '不包含', value: 'ncont'},
|
{label: '不包含', value: '$notIncludes'},
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '非空', value: '$notNull'},
|
||||||
{label: '为空', value: 'null'},
|
{label: '为空', value: '$null'},
|
||||||
],
|
],
|
||||||
number: [
|
number: [
|
||||||
{label: '等于', value: 'eq'},
|
{label: '等于', value: 'eq', selected: true},
|
||||||
{label: '不等于', value: 'neq'},
|
{label: '不等于', value: 'ne'},
|
||||||
{label: '大于', value: 'gt'},
|
{label: '大于', value: 'gt'},
|
||||||
{label: '大于等于', value: 'gte'},
|
{label: '大于等于', value: 'gte'},
|
||||||
{label: '小于', value: 'lt'},
|
{label: '小于', value: 'lt'},
|
||||||
{label: '小于等于', value: 'lte'},
|
{label: '小于等于', value: 'lte'},
|
||||||
{label: '介于', value: 'between'},
|
{label: '介于', value: 'between'},
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '非空', value: '$notNull'},
|
||||||
{label: '为空', value: 'null'},
|
{label: '为空', value: '$null'},
|
||||||
],
|
],
|
||||||
file: [
|
file: [
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '存在', value: 'id.gt'},
|
||||||
{label: '为空', value: 'null'},
|
{label: '不存在', value: 'id.$null'},
|
||||||
],
|
],
|
||||||
boolean: [
|
boolean: [
|
||||||
{label: '等于', value: 'eq'},
|
{label: '是', value: '$isTruly', selected: true},
|
||||||
|
{label: '否', value: '$isFalsy'},
|
||||||
],
|
],
|
||||||
choices: [
|
select: [
|
||||||
{label: '等于', value: 'eq'},
|
{label: '等于', value: 'eq', selected: true},
|
||||||
{label: '不等于', value: 'neq'},
|
{label: '不等于', value: 'ne'},
|
||||||
{label: '包含', value: 'cont'},
|
{label: '包含', value: '$anyOf'},
|
||||||
{label: '不包含', value: 'ncont'},
|
{label: '不包含', value: '$noneOf'},
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '非空', value: '$notNull'},
|
||||||
{label: '为空', value: 'null'},
|
{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: [
|
datetime: [
|
||||||
{label: '等于', value: 'eq'},
|
{label: '等于', value: 'eq', selected: true},
|
||||||
{label: '不等于', value: 'neq'},
|
{label: '不等于', value: 'neq'},
|
||||||
{label: '大于', value: 'gt'},
|
{label: '大于', value: 'gt'},
|
||||||
{label: '大于等于', value: 'gte'},
|
{label: '大于等于', value: 'gte'},
|
||||||
{label: '小于', value: 'lt'},
|
{label: '小于', value: 'lt'},
|
||||||
{label: '小于等于', value: 'lte'},
|
{label: '小于等于', value: 'lte'},
|
||||||
{label: '介于', value: 'between'},
|
{label: '介于', value: 'between'},
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '非空', value: '$notNull'},
|
||||||
{label: '为空', value: 'null'},
|
{label: '为空', value: '$null'},
|
||||||
{label: '是今天', value: 'now'},
|
// {label: '是今天', value: 'now'},
|
||||||
{label: '在今天之前', value: 'before_today'},
|
// {label: '在今天之前', value: 'before_today'},
|
||||||
{label: '在今天之后', value: 'after_today'},
|
// {label: '在今天之后', value: 'after_today'},
|
||||||
],
|
],
|
||||||
linkTo: [
|
time: [
|
||||||
{label: '包含', value: 'cont'},
|
{label: '等于', value: 'eq', selected: true},
|
||||||
{label: '不包含', value: 'ncont'},
|
{label: '不等于', value: 'neq'},
|
||||||
{label: '非空', value: 'notnull'},
|
{label: '大于', value: 'gt'},
|
||||||
{label: '为空', value: 'null'},
|
{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 = {
|
const op = {
|
||||||
@ -188,6 +213,15 @@ const op = {
|
|||||||
percent: OP_MAP.number,
|
percent: OP_MAP.number,
|
||||||
datetime: OP_MAP.datetime,
|
datetime: OP_MAP.datetime,
|
||||||
date: 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) => {
|
const StringInput = (props) => {
|
||||||
@ -203,41 +237,101 @@ const controls = {
|
|||||||
string: StringInput,
|
string: StringInput,
|
||||||
textarea: StringInput,
|
textarea: StringInput,
|
||||||
number: InputNumber,
|
number: InputNumber,
|
||||||
// datetime: DatePicker,
|
percent: InputNumber,
|
||||||
date: (props) => {
|
boolean: BooleanControl,
|
||||||
const { value, onChange, ...restProps } = props;
|
checkbox: BooleanControl,
|
||||||
const m = moment(value, 'YYYY-MM-DD HH:mm:ss');
|
select: OptionControl,
|
||||||
return (
|
radio: OptionControl,
|
||||||
<DatePicker value={m.isValid() ? m : null} onChange={(value) => {
|
checkboxes: OptionControl,
|
||||||
onChange(value ? value.format('YYYY-MM-DD HH:mm:ss') : null)
|
multipleSelect: OptionControl,
|
||||||
console.log(value.format('YYYY-MM-DD HH:mm:ss'));
|
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) {
|
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 [type, setType] = useState('string');
|
||||||
|
const [field, setField] = useState<any>({});
|
||||||
|
const [dataSource, setDataSource] = useState(props.dataSource||{});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const field = fields.find(field => field.name === dataSource.column);
|
const field = fields.find(field => field.name === props.dataSource.column);
|
||||||
if (field) {
|
if (field) {
|
||||||
|
setField(field);
|
||||||
setType(field.component.type);
|
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});
|
let ValueControl = controls[type]||controls.string;
|
||||||
const 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 (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Select value={dataSource.column}
|
<Select value={dataSource.column}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const field = fields.find(field => field.name === value);
|
const field = fields.find(field => field.name === value);
|
||||||
if (field) {
|
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 }}
|
style={{ width: 120 }}
|
||||||
placeholder={'选择字段'}>
|
placeholder={'选择字段'}>
|
||||||
@ -249,18 +343,20 @@ export function FilterItem(props: FilterItemProps) {
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onChange({...dataSource, op: 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.Option value={option.value}>{option.label}</Select.Option>
|
||||||
))}
|
))} */}
|
||||||
</Select>
|
</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});
|
onChange({...dataSource, value: value});
|
||||||
}}
|
}}
|
||||||
style={{ width: 180 }}
|
style={{ width: 180 }}
|
||||||
/>
|
/>
|
||||||
{showDeleteButton && (
|
{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);
|
onDelete && onDelete(e);
|
||||||
}}><CloseCircleOutlined /></Button>
|
}}><CloseCircleOutlined /></Button>
|
||||||
)}
|
)}
|
||||||
@ -270,12 +366,18 @@ export function FilterItem(props: FilterItemProps) {
|
|||||||
|
|
||||||
function toFilter(values: any) {
|
function toFilter(values: any) {
|
||||||
let filter: 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') {
|
if (type === 'group') {
|
||||||
filter = {
|
filter = {
|
||||||
[andor]: list.map(value => toFilter(value)).filter(Boolean)
|
[andor]: list.map(value => toFilter(value)).filter(Boolean)
|
||||||
}
|
}
|
||||||
} else if (type === 'item' && column && op) {
|
} 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 = {
|
filter = {
|
||||||
[`${column}`]: {[op]: value},
|
[`${column}`]: {[op]: value},
|
||||||
}
|
}
|
||||||
@ -314,7 +416,7 @@ export const Filter = connect({
|
|||||||
};
|
};
|
||||||
const { value, onChange, ...restProps } = props;
|
const { value, onChange, ...restProps } = props;
|
||||||
console.log('valuevaluevaluevaluevaluevalue', value);
|
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);
|
console.log(values);
|
||||||
onChange(toFilter(values));
|
onChange(toFilter(values));
|
||||||
}} {...restProps}/>
|
}} {...restProps}/>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.filter-remove-link {
|
||||||
|
color: inherit;
|
||||||
|
}
|
@ -1222,13 +1222,6 @@ describe('belongsToMany', () => {
|
|||||||
});
|
});
|
||||||
expect(await post.countTags()).toBe(1);
|
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 () => {
|
it('update with model', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: [tag1, tag2],
|
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'] }
|
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', () => {
|
describe('group by logical operator', () => {
|
||||||
@ -283,6 +396,37 @@ describe('utils.toWhere', () => {
|
|||||||
return expect(where);
|
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', () => {
|
it('with included association where', () => {
|
||||||
toWhereExpect({
|
toWhereExpect({
|
||||||
col1: 'val1',
|
col1: 'val1',
|
||||||
@ -295,6 +439,7 @@ describe('utils.toWhere', () => {
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
col1: 12,
|
col1: 12,
|
||||||
|
'col2.lt': 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'posts.col3.ilike': 'aa',
|
'posts.col3.ilike': 'aa',
|
||||||
@ -313,7 +458,10 @@ describe('utils.toWhere', () => {
|
|||||||
},
|
},
|
||||||
$__include: {
|
$__include: {
|
||||||
user: {
|
user: {
|
||||||
col1: 12
|
col1: 12,
|
||||||
|
col2: {
|
||||||
|
[Op.lt]: 2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -248,6 +248,7 @@ export abstract class Model extends SequelizeModel {
|
|||||||
associations: this.associations,
|
associations: this.associations,
|
||||||
dialect: this.sequelize.getDialect(),
|
dialect: this.sequelize.getDialect(),
|
||||||
ctx: context,
|
ctx: context,
|
||||||
|
database: this.database,
|
||||||
});
|
});
|
||||||
if (page || perPage) {
|
if (page || perPage) {
|
||||||
data.limit = perPage === -1 ? MAX_LIMIT : Math.min(perPage || DEFAULT_LIMIT, MAX_LIMIT);
|
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 Model, { ModelCtor } from './model';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import op from './op';
|
||||||
const op = new Map();
|
import Database from './database';
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToWhereContext {
|
interface ToWhereContext {
|
||||||
model?: ModelCtor<Model> | Model | typeof Model;
|
model?: ModelCtor<Model> | Model | typeof Model;
|
||||||
associations?: any;
|
associations?: any;
|
||||||
dialect?: string;
|
dialect?: string;
|
||||||
|
database?: Database;
|
||||||
ctx?: any;
|
ctx?: any;
|
||||||
|
prefix?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toWhere(options: any, context: ToWhereContext = {}) {
|
export function toWhere(options: any, context: ToWhereContext = {}) {
|
||||||
@ -25,18 +20,20 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
|||||||
if (Array.isArray(options)) {
|
if (Array.isArray(options)) {
|
||||||
return options.map((item) => toWhere(item, context));
|
return options.map((item) => toWhere(item, context));
|
||||||
}
|
}
|
||||||
const { model, associations = {}, ctx, dialect } = context;
|
const { prefix, model, associations = {}, ctx, dialect } = context;
|
||||||
const items = {};
|
const items = {};
|
||||||
// 先处理「点号」的问题
|
// 先处理「点号」的问题
|
||||||
for (const key in options) {
|
for (const key in options) {
|
||||||
_.set(items, key, options[key]);
|
_.set(items, key, options[key]);
|
||||||
}
|
}
|
||||||
const values = {};
|
let values = {};
|
||||||
for (const key in items) {
|
for (const key in items) {
|
||||||
|
const childPreifx = prefix ? `${prefix}.${key}` : key;
|
||||||
if (associations[key]) {
|
if (associations[key]) {
|
||||||
values['$__include'] = values['$__include'] || {}
|
values['$__include'] = values['$__include'] || {}
|
||||||
values['$__include'][key] = toWhere(items[key], {
|
values['$__include'][key] = toWhere(items[key], {
|
||||||
...context,
|
...context,
|
||||||
|
prefix: childPreifx,
|
||||||
model: associations[key].target,
|
model: associations[key].target,
|
||||||
associations: associations[key].target.associations,
|
associations: associations[key].target.associations,
|
||||||
});
|
});
|
||||||
@ -52,7 +49,42 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TODO: to fix same op key as field name
|
// 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;
|
return values;
|
||||||
@ -63,6 +95,7 @@ interface ToIncludeContext {
|
|||||||
sourceAlias?: string;
|
sourceAlias?: string;
|
||||||
associations?: any;
|
associations?: any;
|
||||||
dialect?: string;
|
dialect?: string;
|
||||||
|
database?: Database;
|
||||||
ctx?: any
|
ctx?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,9 @@ export function parseQuery(input: string): any {
|
|||||||
// 自带 query 处理的不太给力,需要用 qs 转一下
|
// 自带 query 处理的不太给力,需要用 qs 转一下
|
||||||
const query = qs.parse(input, {
|
const query = qs.parse(input, {
|
||||||
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
||||||
strictNullHandling: true
|
strictNullHandling: true,
|
||||||
|
// 逗号分隔转换为数组
|
||||||
|
comma: true
|
||||||
});
|
});
|
||||||
// filter 支持 json string
|
// filter 支持 json string
|
||||||
if (typeof query.filter === 'string') {
|
if (typeof query.filter === 'string') {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
import { pathToRegexp } from 'path-to-regexp';
|
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';
|
import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database';
|
||||||
|
|
||||||
interface MiddlewareOptions extends KoaMiddlewareOptions {
|
interface MiddlewareOptions extends KoaMiddlewareOptions {
|
||||||
@ -95,14 +95,7 @@ export function middleware(options: MiddlewareOptions = {}) {
|
|||||||
ctx.action = resourcer.getAction(resourceName, params.actionName).clone();
|
ctx.action = resourcer.getAction(resourceName, params.actionName).clone();
|
||||||
ctx.action.setContext(ctx);
|
ctx.action.setContext(ctx);
|
||||||
// 自带 query 处理的不太给力,需要用 qs 转一下
|
// 自带 query 处理的不太给力,需要用 qs 转一下
|
||||||
const query = qs.parse(ctx.request.querystring, {
|
const query = parseQuery(ctx.request.querystring);
|
||||||
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
|
||||||
strictNullHandling: true
|
|
||||||
});
|
|
||||||
// filter 支持 json string
|
|
||||||
if (typeof query.filter === 'string') {
|
|
||||||
query.filter = JSON.parse(query.filter);
|
|
||||||
}
|
|
||||||
// 兼容 ctx.params 的处理,之后的版本里会去掉
|
// 兼容 ctx.params 的处理,之后的版本里会去掉
|
||||||
ctx[paramsKey] = {
|
ctx[paramsKey] = {
|
||||||
table: params.resourceName,
|
table: params.resourceName,
|
||||||
|
Loading…
Reference in New Issue
Block a user