diff --git a/lib/permissions/minimally-encoded-raw-thread-info-validators.js b/lib/permissions/minimally-encoded-raw-thread-info-validators.js index 277421132..48ac03cae 100644 --- a/lib/permissions/minimally-encoded-raw-thread-info-validators.js +++ b/lib/permissions/minimally-encoded-raw-thread-info-validators.js @@ -1,109 +1,122 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { tHexEncodedPermissionsBitmask, tHexEncodedRolePermission, } from './minimally-encoded-thread-permissions.js'; import { specialRoleValidator } from './special-roles.js'; import type { MemberInfoWithPermissions, ThreadCurrentUserInfo, RawThreadInfo, RoleInfo, RoleInfoWithoutSpecialRole, RawThreadInfoWithoutSpecialRole, + MinimallyEncodedThickMemberInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; +import { threadSubscriptionValidator } from '../types/subscription-types.js'; import { type LegacyRawThreadInfo, legacyMemberInfoValidator, legacyRawThreadInfoValidator, legacyThreadCurrentUserInfoValidator, } from '../types/thread-types.js'; -import { tBool, tID, tShape } from '../utils/validation-utils.js'; +import { tBool, tID, tShape, tUserID } from '../utils/validation-utils.js'; const threadCurrentUserInfoValidator: TInterface = tShape({ ...legacyThreadCurrentUserInfoValidator.meta.props, minimallyEncoded: tBool(true), permissions: tHexEncodedPermissionsBitmask, }); const roleInfoValidatorBase = { id: tID, name: t.String, minimallyEncoded: tBool(true), permissions: t.list(tHexEncodedRolePermission), }; const roleInfoValidator: TInterface = tShape({ ...roleInfoValidatorBase, specialRole: t.maybe(specialRoleValidator), }); type RoleInfoPossiblyWithIsDefaultField = $ReadOnly<{ ...RoleInfo, +isDefault?: boolean, }>; // This validator is to be used in `convertClientDBThreadInfoToRawThreadInfo` // which validates the persisted JSON blob BEFORE any migrations are run. // `roleInfoValidator` will fail for persisted `RoleInfo`s that include // the `isDefault` field. Figured it made sense to create a separate validator // instead of adding complexity to `roleInfoValidator` which should maintain // 1:1 correspondance with the `RoleInfo` type. const persistedRoleInfoValidator: TInterface = tShape({ id: tID, name: t.String, minimallyEncoded: tBool(true), permissions: t.list(tHexEncodedRolePermission), specialRole: t.maybe(specialRoleValidator), isDefault: t.maybe(t.Boolean), }); const memberInfoWithPermissionsValidator: TInterface = tShape({ ...legacyMemberInfoValidator.meta.props, minimallyEncoded: tBool(true), permissions: tHexEncodedPermissionsBitmask, }); +const minimallyEncodedThickMemberInfoValidator: TInterface = + tShape({ + minimallyEncoded: tBool(true), + id: tUserID, + role: t.maybe(tID), + permissions: tHexEncodedPermissionsBitmask, + isSender: t.Boolean, + subscription: threadSubscriptionValidator, + }); + const rawThreadInfoValidator: TInterface = tShape( { ...legacyRawThreadInfoValidator.meta.props, minimallyEncoded: tBool(true), members: t.list(memberInfoWithPermissionsValidator), roles: t.dict(tID, roleInfoValidator), currentUser: threadCurrentUserInfoValidator, }, ); const roleInfoWithoutSpecialRolesValidator: TInterface = tShape({ ...roleInfoValidatorBase, isDefault: t.maybe(t.Boolean), }); const rawThreadInfoWithoutSpecialRoles: TInterface = tShape({ ...rawThreadInfoValidator.meta.props, roles: t.dict(tID, roleInfoWithoutSpecialRolesValidator), }); const mixedRawThreadInfoValidator: TUnion< LegacyRawThreadInfo | RawThreadInfo | RawThreadInfoWithoutSpecialRole, > = t.union([ legacyRawThreadInfoValidator, rawThreadInfoValidator, rawThreadInfoWithoutSpecialRoles, ]); export { memberInfoWithPermissionsValidator, + minimallyEncodedThickMemberInfoValidator, roleInfoValidator, persistedRoleInfoValidator, threadCurrentUserInfoValidator, rawThreadInfoValidator, mixedRawThreadInfoValidator, }; diff --git a/lib/utils/thread-ops-utils.js b/lib/utils/thread-ops-utils.js index 2385f204d..89a9461a5 100644 --- a/lib/utils/thread-ops-utils.js +++ b/lib/utils/thread-ops-utils.js @@ -1,169 +1,169 @@ // @flow import invariant from 'invariant'; import { memberInfoWithPermissionsValidator, persistedRoleInfoValidator, threadCurrentUserInfoValidator, + minimallyEncodedThickMemberInfoValidator, } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import type { RawThreadInfo, RoleInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { decodeMinimallyEncodedRawThreadInfo, minimallyEncodeMemberInfo, minimallyEncodeRoleInfo, minimallyEncodeThreadCurrentUserInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { assertThreadType, threadTypeIsThick, assertThinThreadType, assertThickThreadType, } from '../types/thread-types-enum.js'; import { type ClientDBThreadInfo, legacyMemberInfoValidator, type LegacyRawThreadInfo, clientLegacyRoleInfoValidator, legacyThreadCurrentUserInfoValidator, } from '../types/thread-types.js'; function convertRawThreadInfoToClientDBThreadInfo( rawThreadInfo: LegacyRawThreadInfo | RawThreadInfo, ): ClientDBThreadInfo { const { minimallyEncoded, thick, ...rest } = rawThreadInfo; return { ...rest, creationTime: rawThreadInfo.creationTime.toString(), members: JSON.stringify(rawThreadInfo.members), roles: JSON.stringify(rawThreadInfo.roles), currentUser: JSON.stringify(rawThreadInfo.currentUser), avatar: rawThreadInfo.avatar ? JSON.stringify(rawThreadInfo.avatar) : null, }; } function convertClientDBThreadInfoToRawThreadInfo( clientDBThreadInfo: ClientDBThreadInfo, ): RawThreadInfo { // 1. Validate and potentially minimally encode `rawMembers`. const rawMembers = JSON.parse(clientDBThreadInfo.members); const minimallyEncodedMembers = rawMembers.map(rawMember => { invariant( - // TODO these must be updated to accept new client-only change - // that subscription field may be present memberInfoWithPermissionsValidator.is(rawMember) || - legacyMemberInfoValidator.is(rawMember), + legacyMemberInfoValidator.is(rawMember) || + minimallyEncodedThickMemberInfoValidator.is(rawMember), 'rawMember must be valid [MinimallyEncoded/Legacy]MemberInfo', ); return rawMember.minimallyEncoded ? rawMember : minimallyEncodeMemberInfo(rawMember); }); // 2. Validate and potentially minimally encode `rawRoles`. const rawRoles = JSON.parse(clientDBThreadInfo.roles); const minimallyEncodedRoles: { +[id: string]: RoleInfo } = Object.keys( rawRoles, ).reduce((acc: { [string]: RoleInfo }, roleID: string) => { const roleInfo = rawRoles[roleID]; invariant( persistedRoleInfoValidator.is(roleInfo) || clientLegacyRoleInfoValidator.is(roleInfo), 'rawRole must be valid [MinimallyEncoded/Legacy]RoleInfo', ); acc[roleID] = roleInfo.minimallyEncoded ? roleInfo : minimallyEncodeRoleInfo(roleInfo); return acc; }, {}); // 3. Validate and potentially minimally encode `rawCurrentUser`. const rawCurrentUser = JSON.parse(clientDBThreadInfo.currentUser); invariant( threadCurrentUserInfoValidator.is(rawCurrentUser) || legacyThreadCurrentUserInfoValidator.is(rawCurrentUser), 'rawCurrentUser must be valid [MinimallyEncoded]ThreadCurrentUserInfo', ); const minimallyEncodedCurrentUser = rawCurrentUser.minimallyEncoded ? rawCurrentUser : minimallyEncodeThreadCurrentUserInfo(rawCurrentUser); let rawThreadInfo: RawThreadInfo; const threadType = assertThreadType(clientDBThreadInfo.type); if (threadTypeIsThick(threadType)) { const thickThreadType = assertThickThreadType(threadType); rawThreadInfo = { minimallyEncoded: true, thick: true, id: clientDBThreadInfo.id, type: thickThreadType, name: clientDBThreadInfo.name, description: clientDBThreadInfo.description, color: clientDBThreadInfo.color, creationTime: Number(clientDBThreadInfo.creationTime), parentThreadID: clientDBThreadInfo.parentThreadID, containingThreadID: clientDBThreadInfo.containingThreadID, community: clientDBThreadInfo.community, members: minimallyEncodedMembers, roles: minimallyEncodedRoles, currentUser: minimallyEncodedCurrentUser, repliesCount: clientDBThreadInfo.repliesCount, pinnedCount: clientDBThreadInfo.pinnedCount, }; } else { const thinThreadType = assertThinThreadType(threadType); rawThreadInfo = { minimallyEncoded: true, id: clientDBThreadInfo.id, type: thinThreadType, name: clientDBThreadInfo.name, description: clientDBThreadInfo.description, color: clientDBThreadInfo.color, creationTime: Number(clientDBThreadInfo.creationTime), parentThreadID: clientDBThreadInfo.parentThreadID, containingThreadID: clientDBThreadInfo.containingThreadID, community: clientDBThreadInfo.community, members: minimallyEncodedMembers, roles: minimallyEncodedRoles, currentUser: minimallyEncodedCurrentUser, repliesCount: clientDBThreadInfo.repliesCount, pinnedCount: clientDBThreadInfo.pinnedCount, }; } if (clientDBThreadInfo.sourceMessageID) { rawThreadInfo = { ...rawThreadInfo, sourceMessageID: clientDBThreadInfo.sourceMessageID, }; } if (clientDBThreadInfo.avatar) { rawThreadInfo = { ...rawThreadInfo, avatar: JSON.parse(clientDBThreadInfo.avatar), }; } return rawThreadInfo; } // WARNING: Do not consume or delete this function! // This function is being left in the codebase **SOLELY** to ensure that // previous `native` redux migrations continue to behave as expected. function deprecatedConvertClientDBThreadInfoToRawThreadInfo( clientDBThreadInfo: ClientDBThreadInfo, ): LegacyRawThreadInfo { const minimallyEncoded = convertClientDBThreadInfoToRawThreadInfo(clientDBThreadInfo); return decodeMinimallyEncodedRawThreadInfo(minimallyEncoded); } export { convertRawThreadInfoToClientDBThreadInfo, convertClientDBThreadInfoToRawThreadInfo, deprecatedConvertClientDBThreadInfoToRawThreadInfo, };