Skip to content

Commit

Permalink
chore(rethinkdb): TeamMember: Phase 1 (#9979)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored Jul 19, 2024
1 parent 6d01097 commit b0c2cf2
Show file tree
Hide file tree
Showing 53 changed files with 539 additions and 861 deletions.
2 changes: 1 addition & 1 deletion packages/client/hooks/useSnacksForNewMeetings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const useSnacksForNewMeetings = (meetingsRef: readonly useSnacksForNewMeetings_m
const [snackedMeeting] = sortedMeetings
if (!snackedMeeting) return
const {id: meetingId, createdBy, createdByUser, name: meetingName} = snackedMeeting
const {preferredName} = createdByUser
const preferredName = createdByUser?.preferredName ?? 'Unknown'
const isInit = createdBy === viewerId
const name = isInit ? 'You' : preferredName
atmosphere.eventEmitter.emit('addSnackbar', {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/database/types/Meeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default abstract class Meeting {
isLegacy?: boolean // true if old version of action meeting
createdAt = new Date()
updatedAt = new Date()
createdBy: string
createdBy: string | null
endedAt: Date | undefined | null = undefined
facilitatorStageId: string | undefined
facilitatorUserId: string
Expand Down
24 changes: 11 additions & 13 deletions packages/server/dataloader/RootDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type RegisterDependsOn = (primaryLoaders: AllPrimaryLoaders | AllPrimaryL

// The RethinkDB logic is a leaky abstraction! It will be gone soon & this will be generic enough to put in its own package
interface GenericDataLoader<TLoaders, TPrimaryLoaderNames> {
clearAll(pkLoaderName: TPrimaryLoaderNames): void
clearAll(pkLoaderName: TPrimaryLoaderNames | TPrimaryLoaderNames[]): void
get<LoaderName extends keyof TLoaders, Loader extends TLoaders[LoaderName]>(
loaderName: LoaderName
): Loader extends (...args: any[]) => any
Expand Down Expand Up @@ -85,8 +85,9 @@ export default class RootDataLoader<
this.dataLoaderOptions = dataLoaderOptions
}

clearAll: DataLoaderInstance['clearAll'] = (pkLoaderName) => {
const dependencies = [pkLoaderName, ...(this.dependentLoaders[pkLoaderName] ?? [])]
clearAll: DataLoaderInstance['clearAll'] = (inPkLoaderName) => {
const pkLoaderNames = Array.isArray(inPkLoaderName) ? inPkLoaderName : [inPkLoaderName]
const dependencies = pkLoaderNames.flatMap((pk) => [pk, ...(this.dependentLoaders[pk] ?? [])])
dependencies.forEach((loaderName) => {
this.loaders[loaderName]?.clearAll()
})
Expand All @@ -95,22 +96,19 @@ export default class RootDataLoader<
let loader = this.loaders[loaderName]
if (loader) return loader
const loaderMaker = loaderMakers[loaderName as keyof typeof loaderMakers]
const dependsOn: RegisterDependsOn = (inPrimaryLoaders) => {
const primaryLoaders = Array.isArray(inPrimaryLoaders) ? inPrimaryLoaders : [inPrimaryLoaders]
primaryLoaders.forEach((primaryLoader) => {
;(this.dependentLoaders[primaryLoader] ??= []).push(loaderName)
})
}
if (loaderMaker instanceof RethinkPrimaryKeyLoaderMaker) {
const {table} = loaderMaker
loader = rethinkPrimaryKeyLoader(this.dataLoaderOptions, table)
} else if (loaderMaker instanceof RethinkForeignKeyLoaderMaker) {
const {fetch, field, pk} = loaderMaker
const basePkLoader = this.get(pk) as any
loader = rethinkForeignKeyLoader(basePkLoader, this.dataLoaderOptions, field, fetch)
loader = rethinkForeignKeyLoader(this, dependsOn, pk, field, fetch)
} else {
const dependsOn: RegisterDependsOn = (inPrimaryLoaders) => {
const primaryLoaders = Array.isArray(inPrimaryLoaders)
? inPrimaryLoaders
: [inPrimaryLoaders]
primaryLoaders.forEach((primaryLoader) => {
;(this.dependentLoaders[primaryLoader] ??= []).push(loaderName)
})
}
loader = (loaderMaker as any)(this, dependsOn)
}
this.loaders[loaderName] = loader!
Expand Down
12 changes: 8 additions & 4 deletions packages/server/dataloader/rethinkForeignKeyLoader.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import DataLoader from 'dataloader'
import {DBType} from '../database/rethinkDriver'
import RootDataLoader, {RegisterDependsOn} from './RootDataLoader'
import UpdatableCacheDataLoader from './UpdatableCacheDataLoader'
import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers'

const rethinkForeignKeyLoader = <T extends keyof DBType>(
standardLoader: DataLoader<string, DBType[T]>,
options: DataLoader.Options<string, DBType[T]>,
parent: RootDataLoader,
dependsOn: RegisterDependsOn,
primaryKeyLoaderName: keyof typeof rethinkPrimaryKeyLoaderMakers,
field: string,
fetchFn: (ids: readonly string[]) => any[] | Promise<any[]>
) => {
const standardLoader = parent.get(primaryKeyLoaderName)
dependsOn(primaryKeyLoaderName)
const batchFn = async (ids: readonly string[]) => {
const items = await fetchFn(ids)
items.forEach((item) => {
standardLoader.clear(item.id).prime(item.id, item)
})
return ids.map((id) => items.filter((item) => item[field] === id))
}
return new UpdatableCacheDataLoader<string, DBType[T][]>(batchFn, options)
return new UpdatableCacheDataLoader<string, DBType[T][]>(batchFn, {...parent.dataLoaderOptions})
}

export default rethinkForeignKeyLoader
3 changes: 1 addition & 2 deletions packages/server/graphql/mutations/addTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums'
import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId'
import AuthToken from '../../database/types/AuthToken'
import generateUID from '../../generateUID'
import getTeamsByOrgIds from '../../postgres/queries/getTeamsByOrgIds'
import removeSuggestedAction from '../../safeMutations/removeSuggestedAction'
import {analytics} from '../../utils/analytics/analytics'
import {getUserId, isUserInOrg} from '../../utils/authorization'
Expand Down Expand Up @@ -54,7 +53,7 @@ export default {

// VALIDATION
const [orgTeams, organization, viewer] = await Promise.all([
getTeamsByOrgIds([orgId], {isArchived: false}),
dataLoader.get('teamsByOrgIds').load(orgId),
dataLoader.get('organizations').loadNonNull(orgId),
dataLoader.get('users').loadNonNull(viewerId)
])
Expand Down
14 changes: 3 additions & 11 deletions packages/server/graphql/mutations/createTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId'
import getRethink from '../../database/rethinkDriver'
import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves'
import Task, {TaskServiceEnum} from '../../database/types/Task'
import TeamMember from '../../database/types/TeamMember'
import generateUID from '../../generateUID'
import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache'
import {analytics} from '../../utils/analytics/analytics'
Expand Down Expand Up @@ -229,17 +228,10 @@ export default {
userId,
updatedAt
}

const {teamMembers} = await r({
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
await r({
task: r.table('Task').insert(task),
history: r.table('TaskHistory').insert(history),
teamMembers: r
.table('TeamMember')
.getAll(teamId, {index: 'teamId'})
.filter({
isNotRemoved: true
})
.coerceTo('array') as unknown as TeamMember[]
history: r.table('TaskHistory').insert(history)
}).run()

handleAddTaskNotifications(teamMembers, task, viewerId, teamId, {
Expand Down
11 changes: 4 additions & 7 deletions packages/server/graphql/mutations/deleteTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,16 @@ export default {
}

// RESOLUTION
const {subscribedUserIds} = await r({
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
const subscribedUserIds = teamMembers.map(({userId}) => userId)
await r({
task: r.table('Task').get(taskId).delete(),
taskHistory: r
.table('TaskHistory')
.between([taskId, r.minval], [taskId, r.maxval], {
index: 'taskIdUpdatedAt'
})
.delete(),
subscribedUserIds: r
.table('TeamMember')
.getAll(teamId, {index: 'teamId'})
.filter({isNotRemoved: true})('userId')
.coerceTo('array') as unknown as string[]
.delete()
}).run()
const {tags, userId: taskUserId} = task

Expand Down
9 changes: 3 additions & 6 deletions packages/server/graphql/mutations/endCheckIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,9 @@ export default {
const pg = getKysely()
await pg.insertInto('TimelineEvent').values(events).execute()
if (team.isOnboardTeam) {
const teamLeadUserId = await r
.table('TeamMember')
.getAll(teamId, {index: 'teamId'})
.filter({isLead: true})
.nth(0)('userId')
.run()
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
const teamLeader = teamMembers.find(({isLead}) => isLead)!
const {userId: teamLeadUserId} = teamLeader

const removedSuggestedActionId = await removeSuggestedAction(
teamLeadUserId,
Expand Down
29 changes: 24 additions & 5 deletions packages/server/graphql/mutations/helpers/createTeamAndLeader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {sql} from 'kysely'
import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId'
import getRethink from '../../../database/rethinkDriver'
import MeetingSettingsAction from '../../../database/types/MeetingSettingsAction'
import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker'
Expand All @@ -7,7 +9,6 @@ import TimelineEventCreatedTeam from '../../../database/types/TimelineEventCreat
import {DataLoaderInstance} from '../../../dataloader/RootDataLoader'
import getKysely from '../../../postgres/getKysely'
import IUser from '../../../postgres/types/IUser'
import addTeamIdToTMS from '../../../safeMutations/addTeamIdToTMS'
import insertNewTeamMember from '../../../safeMutations/insertNewTeamMember'

interface ValidNewTeam {
Expand All @@ -24,7 +25,7 @@ export default async function createTeamAndLeader(
dataLoader: DataLoaderInstance
) {
const r = await getRethink()
const {id: userId} = user
const {id: userId, picture, preferredName, email} = user
const {id: teamId, orgId} = newTeam
const organization = await dataLoader.get('organizations').loadNonNull(orgId)
const {tier, trialStartDate} = organization
Expand All @@ -44,14 +45,32 @@ export default async function createTeamAndLeader(
const pg = getKysely()
await Promise.all([
pg
.with('Team', (qc) => qc.insertInto('Team').values(verifiedTeam))
.with('TeamInsert', (qc) => qc.insertInto('Team').values(verifiedTeam))
.with('UserUpdate', (qc) =>
qc
.updateTable('User')
.set({tms: sql`arr_append_uniq("tms", ${teamId})`})
.where('id', '=', userId)
)
.with('TeamMemberInsert', (qc) =>
qc.insertInto('TeamMember').values({
id: TeamMemberId.join(teamId, userId),
teamId,
userId,
picture,
preferredName,
email,
isLead: true,
openDrawer: 'manageTeam'
})
)
.insertInto('TimelineEvent')
.values(timelineEvent)
.execute(),
// add meeting settings
r.table('MeetingSettings').insert(meetingSettings).run(),
// denormalize common fields to team member
insertNewTeamMember(user, teamId),
addTeamIdToTMS(userId, teamId)
insertNewTeamMember(user, teamId, dataLoader)
])
dataLoader.clearAll(['teams', 'users', 'teamMembers', 'timelineEvents', 'meetingSettings'])
}
18 changes: 8 additions & 10 deletions packages/server/graphql/mutations/helpers/removeFromOrg.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {sql} from 'kysely'
import {InvoiceItemType} from 'parabol-client/types/constEnums'
import adjustUserCount from '../../../billing/helpers/adjustUserCount'
import getRethink from '../../../database/rethinkDriver'
import getKysely from '../../../postgres/getKysely'
import getTeamsByOrgIds from '../../../postgres/queries/getTeamsByOrgIds'
import {Logger} from '../../../utils/Logger'
import setUserTierForUserIds from '../../../utils/setUserTierForUserIds'
import {DataLoaderWorker} from '../../graphql'
Expand All @@ -16,15 +14,15 @@ const removeFromOrg = async (
evictorUserId: string | undefined,
dataLoader: DataLoaderWorker
) => {
const r = await getRethink()
const pg = getKysely()
const orgTeams = await getTeamsByOrgIds([orgId])
// TODO consider a teamMembersByOrgId dataloader if this pattern pops up more
const [orgTeams, allTeamMembers] = await Promise.all([
dataLoader.get('teamsByOrgIds').load(orgId),
dataLoader.get('teamMembersByUserId').load(userId)
])
const teamIds = orgTeams.map((team) => team.id)
const teamMemberIds = (await r
.table('TeamMember')
.getAll(r.args(teamIds), {index: 'teamId'})
.filter({userId, isNotRemoved: true})('id')
.run()) as string[]
const teamMembers = allTeamMembers.filter((teamMember) => teamIds.includes(teamMember.teamId))
const teamMemberIds = teamMembers.map((teamMember) => teamMember.id)

const perTeamRes = await Promise.all(
teamMemberIds.map((teamMemberId) => {
Expand Down Expand Up @@ -53,7 +51,7 @@ const removeFromOrg = async (
.executeTakeFirstOrThrow(),
dataLoader.get('users').loadNonNull(userId)
])

dataLoader.clearAll('organizationUsers')
// need to make sure the org doc is updated before adjusting this
const {role} = organizationUser
if (role && ['BILLING_LEADER', 'ORG_ADMIN'].includes(role)) {
Expand Down
30 changes: 20 additions & 10 deletions packages/server/graphql/mutations/helpers/removeTeamMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import EstimateStage from '../../../database/types/EstimateStage'
import NotificationKickedOut from '../../../database/types/NotificationKickedOut'
import Task from '../../../database/types/Task'
import UpdatesStage from '../../../database/types/UpdatesStage'
import removeUserTms from '../../../postgres/queries/removeUserTms'
import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId'
import getKysely from '../../../postgres/getKysely'
import archiveTasksForDB from '../../../safeMutations/archiveTasksForDB'
import errorFilter from '../../errorFilter'
import {DataLoaderWorker} from '../../graphql'
Expand All @@ -27,10 +26,11 @@ const removeTeamMember = async (
) => {
const {evictorUserId} = options
const r = await getRethink()
const pg = getKysely()
const now = new Date()
const {userId, teamId} = fromTeamMemberId(teamMemberId)
// see if they were a leader, make a new guy leader so later we can reassign tasks
const activeTeamMembers = await r.table('TeamMember').getAll(teamId, {index: 'teamId'}).run()
const activeTeamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
const teamMember = activeTeamMembers.find((t) => t.id === teamMemberId)
const {isLead, isNotRemoved} = teamMember ?? {}
// if the guy being removed is the leader & not the last, pick a new one. else, use him
Expand All @@ -40,17 +40,18 @@ const removeTeamMember = async (
}

if (activeTeamMembers.length === 1) {
const updates = {
isArchived: true,
updatedAt: new Date()
}
await Promise.all([
// archive single-person teams
updateTeamByTeamId(updates, teamId),
pg.updateTable('Team').set({isArchived: true}).where('id', '=', teamId).execute(),
// delete all tasks belonging to a 1-person team
r.table('Task').getAll(teamId, {index: 'teamId'}).delete()
])
} else if (isLead) {
await pg
.updateTable('TeamMember')
.set(({not}) => ({isLead: not('isLead')}))
.where('id', 'in', [teamMemberId, teamLeader.id])
.execute()
// assign new leader, remove old leader flag
await r({
newTeamLead: r.table('TeamMember').get(teamLeader.id).update({
Expand All @@ -60,6 +61,11 @@ const removeTeamMember = async (
}).run()
}

await pg
.updateTable('TeamMember')
.set({isNotRemoved: false})
.where('id', '=', teamMemberId)
.execute()
// assign active tasks to the team lead
const {integratedTasksToArchive, reassignedTasks} = await r({
teamMember: r.table('TeamMember').get(teamMemberId).update({
Expand Down Expand Up @@ -92,8 +98,12 @@ const removeTeamMember = async (
)('changes')('new_val')
.default([]) as unknown as Task[]
}).run()

await removeUserTms(teamId, userId)
await pg
.updateTable('User')
.set(({fn, ref, val}) => ({tms: fn('ARRAY_REMOVE', [ref('tms'), val(teamId)])}))
.where('id', '=', userId)
.execute()
dataLoader.clearAll(['users', 'teamMembers'])
const user = await dataLoader.get('users').load(userId)

let notificationId: string | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,9 @@ const safeEndRetrospective = async ({
await pg.insertInto('TimelineEvent').values(events).execute()

if (team.isOnboardTeam) {
const teamLeadUserId = await r
.table('TeamMember')
.getAll(teamId, {index: 'teamId'})
.filter({isLead: true})
.nth(0)('userId')
.run()
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
const teamLead = teamMembers.find((teamMember) => teamMember.isLead)!
const teamLeadUserId = teamLead.userId

const removedSuggestedActionId = await removeSuggestedAction(teamLeadUserId, 'tryRetroMeeting')
if (removedSuggestedActionId) {
Expand Down
Loading

0 comments on commit b0c2cf2

Please sign in to comment.