299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
import { GraphQLError } from 'graphql'
|
|
import { randomUUID, createHash } from 'crypto'
|
|
import { prisma } from '../db.js'
|
|
import { requireScopes, type AuthContext } from '../auth.js'
|
|
import { materializeTeamFromContext } from '../team-materialization.js'
|
|
|
|
export const teamTypeDefs = `#graphql
|
|
type TeamUser {
|
|
id: String
|
|
firstName: String
|
|
lastName: String
|
|
phone: String
|
|
avatarId: String
|
|
createdAt: String
|
|
}
|
|
|
|
type TeamMember {
|
|
user: TeamUser
|
|
role: String!
|
|
joinedAt: String
|
|
}
|
|
|
|
type TeamAddress {
|
|
uuid: String!
|
|
name: String!
|
|
address: String!
|
|
latitude: Float
|
|
longitude: Float
|
|
isDefault: Boolean!
|
|
countryCode: String
|
|
createdAt: String
|
|
processedAt: String
|
|
status: String!
|
|
}
|
|
|
|
type SelectedLocation {
|
|
type: String
|
|
uuid: String
|
|
name: String
|
|
latitude: Float
|
|
longitude: Float
|
|
}
|
|
|
|
type Team {
|
|
id: String!
|
|
name: String!
|
|
ownerId: String
|
|
members: [TeamMember]
|
|
addresses: [TeamAddress]
|
|
selectedLocation: SelectedLocation
|
|
}
|
|
|
|
input InviteMemberInput {
|
|
email: String!
|
|
role: String
|
|
}
|
|
|
|
input CreateTeamAddressInput {
|
|
name: String!
|
|
address: String!
|
|
latitude: Float
|
|
longitude: Float
|
|
countryCode: String
|
|
isDefault: Boolean
|
|
}
|
|
|
|
input UpdateTeamAddressInput {
|
|
uuid: String!
|
|
name: String
|
|
address: String
|
|
latitude: Float
|
|
longitude: Float
|
|
countryCode: String
|
|
isDefault: Boolean
|
|
}
|
|
|
|
input SetSelectedLocationInput {
|
|
type: String!
|
|
uuid: String!
|
|
name: String!
|
|
latitude: Float!
|
|
longitude: Float!
|
|
}
|
|
|
|
type InviteMemberResult { success: Boolean!, message: String }
|
|
type CreateTeamAddressResult { success: Boolean!, message: String, workflowId: String }
|
|
type UpdateTeamAddressResult { success: Boolean!, address: TeamAddress }
|
|
type DeleteTeamAddressResult { success: Boolean! }
|
|
type SetSelectedLocationResult { success: Boolean! }
|
|
|
|
type Query {
|
|
team: Team
|
|
getTeam(teamId: String!): Team
|
|
teamMembers: [TeamMember]
|
|
teamAddresses: [TeamAddress]
|
|
}
|
|
|
|
type Mutation {
|
|
inviteMember(input: InviteMemberInput!): InviteMemberResult
|
|
createTeamAddress(input: CreateTeamAddressInput!): CreateTeamAddressResult
|
|
updateTeamAddress(input: UpdateTeamAddressInput!): UpdateTeamAddressResult
|
|
deleteTeamAddress(uuid: String!): DeleteTeamAddressResult
|
|
setSelectedLocation(input: SetSelectedLocationInput!): SetSelectedLocationResult
|
|
}
|
|
`
|
|
|
|
async function getTeamByUuid(uuid: string) {
|
|
return prisma.team.findUnique({
|
|
where: { uuid },
|
|
include: {
|
|
members: { include: { user: { include: { profile: true } } } },
|
|
addresses: { orderBy: { createdAt: 'desc' } },
|
|
},
|
|
})
|
|
}
|
|
|
|
function mapTeam(team: NonNullable<Awaited<ReturnType<typeof getTeamByUuid>>>) {
|
|
return {
|
|
id: team.uuid,
|
|
name: team.name,
|
|
ownerId: team.ownerId?.toString() ?? null,
|
|
members: team.members.map(m => ({
|
|
user: m.user ? {
|
|
id: m.user.profile?.logtoId ?? m.user.id.toString(),
|
|
firstName: m.user.firstName,
|
|
lastName: m.user.lastName,
|
|
phone: m.user.profile?.phone ?? '',
|
|
avatarId: m.user.profile?.avatarId ?? null,
|
|
createdAt: m.user.dateJoined.toISOString(),
|
|
} : null,
|
|
role: m.role,
|
|
joinedAt: m.joinedAt.toISOString(),
|
|
})),
|
|
addresses: team.addresses.map(a => ({
|
|
uuid: a.uuid, name: a.name, address: a.address,
|
|
latitude: a.latitude, longitude: a.longitude,
|
|
isDefault: a.isDefault, countryCode: a.countryCode,
|
|
createdAt: a.createdAt.toISOString(),
|
|
processedAt: a.processedAt?.toISOString() ?? null,
|
|
status: a.status,
|
|
})),
|
|
selectedLocation: team.selectedLocationType ? {
|
|
type: team.selectedLocationType,
|
|
uuid: team.selectedLocationUuid,
|
|
name: team.selectedLocationName,
|
|
latitude: team.selectedLocationLat,
|
|
longitude: team.selectedLocationLon,
|
|
} : null,
|
|
}
|
|
}
|
|
|
|
export const teamResolvers = {
|
|
Query: {
|
|
team: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid) throw new GraphQLError('Team UUID not found')
|
|
await materializeTeamFromContext(ctx)
|
|
const team = await getTeamByUuid(ctx.teamUuid)
|
|
return team ? mapTeam(team) : null
|
|
},
|
|
|
|
getTeam: async (_: unknown, args: { teamId: string }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
const team = await getTeamByUuid(args.teamId)
|
|
return team ? mapTeam(team) : null
|
|
},
|
|
|
|
teamMembers: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid) return []
|
|
await materializeTeamFromContext(ctx)
|
|
const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } })
|
|
if (!team) return []
|
|
const members = await prisma.teamMember.findMany({
|
|
where: { teamId: team.id },
|
|
include: { user: { include: { profile: true } } },
|
|
})
|
|
return members.map(m => ({
|
|
user: m.user ? {
|
|
id: m.user.profile?.logtoId ?? m.user.id.toString(),
|
|
firstName: m.user.firstName, lastName: m.user.lastName,
|
|
phone: m.user.profile?.phone ?? '', avatarId: m.user.profile?.avatarId ?? null,
|
|
createdAt: m.user.dateJoined.toISOString(),
|
|
} : null,
|
|
role: m.role, joinedAt: m.joinedAt.toISOString(),
|
|
}))
|
|
},
|
|
|
|
teamAddresses: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid) return []
|
|
await materializeTeamFromContext(ctx)
|
|
const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } })
|
|
if (!team) return []
|
|
const addrs = await prisma.teamAddress.findMany({ where: { teamId: team.id }, orderBy: { createdAt: 'desc' } })
|
|
return addrs.map(a => ({
|
|
uuid: a.uuid, name: a.name, address: a.address,
|
|
latitude: a.latitude, longitude: a.longitude,
|
|
isDefault: a.isDefault, countryCode: a.countryCode,
|
|
createdAt: a.createdAt.toISOString(),
|
|
processedAt: a.processedAt?.toISOString() ?? null,
|
|
status: a.status,
|
|
}))
|
|
},
|
|
},
|
|
|
|
Mutation: {
|
|
inviteMember: async (_: unknown, args: { input: { email: string; role?: string } }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid || !ctx.userId) throw new GraphQLError('Not authenticated')
|
|
await materializeTeamFromContext(ctx)
|
|
const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } })
|
|
if (!team) throw new GraphQLError('Team not found')
|
|
const invitation = await prisma.teamInvitation.create({
|
|
data: {
|
|
teamId: team.id,
|
|
email: args.input.email,
|
|
role: args.input.role || 'MEMBER',
|
|
invitedBy: ctx.userId,
|
|
status: 'PENDING',
|
|
},
|
|
})
|
|
const token = randomUUID()
|
|
await prisma.teamInvitationToken.create({
|
|
data: {
|
|
invitationId: invitation.id,
|
|
tokenHash: createHash('sha256').update(token).digest('hex'),
|
|
workflowStatus: 'active',
|
|
},
|
|
})
|
|
return { success: true, message: 'Invitation created' }
|
|
},
|
|
|
|
createTeamAddress: async (_: unknown, args: { input: { name: string; address: string; latitude?: number; longitude?: number; countryCode?: string; isDefault?: boolean } }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid) throw new GraphQLError('Not authenticated')
|
|
await materializeTeamFromContext(ctx)
|
|
const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } })
|
|
if (!team) throw new GraphQLError('Team not found')
|
|
const addr = await prisma.teamAddress.create({
|
|
data: {
|
|
teamId: team.id, name: args.input.name, address: args.input.address,
|
|
latitude: args.input.latitude, longitude: args.input.longitude,
|
|
countryCode: args.input.countryCode || '', isDefault: args.input.isDefault || false,
|
|
status: 'active',
|
|
processedAt: new Date(),
|
|
},
|
|
})
|
|
return { success: true, message: 'Address created', workflowId: null }
|
|
},
|
|
|
|
updateTeamAddress: async (_: unknown, args: { input: { uuid: string; name?: string; address?: string; latitude?: number; longitude?: number; countryCode?: string; isDefault?: boolean } }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
const data: Record<string, unknown> = {}
|
|
if (args.input.name !== undefined) data.name = args.input.name
|
|
if (args.input.address !== undefined) data.address = args.input.address
|
|
if (args.input.latitude !== undefined) data.latitude = args.input.latitude
|
|
if (args.input.longitude !== undefined) data.longitude = args.input.longitude
|
|
if (args.input.countryCode !== undefined) data.countryCode = args.input.countryCode
|
|
if (args.input.isDefault !== undefined) data.isDefault = args.input.isDefault
|
|
const addr = await prisma.teamAddress.update({ where: { uuid: args.input.uuid }, data })
|
|
return {
|
|
success: true,
|
|
address: {
|
|
uuid: addr.uuid, name: addr.name, address: addr.address,
|
|
latitude: addr.latitude, longitude: addr.longitude,
|
|
isDefault: addr.isDefault, countryCode: addr.countryCode,
|
|
createdAt: addr.createdAt.toISOString(),
|
|
processedAt: addr.processedAt?.toISOString() ?? null,
|
|
status: addr.status,
|
|
},
|
|
}
|
|
},
|
|
|
|
deleteTeamAddress: async (_: unknown, args: { uuid: string }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
await prisma.teamAddress.delete({ where: { uuid: args.uuid } })
|
|
return { success: true }
|
|
},
|
|
|
|
setSelectedLocation: async (_: unknown, args: { input: { type: string; uuid: string; name: string; latitude: number; longitude: number } }, ctx: AuthContext) => {
|
|
requireScopes(ctx, 'teams:member')
|
|
if (!ctx.teamUuid) throw new GraphQLError('Not authenticated')
|
|
await materializeTeamFromContext(ctx)
|
|
await prisma.team.update({
|
|
where: { uuid: ctx.teamUuid },
|
|
data: {
|
|
selectedLocationType: args.input.type,
|
|
selectedLocationUuid: args.input.uuid,
|
|
selectedLocationName: args.input.name,
|
|
selectedLocationLat: args.input.latitude,
|
|
selectedLocationLon: args.input.longitude,
|
|
},
|
|
})
|
|
return { success: true }
|
|
},
|
|
},
|
|
}
|