From 75d7bf5ed4fbdc5aecf5f0f00a1335933b3010fb Mon Sep 17 00:00:00 2001 From: zkao Date: Sat, 29 Apr 2023 02:08:11 -0600 Subject: [PATCH] Add more typing for history objects (#277) --- src/db/ChangeLogType.ts | 16 ++++++++++---- src/db/edit/streamListener.ts | 22 +++++++++++--------- src/graphql/history/HistoryFieldResolvers.ts | 21 ++++++++++--------- src/graphql/history/HistoryQueries.ts | 4 +++- src/model/ChangeLogDataSource.ts | 2 +- src/utils/helpers.ts | 16 ++++++++++++++ 6 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/db/ChangeLogType.ts b/src/db/ChangeLogType.ts index 49a495b1..8a57b894 100644 --- a/src/db/ChangeLogType.ts +++ b/src/db/ChangeLogType.ts @@ -6,6 +6,11 @@ import { ClimbEditOperationType, ClimbType } from './ClimbTypes.js' import { OperationType as OrganizationOpType, OrganizationType } from './OrganizationTypes.js' export type DBOperation = 'insert' | 'update' | 'delete' +export enum DocumentKind { + areas = 'areas', + climbs = 'climbs', + organizations = 'organizations' +} export interface ChangeLogType { _id: mongose.Types.ObjectId @@ -14,7 +19,7 @@ export interface ChangeLogType { changes: Array> } -// DIY since ResumeToke is defined as unknown in mongo TS +// DIY since ResumeToken is defined as unknown in mongo TS export interface ResumeToken { _data: string } @@ -29,7 +34,7 @@ export interface BaseChangeRecordType @@ -56,7 +61,10 @@ export type AreaChangeRecordType = BaseChangeRecordType export type ClimbChangeLogType = ChangeLogType export type OrganizationChangeLogType = ChangeLogType -export type SupportedCollectionTypes = AreaType & WithDiscriminator | ClimbType & WithDiscriminator +export type SupportedCollectionTypes = + | AreaType & WithDiscriminator + | ClimbType & WithDiscriminator + | OrganizationType & WithDiscriminator export interface GetHistoryInputFilterType { uuidList: string[] diff --git a/src/db/edit/streamListener.ts b/src/db/edit/streamListener.ts index a8223c39..5991d3fa 100644 --- a/src/db/edit/streamListener.ts +++ b/src/db/edit/streamListener.ts @@ -4,10 +4,11 @@ import dot from 'dot-object' import { changelogDataSource } from '../../model/ChangeLogDataSource.js' import { logger } from '../../logger.js' -import { BaseChangeRecordType, ResumeToken, UpdateDescription, DBOperation, SupportedCollectionTypes } from '../ChangeLogType.js' +import { BaseChangeRecordType, ResumeToken, UpdateDescription, DBOperation, SupportedCollectionTypes, DocumentKind } from '../ChangeLogType.js' import { checkVar } from '../index.js' import { updateAreaIndex } from '../export/Typesense/Client.js' import { AreaType } from '../AreaTypes.js' +import { exhaustiveCheck } from '../../utils/helpers.js' /** * Start a new stream listener to track changes @@ -49,7 +50,7 @@ const onChange = (change: ChangeStreamDocument): void => { case 'replace': case 'update': { let dbOp: DBOperation = 'update' - const source = change.ns.coll + const source = DocumentKind[change.ns.coll] const { fullDocument, _id, updateDescription } = change as ChangeStreamUpdateDocument if (fullDocument?._deleting != null) { dbOp = 'delete' @@ -60,7 +61,7 @@ const onChange = (change: ChangeStreamDocument): void => { } case 'insert': { const dbOp = 'insert' - const source = change.ns.coll + const source = DocumentKind[change.ns.coll] const { fullDocument, _id } = change void recordChange({ _id: _id as ResumeToken, source, fullDocument: fullDocument as SupportedCollectionTypes, dbOp }) break @@ -70,7 +71,7 @@ const onChange = (change: ChangeStreamDocument): void => { interface ChangeRecordType { _id: ResumeToken - source: string + source: DocumentKind fullDocument: SupportedCollectionTypes updateDescription?: any dbOp: DBOperation @@ -79,41 +80,42 @@ interface ChangeRecordType { const recordChange = async ({ source, dbOp, fullDocument, updateDescription, _id }: ChangeRecordType): Promise => { fullDocument.kind = source switch (source) { - case 'climbs': { + case DocumentKind.climbs: { const newDocument: BaseChangeRecordType = { _id, dbOp, fullDocument, updateDescription: dotifyUpdateDescription(updateDescription), - kind: 'climbs' + kind: DocumentKind.climbs } void changelogDataSource.record(newDocument) break } - case 'areas': { + case DocumentKind.areas: { const newDocument: BaseChangeRecordType = { _id, dbOp, fullDocument, updateDescription: dotifyUpdateDescription(updateDescription), - kind: 'areas' + kind: DocumentKind.areas } void changelogDataSource.record(newDocument) void updateAreaIndex(fullDocument as AreaType, dbOp) break } - case 'organizations': { + case DocumentKind.organizations: { const newDocument: BaseChangeRecordType = { _id, dbOp, fullDocument, updateDescription: dotifyUpdateDescription(updateDescription), - kind: 'organizations' + kind: DocumentKind.organizations } void changelogDataSource.record(newDocument) break } default: + exhaustiveCheck(source) } } diff --git a/src/graphql/history/HistoryFieldResolvers.ts b/src/graphql/history/HistoryFieldResolvers.ts index a21949d5..f036c247 100644 --- a/src/graphql/history/HistoryFieldResolvers.ts +++ b/src/graphql/history/HistoryFieldResolvers.ts @@ -1,4 +1,5 @@ -import { ChangeLogType, BaseChangeRecordType, SupportedCollectionTypes } from '../../db/ChangeLogType.js' +import { ChangeLogType, BaseChangeRecordType, SupportedCollectionTypes, DocumentKind } from '../../db/ChangeLogType.js' +import { exhaustiveCheck } from '../../utils/helpers.js' /** * Customize to resolve individual fields @@ -24,16 +25,16 @@ const resolvers = { Document: { __resolveType (node: SupportedCollectionTypes) { - if (node.kind === 'areas') { - return 'Area' + switch (node.kind) { + case DocumentKind.areas: + return 'Area' + case DocumentKind.climbs: + return 'Climb' + case DocumentKind.organizations: + return 'Organization' + default: + return exhaustiveCheck(node.kind) } - if (node.kind === 'climbs') { - return 'Climb' - } - if (node.kind === 'organizations') { - return 'Organization' - } - return null } } } diff --git a/src/graphql/history/HistoryQueries.ts b/src/graphql/history/HistoryQueries.ts index 4af61fa4..5ea9a5e3 100644 --- a/src/graphql/history/HistoryQueries.ts +++ b/src/graphql/history/HistoryQueries.ts @@ -11,8 +11,10 @@ const HistoryQueries = { getChangeHistory: async (_, { filter }, { dataSources }): Promise => { const { history }: DataSourcesType = dataSources const { uuidList }: GetHistoryInputFilterType = filter ?? {} + // Note: userUuid, fromDate, toDate filters don't currently work. + // Note: though we pull uuidList, we don't use it either. - // convert array of uuid in string to UUID[] + // Convert array of uuid in string to UUID[] const muidList = uuidList?.map(entry => muid.from(entry)) ?? [] return await history.getChangeSets(muidList) }, diff --git a/src/model/ChangeLogDataSource.ts b/src/model/ChangeLogDataSource.ts index f8d6af11..7b8f2344 100644 --- a/src/model/ChangeLogDataSource.ts +++ b/src/model/ChangeLogDataSource.ts @@ -69,7 +69,7 @@ export default class ChangeLogDataSource extends MongoDataSource * @param uuidList optional filter * @returns change sets */ - async getChangeSets (uuidList: MUUID[]): Promise> { + async getChangeSets (uuidList: MUUID[]): Promise> { const rs = await this.changeLogModel.aggregate([ { $sort: { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 6a2e4ede..0e112bd8 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -22,3 +22,19 @@ export const isMuuidHexStr = (s: string): boolean => { const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ return regex.test(s) } + +/** + * Ensures that type-checking errors out if enums are not + * handlded exhaustively in switch statements. + * Eg. + * switch(val) { + * case enumOne: + * ... + * default: + * exhaustiveCheck(val) + * } + * @param _value + */ +export function exhaustiveCheck (_value: never): never { + throw new Error(`ERROR! Enum not handled for ${JSON.stringify(_value)}`) +}