From ea6652f88a5b63284f21a5868eb888e1cd58565b Mon Sep 17 00:00:00 2001 From: chuan6 Date: Wed, 22 Aug 2018 17:45:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(utils):=20=E6=B7=BB=E5=8A=A0=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E6=8D=AE=E8=BD=AC=E6=8D=A2=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=87=BD=E6=95=B0=20normBulkUpdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在批量接口使用 PUT 请求时,后端 response 上往往不会把所有更新的对象一 一罗列,而是以 `{ xxIds: Id[], ...updatedFields }` 的压缩形式呈现。这 样的数据是没办法直接更新到缓存层(RDB)的。 这里添加的 normBulkUpdate 函数可以用来将后端返回的这种压缩形式转换为缓 存层能接收的形式。如: ``` normBulkUpdate({ taskIds: ['123', '456'], isArchived: true, updated: '2018-08-21T05:43:10.000Z' }, 'taskIds', '_id') ``` 会得到 ``` [ {_id: '123', isArchived: true, updated: '2018-08-21T05:43:10.000Z'}, {_id: '456', isArchived: true, updated: '2018-08-21T05:43:10.000Z'} ] ``` 更多边界条件的行为定义,请见相应测试。 --- src/index.ts | 4 +-- src/utils/httpclient.ts | 56 +++++++++++++++++++++++++++++ src/utils/index.ts | 1 + src/utils/internalTypes.ts | 2 ++ test/app.ts | 1 + test/utils/httpclient.ts | 74 ++++++++++++++++++++++++++++++++++++++ test/utils/index.ts | 1 + 7 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/utils/httpclient.ts create mode 100644 test/utils/httpclient.ts create mode 100644 test/utils/index.ts diff --git a/src/index.ts b/src/index.ts index 96b8525dd..10de28bf2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ /// import 'tslib' -import { forEach, clone, uuid, concat, dropEle, hasMorePages, pagination, eventToRE } from './utils' +import { forEach, clone, uuid, concat, dropEle, hasMorePages, pagination, eventToRE, normBulkUpdate } from './utils' -export { hasMorePages, pagination } +export { hasMorePages, pagination, normBulkUpdate } export const Utils = { forEach, clone, uuid, concat, dropEle } export { eventParser } from './sockets/EventParser' diff --git a/src/utils/httpclient.ts b/src/utils/httpclient.ts new file mode 100644 index 000000000..f3c8be9e8 --- /dev/null +++ b/src/utils/httpclient.ts @@ -0,0 +1,56 @@ +import { Omit } from './internalTypes' + +/** + * 从类型 T(如 `{ _taskId: TaskId[] }`)上获得给定字段 K + * (如 `_taskId`)对应的数组的元素类型(如 `TaskId`)。如果 + * `T[K]` 不是数组类型,则放弃类型推断,返回 any。 + */ +export type ArrayPropertyElement = T[K] extends (infer U)[] + ? U + : any + +/** + * 生成的结果类型,替换了类型 T(如 `{ taskIds: TaskId[], isArchived: boolean }`) + * 上的字段 K(如 `taskIds`)为字段 S(如 `_id`),而字段 S + * 对应的值类型则是字段 K 对应的数组值的元素类型(如 `TaskId`)。 + * 如果 `T[K]` 不是数组类型,则字段 S(如 `_id`)的类型将是 any。 + */ +export type NormBulkUpdateResult = Array< + Record> & Omit +> + +/** + * 将批量 PUT 的返回结果转变为可以直接被缓存层消费的数据。 + * 用法如:有 response$ 的元素形状为 + * `{ taskIds: TaskId[], isArchived: boolean, updated: string }` + * 则调用 `normBulkUpdate(response$, 'taskIds', '_id')` 将推出元素形状为 + * `{ _id: TaskId, isArchived: boolean, updated: string }[]` + * 的数据。 + */ +export function normBulkUpdate< + T extends {}, + K extends keyof T, + U extends string +>( + response: T, + responseIdsField: K, + entityIdField: U +): NormBulkUpdateResult { + const { [responseIdsField]: ids, ...rest } = response as any + + return ids + ? (ids as Array>).map((id) => { + const existingValue = rest[entityIdField] + if (existingValue != null && existingValue !== id) { + throw new Error( + `normBulkUpdate: specified key-value pair(${entityIdField}-${id})` + + ` is conflicting to an existing one(${entityIdField}-${existingValue}).)` + ) + } + return { + ...rest, + [entityIdField]: id + } + }) + : [] +} diff --git a/src/utils/index.ts b/src/utils/index.ts index dfe59e77e..44003f7dc 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './helper' export * from './internalTypes' export { createProxy } from './proxy' export { eventToRegexp as eventToRE } from './eventToRegexp' +export * from './httpclient' diff --git a/src/utils/internalTypes.ts b/src/utils/internalTypes.ts index a5da2075c..27d4894bd 100644 --- a/src/utils/internalTypes.ts +++ b/src/utils/internalTypes.ts @@ -18,6 +18,8 @@ export type Dict = { [key: string]: T } +export type Omit = Pick> + export type TableInfo = { tabName: string, pkName: string diff --git a/test/app.ts b/test/app.ts index 0cf16af35..6fe98e8ee 100644 --- a/test/app.ts +++ b/test/app.ts @@ -13,3 +13,4 @@ import './mock' import './apis' import './sockets' import './net' +import './utils/index' diff --git a/test/utils/httpclient.ts b/test/utils/httpclient.ts new file mode 100644 index 000000000..f50b39bb3 --- /dev/null +++ b/test/utils/httpclient.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai' +import { it, describe } from 'tman' +import { TaskId } from 'teambition-types' +import { normBulkUpdate } from '../../src/utils' + +describe('httpclient utils spec', () => { + it(`${normBulkUpdate.name} should produce [] when responseIdsField is undefined`, () => { + expect(normBulkUpdate( + { + taskIds: ['123', '456'], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + } as any, + '_taskIds', + '_id' + )).to.deep.equal([]) + }) + + it(`${normBulkUpdate.name} should produce [] when responseIdsField is empty`, () => { + expect(normBulkUpdate( + { + taskIds: [], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + }, + 'taskIds', + '_id' + )).to.deep.equal([]) + }) + + it(`${normBulkUpdate.name} should work when responseIdsField and entityIdField are the same`, () => { + expect(normBulkUpdate( + { + _id: ['123' as TaskId, '456' as TaskId], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + }, + '_id', + '_id' + )).to.deep.equal([ + { _id: '123', isArchived: true, updated: '2018-08-21T05:43:10.000Z' }, + { _id: '456', isArchived: true, updated: '2018-08-21T05:43:10.000Z' } + ]) + }) + + it(`${normBulkUpdate.name} should work when responseIdsField and entityIdField are different`, () => { + expect(normBulkUpdate( + { + taskIds: ['123' as TaskId, '456' as TaskId], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + }, + 'taskIds', + '_id' + )).to.deep.equal([ + { _id: '123', isArchived: true, updated: '2018-08-21T05:43:10.000Z' }, + { _id: '456', isArchived: true, updated: '2018-08-21T05:43:10.000Z' } + ]) + }) + + it(`${normBulkUpdate.name} should throw when property conflict happends`, () => { + expect(() => normBulkUpdate( + { + taskIds: ['123', '456'], + cid: '789', // 语义为 child id + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + }, + 'taskIds', + 'cid' // 语义为 backbone 的 client id + )).to.throw() + }) + +}) diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 000000000..00442d7b4 --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1 @@ +import './httpclient'