Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apis/api-gateway/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ type Mutation @join__type(graph: API_ANALYTICS) @join__type(graph: API_JOURNEYS
"""
timezone: String
): JourneyVisitorGoogleSheetExportResult! @join__field(graph: API_JOURNEYS_MODERN)
userDeleteJourneysCheck(userId: String!) : UserDeleteJourneysCheckResult! @join__field(graph: API_JOURNEYS_MODERN)
userDeleteJourneysConfirm(userId: String!) : UserDeleteJourneysConfirmResult! @join__field(graph: API_JOURNEYS_MODERN)
audioPreviewCreate(input: MutationAudioPreviewCreateInput!) : AudioPreview! @join__field(graph: API_LANGUAGES)
audioPreviewUpdate(input: MutationAudioPreviewUpdateInput!) : AudioPreview! @join__field(graph: API_LANGUAGES)
audioPreviewDelete(languageId: ID!) : AudioPreview! @join__field(graph: API_LANGUAGES)
Expand Down Expand Up @@ -554,6 +556,8 @@ type Mutation @join__type(graph: API_ANALYTICS) @join__type(graph: API_JOURNEYS
userImpersonate(email: String!) : String @join__field(graph: API_USERS)
createVerificationRequest(input: CreateVerificationRequestInput) : Boolean @join__field(graph: API_USERS)
validateEmail(email: String!, token: String!) : AuthenticatedUser @join__field(graph: API_USERS)
userDeleteCheck(idType: UserDeleteIdType!, id: String!) : UserDeleteCheckResult! @join__field(graph: API_USERS)
userDeleteConfirm(idType: UserDeleteIdType!, id: String!) : UserDeleteResult! @join__field(graph: API_USERS)
}

type MutationSiteCreateSuccess @join__type(graph: API_ANALYTICS) {
Expand Down Expand Up @@ -2546,6 +2550,31 @@ type TemplateFamilyStatsEventResponse @join__type(graph: API_JOURNEYS_MODERN) {
visitors: Int!
}

type UserDeleteJourneysCheckResult @join__type(graph: API_JOURNEYS_MODERN) {
journeysToDelete: Int!
journeysToTransfer: Int!
journeysToRemove: Int!
teamsToDelete: Int!
teamsToTransfer: Int!
teamsToRemove: Int!
logs: [UserDeleteJourneysLogEntry!]!
}

type UserDeleteJourneysConfirmResult @join__type(graph: API_JOURNEYS_MODERN) {
success: Boolean!
deletedJourneyIds: [String!]!
deletedTeamIds: [String!]!
deletedUserJourneyIds: [String!]!
deletedUserTeamIds: [String!]!
logs: [UserDeleteJourneysLogEntry!]!
}

type UserDeleteJourneysLogEntry @join__type(graph: API_JOURNEYS_MODERN) {
message: String!
level: String!
timestamp: String!
}

type YouTube @join__type(graph: API_JOURNEYS_MODERN, key: "id primaryLanguageId", extension: true) {
id: ID!
primaryLanguageId: ID
Expand Down Expand Up @@ -3288,6 +3317,30 @@ type AnonymousUser implements User @join__type(graph: API_USERS, key: "id") @jo
id: ID!
}

type UserDeleteCheckResult @join__type(graph: API_USERS) {
userId: String!
userEmail: String
userFirstName: String!
journeysToDelete: Int!
journeysToTransfer: Int!
journeysToRemove: Int!
teamsToDelete: Int!
teamsToTransfer: Int!
teamsToRemove: Int!
logs: [UserDeleteLogEntry!]!
}

type UserDeleteLogEntry @join__type(graph: API_USERS) {
message: String!
level: String!
timestamp: String!
}

type UserDeleteResult @join__type(graph: API_USERS) {
success: Boolean!
logs: [UserDeleteLogEntry!]!
}

interface BaseError @join__type(graph: API_ANALYTICS) @join__type(graph: API_MEDIA) {
message: String
}
Expand Down Expand Up @@ -3889,6 +3942,11 @@ enum App @join__type(graph: API_USERS) {
JesusFilmOne @join__enumValue(graph: API_USERS)
}

enum UserDeleteIdType @join__type(graph: API_USERS) {
databaseId @join__enumValue(graph: API_USERS)
email @join__enumValue(graph: API_USERS)
}

input SiteCreateInput @join__type(graph: API_ANALYTICS) {
domain: String!
goals: [String!]
Expand Down
27 changes: 27 additions & 0 deletions apis/api-journeys-modern/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,8 @@ type Mutation {
"""
timezone: String
): JourneyVisitorGoogleSheetExportResult!
userDeleteJourneysCheck(userId: String!): UserDeleteJourneysCheckResult!
userDeleteJourneysConfirm(userId: String!): UserDeleteJourneysConfirmResult!
}

