diff --git a/lib/utils/conversion-utils.js b/lib/utils/conversion-utils.js index 19c001ed5..dc24bae69 100644 --- a/lib/utils/conversion-utils.js +++ b/lib/utils/conversion-utils.js @@ -1,145 +1,161 @@ // @flow import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import type { TInterface, TType } from 'tcomb'; import { convertIDToNewSchema } from './migration-utils.js'; -import { assertWithValidator, tID } from './validation-utils.js'; +import { assertWithValidator, tID, tUserID } from './validation-utils.js'; import { getPendingThreadID, parsePendingThreadID, } from '../shared/thread-utils.js'; function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = (id: string) => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } return convertIDToNewSchema(id, serverPrefixID); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = (id: string) => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } const pendingIDContents = parsePendingThreadID(id); if (!pendingIDContents) { throw new Error('invalid_client_id_prefix'); } if (!pendingIDContents.sourceMessageID) { return id; } return getPendingThreadID( pendingIDContents.threadType, pendingIDContents.memberIDs, pendingIDContents.sourceMessageID.substr(prefix.length), ); }; return convertObject(outputValidator, data, [tID], conversionFunction); } +function extractUserIDsFromPayload( + outputValidator: TType, + data: T, +): $ReadOnlyArray { + const result = new Set(); + const conversionFunction = (id: string) => { + result.add(id); + return id; + }; + + convertObject(outputValidator, data, [tUserID], conversionFunction); + + return [...result]; +} + function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result: { [string]: mixed } = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } export { convertClientIDsToServerIDs, convertServerIDsToClientIDs, + extractUserIDsFromPayload, convertObject, }; diff --git a/lib/utils/conversion-utils.test.js b/lib/utils/conversion-utils.test.js index b518d8cf2..7d9418242 100644 --- a/lib/utils/conversion-utils.test.js +++ b/lib/utils/conversion-utils.test.js @@ -1,86 +1,121 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import { + extractUserIDsFromPayload, convertServerIDsToClientIDs, convertClientIDsToServerIDs, } from './conversion-utils.js'; import { tShape, tID, idSchemaRegex } from './validation-utils.js'; +import { fetchMessageInfosResponseValidator } from '../types/validators/message-validators.js'; type ComplexType = { +ids: { +[string]: $ReadOnlyArray } }; describe('id conversion', () => { it('should convert string id', () => { const validator = tShape<{ +id: string }>({ id: tID }); const serverData = { id: '1' }; const clientData = { id: '0|1' }; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); it('should convert a complex type', () => { const validator = tShape({ ids: t.dict(tID, t.list(tID)) }); const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } }; const clientData = { ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] }, }; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); it('should convert a refinement', () => { const validator = t.refinement(tID, () => true); const serverData = '1'; const clientData = '0|1'; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); }); describe('idSchemaRegex tests', () => { it('should capture ids', () => { const regex = new RegExp(`^(${idSchemaRegex})$`); const ids = ['123|123', '0|0', '123', '0']; for (const id of ids) { const result = regex.exec(id); expect(result).not.toBeNull(); invariant(result, 'result is not null'); const matches = [...result]; expect(matches).toHaveLength(2); expect(matches[1]).toBe(id); } }); }); describe('Pending ids tests', () => { it('should convert pending ids', () => { const validator = t.list(tID); const serverData = ['pending/sidebar/1', 'pending/type4/1+2+3']; const clientData = ['pending/sidebar/0|1', 'pending/type4/1+2+3']; expect( convertServerIDsToClientIDs('0', validator, serverData), ).toStrictEqual(clientData); expect( convertClientIDsToServerIDs('0', validator, clientData), ).toStrictEqual(serverData); }); }); + +describe('extractUserIDsFromPayload', () => { + it('should extract all user ids from payload', () => { + const payload = { + rawMessageInfos: [ + { + type: 0, + threadID: '1000', + creatorID: '0', + time: 0, + text: 'test', + id: '2000', + }, + { + type: 0, + threadID: '1000', + creatorID: '1', + time: 0, + text: 'test', + id: '2001', + }, + ], + truncationStatuses: {}, + userInfos: { + ['100']: { id: '100', username: 'test1' }, + ['200']: { id: '200', username: 'test2' }, + }, + }; + expect( + extractUserIDsFromPayload(fetchMessageInfosResponseValidator, payload), + ).toEqual(['0', '1', '100', '200']); + }); +});