Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
63 changes: 61 additions & 2 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,7 @@ 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)
}

type MutationSiteCreateSuccess @join__type(graph: API_ANALYTICS) {
Expand Down Expand Up @@ -2523,8 +2526,9 @@ type PlausibleStatsResponse @join__type(graph: API_JOURNEYS_MODERN) {
timeOnPage: Float
}

type Subscription @join__type(graph: API_JOURNEYS_MODERN) {
journeyAiTranslateCreateSubscription(input: JourneyAiTranslateInput!) : JourneyAiTranslateProgress!
type Subscription @join__type(graph: API_JOURNEYS_MODERN) @join__type(graph: API_USERS) {
journeyAiTranslateCreateSubscription(input: JourneyAiTranslateInput!) : JourneyAiTranslateProgress! @join__field(graph: API_JOURNEYS_MODERN)
userDeleteConfirm(idType: UserDeleteIdType!, id: String!) : UserDeleteConfirmProgress! @join__field(graph: API_USERS)
}

type TemplateFamilyStatsAggregateResponse @join__type(graph: API_JOURNEYS_MODERN) {
Expand All @@ -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,31 @@ 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 UserDeleteConfirmProgress @join__type(graph: API_USERS) {
log: UserDeleteLogEntry!
done: Boolean!
success: Boolean
}

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

interface BaseError @join__type(graph: API_ANALYTICS) @join__type(graph: API_MEDIA) {
message: String
}
Expand Down Expand Up @@ -3889,6 +3943,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
30 changes: 30 additions & 0 deletions apis/api-journeys-modern/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,9 @@ enum MessagePlatform {
checkBroken
checkContained
settings
discord
signal
weChat
}

type MultiselectBlock implements Block
Expand Down Expand Up @@ -1406,6 +1409,8 @@ type Mutation {
"""
timezone: String
): JourneyVisitorGoogleSheetExportResult!
userDeleteJourneysCheck(userId: String!): UserDeleteJourneysCheckResult!
userDeleteJourneysConfirm(userId: String!): UserDeleteJourneysConfirmResult!
}

input MutationJourneyLanguageAiDetectInput {
Expand Down Expand Up @@ -2288,6 +2293,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'
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { prisma } from '@core/prisma/journeys/client'

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

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

export async function checkJourneysData(
userId: string
): Promise<CheckJourneysDataResult> {
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
}
}
Loading
Loading