Skip to content

Commit

Permalink
chore: refactor ReflectionGroup to SDL pattern (#9807)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <matt.krick@gmail.com>
  • Loading branch information
mattkrick authored Jun 6, 2024
1 parent b683dc8 commit 695646c
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 147 deletions.
1 change: 0 additions & 1 deletion packages/client/mutations/AutogroupMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ graphql`
reflectionGroups {
id
title
smartTitle
reflections {
id
plaintextContent
Expand Down
3 changes: 1 addition & 2 deletions packages/client/mutations/EndDraggingReflectionMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ const EndDraggingReflectionMutation: SimpleMutation<TEndDraggingReflectionMutati
meetingId: reflection.getValue('meetingId') as string,
isActive: true,
sortOrder: 0,
updatedAt: nowISO,
voterIds: []
updatedAt: nowISO
}
reflectionGroupProxy = createProxyRecord(store, 'RetroReflectionGroup', reflectionGroup)
updateProxyRecord(reflection, {sortOrder: 0, reflectionGroupId: newReflectionGroupId})
Expand Down
1 change: 0 additions & 1 deletion packages/client/mutations/ResetReflectionGroupsMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ graphql`
id
title
promptId
smartTitle
reflections {
id
plaintextContent
Expand Down
7 changes: 3 additions & 4 deletions packages/server/graphql/composeResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
This file accepts resolvers and permissions and applies permissions as higher order functions to those resolvers
*/
import {defaultFieldResolver} from 'graphql'
import {allow} from 'graphql-shield'
import type {ShieldRule} from 'graphql-shield/dist/types'
import hash from 'object-hash'
Expand Down Expand Up @@ -79,10 +80,8 @@ const composeResolvers = <T extends ResolverMap>(resolverMap: T, permissionMap:
nextResolverFieldMap[resolverFieldName] = wrapResolve(resolve as Resolver, rule)
})
} else {
const unwrappedResolver = nextResolverFieldMap[fieldName]
if (!unwrappedResolver) {
throw new Error(`No resolver exists for field: ${fieldName}`)
}
// use default if a resolver isn't provided, e.g. a field exists in the DB but only available to superusers via GQL
const unwrappedResolver = nextResolverFieldMap[fieldName] || defaultFieldResolver
nextResolverFieldMap[fieldName] = wrapResolve(unwrappedResolver, rule)
}
})
Expand Down
4 changes: 4 additions & 0 deletions packages/server/graphql/public/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ const permissionMap: PermissionMap<Resolvers> = {
isOrgTier<'Organization.saml'>('source.id', 'enterprise')
)
},
RetroReflectionGroup: {
smartTitle: isSuperUser,
voterIds: isSuperUser
},
User: {
domains: or(isSuperUser, isUserViewer)
}
Expand Down
38 changes: 37 additions & 1 deletion packages/server/graphql/public/types/RetroReflectionGroup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
import {Selectable} from 'kysely'
import MeetingRetrospective from '../../../database/types/MeetingRetrospective'
import Reflection from '../../../database/types/Reflection'
import {RetroReflectionGroup as TRetroReflectionGroup} from '../../../postgres/pg'
import {getUserId} from '../../../utils/authorization'
import {RetroReflectionGroupResolvers} from '../resolverTypes'

export interface RetroReflectionGroupSource extends Selectable<TRetroReflectionGroup> {}

const RetroReflectionGroup: RetroReflectionGroupResolvers = {}
const RetroReflectionGroup: RetroReflectionGroupResolvers = {
meeting: async ({meetingId}, _args, {dataLoader}) => {
const retroMeeting = await dataLoader.get('newMeetings').load(meetingId)
return retroMeeting as MeetingRetrospective
},
prompt: ({promptId}, _args, {dataLoader}) => {
return dataLoader.get('reflectPrompts').load(promptId)
},
reflections: async ({id: reflectionGroupId, meetingId}, _args, {dataLoader}) => {
// use meetingId so we only hit the DB once instead of once per group
const reflections = await dataLoader.get('retroReflectionsByMeetingId').load(meetingId)
const filteredReflections = reflections.filter(
(reflection: Reflection) => reflection.reflectionGroupId === reflectionGroupId
)
filteredReflections.sort((a: Reflection, b: Reflection) => (a.sortOrder < b.sortOrder ? 1 : -1))
return filteredReflections
},
team: async ({meetingId}, _args, {dataLoader}) => {
const meeting = await dataLoader.get('newMeetings').load(meetingId)
return dataLoader.get('teams').loadNonNull(meeting.teamId)
},
titleIsUserDefined: ({title, smartTitle}) => {
return title ? title !== smartTitle : false
},
voteCount: ({voterIds}) => {
return voterIds ? voterIds.length : 0
},
viewerVoteCount: ({voterIds}, _args, {authToken}) => {
const viewerId = getUserId(authToken)
return voterIds
? voterIds.reduce((sum, voterId) => (voterId === viewerId ? sum + 1 : sum), 0)
: 0
}
}

