Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
71 changes: 65 additions & 6 deletions apis/api-gateway/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,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 @@ -559,6 +561,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 @@ -2531,8 +2534,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 @@ -2554,6 +2558,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 @@ -3301,6 +3330,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 @@ -3649,10 +3703,10 @@ enum MessagePlatform @join__type(graph: API_JOURNEYS) @join__type(graph: API_JO
menu1 @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
checkBroken @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
checkContained @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
settings @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
discord @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
signal @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
weChat @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
settings @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
discord @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
signal @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
weChat @join__enumValue(graph: API_JOURNEYS) @join__enumValue(graph: API_JOURNEYS_MODERN)
}

enum ButtonAction @join__type(graph: API_JOURNEYS) @join__type(graph: API_JOURNEYS_MODERN) {
Expand Down Expand Up @@ -3905,6 +3959,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: 27 additions & 3 deletions apis/api-journeys-modern/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1268,9 +1268,6 @@ enum MessagePlatform {
checkBroken
checkContained
settings
discord
signal
weChat
}

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

input MutationJourneyLanguageAiDetectInput {
Expand Down Expand Up @@ -2301,6 +2300,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,180 @@
import { prismaMock } from '../../../../test/prismaMock'

import { checkJourneysData } from './checkJourneysData'

describe('checkJourneysData', () => {
it('should return zero counts when user has no journeys or teams', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([])
prismaMock.userTeam.findMany.mockResolvedValueOnce([])
prismaMock.userRole.count.mockResolvedValueOnce(0)
prismaMock.journeyProfile.count.mockResolvedValueOnce(0)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

expect(result.journeysToDelete).toBe(0)
expect(result.journeysToTransfer).toBe(0)
expect(result.journeysToRemove).toBe(0)
expect(result.teamsToDelete).toBe(0)
expect(result.teamsToTransfer).toBe(0)
expect(result.teamsToRemove).toBe(0)
expect(result.logs.length).toBeGreaterThan(0)
})

it('should count sole-accessor journeys as toDelete', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([
{
id: 'uj1',
userId: 'user-1',
role: 'owner',
journey: {
id: 'j1',
title: 'My Journey',
userJourneys: [{ id: 'uj1', userId: 'user-1', role: 'owner' }]
}
}
] as any)
prismaMock.userTeam.findMany.mockResolvedValueOnce([])
prismaMock.userRole.count.mockResolvedValueOnce(0)
prismaMock.journeyProfile.count.mockResolvedValueOnce(0)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

expect(result.journeysToDelete).toBe(1)
expect(result.journeysToTransfer).toBe(0)
expect(result.journeysToRemove).toBe(0)
})

it('should count shared journeys where user is owner as toTransfer', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([
{
id: 'uj1',
userId: 'user-1',
role: 'owner',
journey: {
id: 'j1',
title: 'Shared Journey',
userJourneys: [
{ id: 'uj1', userId: 'user-1', role: 'owner' },
{ id: 'uj2', userId: 'user-2', role: 'editor' }
]
}
}
] as any)
prismaMock.userTeam.findMany.mockResolvedValueOnce([])
prismaMock.userRole.count.mockResolvedValueOnce(0)
prismaMock.journeyProfile.count.mockResolvedValueOnce(0)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

expect(result.journeysToDelete).toBe(0)
expect(result.journeysToTransfer).toBe(1)
expect(result.journeysToRemove).toBe(0)
})

it('should count shared journeys where user is not owner as toRemove', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([
{
id: 'uj1',
userId: 'user-1',
role: 'editor',
journey: {
id: 'j1',
title: 'Other Journey',
userJourneys: [
{ id: 'uj1', userId: 'user-1', role: 'editor' },
{ id: 'uj2', userId: 'user-2', role: 'owner' }
]
}
}
] as any)
prismaMock.userTeam.findMany.mockResolvedValueOnce([])
prismaMock.userRole.count.mockResolvedValueOnce(0)
prismaMock.journeyProfile.count.mockResolvedValueOnce(0)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

expect(result.journeysToDelete).toBe(0)
expect(result.journeysToTransfer).toBe(0)
expect(result.journeysToRemove).toBe(1)
})

it('should count sole-member teams as toDelete', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([])
prismaMock.userTeam.findMany.mockResolvedValueOnce([
{
id: 'ut1',
userId: 'user-1',
role: 'manager',
team: {
id: 't1',
title: 'My Team',
userTeams: [{ id: 'ut1', userId: 'user-1', role: 'manager' }]
}
}
] as any)
prismaMock.userRole.count.mockResolvedValueOnce(0)
prismaMock.journeyProfile.count.mockResolvedValueOnce(0)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

expect(result.teamsToDelete).toBe(1)
})

it('should report related records to clean up', async () => {
prismaMock.userJourney.findMany.mockResolvedValueOnce([])
prismaMock.userTeam.findMany.mockResolvedValueOnce([])
prismaMock.userRole.count.mockResolvedValueOnce(1)
prismaMock.journeyProfile.count.mockResolvedValueOnce(1)
prismaMock.integration.count.mockResolvedValueOnce(0)
prismaMock.visitor.count.mockResolvedValueOnce(0)
prismaMock.journeyNotification.count.mockResolvedValueOnce(0)
prismaMock.userTeamInvite.count.mockResolvedValueOnce(0)
prismaMock.userInvite.count.mockResolvedValueOnce(0)
prismaMock.journeyEventsExportLog.count.mockResolvedValueOnce(0)
prismaMock.journeyTheme.count.mockResolvedValueOnce(0)

const result = await checkJourneysData('user-1')

const cleanupLog = result.logs.find((l) =>
l.message.includes('Related records')
)
expect(cleanupLog).toBeDefined()
expect(cleanupLog?.message).toContain('UserRole(1)')
expect(cleanupLog?.message).toContain('JourneyProfile(1)')
})
})
Loading
Loading