input MutationJourneyLanguageAiDetectInput {
Expand Down Expand Up @@ -2288,6 +2290,31 @@ type UserAgent
os: OperatingSystem!
}

type UserDeleteJourneysCheckResult {
journeysToDelete: Int!
journeysToTransfer: Int!
journeysToRemove: Int!
teamsToDelete: Int!
teamsToTransfer: Int!
teamsToRemove: Int!
logs: [UserDeleteJourneysLogEntry!]!
}

type UserDeleteJourneysConfirmResult {
success: Boolean!
deletedJourneyIds: [String!]!
deletedTeamIds: [String!]!
deletedUserJourneyIds: [String!]!
deletedUserTeamIds: [String!]!
logs: [UserDeleteJourneysLogEntry!]!
}

type UserDeleteJourneysLogEntry {
message: String!
level: String!
timestamp: String!
}

type UserInvite
@key(fields: "id")
@shareable
Expand Down
1 change: 1 addition & 0 deletions apis/api-journeys-modern/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import './plausible'
import './qrCode'
import './team'
import './user'
import './userDelete'
import './userInvite'
import './userJourney'
import './userRole'
Expand Down
2 changes: 2 additions & 0 deletions apis/api-journeys-modern/src/schema/userDelete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './userDeleteJourneysCheck'
import './userDeleteJourneysConfirm'
9 changes: 9 additions & 0 deletions apis/api-journeys-modern/src/schema/userDelete/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface LogEntry {
message: string
level: string
timestamp: string
}

