Migrate teams backend from Django to Express + Apollo Server + Prisma
Build Docker Image / build (push) Successful in 2m8s

Replace Django/Graphene stack with TypeScript Express server using Apollo Server v4
with 4 GraphQL endpoints (public/user/team/m2m) and Prisma ORM mapped to existing tables.
This commit is contained in:
Ruslan Bakiev
2026-03-09 09:26:41 +07:00
parent e52f5947a2
commit d9f1a066ce
82 changed files with 5164 additions and 3820 deletions
+286
View File
@@ -0,0 +1,286 @@
import { GraphQLError } from 'graphql'
import { prisma } from '../db.js'
import { requireScopes, type AuthContext } from '../auth.js'
import { startAddressWorkflow, startInviteWorkflow } from '../services/temporal.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')
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 []
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 []
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')
try {
await startInviteWorkflow(ctx.teamUuid, args.input.email, args.input.role || 'MEMBER', ctx.userId)
return { success: true, message: 'Invitation workflow started' }
} catch (e) {
console.error('Failed to start invite workflow:', e)
return { success: false, message: String(e) }
}
},
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')
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,
},
})
try {
const wfId = await startAddressWorkflow(
ctx.teamUuid, addr.uuid, addr.name, addr.address,
addr.latitude ?? undefined, addr.longitude ?? undefined,
addr.countryCode || undefined, addr.isDefault,
)
return { success: true, message: 'Address created', workflowId: wfId }
} catch (e) {
console.error('Failed to start address workflow:', e)
return { success: true, message: 'Address created but workflow failed', 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 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 }
},
},
}