From 74674412763eccbeb0ff97a7f25796711e14d902 Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 8 Dec 2020 14:27:51 +0800 Subject: [PATCH] fix: field filter logic for create/update (#34) * fix: field filter logic for create/update * fix: add test cases --- .../actions/src/__tests__/actions/create1.ts | 2 +- packages/actions/src/__tests__/create.test.ts | 24 ++++- packages/actions/src/__tests__/utils.test.ts | 87 +++++++++++++++++++ packages/actions/src/index.ts | 1 + packages/actions/src/utils.ts | 65 +++++++++++++- .../src/__tests__/model/parseApiJson.test.ts | 33 ++++++- packages/database/src/utils.ts | 74 +++++++++------- 7 files changed, 249 insertions(+), 37 deletions(-) create mode 100644 packages/actions/src/__tests__/utils.test.ts diff --git a/packages/actions/src/__tests__/actions/create1.ts b/packages/actions/src/__tests__/actions/create1.ts index e9a3b25a2..e5b132cef 100644 --- a/packages/actions/src/__tests__/actions/create1.ts +++ b/packages/actions/src/__tests__/actions/create1.ts @@ -9,7 +9,7 @@ export default { }, fields: { - except: ['sort'] + except: ['sort', 'user.profile', 'comments.status'] }, handler: create diff --git a/packages/actions/src/__tests__/create.test.ts b/packages/actions/src/__tests__/create.test.ts index eb4dd4f40..559bf458a 100644 --- a/packages/actions/src/__tests__/create.test.ts +++ b/packages/actions/src/__tests__/create.test.ts @@ -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 () => { diff --git a/packages/actions/src/__tests__/utils.test.ts b/packages/actions/src/__tests__/utils.test.ts new file mode 100644 index 000000000..5d0dbf689 --- /dev/null +++ b/packages/actions/src/__tests__/utils.test.ts @@ -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' } + ] + }); + }); + }); +}); diff --git a/packages/actions/src/index.ts b/packages/actions/src/index.ts index 2855d163c..366bb2de9 100644 --- a/packages/actions/src/index.ts +++ b/packages/actions/src/index.ts @@ -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; diff --git a/packages/actions/src/utils.ts b/packages/actions/src/utils.ts index ef48f84e6..62f318bf2 100644 --- a/packages/actions/src/utils.ts +++ b/packages/actions/src/utils.ts @@ -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 }); } diff --git a/packages/database/src/__tests__/model/parseApiJson.test.ts b/packages/database/src/__tests__/model/parseApiJson.test.ts index da5db5e91..6de55d487 100644 --- a/packages/database/src/__tests__/model/parseApiJson.test.ts +++ b/packages/database/src/__tests__/model/parseApiJson.test.ts @@ -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({ diff --git a/packages/database/src/utils.ts b/packages/database/src/utils.ts index 1e9eeadf7..46caa5f74 100644 --- a/packages/database/src/utils.ts +++ b/packages/database/src/utils.ts @@ -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 = 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; }