export default RetroReflectionGroup
140 changes: 2 additions & 138 deletions packages/server/graphql/types/RetroReflectionGroup.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,9 @@
import {
GraphQLBoolean,
GraphQLFloat,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString
} from 'graphql'
import Reflection from '../../database/types/Reflection'
import {getUserId} from '../../utils/authorization'
import {GraphQLObjectType} from 'graphql'
import {GQLContext} from '../graphql'
import {resolveForSU} from '../resolvers'
import CommentorDetails from './CommentorDetails'
import GraphQLISO8601Type from './GraphQLISO8601Type'
import ReflectPrompt from './ReflectPrompt'
import RetroReflection from './RetroReflection'
import RetrospectiveMeeting from './RetrospectiveMeeting'
import Team from './Team'

const RetroReflectionGroup: GraphQLObjectType = new GraphQLObjectType<any, GQLContext>({
name: 'RetroReflectionGroup',
description: 'A reflection group created during the group phase of a retrospective',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLID),
description: 'shortid'
},
commentors: {
type: new GraphQLList(new GraphQLNonNull(CommentorDetails)),
description: 'A list of users currently commenting',
deprecationReason: 'Moved to ThreadConnection. Can remove Jun-01-2021',
resolve: ({commentor = []}) => {
return commentor
}
},
createdAt: {
type: new GraphQLNonNull(GraphQLISO8601Type),
description: 'The timestamp the meeting was created'
},
isActive: {
type: new GraphQLNonNull(GraphQLBoolean),
description: 'True if the group has not been removed, else false'
},
meetingId: {
type: new GraphQLNonNull(GraphQLID),
description: 'The foreign key to link a reflection group to its meeting'
},
meeting: {
type: new GraphQLNonNull(RetrospectiveMeeting),
description: 'The retrospective meeting this reflection was created in',
resolve: ({meetingId}, _args: unknown, {dataLoader}) => {
return dataLoader.get('newMeetings').load(meetingId)
}
},
prompt: {
type: new GraphQLNonNull(ReflectPrompt),
resolve: ({promptId}, _args: unknown, {dataLoader}) => {
return dataLoader.get('reflectPrompts').load(promptId)
}
},
promptId: {
type: new GraphQLNonNull(GraphQLID),
description: 'The foreign key to link a reflection group to its prompt. Immutable.'
},
reflections: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(RetroReflection))),
resolve: async ({id: reflectionGroupId, meetingId}, _args: unknown, {dataLoader}) => {
// use meetingId so we only hit the DB once instead of once per group
const reflections = await dataLoader.get('retroReflectionsByMeetingId').load(meetingId)
const filteredReflections = reflections.filter(
(reflection: Reflection) => reflection.reflectionGroupId === reflectionGroupId
)
filteredReflections.sort((a: Reflection, b: Reflection) =>
a.sortOrder < b.sortOrder ? 1 : -1
)
return filteredReflections
}
},
smartTitle: {
type: GraphQLString,
description: 'Our auto-suggested title, to be compared to the actual title for analytics',
resolve: resolveForSU('smartTitle')
},
sortOrder: {
type: new GraphQLNonNull(GraphQLFloat),
description: 'The sort order of the reflection group'
},
discussionPromptQuestion: {
type: GraphQLString,
description: `The AI generated question to prompt and engage the discussion of this reflection group`
},
team: {
type: Team,
description: 'The team that is running the retro',
resolve: async ({meetingId}, _args: unknown, {dataLoader}) => {
const meeting = await dataLoader.get('newMeetings').load(meetingId)
return dataLoader.get('teams').load(meeting.teamId)
}
},
title: {
type: GraphQLString,
description: 'The title of the grouping of the retrospective reflections'
},
titleIsUserDefined: {
type: new GraphQLNonNull(GraphQLBoolean),
description: 'true if a user wrote the title, else false',
resolve: ({title, smartTitle}) => {
return title ? title !== smartTitle : false
}
},
updatedAt: {
type: GraphQLISO8601Type,
description: 'The timestamp the meeting was updated at'
},
voterIds: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))),
description: 'A list of voterIds (userIds). Not available to team to preserve anonymity',
resolve: resolveForSU('voterIds')
},
voteCount: {
type: new GraphQLNonNull(GraphQLInt),
description: 'The number of votes this group has received',
resolve: ({voterIds}) => {
return voterIds ? voterIds.length : 0
}
},
viewerVoteCount: {
type: GraphQLInt,
description: 'The number of votes the viewer has given this group',
resolve: ({voterIds}, _args: unknown, {authToken}) => {
const viewerId = getUserId(authToken)
return voterIds
? voterIds.reduce(
(sum: number, voterId: string) => (voterId === viewerId ? sum + 1 : sum),
0
)
: 0
}
}
})
fields: {}
})

export default RetroReflectionGroup

0 comments on commit 695646c

Please sign in to comment.