Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor ReflectionGroup to SDL pattern #9807

Merged
merged 8 commits into from
Jun 6, 2024
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
Loading