export function createLog(message: string, level = 'info'): LogEntry {
return { message, level, timestamp: new Date().toISOString() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { prisma } from '@core/prisma/journeys/client'

import { builder } from '../builder'

import { LogEntry, createLog } from './types'

const UserDeleteJourneysLogEntry = builder.objectRef<LogEntry>(
'UserDeleteJourneysLogEntry'
)

builder.objectType(UserDeleteJourneysLogEntry, {
fields: (t) => ({
message: t.exposeString('message', { nullable: false }),
level: t.exposeString('level', { nullable: false }),
timestamp: t.exposeString('timestamp', { nullable: false })
})
})

interface JourneysCheckResultShape {
journeysToDelete: number
journeysToTransfer: number
journeysToRemove: number
teamsToDelete: number
teamsToTransfer: number
teamsToRemove: number
logs: LogEntry[]
}

const UserDeleteJourneysCheckResult =
builder.objectRef<JourneysCheckResultShape>('UserDeleteJourneysCheckResult')

builder.objectType(UserDeleteJourneysCheckResult, {
fields: (t) => ({
journeysToDelete: t.exposeInt('journeysToDelete', { nullable: false }),
journeysToTransfer: t.exposeInt('journeysToTransfer', { nullable: false }),
journeysToRemove: t.exposeInt('journeysToRemove', { nullable: false }),
teamsToDelete: t.exposeInt('teamsToDelete', { nullable: false }),
teamsToTransfer: t.exposeInt('teamsToTransfer', { nullable: false }),
teamsToRemove: t.exposeInt('teamsToRemove', { nullable: false }),
logs: t.field({
type: [UserDeleteJourneysLogEntry],
nullable: false,
resolve: (parent) => parent.logs
})
})
})

builder.mutationField('userDeleteJourneysCheck', (t) =>
t.withAuth({ isValidInterop: true }).field({
type: UserDeleteJourneysCheckResult,
nullable: false,
args: {
userId: t.arg.string({ required: true })
},
resolve: async (_parent, { userId }) => {
const logs: LogEntry[] = []

// Check journeys
const userJourneys = await prisma.userJourney.findMany({
where: { userId },
include: {
journey: {
select: {
id: true,
title: true,
userJourneys: {
select: { id: true, userId: true, role: true }
}
}
}
}
})

logs.push(createLog(`${userJourneys.length} journeys found`))

let journeysToDelete = 0
let journeysToTransfer = 0
let journeysToRemove = 0

for (const uj of userJourneys) {
const others = uj.journey.userJourneys.filter(
(j) => j.userId !== userId
)
if (others.length === 0) {
journeysToDelete++
} else if (uj.role === 'owner') {
journeysToTransfer++
} else {
journeysToRemove++
}
}

if (journeysToDelete > 0)
logs.push(
createLog(
`${journeysToDelete} journeys will be deleted (user is sole accessor)`
)
)
if (journeysToTransfer > 0)
logs.push(
createLog(
`${journeysToTransfer} journey ownerships will be transferred`
)
)
if (journeysToRemove > 0)
logs.push(
createLog(`${journeysToRemove} journey memberships will be removed`)
)

// Check teams
const userTeams = await prisma.userTeam.findMany({
where: { userId },
include: {
team: {
select: {
id: true,
title: true,
userTeams: {
select: { id: true, userId: true, role: true }
}
}
}
}
})

logs.push(createLog(`${userTeams.length} teams found`))

let teamsToDelete = 0
let teamsToTransfer = 0
let teamsToRemove = 0

for (const ut of userTeams) {
const others = ut.team.userTeams.filter((t) => t.userId !== userId)
if (others.length === 0) {
teamsToDelete++
} else if (ut.role === 'manager') {
teamsToTransfer++
} else {
teamsToRemove++
}
}

if (teamsToDelete > 0)
logs.push(
createLog(
`${teamsToDelete} teams will be deleted (user is sole member)`
)
)
if (teamsToTransfer > 0)
logs.push(
createLog(`${teamsToTransfer} team manager roles will be transferred`)
)
if (teamsToRemove > 0)
logs.push(
createLog(`${teamsToRemove} team memberships will be removed`)
)

// Check related records
const [
userRole,
journeyProfile,
integration,
visitor,
journeyNotification,
userTeamInvite,
userInvite,
journeyEventsExportLog,
journeyTheme
] = await Promise.all([
prisma.userRole.count({ where: { userId } }),
prisma.journeyProfile.count({ where: { userId } }),
prisma.integration.count({ where: { userId } }),
prisma.visitor.count({ where: { userId } }),
prisma.journeyNotification.count({ where: { userId } }),
prisma.userTeamInvite.count({
where: { OR: [{ senderId: userId }, { receipientId: userId }] }
}),
prisma.userInvite.count({ where: { senderId: userId } }),
prisma.journeyEventsExportLog.count({ where: { userId } }),
prisma.journeyTheme.count({ where: { userId } })
])

const tablesToClean: string[] = []
if (userRole > 0) tablesToClean.push(`UserRole(${userRole})`)
if (journeyProfile > 0)
tablesToClean.push(`JourneyProfile(${journeyProfile})`)
if (integration > 0) tablesToClean.push(`Integration(${integration})`)
if (visitor > 0) tablesToClean.push(`Visitor(${visitor})`)
if (journeyNotification > 0)
tablesToClean.push(`JourneyNotification(${journeyNotification})`)
if (userTeamInvite > 0)
tablesToClean.push(`UserTeamInvite(${userTeamInvite})`)
if (userInvite > 0) tablesToClean.push(`UserInvite(${userInvite})`)
if (journeyEventsExportLog > 0)
tablesToClean.push(`JourneyEventsExportLog(${journeyEventsExportLog})`)
if (journeyTheme > 0) tablesToClean.push(`JourneyTheme(${journeyTheme})`)

if (tablesToClean.length > 0) {
logs.push(
createLog(`Related records to clean up: ${tablesToClean.join(', ')}`)
)
} else {
logs.push(createLog('No additional related records found'))
}

logs.push(createLog('Check complete. Ready for deletion confirmation.'))

return {
journeysToDelete,
journeysToTransfer,
journeysToRemove,
teamsToDelete,
teamsToTransfer,
teamsToRemove,
logs
}
}
})
)

export { UserDeleteJourneysLogEntry }
Loading
Loading