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: {
|
||||
except: ['sort']
|
||||
except: ['sort', 'user.profile', 'comments.status']
|
||||
},
|
||||
|
||||
handler: create
|
||||
|
@ -40,9 +40,31 @@ describe('create', () => {
|
||||
.post('/posts:create1')
|
||||
.send({
|
||||
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.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 () => {
|
||||
|
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';
|
||||
|
||||
export * as utils from './utils';
|
||||
export * as actions from './actions';
|
||||
export * as middlewares from './middlewares';
|
||||
export default actions;
|
||||
|
@ -6,5 +6,68 @@ export function filterByFields(data: any, fields: any = {}): any {
|
||||
except = []
|
||||
} = 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({
|
||||
name: 'bars',
|
||||
fields: [
|
||||
@ -38,6 +46,10 @@ beforeAll(() => {
|
||||
type: 'belongsTo',
|
||||
name: 'foo',
|
||||
},
|
||||
{
|
||||
type: 'hasOne',
|
||||
name: 'bob'
|
||||
}
|
||||
],
|
||||
});
|
||||
db.table({
|
||||
@ -114,6 +126,25 @@ describe('parseApiJson', () => {
|
||||
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', () => {
|
||||
const data = Foo.parseApiJson({
|
||||
|
@ -66,13 +66,47 @@ interface ToIncludeContext {
|
||||
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 = {}) {
|
||||
function makeFields(key) {
|
||||
if (!Array.isArray(items[key])) {
|
||||
return;
|
||||
}
|
||||
items[key].forEach(field => {
|
||||
// 按点分隔转化为数组
|
||||
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
||||
// 当前列
|
||||
const col = arr.shift();
|
||||
// 内嵌的情况
|
||||
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;
|
||||
|
||||
let where = options.where || {};
|
||||
|
||||
if (options.filter) {
|
||||
where = toWhere(options.filter, {
|
||||
if (filter) {
|
||||
where = toWhere(filter, {
|
||||
model,
|
||||
associations,
|
||||
ctx,
|
||||
@ -141,36 +175,6 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
|
||||
const items = Array.isArray(fields) ? { only: fields } : fields;
|
||||
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('appends');
|
||||
@ -260,6 +264,8 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
}
|
||||
|
||||
if (include.size > 0) {
|
||||
// TODO(bug): 当遇到多层关联时,attributes 控制不正确
|
||||
// ['user.profile.age', 'user.status', 'user', 'title', 'status']
|
||||
if (!data.attributes) {
|
||||
data.attributes = [];
|
||||
}
|
||||
@ -275,6 +281,8 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
data.scopes = scopes;
|
||||
}
|
||||
|
||||
const order = toOrder(options.sort, model);
|
||||
|
||||
if (order.length > 0) {
|
||||
data.order = order;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user