From 1bfe236fbaa8d9c69b34170aae16e0b5439547a4 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', '_id')({ taskIds: ['123', '456'], isArchived: true, updated: '2018-08-21T05:43:10.000Z' }) ``` 会得到 ``` [ {_id: '123', isArchived: true, updated: '2018-08-21T05:43:10.000Z'}, {_id: '456', isArchived: true, updated: '2018-08-21T05:43:10.000Z'} ] ``` 而在常见的 Observable response$ 上,则可以 ``` response$.map(normBulkUpdate('taskIds', '_id')) ``` 更多边界条件的行为定义,请见相应测试。 --- src/index.ts | 4 +- src/utils/httpclient.ts | 66 +++++++++++++++++++++++++++++++ src/utils/index.ts | 1 + src/utils/internalTypes.ts | 2 + test/app.ts | 1 + test/utils/httpclient.ts | 79 ++++++++++++++++++++++++++++++++++++++ test/utils/index.ts | 1 + 7 files changed, 152 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..dce46f8de --- /dev/null +++ b/src/utils/httpclient.ts @@ -0,0 +1,66 @@ +import { Omit } from './internalTypes' +import { SDKLogger } from './Logger' + +/** + * 从类型 T(如 `{ taskIds: TaskId[] }`)上获得给定字段 K + * (如 `taskIds`)对应的数组的元素类型(如 `TaskId`)。如果 + * `T[K]` 不是数组类型,则放弃类型推断,返回 any。 + */ +export type ArrayPropertyElement< + T, + K extends keyof T +> = 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< + T, + K extends keyof T, + U extends string +> = Array> & Omit> + +/** + * 将批量 PUT 的返回结果转变为可以直接被缓存层消费的数据。 + * 用法如:有 response$ 的元素形状为 + * `{ taskIds: TaskId[], isArchived: boolean, updated: string }` + * 则 `response$.map(normBulkUpdate('taskIds', '_id'))` 将推出元素形状为 + * `{ _id: TaskId, isArchived: boolean, updated: string }[]` + * 的数据。 + */ +export const normBulkUpdate = < + T, + K extends keyof T = keyof T, + U extends string = string +>( + responseIdsField: K, + entityIdField: U +) => ( + response: T +): NormBulkUpdateResult => { + if (response == null || typeof response !== 'object') { + return [] + } + const { [responseIdsField]: ids, ...rest } = response as any + return !ids + ? [] + : ids + .map((id: ArrayPropertyElement) => { + const currentValue = rest[entityIdField] + + if (currentValue == null || currentValue === id /* not likely */) { + return { ...rest, [entityIdField]: id } + } + + const incoming = `${entityIdField}-${id}` + const current = `${entityIdField}-${currentValue}` + SDKLogger.warn('normBulkUpdate:' + + ` specified key-value pair(${incoming})` + + ` conflicts with an existing one(${current}).)` + ) + return rest + }) +} 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..5a6726074 --- /dev/null +++ b/test/utils/httpclient.ts @@ -0,0 +1,79 @@ +import { expect } from 'chai' +import { it, describe } from 'tman' +import { TaskId } from 'teambition-types' +import { normBulkUpdate } from '../../src/utils' + +describe('httpclient utils spec', () => { + + type ResponsePayload = { + taskIds: TaskId[] + isArchived: boolean + updated: string + } + + it('normBoldUpdate should produce [] for undefined response', () => { + const norm = normBulkUpdate('taskIds', '_id') + expect(norm(undefined)).to.deep.equal([]) + expect(norm(null)).to.deep.equal([]) + expect(norm('success')).to.deep.equal([]) + }) + + it('normBulkUpdate should produce [] when responseIdsField is undefined', () => { + const norm = normBulkUpdate('_taskIds', '_id') + expect(norm({ + taskIds: ['123', '456'], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + })).to.deep.equal([]) + }) + + it('normBulkUpdate should produce [] when responseIdsField is empty', () => { + const norm = normBulkUpdate('taskIds', '_id') + expect(norm({ + taskIds: [], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + })).to.deep.equal([]) + }) + + it('normBulkUpdate should work when responseIdsField and entityIdField are the same', () => { + const norm = normBulkUpdate('_id', '_id') + expect(norm({ + _id: ['123' as TaskId, '456' as TaskId], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + })).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 should work when responseIdsField and entityIdField are different', () => { + const norm = normBulkUpdate('taskIds', '_id') + expect(norm({ + taskIds: ['123' as TaskId, '456' as TaskId], + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + })).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 should not overwrite when property conflict happends', () => { + const norm = normBulkUpdate( + 'taskIds', + 'cid' // 语义为 backbone 的 client id + ) + expect(norm({ + taskIds: ['123', '456'], + cid: '789', // 语义为 child id + isArchived: true, + updated: '2018-08-21T05:43:10.000Z' + })).to.deep.equal([ + { cid: '789', isArchived: true, updated: '2018-08-21T05:43:10.000Z' }, + { cid: '789', isArchived: true, updated: '2018-08-21T05:43:10.000Z' } + ]) + }) + +}) 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'