mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:25:15 +00:00
fix: field filter logic for create/update (#34)
* fix: field filter logic for create/update * fix: add test cases
This commit is contained in:
parent
caa98f6d08
commit
7467441276
@ -9,7 +9,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
except: ['sort']
|
except: ['sort', 'user.profile', 'comments.status']
|
||||||
},
|
},
|
||||||
|
|
||||||
handler: create
|
handler: create
|
||||||
|
@ -40,9 +40,31 @@ describe('create', () => {
|
|||||||
.post('/posts:create1')
|
.post('/posts:create1')
|
||||||
.send({
|
.send({
|
||||||
title: 'title1',
|
title: 'title1',
|
||||||
sort: 100
|
sort: 100,
|
||||||
|
user: { name: 'aaa', profile: { email: 'email' } },
|
||||||
|
comments: [
|
||||||
|
{ content: 'comment1', status: 'published' },
|
||||||
|
{ content: 'comment2', status: 'draft' },
|
||||||
|
]
|
||||||
});
|
});
|
||||||
expect(response.body.sort).toBe(null);
|
expect(response.body.sort).toBe(null);
|
||||||
|
expect(response.body.user_id).toBe(1);
|
||||||
|
|
||||||
|
const postWithUser = await agent
|
||||||
|
.get(`/posts/${response.body.id}?fields=user`);
|
||||||
|
expect(postWithUser.body.user.id).toBe(1);
|
||||||
|
|
||||||
|
const user = await agent
|
||||||
|
.get(`/users/${postWithUser.body.user.id}?fields=profile`);
|
||||||
|
expect(user.body.profile).toBe(null);
|
||||||
|
|
||||||
|
const postWithComments = await agent
|
||||||
|
.get(`/posts/${response.body.id}?fields=comments`);
|
||||||
|
const comments = postWithComments.body.comments.map(({ content, status }) => ({ content, status }));
|
||||||
|
expect(comments).toEqual([
|
||||||
|
{ content: 'comment1', status: null },
|
||||||
|
{ content: 'comment2', status: null },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create with options.fields.only by custom action', async () => {
|
it('create with options.fields.only by custom action', async () => {
|
||||||
|
87
packages/actions/src/__tests__/utils.test.ts
Normal file
87
packages/actions/src/__tests__/utils.test.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { initDatabase, agent } from './index';
|
||||||
|
import { filterByFields } from '../utils';
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
describe('filterByFields', () => {
|
||||||
|
it('only fields', async () => {
|
||||||
|
const values = filterByFields({
|
||||||
|
title: 'title1',
|
||||||
|
sort: 100,
|
||||||
|
user: { name: 'aaa' }
|
||||||
|
}, ['title'])
|
||||||
|
expect(values).toEqual({
|
||||||
|
title: 'title1'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('except fields', async () => {
|
||||||
|
const values = filterByFields({
|
||||||
|
title: 'title1',
|
||||||
|
sort: 100,
|
||||||
|
user: { name: 'aaa', profile: { email: 'email' } }
|
||||||
|
}, {
|
||||||
|
except: ['sort', 'user.profile']
|
||||||
|
})
|
||||||
|
expect(values).toEqual({
|
||||||
|
title: 'title1',
|
||||||
|
user: { name: 'aaa' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only and except fields', async () => {
|
||||||
|
const values = filterByFields({
|
||||||
|
title: 'title1',
|
||||||
|
sort: 100,
|
||||||
|
user: { name: 'aaa', profile: { email: 'email' } }
|
||||||
|
}, {
|
||||||
|
only: ['user'],
|
||||||
|
except: ['sort', 'user.profile']
|
||||||
|
})
|
||||||
|
expect(values).toEqual({
|
||||||
|
user: { name: 'aaa' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only and except fields with array', async () => {
|
||||||
|
const values = filterByFields({
|
||||||
|
title: 'title1',
|
||||||
|
comments: [
|
||||||
|
{ content: 'comment1', status: 'published', sort: 1 },
|
||||||
|
{ content: 'comment2', status: 'draft', sort: 2 }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
only: ['comments'],
|
||||||
|
except: ['comments.status', 'comments.sort']
|
||||||
|
});
|
||||||
|
expect(values).toEqual({
|
||||||
|
comments: [
|
||||||
|
{ content: 'comment1' },
|
||||||
|
{ content: 'comment2' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only and except fields with array', async () => {
|
||||||
|
const values = filterByFields({
|
||||||
|
title: 'title1',
|
||||||
|
user: { name: 'aaa', profile: { email: 'email' } },
|
||||||
|
comments: [
|
||||||
|
{ content: 'comment1', status: 'published', sort: 1 },
|
||||||
|
{ content: 'comment2', status: 'draft', sort: 2 }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
only: ['comments.content'],
|
||||||
|
except: ['user.name']
|
||||||
|
});
|
||||||
|
expect(values).toEqual({
|
||||||
|
user: {
|
||||||
|
profile: { email: 'email' }
|
||||||
|
},
|
||||||
|
comments: [
|
||||||
|
{ content: 'comment1' },
|
||||||
|
{ content: 'comment2' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
export * as utils from './utils';
|
||||||
export * as actions from './actions';
|
export * as actions from './actions';
|
||||||
export * as middlewares from './middlewares';
|
export * as middlewares from './middlewares';
|
||||||
export default actions;
|
export default actions;
|
||||||
|
@ -6,5 +6,68 @@ export function filterByFields(data: any, fields: any = {}): any {
|
|||||||
except = []
|
except = []
|
||||||
} = fields;
|
} = fields;
|
||||||
|
|
||||||
return _.omit(only ? _.pick(data, only): data, except);
|
// ['user.profile.age', 'user.status', 'user', 'title', 'status']
|
||||||
|
// to
|
||||||
|
// {
|
||||||
|
// user: {
|
||||||
|
// profile: { age: true },
|
||||||
|
// status: true
|
||||||
|
// },
|
||||||
|
// title: true,
|
||||||
|
// status: true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
function makeMap(array: string[]) {
|
||||||
|
const map = {};
|
||||||
|
array.forEach(key => {
|
||||||
|
_.set(map, key, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyMap = only ? makeMap(only) : null;
|
||||||
|
const exceptMap = makeMap(except);
|
||||||
|
|
||||||
|
function filter(value, { onlyMap, exceptMap }: { onlyMap?: any, exceptMap?: any }) {
|
||||||
|
const isArray = Array.isArray(value);
|
||||||
|
const values = isArray ? value : [value];
|
||||||
|
|
||||||
|
const results = values.map(v => {
|
||||||
|
const result = {};
|
||||||
|
Object.keys(v).forEach(key => {
|
||||||
|
// 未定义 except 时继续判断 only
|
||||||
|
if (!exceptMap || typeof exceptMap[key] === 'undefined') {
|
||||||
|
if (onlyMap) {
|
||||||
|
if (typeof onlyMap[key] !== 'undefined') {
|
||||||
|
// 防止 fields 参数和传入类型不匹配时的报错
|
||||||
|
if (onlyMap[key] === true || typeof v[key] !== 'object') {
|
||||||
|
result[key] = v[key];
|
||||||
|
} else {
|
||||||
|
result[key] = filter(v[key], { onlyMap: onlyMap[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[key] = v[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 定义了 except 子级
|
||||||
|
if (typeof exceptMap[key] === 'object' && typeof v[key] === 'object') {
|
||||||
|
result[key] = filter(v[key], {
|
||||||
|
// onlyMap[key] === true 或不存在时,对子级只考虑 except
|
||||||
|
onlyMap: onlyMap && typeof onlyMap[key] === 'object' ? onlyMap[key] : null,
|
||||||
|
exceptMap: exceptMap[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 其他情况直接跳过
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isArray ? results : results[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter(data, { onlyMap, exceptMap });
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,15 @@ beforeAll(() => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
db.table({
|
||||||
|
name: 'bobs',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'bar'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
db.table({
|
db.table({
|
||||||
name: 'bars',
|
name: 'bars',
|
||||||
fields: [
|
fields: [
|
||||||
@ -38,6 +46,10 @@ beforeAll(() => {
|
|||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'hasOne',
|
||||||
|
name: 'bob'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
db.table({
|
db.table({
|
||||||
@ -114,6 +126,25 @@ describe('parseApiJson', () => {
|
|||||||
include: ['col']
|
include: ['col']
|
||||||
}});
|
}});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO(bug): 当遇到多层关联时,attributes 控制不正确
|
||||||
|
it.skip('assciation fields', () => {
|
||||||
|
expect(Foo.parseApiJson({
|
||||||
|
fields: ['bars.bob', 'bars'],
|
||||||
|
})).toEqual({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
association: 'bars',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
association: 'bob',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
distinct: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('filter and fields', () => {
|
it('filter and fields', () => {
|
||||||
const data = Foo.parseApiJson({
|
const data = Foo.parseApiJson({
|
||||||
|
@ -66,13 +66,47 @@ interface ToIncludeContext {
|
|||||||
ctx?: any
|
ctx?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toOrder(sort: string | string[], model: any): string[][] {
|
||||||
|
if (sort && typeof sort === 'string') {
|
||||||
|
sort = sort.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = [];
|
||||||
|
|
||||||
|
if (Array.isArray(sort) && sort.length > 0) {
|
||||||
|
sort.forEach(key => {
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
order.push(key);
|
||||||
|
} else {
|
||||||
|
const direction = key[0] === '-' ? 'DESC' : 'ASC';
|
||||||
|
const keys = key.replace(/^-/, '').split('.');
|
||||||
|
const field = keys.pop();
|
||||||
|
const by = [];
|
||||||
|
let associationModel = model;
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const association = model.associations[keys[i]];
|
||||||
|
if (association && association.target) {
|
||||||
|
associationModel = association.target;
|
||||||
|
by.push(associationModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order.push([...by, field, direction]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
export function toInclude(options: any, context: ToIncludeContext = {}) {
|
export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||||
function makeFields(key) {
|
function makeFields(key) {
|
||||||
if (!Array.isArray(items[key])) {
|
if (!Array.isArray(items[key])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items[key].forEach(field => {
|
items[key].forEach(field => {
|
||||||
|
// 按点分隔转化为数组
|
||||||
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
||||||
|
// 当前列
|
||||||
const col = arr.shift();
|
const col = arr.shift();
|
||||||
// 内嵌的情况
|
// 内嵌的情况
|
||||||
if (arr.length > 0) {
|
if (arr.length > 0) {
|
||||||
@ -111,13 +145,13 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fields = [] } = options;
|
const { fields = [], filter } = options;
|
||||||
const { model, sourceAlias, associations = {}, ctx, dialect } = context;
|
const { model, sourceAlias, associations = {}, ctx, dialect } = context;
|
||||||
|
|
||||||
let where = options.where || {};
|
let where = options.where || {};
|
||||||
|
|
||||||
if (options.filter) {
|
if (filter) {
|
||||||
where = toWhere(options.filter, {
|
where = toWhere(filter, {
|
||||||
model,
|
model,
|
||||||
associations,
|
associations,
|
||||||
ctx,
|
ctx,
|
||||||
@ -141,36 +175,6 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
|
|
||||||
const items = Array.isArray(fields) ? { only: fields } : fields;
|
const items = Array.isArray(fields) ? { only: fields } : fields;
|
||||||
items.appends = items.appends || [];
|
items.appends = items.appends || [];
|
||||||
|
|
||||||
let sort = options.sort;
|
|
||||||
|
|
||||||
if (sort && typeof sort === 'string') {
|
|
||||||
sort = sort.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
const order = [];
|
|
||||||
|
|
||||||
if (Array.isArray(sort) && sort.length > 0) {
|
|
||||||
sort.forEach(key => {
|
|
||||||
if (Array.isArray(key)) {
|
|
||||||
order.push(key);
|
|
||||||
} else {
|
|
||||||
const direction = key[0] === '-' ? 'DESC' : 'ASC';
|
|
||||||
const keys = key.replace(/^-/, '').split('.');
|
|
||||||
const field = keys.pop();
|
|
||||||
const by = [];
|
|
||||||
let associationModel = model;
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
const association = model.associations[keys[i]];
|
|
||||||
if (association && association.target) {
|
|
||||||
associationModel = association.target;
|
|
||||||
by.push(associationModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
order.push([...by, field, direction]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
makeFields('only');
|
makeFields('only');
|
||||||
makeFields('appends');
|
makeFields('appends');
|
||||||
@ -260,6 +264,8 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (include.size > 0) {
|
if (include.size > 0) {
|
||||||
|
// TODO(bug): 当遇到多层关联时,attributes 控制不正确
|
||||||
|
// ['user.profile.age', 'user.status', 'user', 'title', 'status']
|
||||||
if (!data.attributes) {
|
if (!data.attributes) {
|
||||||
data.attributes = [];
|
data.attributes = [];
|
||||||
}
|
}
|
||||||
@ -275,6 +281,8 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
data.scopes = scopes;
|
data.scopes = scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const order = toOrder(options.sort, model);
|
||||||
|
|
||||||
if (order.length > 0) {
|
if (order.length > 0) {
|
||||||
data.order = order;
|
data.order = order;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user