diff --git a/apis/api-gateway/schema.graphql b/apis/api-gateway/schema.graphql index 7da4e7190c2..a4a35fbd8c0 100644 --- a/apis/api-gateway/schema.graphql +++ b/apis/api-gateway/schema.graphql @@ -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) @@ -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) { @@ -2526,8 +2529,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) { @@ -2549,6 +2553,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 @@ -3296,6 +3325,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 } @@ -3644,10 +3698,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) { @@ -3900,6 +3954,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!] diff --git a/apis/api-journeys-modern/schema.graphql b/apis/api-journeys-modern/schema.graphql index eae9d8b31a8..22798b1dd5c 100644 --- a/apis/api-journeys-modern/schema.graphql +++ b/apis/api-journeys-modern/schema.graphql @@ -1268,9 +1268,6 @@ enum MessagePlatform { checkBroken checkContained settings - discord - signal - weChat } type MultiselectBlock implements Block @@ -1418,6 +1415,8 @@ type Mutation { """ timezone: String ): JourneyVisitorGoogleSheetExportResult! + userDeleteJourneysCheck(userId: String!): UserDeleteJourneysCheckResult! + userDeleteJourneysConfirm(userId: String!): UserDeleteJourneysConfirmResult! } input MutationJourneyLanguageAiDetectInput { @@ -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 diff --git a/apis/api-journeys-modern/src/schema/schema.ts b/apis/api-journeys-modern/src/schema/schema.ts index 509d23220cc..d0310336ba5 100644 --- a/apis/api-journeys-modern/src/schema/schema.ts +++ b/apis/api-journeys-modern/src/schema/schema.ts @@ -25,6 +25,7 @@ import './plausible' import './qrCode' import './team' import './user' +import './userDelete' import './userInvite' import './userJourney' import './userRole' diff --git a/apis/api-journeys-modern/src/schema/userDelete/index.ts b/apis/api-journeys-modern/src/schema/userDelete/index.ts new file mode 100644 index 00000000000..15b19535cc4 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/index.ts @@ -0,0 +1,2 @@ +import './userDeleteJourneysCheck' +import './userDeleteJourneysConfirm' diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.spec.ts b/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.spec.ts new file mode 100644 index 00000000000..6ee003c2874 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.spec.ts @@ -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)') + }) +}) diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.ts b/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.ts new file mode 100644 index 00000000000..3aa541a93a9 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/checkJourneysData.ts @@ -0,0 +1,175 @@ +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 { + 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) { + // Exclude inviteRequested — same logic as deleteJourneysData to keep + // check/confirm counts consistent. + const others = uj.journey.userJourneys.filter( + (j) => j.userId !== userId && j.role !== 'inviteRequested' + ) + 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 + } +} diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.spec.ts b/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.spec.ts new file mode 100644 index 00000000000..d4b21c2152b --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.spec.ts @@ -0,0 +1,120 @@ +import { prismaMock } from '../../../../test/prismaMock' + +import { deleteJourneysData } from './deleteJourneysData' + +describe('deleteJourneysData', () => { + // Nitpick: added beforeEach to prevent cross-test mock contamination + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should complete all phases successfully with no data', async () => { + const txMock = { + userJourney: { findMany: jest.fn().mockResolvedValue([]) }, + userTeam: { findMany: jest.fn().mockResolvedValue([]) } + } + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + prismaMock.userJourney.findMany.mockResolvedValueOnce([]) + prismaMock.userJourney.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.userTeam.findMany.mockResolvedValueOnce([]) + prismaMock.userTeam.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.journeyNotification.deleteMany.mockResolvedValueOnce({ + count: 0 + }) + prismaMock.userTeamInvite.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.userInvite.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.journeyEventsExportLog.deleteMany.mockResolvedValueOnce({ + count: 0 + }) + prismaMock.journeyTheme.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.journeyProfile.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.integration.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.userRole.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.visitor.deleteMany.mockResolvedValueOnce({ count: 0 }) + + const result = await deleteJourneysData('user-1') + + expect(result.success).toBe(true) + expect(result.deletedJourneyIds).toEqual([]) + expect(result.deletedTeamIds).toEqual([]) + }) + + it('should transfer ownership and delete sole-accessor journeys', async () => { + const txMock = { + userJourney: { + findMany: jest.fn().mockResolvedValue([ + { + id: 'uj1', + userId: 'user-1', + role: 'owner', + journey: { + id: 'j1', + title: 'Shared', + userJourneys: [ + { id: 'uj1', userId: 'user-1', role: 'owner' }, + { id: 'uj2', userId: 'user-2', role: 'editor' } + ] + } + }, + { + id: 'uj3', + userId: 'user-1', + role: 'owner', + journey: { + id: 'j2', + title: 'Solo', + userJourneys: [{ id: 'uj3', userId: 'user-1', role: 'owner' }] + } + } + ]), + updateMany: jest.fn().mockResolvedValue({ count: 1 }) + }, + userTeam: { findMany: jest.fn().mockResolvedValue([]) } + } + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + prismaMock.userJourney.findMany.mockResolvedValueOnce([ + { id: 'uj1' }, + { id: 'uj3' } + ] as any) + prismaMock.userJourney.deleteMany.mockResolvedValueOnce({ count: 2 }) + prismaMock.userTeam.findMany.mockResolvedValueOnce([]) + prismaMock.userTeam.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.event.deleteMany.mockResolvedValueOnce({ count: 5 }) + prismaMock.journeyVisitor.deleteMany.mockResolvedValueOnce({ count: 2 }) + prismaMock.action.deleteMany.mockResolvedValueOnce({ count: 3 }) + prismaMock.block.deleteMany.mockResolvedValueOnce({ count: 10 }) + prismaMock.journey.delete.mockResolvedValueOnce({} as any) + prismaMock.journeyNotification.deleteMany.mockResolvedValueOnce({ + count: 0 + }) + prismaMock.userTeamInvite.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.userInvite.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.journeyEventsExportLog.deleteMany.mockResolvedValueOnce({ + count: 0 + }) + prismaMock.journeyTheme.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.journeyProfile.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.integration.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.userRole.deleteMany.mockResolvedValueOnce({ count: 0 }) + prismaMock.visitor.deleteMany.mockResolvedValueOnce({ count: 0 }) + + const result = await deleteJourneysData('user-1') + + expect(result.success).toBe(true) + expect(result.deletedJourneyIds).toEqual(['j2']) + expect(txMock.userJourney.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + data: { role: 'owner' } + }) + ) + }) + + it('should return success false on error', async () => { + prismaMock.$transaction.mockRejectedValueOnce(new Error('DB crashed')) + + const result = await deleteJourneysData('user-1') + + expect(result.success).toBe(false) + expect(result.logs.some((l) => l.level === 'error')).toBe(true) + }) +}) diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.ts b/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.ts new file mode 100644 index 00000000000..5aa453b5064 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/deleteJourneysData.ts @@ -0,0 +1,263 @@ +import { prisma } from '@core/prisma/journeys/client' + +import { LogEntry, createLog } from './types' + +export interface DeleteJourneysDataResult { + success: boolean + deletedJourneyIds: string[] + deletedTeamIds: string[] + deletedUserJourneyIds: string[] + deletedUserTeamIds: string[] + logs: LogEntry[] +} + +export async function deleteJourneysData( + userId: string +): Promise { + const logs: LogEntry[] = [] + const deletedUserJourneyIds: string[] = [] + const deletedUserTeamIds: string[] = [] + const journeyIdsToDelete: string[] = [] + const teamIdsToDelete: string[] = [] + + try { + // Phase 1: Analyze and transfer ownership (lightweight, use transaction) + // Logs are buffered inside the transaction and only merged after commit so + // a rollback doesn't leave misleading "transferred ownership" entries. + const txLogs = await prisma.$transaction(async (tx) => { + const localLogs: LogEntry[] = [] + + const userJourneys = await tx.userJourney.findMany({ + where: { userId }, + include: { + journey: { + select: { + id: true, + title: true, + userJourneys: { + select: { id: true, userId: true, role: true } + } + } + } + } + }) + + deletedUserJourneyIds.push(...userJourneys.map((uj) => uj.id)) + + for (const uj of userJourneys) { + // Exclude inviteRequested — pending invites are not accepted + // collaborators and must not be counted when deciding whether to + // transfer ownership or mark a journey for deletion. + const others = uj.journey.userJourneys.filter( + (j) => j.userId !== userId && j.role !== 'inviteRequested' + ) + if (others.length === 0) { + journeyIdsToDelete.push(uj.journey.id) + } else if (uj.role === 'owner') { + const existingOwner = others.find((o) => o.role === 'owner') + if (existingOwner != null) { + localLogs.push( + createLog( + `Journey "${uj.journey.title}" already has another owner (${existingOwner.userId}), skipping transfer` + ) + ) + } else { + const nextOwner = + others.find((o) => o.role === 'editor') ?? others[0] + await tx.userJourney.updateMany({ + where: { + journeyId: uj.journey.id, + userId: nextOwner.userId + }, + data: { role: 'owner' } + }) + localLogs.push( + createLog( + `🔄 Transferred ownership of journey "${uj.journey.title}" to user ${nextOwner.userId}` + ) + ) + } + } + } + + const userTeams = await tx.userTeam.findMany({ + where: { userId }, + include: { + team: { + select: { + id: true, + title: true, + userTeams: { + select: { id: true, userId: true, role: true } + } + } + } + } + }) + + deletedUserTeamIds.push(...userTeams.map((ut) => ut.id)) + + for (const ut of userTeams) { + const others = ut.team.userTeams.filter((t) => t.userId !== userId) + if (others.length === 0) { + teamIdsToDelete.push(ut.team.id) + } else if (ut.role === 'manager') { + const existingManager = others.find((o) => o.role === 'manager') + if (existingManager != null) { + localLogs.push( + createLog( + `Team "${ut.team.title}" already has another manager (${existingManager.userId}), skipping transfer` + ) + ) + } else { + const nextManager = others[0] + await tx.userTeam.updateMany({ + where: { + teamId: ut.team.id, + userId: nextManager.userId + }, + data: { role: 'manager' } + }) + localLogs.push( + createLog( + `🔄 Transferred manager role of team "${ut.team.title}" to user ${nextManager.userId}` + ) + ) + } + } + } + + return localLogs + }) + logs.push(...txLogs) + logs.push(createLog('✅ Ownership transfers completed')) + + // Phase 2: Remove user memberships (IDs already collected in Phase 1) + await prisma.userJourney.deleteMany({ where: { userId } }) + logs.push( + createLog( + `🗑️ Removed ${deletedUserJourneyIds.length} user-journey memberships` + ) + ) + + await prisma.userTeam.deleteMany({ where: { userId } }) + logs.push( + createLog(`🗑️ Removed ${deletedUserTeamIds.length} user-team memberships`) + ) + + // Phase 3: Delete sole-accessor journeys + if (journeyIdsToDelete.length > 0) { + // Pre-delete heavy child records in parallel + const [eventCount, journeyVisitorCount, actionCount] = await Promise.all([ + prisma.event.deleteMany({ + where: { journeyId: { in: journeyIdsToDelete } } + }), + prisma.journeyVisitor.deleteMany({ + where: { journeyId: { in: journeyIdsToDelete } } + }), + prisma.action.deleteMany({ + where: { journeyId: { in: journeyIdsToDelete } } + }) + ]) + // Blocks after actions (actions reference blocks) + const blockCount = await prisma.block.deleteMany({ + where: { journeyId: { in: journeyIdsToDelete } } + }) + + if (eventCount.count > 0) + logs.push(createLog(`🗑️ Deleted ${eventCount.count} events`)) + if (journeyVisitorCount.count > 0) + logs.push( + createLog(`🗑️ Deleted ${journeyVisitorCount.count} journey visitors`) + ) + if (actionCount.count > 0) + logs.push(createLog(`🗑️ Deleted ${actionCount.count} actions`)) + if (blockCount.count > 0) + logs.push(createLog(`🗑️ Deleted ${blockCount.count} blocks`)) + + // Now delete the journeys (cascades are already cleared) + await prisma.journey.deleteMany({ + where: { id: { in: journeyIdsToDelete } } + }) + logs.push(createLog(`🗑️ Deleted ${journeyIdsToDelete.length} journeys`)) + } + + // Phase 4: Delete sole-member teams + if (teamIdsToDelete.length > 0) { + await prisma.team.deleteMany({ + where: { id: { in: teamIdsToDelete } } + }) + logs.push(createLog(`🗑️ Deleted ${teamIdsToDelete.length} teams`)) + } + + // Phase 5: Clean up related records (all independent, run in parallel) + const [ + journeyNotifications, + userTeamInvites, + userInvites, + exportLogs, + journeyThemes, + journeyProfile, + integrations, + userRoles, + visitors + ] = await Promise.all([ + prisma.journeyNotification.deleteMany({ where: { userId } }), + prisma.userTeamInvite.deleteMany({ + where: { OR: [{ senderId: userId }, { receipientId: userId }] } + }), + prisma.userInvite.deleteMany({ where: { senderId: userId } }), + prisma.journeyEventsExportLog.deleteMany({ where: { userId } }), + prisma.journeyTheme.deleteMany({ where: { userId } }), + prisma.journeyProfile.deleteMany({ where: { userId } }), + prisma.integration.deleteMany({ where: { userId } }), + prisma.userRole.deleteMany({ where: { userId } }), + prisma.visitor.deleteMany({ where: { userId } }) + ]) + + if (journeyNotifications.count > 0) + logs.push( + createLog(`Deleted ${journeyNotifications.count} journey notifications`) + ) + if (userTeamInvites.count > 0) + logs.push(createLog(`Deleted ${userTeamInvites.count} team invites`)) + if (userInvites.count > 0) + logs.push(createLog(`Deleted ${userInvites.count} journey invites`)) + if (exportLogs.count > 0) + logs.push(createLog(`Deleted ${exportLogs.count} export logs`)) + if (journeyThemes.count > 0) + logs.push(createLog(`Deleted ${journeyThemes.count} journey themes`)) + if (journeyProfile.count > 0) + logs.push(createLog(`Deleted ${journeyProfile.count} journey profile`)) + if (integrations.count > 0) + logs.push(createLog(`Deleted ${integrations.count} integrations`)) + if (userRoles.count > 0) + logs.push(createLog(`Deleted ${userRoles.count} user roles`)) + if (visitors.count > 0) + logs.push(createLog(`Deleted ${visitors.count} visitor records`)) + + logs.push(createLog('✅ Journeys database cleanup completed')) + + return { + success: true, + deletedJourneyIds: journeyIdsToDelete, + deletedTeamIds: teamIdsToDelete, + deletedUserJourneyIds, + deletedUserTeamIds, + logs + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + logs.push( + createLog(`❌ Journeys database cleanup failed: ${message}`, 'error') + ) + return { + success: false, + deletedJourneyIds: [], + deletedTeamIds: [], + deletedUserJourneyIds: [], + deletedUserTeamIds: [], + logs + } + } +} diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/index.ts b/apis/api-journeys-modern/src/schema/userDelete/service/index.ts new file mode 100644 index 00000000000..2a41f425bef --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/index.ts @@ -0,0 +1,6 @@ +export { checkJourneysData } from './checkJourneysData' +export type { CheckJourneysDataResult } from './checkJourneysData' +export { deleteJourneysData } from './deleteJourneysData' +export type { DeleteJourneysDataResult } from './deleteJourneysData' +export type { LogEntry } from './types' +export { createLog } from './types' diff --git a/apis/api-journeys-modern/src/schema/userDelete/service/types.ts b/apis/api-journeys-modern/src/schema/userDelete/service/types.ts new file mode 100644 index 00000000000..fc067c8b20d --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/service/types.ts @@ -0,0 +1,12 @@ +// Nitpick: extracted into a named type to avoid repeating the union +export type LogLevel = 'info' | 'warn' | 'error' + +export interface LogEntry { + message: string + level: LogLevel + timestamp: string +} + +export function createLog(message: string, level: LogLevel = 'info'): LogEntry { + return { message, level, timestamp: new Date().toISOString() } +} diff --git a/apis/api-journeys-modern/src/schema/userDelete/types.ts b/apis/api-journeys-modern/src/schema/userDelete/types.ts new file mode 100644 index 00000000000..fe1e4515fb7 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/types.ts @@ -0,0 +1,2 @@ +export type { LogEntry } from './service' +export { createLog } from './service' diff --git a/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysCheck.ts b/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysCheck.ts new file mode 100644 index 00000000000..01553ed2ee3 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysCheck.ts @@ -0,0 +1,61 @@ +import { builder } from '../builder' + +import { type LogEntry, checkJourneysData } from './service' + +const UserDeleteJourneysLogEntry = builder.objectRef( + '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 UserDeleteJourneysCheckResultShape { + journeysToDelete: number + journeysToTransfer: number + journeysToRemove: number + teamsToDelete: number + teamsToTransfer: number + teamsToRemove: number + logs: LogEntry[] +} + +const UserDeleteJourneysCheckResult = + builder.objectRef( + '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 + }) + }) +}) + +export { UserDeleteJourneysLogEntry } + +builder.mutationField('userDeleteJourneysCheck', (t) => + t.withAuth({ isValidInterop: true }).field({ + type: UserDeleteJourneysCheckResult, + nullable: false, + args: { + userId: t.arg.string({ required: true }) + }, + resolve: async (_parent, { userId }) => { + return await checkJourneysData(userId) + } + }) +) diff --git a/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysConfirm.ts b/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysConfirm.ts new file mode 100644 index 00000000000..9f781c86653 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/userDelete/userDeleteJourneysConfirm.ts @@ -0,0 +1,52 @@ +import { builder } from '../builder' + +import { type LogEntry, deleteJourneysData } from './service' +import { UserDeleteJourneysLogEntry } from './userDeleteJourneysCheck' + +interface UserDeleteJourneysConfirmResultShape { + success: boolean + deletedJourneyIds: string[] + deletedTeamIds: string[] + deletedUserJourneyIds: string[] + deletedUserTeamIds: string[] + logs: LogEntry[] +} + +const UserDeleteJourneysConfirmResult = + builder.objectRef( + 'UserDeleteJourneysConfirmResult' + ) + +builder.objectType(UserDeleteJourneysConfirmResult, { + fields: (t) => ({ + success: t.exposeBoolean('success', { nullable: false }), + deletedJourneyIds: t.exposeStringList('deletedJourneyIds', { + nullable: false + }), + deletedTeamIds: t.exposeStringList('deletedTeamIds', { nullable: false }), + deletedUserJourneyIds: t.exposeStringList('deletedUserJourneyIds', { + nullable: false + }), + deletedUserTeamIds: t.exposeStringList('deletedUserTeamIds', { + nullable: false + }), + logs: t.field({ + type: [UserDeleteJourneysLogEntry], + nullable: false, + resolve: (parent) => parent.logs + }) + }) +}) + +builder.mutationField('userDeleteJourneysConfirm', (t) => + t.withAuth({ isValidInterop: true }).field({ + type: UserDeleteJourneysConfirmResult, + nullable: false, + args: { + userId: t.arg.string({ required: true }) + }, + resolve: async (_parent, { userId }) => { + return await deleteJourneysData(userId) + } + }) +) diff --git a/apis/api-users/infrastructure/locals.tf b/apis/api-users/infrastructure/locals.tf index 49f4247bc03..98fc8cfd5de 100644 --- a/apis/api-users/infrastructure/locals.tf +++ b/apis/api-users/infrastructure/locals.tf @@ -5,6 +5,7 @@ locals { "AWS_SECRET_ACCESS_KEY", "EXAMPLE_EMAIL_TOKEN", "GATEWAY_HMAC_SECRET", + "GATEWAY_URL", "GOOGLE_APPLICATION_JSON", "INTEROP_TOKEN", "JESUS_FILM_PROJECT_VERIFY_URL", diff --git a/apis/api-users/schema.graphql b/apis/api-users/schema.graphql index 0696bea15b8..d9c238e9b4c 100644 --- a/apis/api-users/schema.graphql +++ b/apis/api-users/schema.graphql @@ -39,6 +39,7 @@ type Mutation { userImpersonate(email: String!): String createVerificationRequest(input: CreateVerificationRequestInput): Boolean validateEmail(email: String!, token: String!): AuthenticatedUser + userDeleteCheck(idType: UserDeleteIdType!, id: String!): UserDeleteCheckResult! } type Query { @@ -47,8 +48,42 @@ type Query { userByEmail(email: String!): AuthenticatedUser } +type Subscription { + userDeleteConfirm(idType: UserDeleteIdType!, id: String!): UserDeleteConfirmProgress! +} + interface User @key(fields: "id") { id: ID! +} + +type UserDeleteCheckResult { + userId: String! + userEmail: String + userFirstName: String! + journeysToDelete: Int! + journeysToTransfer: Int! + journeysToRemove: Int! + teamsToDelete: Int! + teamsToTransfer: Int! + teamsToRemove: Int! + logs: [UserDeleteLogEntry!]! +} + +type UserDeleteConfirmProgress { + log: UserDeleteLogEntry! + done: Boolean! + success: Boolean +} + +enum UserDeleteIdType { + databaseId + email +} + +type UserDeleteLogEntry { + message: String! + level: String! + timestamp: String! } \ No newline at end of file diff --git a/apis/api-users/src/schema/builder.ts b/apis/api-users/src/schema/builder.ts index 93565780fa3..16c8df70bad 100644 --- a/apis/api-users/src/schema/builder.ts +++ b/apis/api-users/src/schema/builder.ts @@ -112,3 +112,4 @@ export const builder = new SchemaBuilder<{ builder.queryType({}) builder.mutationType({}) +builder.subscriptionType({}) diff --git a/apis/api-users/src/schema/schema.ts b/apis/api-users/src/schema/schema.ts index 312bf6e9b3e..21f3205763e 100644 --- a/apis/api-users/src/schema/schema.ts +++ b/apis/api-users/src/schema/schema.ts @@ -2,6 +2,7 @@ // and object type in the schema import './user' +import './userDelete' import { builder } from './builder' diff --git a/apis/api-users/src/schema/user/user.ts b/apis/api-users/src/schema/user/user.ts index f9e3e05553b..11113a7fc48 100644 --- a/apis/api-users/src/schema/user/user.ts +++ b/apis/api-users/src/schema/user/user.ts @@ -11,6 +11,15 @@ import { AnonymousUser, AuthenticatedUser, User } from './objects' import { validateEmail } from './validateEmail' import { verifyUser } from './verifyUser' +function isFirebaseNotFound(error: unknown): boolean { + return ( + error != null && + typeof error === 'object' && + 'code' in error && + (error as { code: string }).code === 'auth/user-not-found' + ) +} + builder.asEntity(User, { key: builder.selection<{ id: string }>('id'), resolveReference: async ({ id }) => { @@ -91,12 +100,22 @@ builder.queryFields((t) => ({ }) }, resolve: async (_parent, { input }, ctx) => { - const user = await findOrFetchUser( - {}, - ctx.currentUser.id, - input?.redirect ?? undefined, - input?.app ?? 'NextSteps' - ) + let user + try { + user = await findOrFetchUser( + {}, + ctx.currentUser.id, + input?.redirect ?? undefined, + input?.app ?? 'NextSteps' + ) + } catch (error) { + if (isFirebaseNotFound(error)) { + throw new GraphQLError('User account has been deleted', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + throw error + } if (user == null) return null // Return appropriate type based on whether user has email diff --git a/apis/api-users/src/schema/userDelete/index.ts b/apis/api-users/src/schema/userDelete/index.ts new file mode 100644 index 00000000000..dc083fb2ec6 --- /dev/null +++ b/apis/api-users/src/schema/userDelete/index.ts @@ -0,0 +1,2 @@ +import './userDeleteCheck' +import './userDeleteConfirm' diff --git a/apis/api-users/src/schema/userDelete/service/deleteUserData.spec.ts b/apis/api-users/src/schema/userDelete/service/deleteUserData.spec.ts new file mode 100644 index 00000000000..38bd55f2d81 --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/deleteUserData.spec.ts @@ -0,0 +1,154 @@ +import { prismaMock } from '../../../../test/prismaMock' + +import { deleteFirebaseUser, deleteUserData } from './deleteUserData' + +const mockDeleteUser = jest.fn() +const mockGetUserByEmail = jest.fn() + +jest.mock('@core/yoga/firebaseClient', () => ({ + auth: { + deleteUser: (...args: unknown[]) => mockDeleteUser(...args), + getUserByEmail: (...args: unknown[]) => mockGetUserByEmail(...args) + } +})) + +const baseInput = { + userDbId: 'db-1', + firebaseUserId: 'fb-uid-1', + firebaseUidOverride: null, + userEmail: 'test@example.com', + userFirstName: 'Test', + userLastName: 'User', + callerUserId: 'caller-1', + callerEmail: 'admin@example.com', + callerFirstName: 'Admin', + callerLastName: 'User', + deletedJourneyIds: ['j1'], + deletedTeamIds: ['t1'], + deletedUserJourneyIds: ['uj1'], + deletedUserTeamIds: ['ut1'] +} + +describe('deleteFirebaseUser', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should delete firebase user by UID', async () => { + mockDeleteUser.mockResolvedValueOnce(undefined) + + const logs = await deleteFirebaseUser('fb-uid-1', null, 'test@example.com') + + expect(mockDeleteUser).toHaveBeenCalledWith('fb-uid-1') + expect(logs.some((l) => l.message.includes('deleted'))).toBe(true) + }) + + it('should try override UID as well', async () => { + mockDeleteUser.mockResolvedValueOnce(undefined) + mockDeleteUser.mockResolvedValueOnce(undefined) + + const logs = await deleteFirebaseUser( + 'fb-uid-1', + 'override-uid', + 'test@example.com' + ) + + expect(mockDeleteUser).toHaveBeenCalledWith('fb-uid-1') + expect(mockDeleteUser).toHaveBeenCalledWith('override-uid') + expect(logs.filter((l) => l.message.includes('deleted')).length).toBe(2) + }) + + it('should fall back to email when UID not found', async () => { + mockDeleteUser + .mockRejectedValueOnce({ code: 'auth/user-not-found' }) + .mockResolvedValueOnce(undefined) + mockGetUserByEmail.mockResolvedValueOnce({ uid: 'email-uid' }) + + const logs = await deleteFirebaseUser('fb-uid-1', null, 'test@example.com') + + expect(mockGetUserByEmail).toHaveBeenCalledWith('test@example.com') + expect(mockDeleteUser).toHaveBeenCalledWith('email-uid') + }) + + it('should return error log on hard firebase failure', async () => { + mockDeleteUser.mockRejectedValueOnce(new Error('Firebase internal error')) + + const logs = await deleteFirebaseUser('fb-uid-1', null, null) + + expect(logs.some((l) => l.level === 'error')).toBe(true) + }) +}) + +describe('deleteUserData', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should successfully delete user data', async () => { + mockDeleteUser.mockResolvedValueOnce(undefined) + prismaMock.userDeleteAuditLog.create.mockResolvedValueOnce({ + id: 'audit-1' + } as any) + prismaMock.user.delete.mockResolvedValueOnce({} as any) + prismaMock.userDeleteAuditLog.update.mockResolvedValueOnce({} as any) + + const result = await deleteUserData(baseInput) + + expect(result.success).toBe(true) + expect(prismaMock.user.delete).toHaveBeenCalledWith({ + where: { id: 'db-1' } + }) + expect(prismaMock.userDeleteAuditLog.update).toHaveBeenCalledWith({ + where: { id: 'audit-1' }, + data: { success: true } + }) + }) + + it('should fail if firebase deletion has hard error', async () => { + // Comment 2: audit log is now created BEFORE Firebase deletion (Comment 3 + // fix), so these mocks are required — the function reaches audit log + // creation before hitting the Firebase error. + prismaMock.userDeleteAuditLog.create.mockResolvedValueOnce({ + id: 'audit-1' + } as any) + mockDeleteUser.mockRejectedValueOnce(new Error('Firebase error')) + prismaMock.userDeleteAuditLog.update.mockResolvedValueOnce({} as any) + + const result = await deleteUserData(baseInput) + + expect(result.success).toBe(false) + expect(prismaMock.user.delete).not.toHaveBeenCalled() + }) + + it('should fail if user record deletion fails', async () => { + mockDeleteUser.mockResolvedValueOnce(undefined) + prismaMock.userDeleteAuditLog.create.mockResolvedValueOnce({ + id: 'audit-1' + } as any) + prismaMock.user.delete.mockRejectedValueOnce(new Error('DB error')) + prismaMock.userDeleteAuditLog.update.mockResolvedValueOnce({} as any) + + const result = await deleteUserData(baseInput) + + expect(result.success).toBe(false) + expect(prismaMock.userDeleteAuditLog.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + errorMessage: expect.stringContaining('DB error') + }) + }) + ) + }) + + it('should fail if audit log creation fails', async () => { + mockDeleteUser.mockResolvedValueOnce(undefined) + prismaMock.userDeleteAuditLog.create.mockRejectedValueOnce( + new Error('Audit error') + ) + + const result = await deleteUserData(baseInput) + + expect(result.success).toBe(false) + expect(prismaMock.user.delete).not.toHaveBeenCalled() + }) +}) diff --git a/apis/api-users/src/schema/userDelete/service/deleteUserData.ts b/apis/api-users/src/schema/userDelete/service/deleteUserData.ts new file mode 100644 index 00000000000..91391f38ffc --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/deleteUserData.ts @@ -0,0 +1,180 @@ +import { prisma } from '@core/prisma/users/client' +import { auth } from '@core/yoga/firebaseClient' + +import { LogEntry, createLog, isFirebaseNotFound } from './types' + +interface DeleteUserDataInput { + userDbId: string + firebaseUserId: string + firebaseUidOverride: string | null + userEmail: string | null + callerUserId: string + callerEmail: string | null + deletedJourneyIds: string[] + deletedTeamIds: string[] + deletedUserJourneyIds: string[] + deletedUserTeamIds: string[] +} + +interface DeleteUserDataResult { + success: boolean + logs: LogEntry[] +} + +export async function deleteFirebaseUser( + firebaseUserId: string, + firebaseUidOverride: string | null, + email: string | null +): Promise { + const logs: LogEntry[] = [] + const uidsToTry = new Set() + + // Primary UID from the DB + uidsToTry.add(firebaseUserId) + + // Override UID from the lookup's email-based Firebase check + if (firebaseUidOverride != null) { + uidsToTry.add(firebaseUidOverride) + } + + let anyDeleted = false + + for (const uid of uidsToTry) { + try { + await auth.deleteUser(uid) + logs.push(createLog(`🔥 Firebase auth record deleted (UID: ${uid})`)) + anyDeleted = true + } catch (error) { + if (isFirebaseNotFound(error)) { + logs.push(createLog(`⚠️ No Firebase auth record for UID: ${uid}`)) + } else { + console.error(`Failed to delete Firebase auth (UID: ${uid}):`, error) + logs.push( + createLog('❌ Failed to delete Firebase auth record', 'error') + ) + return logs + } + } + } + + // Final safety check: if nothing was deleted by UID, try by email + if (!anyDeleted && email != null) { + try { + const fbUser = await auth.getUserByEmail(email) + await auth.deleteUser(fbUser.uid) + logs.push( + createLog( + `🔥 Firebase auth record deleted via email fallback (UID: ${fbUser.uid})` + ) + ) + } catch (error) { + if (isFirebaseNotFound(error)) { + logs.push(createLog('🔥 No Firebase auth record found by email either')) + } else { + console.error( + 'Failed to delete Firebase auth via email fallback:', + error + ) + logs.push( + createLog( + '❌ Failed to delete Firebase auth via email fallback', + 'error' + ) + ) + return logs + } + } + } + + return logs +} + +export async function deleteUserData( + input: DeleteUserDataInput +): Promise { + const logs: LogEntry[] = [] + + // Create audit log FIRST — before any irreversible action so there is + // always a durable record of the deletion attempt even if a subsequent + // step fails. + let auditLog: { id: string } | null = null + try { + auditLog = await prisma.userDeleteAuditLog.create({ + data: { + deletedUserId: input.userDbId, + callerUserId: input.callerUserId, + callerEmail: input.callerEmail, + deletedJourneyIds: input.deletedJourneyIds, + deletedTeamIds: input.deletedTeamIds, + deletedUserJourneyIds: input.deletedUserJourneyIds, + deletedUserTeamIds: input.deletedUserTeamIds, + success: false + } + }) + logs.push(createLog('📝 Audit log created')) + } catch (error) { + console.error('Failed to create audit log:', error) + logs.push( + createLog('❌ Failed to create audit log. Aborting deletion.', 'error') + ) + return { success: false, logs } + } + + // 2. Delete Firebase auth record(s) + const fbLogs = await deleteFirebaseUser( + input.firebaseUserId, + input.firebaseUidOverride, + input.userEmail + ) + logs.push(...fbLogs) + + const hasFirebaseError = fbLogs.some((log) => log.level === 'error') + if (hasFirebaseError) { + // Best-effort update — the user is NOT yet deleted, so a failure here + // just leaves the audit record with success: false (correct). + try { + await prisma.userDeleteAuditLog.update({ + where: { id: auditLog.id }, + data: { errorMessage: 'Firebase deletion failed' } + }) + } catch { + // best-effort + } + return { success: false, logs } + } + + // 3. Delete User record + try { + await prisma.user.delete({ where: { id: input.userDbId } }) + logs.push(createLog('🗑️ User record deleted from database')) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + // Best-effort update — don't let an audit log write failure mask the + // real error or report the deletion as failed twice. + try { + await prisma.userDeleteAuditLog.update({ + where: { id: auditLog.id }, + data: { errorMessage: `Failed to delete user record: ${message}` } + }) + } catch { + // best-effort + } + console.error('Failed to delete user record:', error) + logs.push(createLog('❌ Failed to delete user record', 'error')) + return { success: false, logs } + } + + // Update audit log to success (best-effort — user is already deleted; + // a transient write failure here must not surface as a deletion failure). + try { + await prisma.userDeleteAuditLog.update({ + where: { id: auditLog.id }, + data: { success: true } + }) + } catch (error) { + console.error('Failed to update audit log to success:', error) + } + + logs.push(createLog('✅ User deleted successfully')) + return { success: true, logs } +} diff --git a/apis/api-users/src/schema/userDelete/service/index.ts b/apis/api-users/src/schema/userDelete/service/index.ts new file mode 100644 index 00000000000..2a78738c02a --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/index.ts @@ -0,0 +1,5 @@ +export { lookupUser } from './lookupUser' +export { deleteFirebaseUser, deleteUserData } from './deleteUserData' +export { callJourneysCheck, callJourneysConfirm } from './journeysInterop' +export type { LogEntry } from './types' +export { createLog } from './types' diff --git a/apis/api-users/src/schema/userDelete/service/journeysInterop.spec.ts b/apis/api-users/src/schema/userDelete/service/journeysInterop.spec.ts new file mode 100644 index 00000000000..e52e65aa10d --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/journeysInterop.spec.ts @@ -0,0 +1,98 @@ +import { callJourneysCheck, callJourneysConfirm } from './journeysInterop' + +const mockMutate = jest.fn() + +jest.mock('@core/yoga/apolloClient', () => ({ + createApolloClient: () => ({ + mutate: (...args: unknown[]) => mockMutate(...args) + }) +})) + +describe('callJourneysCheck', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return check result on success', async () => { + const expected = { + journeysToDelete: 2, + journeysToTransfer: 1, + journeysToRemove: 3, + teamsToDelete: 0, + teamsToTransfer: 1, + teamsToRemove: 0, + logs: [{ message: 'test', level: 'info', timestamp: '2026-01-01' }] + } + mockMutate.mockResolvedValueOnce({ + data: { userDeleteJourneysCheck: expected } + }) + + const result = await callJourneysCheck('user-123') + + expect(result).toEqual(expected) + expect(mockMutate).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { userId: 'user-123' }, + fetchPolicy: 'no-cache' + }) + ) + }) + + it('should return fallback on null data', async () => { + mockMutate.mockResolvedValueOnce({ data: null }) + + const result = await callJourneysCheck('user-123') + + expect(result.journeysToDelete).toBe(0) + expect(result.logs[0].level).toBe('error') + }) + + it('should throw on exception so callers can distinguish failure from empty', async () => { + // Comment 5: function now rethrows instead of returning all-zero counts, + // so callers can tell a network failure apart from "nothing to clean up". + mockMutate.mockRejectedValueOnce(new Error('Network error')) + + await expect(callJourneysCheck('user-123')).rejects.toThrow('Network error') + }) +}) + +describe('callJourneysConfirm', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return confirm result on success', async () => { + const expected = { + success: true, + deletedJourneyIds: ['j1'], + deletedTeamIds: ['t1'], + deletedUserJourneyIds: ['uj1'], + deletedUserTeamIds: ['ut1'], + logs: [{ message: 'done', level: 'info', timestamp: '2026-01-01' }] + } + mockMutate.mockResolvedValueOnce({ + data: { userDeleteJourneysConfirm: expected } + }) + + const result = await callJourneysConfirm('user-123') + + expect(result).toEqual(expected) + }) + + it('should return failure on null data', async () => { + mockMutate.mockResolvedValueOnce({ data: null }) + + const result = await callJourneysConfirm('user-123') + + expect(result.success).toBe(false) + expect(result.logs[0].level).toBe('error') + }) + + it('should throw on exception so callers can distinguish failure from empty', async () => { + // Comment 5: function now rethrows instead of returning success:false with + // all-zero counts, consistent with callJourneysCheck behaviour. + mockMutate.mockRejectedValueOnce(new Error('Timeout')) + + await expect(callJourneysConfirm('user-123')).rejects.toThrow('Timeout') + }) +}) diff --git a/apis/api-users/src/schema/userDelete/service/journeysInterop.ts b/apis/api-users/src/schema/userDelete/service/journeysInterop.ts new file mode 100644 index 00000000000..f70552d35c1 --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/journeysInterop.ts @@ -0,0 +1,135 @@ +import { gql } from '@apollo/client' + +import { createApolloClient } from '@core/yoga/apolloClient' + +import { LogEntry, createLog } from './types' + +const apolloClient = createApolloClient('api-users') +const INTEROP_TIMEOUT_MS = 120_000 + +interface JourneysCheckResult { + journeysToDelete: number + journeysToTransfer: number + journeysToRemove: number + teamsToDelete: number + teamsToTransfer: number + teamsToRemove: number + logs: LogEntry[] +} + +interface JourneysConfirmResult { + success: boolean + deletedJourneyIds: string[] + deletedTeamIds: string[] + deletedUserJourneyIds: string[] + deletedUserTeamIds: string[] + logs: LogEntry[] +} + +const USER_DELETE_JOURNEYS_CHECK = gql` + mutation UserDeleteJourneysCheck($userId: String!) { + userDeleteJourneysCheck(userId: $userId) { + journeysToDelete + journeysToTransfer + journeysToRemove + teamsToDelete + teamsToTransfer + teamsToRemove + logs { + message + level + timestamp + } + } + } +` + +const USER_DELETE_JOURNEYS_CONFIRM = gql` + mutation UserDeleteJourneysConfirm($userId: String!) { + userDeleteJourneysConfirm(userId: $userId) { + success + deletedJourneyIds + deletedTeamIds + deletedUserJourneyIds + deletedUserTeamIds + logs { + message + level + timestamp + } + } + } +` + +export async function callJourneysCheck( + userId: string +): Promise { + try { + const { data } = await apolloClient.mutate<{ + userDeleteJourneysCheck: JourneysCheckResult + }>({ + mutation: USER_DELETE_JOURNEYS_CHECK, + variables: { userId }, + fetchPolicy: 'no-cache', + context: { + fetchOptions: { signal: AbortSignal.timeout(INTEROP_TIMEOUT_MS) } + } + }) + + if (data?.userDeleteJourneysCheck == null) { + return { + journeysToDelete: 0, + journeysToTransfer: 0, + journeysToRemove: 0, + teamsToDelete: 0, + teamsToTransfer: 0, + teamsToRemove: 0, + logs: [createLog('❌ No data returned from journeys check', 'error')] + } + } + + return data.userDeleteJourneysCheck + } catch (error) { + // Rethrow so callers can distinguish a network/API failure from a + // legitimate "nothing to clean up" result (returning all-zero counts + // made interop failures indistinguishable from a clean empty result). + console.error('Journeys check failed:', error) + throw error + } +} + +export async function callJourneysConfirm( + userId: string +): Promise { + try { + const { data } = await apolloClient.mutate<{ + userDeleteJourneysConfirm: JourneysConfirmResult + }>({ + mutation: USER_DELETE_JOURNEYS_CONFIRM, + variables: { userId }, + fetchPolicy: 'no-cache', + context: { + fetchOptions: { signal: AbortSignal.timeout(INTEROP_TIMEOUT_MS) } + } + }) + + if (data?.userDeleteJourneysConfirm == null) { + return { + success: false, + deletedJourneyIds: [], + deletedTeamIds: [], + deletedUserJourneyIds: [], + deletedUserTeamIds: [], + logs: [createLog('❌ No data returned from journeys confirm', 'error')] + } + } + + return data.userDeleteJourneysConfirm + } catch (error) { + // Rethrow so callers can distinguish a network/API failure from a + // legitimate empty result — swallowing here made interop failures + // indistinguishable from a successful no-op deletion. + console.error('Journeys deletion failed:', error) + throw error + } +} diff --git a/apis/api-users/src/schema/userDelete/service/lookupUser.spec.ts b/apis/api-users/src/schema/userDelete/service/lookupUser.spec.ts new file mode 100644 index 00000000000..2283cfa591c --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/lookupUser.spec.ts @@ -0,0 +1,131 @@ +import { GraphQLError } from 'graphql' + +import { prismaMock } from '../../../../test/prismaMock' + +import { lookupUser } from './lookupUser' + +const mockGetUser = jest.fn() +const mockGetUserByEmail = jest.fn() + +jest.mock('@core/yoga/firebaseClient', () => ({ + auth: { + getUser: (...args: unknown[]) => mockGetUser(...args), + getUserByEmail: (...args: unknown[]) => mockGetUserByEmail(...args) + } +})) + +const mockUser = { + id: 'db-id-1', + userId: 'firebase-uid-1', + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + imageUrl: null, + superAdmin: false, + emailVerified: true, + createdAt: new Date() +} + +describe('lookupUser', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should find user by email with firebase by UID', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(mockUser as any) + mockGetUser.mockResolvedValueOnce({ + uid: 'firebase-uid-1', + email: 'john@example.com', + disabled: false, + providerData: [{ providerId: 'google.com' }] + }) + + const result = await lookupUser('email', 'john@example.com') + + expect(result.user).toEqual(mockUser) + expect(result.firebase.exists).toBe(true) + expect(result.firebase.uid).toBe('firebase-uid-1') + expect(prismaMock.user.findUnique).toHaveBeenCalledWith({ + where: { email: 'john@example.com' } + }) + }) + + it('should find user by databaseId', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(mockUser as any) + mockGetUser.mockResolvedValueOnce({ + uid: 'firebase-uid-1', + email: 'john@example.com', + disabled: false, + providerData: [] + }) + + const result = await lookupUser('databaseId', 'db-id-1') + + expect(prismaMock.user.findUnique).toHaveBeenCalledWith({ + where: { id: 'db-id-1' } + }) + expect(result.user).toEqual(mockUser) + }) + + it('should fall back to firebase email lookup when UID not found', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(mockUser as any) + mockGetUser.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + mockGetUserByEmail.mockResolvedValueOnce({ + uid: 'different-uid', + email: 'john@example.com', + disabled: false, + providerData: [{ providerId: 'password' }] + }) + + const result = await lookupUser('email', 'john@example.com') + + expect(result.firebase.exists).toBe(true) + expect(result.firebase.uid).toBe('different-uid') + }) + + it('should return firebase-only when no DB user but firebase exists by email', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(null) + mockGetUser.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + mockGetUserByEmail.mockResolvedValueOnce({ + uid: 'orphan-uid', + email: 'orphan@example.com', + disabled: false, + providerData: [{ providerId: 'google.com' }] + }) + + const result = await lookupUser('email', 'orphan@example.com') + + expect(result.user).toBeNull() + expect(result.firebase.exists).toBe(true) + expect(result.firebase.uid).toBe('orphan-uid') + }) + + it('should throw NOT_FOUND when no DB user and no firebase by email', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(null) + mockGetUser.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + mockGetUserByEmail.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + + await expect(lookupUser('email', 'gone@example.com')).rejects.toThrow( + GraphQLError + ) + }) + + it('should throw NOT_FOUND when no DB user by databaseId', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(null) + + await expect(lookupUser('databaseId', 'nonexistent-id')).rejects.toThrow( + GraphQLError + ) + }) + + it('should report no firebase record when both UID and email fail', async () => { + prismaMock.user.findUnique.mockResolvedValueOnce(mockUser as any) + mockGetUser.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + mockGetUserByEmail.mockRejectedValueOnce({ code: 'auth/user-not-found' }) + + const result = await lookupUser('email', 'john@example.com') + + expect(result.user).toEqual(mockUser) + expect(result.firebase.exists).toBe(false) + }) +}) diff --git a/apis/api-users/src/schema/userDelete/service/lookupUser.ts b/apis/api-users/src/schema/userDelete/service/lookupUser.ts new file mode 100644 index 00000000000..8b9b759d3bb --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/lookupUser.ts @@ -0,0 +1,154 @@ +import { GraphQLError } from 'graphql' + +import { User, prisma } from '@core/prisma/users/client' +import { auth } from '@core/yoga/firebaseClient' + +import { LogEntry, createLog, isFirebaseNotFound } from './types' + +interface FirebaseStatus { + exists: boolean + uid: string | null + email: string | null + disabled: boolean + providerIds: string[] +} + +interface LookupResult { + user: User | null + firebase: FirebaseStatus + logs: LogEntry[] +} + +async function checkFirebaseUser( + userId: string, + email: string | null +): Promise<{ status: FirebaseStatus; logs: LogEntry[] }> { + const logs: LogEntry[] = [] + + // Skip UID lookup when userId is empty — an empty string triggers + // auth/invalid-uid in Firebase which is NOT a not-found error and must not + // be swallowed. This path is hit when calling checkFirebaseUser('', email) + // for a firebase-only email lookup. + if (userId !== '') { + try { + const fbUser = await auth.getUser(userId) + const providerIds = fbUser.providerData.map((p) => p.providerId) + logs.push( + createLog( + `🔥 Firebase user found by UID: ${fbUser.email ?? 'no email'} (providers: ${providerIds.join(', ') || 'none'}, disabled: ${fbUser.disabled})` + ) + ) + return { + status: { + exists: true, + uid: fbUser.uid, + email: fbUser.email ?? null, + disabled: fbUser.disabled, + providerIds + }, + logs + } + } catch (error) { + if (!isFirebaseNotFound(error)) { + // Only user-not-found should fall through to the email fallback; + // transient failures (network, auth permissions) must not be silently + // treated as "user absent". + throw error + } + } + } + + // UID not found or skipped — try by email as fallback + if (email != null) { + try { + const fbUser = await auth.getUserByEmail(email) + const providerIds = fbUser.providerData.map((p) => p.providerId) + logs.push( + createLog( + `⚠️ Firebase user NOT found by UID (${userId}) but FOUND by email (${email}). Firebase UID: ${fbUser.uid} (providers: ${providerIds.join(', ') || 'none'}, disabled: ${fbUser.disabled})`, + 'warn' + ) + ) + return { + status: { + exists: true, + uid: fbUser.uid, + email: fbUser.email ?? null, + disabled: fbUser.disabled, + providerIds + }, + logs + } + } catch (error) { + if (!isFirebaseNotFound(error)) { + throw error + } + } + } + + logs.push(createLog('🔥 No Firebase auth record found')) + return { + status: { + exists: false, + uid: null, + email: null, + disabled: false, + providerIds: [] + }, + logs + } +} + +export async function lookupUser( + idType: 'databaseId' | 'email', + id: string +): Promise { + const logs: LogEntry[] = [] + logs.push(createLog(`🔍 Looking for user by ${idType}: ${id}`)) + + const user = + idType === 'email' + ? await prisma.user.findUnique({ where: { email: id } }) + : await prisma.user.findUnique({ where: { id } }) + + if (user != null) { + logs.push( + createLog( + `✅ User found: ${user.firstName} ${user.lastName ?? ''} (${user.email ?? 'no email'})` + ) + ) + + const { status: firebase, logs: fbLogs } = await checkFirebaseUser( + user.userId, + user.email + ) + logs.push(...fbLogs) + + return { user, firebase, logs } + } + + // No DB user — check Firebase directly (only possible with email lookup) + logs.push(createLog(`⚠️ No database user found for ${idType}: ${id}`, 'warn')) + + if (idType === 'email') { + const { status: firebase, logs: fbLogs } = await checkFirebaseUser('', id) + logs.push(...fbLogs) + + if (firebase.exists) { + logs.push( + createLog( + '⚠️ Firebase-only account detected — no database record, but Firebase auth exists', + 'warn' + ) + ) + return { user: null, firebase, logs } + } + } + + // Log identifying details server-side only; do not expose the raw id value + // in the client-facing error message. + console.error(`User not found with ${idType}:`, id) + throw new GraphQLError('User not found', { + extensions: { code: 'NOT_FOUND' } + }) +} diff --git a/apis/api-users/src/schema/userDelete/service/types.spec.ts b/apis/api-users/src/schema/userDelete/service/types.spec.ts new file mode 100644 index 00000000000..257e5dc1d3c --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/types.spec.ts @@ -0,0 +1,28 @@ +import { createLog } from './types' + +describe('createLog', () => { + it('should create a log entry with default info level', () => { + const log = createLog('test message') + expect(log.message).toBe('test message') + expect(log.level).toBe('info') + expect(log.timestamp).toBeDefined() + }) + + it('should create a log entry with explicit level', () => { + const log = createLog('error occurred', 'error') + expect(log.level).toBe('error') + }) + + it('should create a log entry with warn level', () => { + const log = createLog('something off', 'warn') + expect(log.level).toBe('warn') + }) + + it('should set a valid ISO timestamp', () => { + const before = new Date().toISOString() + const log = createLog('test') + const after = new Date().toISOString() + expect(log.timestamp >= before).toBe(true) + expect(log.timestamp <= after).toBe(true) + }) +}) diff --git a/apis/api-users/src/schema/userDelete/service/types.ts b/apis/api-users/src/schema/userDelete/service/types.ts new file mode 100644 index 00000000000..7a6d86cdef7 --- /dev/null +++ b/apis/api-users/src/schema/userDelete/service/types.ts @@ -0,0 +1,21 @@ +// Nitpick: extracted into a named type to avoid repeating the union +export type LogLevel = 'info' | 'warn' | 'error' + +export interface LogEntry { + message: string + level: LogLevel + timestamp: string +} + +export function createLog(message: string, level: LogLevel = 'info'): LogEntry { + return { message, level, timestamp: new Date().toISOString() } +} + +export function isFirebaseNotFound(error: unknown): boolean { + return ( + error != null && + typeof error === 'object' && + 'code' in error && + error.code === 'auth/user-not-found' + ) +} diff --git a/apis/api-users/src/schema/userDelete/userDeleteCheck.ts b/apis/api-users/src/schema/userDelete/userDeleteCheck.ts new file mode 100644 index 00000000000..bf157c538d5 --- /dev/null +++ b/apis/api-users/src/schema/userDelete/userDeleteCheck.ts @@ -0,0 +1,114 @@ +import { builder } from '../builder' + +import { + type LogEntry, + callJourneysCheck, + createLog, + lookupUser +} from './service' + +const UserDeleteIdType = builder.enumType('UserDeleteIdType', { + values: ['databaseId', 'email'] as const +}) + +const UserDeleteLogEntry = builder.objectRef('UserDeleteLogEntry') + +builder.objectType(UserDeleteLogEntry, { + fields: (t) => ({ + message: t.exposeString('message', { nullable: false }), + level: t.exposeString('level', { nullable: false }), + timestamp: t.exposeString('timestamp', { nullable: false }) + }) +}) + +interface UserDeleteCheckResultShape { + userId: string + userEmail: string | null + userFirstName: string + journeysToDelete: number + journeysToTransfer: number + journeysToRemove: number + teamsToDelete: number + teamsToTransfer: number + teamsToRemove: number + logs: LogEntry[] +} + +const UserDeleteCheckResult = builder.objectRef( + 'UserDeleteCheckResult' +) + +builder.objectType(UserDeleteCheckResult, { + fields: (t) => ({ + userId: t.exposeString('userId', { nullable: false }), + userEmail: t.exposeString('userEmail'), + userFirstName: t.exposeString('userFirstName', { nullable: false }), + 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: [UserDeleteLogEntry], + nullable: false, + resolve: (parent) => parent.logs + }) + }) +}) + +builder.mutationField('userDeleteCheck', (t) => + t.withAuth({ isSuperAdmin: true }).field({ + type: UserDeleteCheckResult, + nullable: false, + args: { + idType: t.arg({ type: UserDeleteIdType, required: true }), + id: t.arg.string({ required: true }) + }, + resolve: async (_parent, { idType, id }) => { + const allLogs: LogEntry[] = [] + + const { user, firebase, logs: lookupLogs } = await lookupUser(idType, id) + allLogs.push(...lookupLogs) + + // Firebase-only account — no DB user, no journeys data + if (user == null) { + allLogs.push( + createLog('📋 Skipping journeys check — no database user to look up') + ) + return { + userId: firebase.uid ?? '', + userEmail: firebase.email, + userFirstName: '(Firebase only)', + journeysToDelete: 0, + journeysToTransfer: 0, + journeysToRemove: 0, + teamsToDelete: 0, + teamsToTransfer: 0, + teamsToRemove: 0, + logs: allLogs + } + } + + allLogs.push(createLog('📋 Checking journeys and teams...')) + + const journeysResult = await callJourneysCheck(user.userId) + allLogs.push(...journeysResult.logs) + + return { + userId: user.id, + userEmail: user.email, + userFirstName: user.firstName, + journeysToDelete: journeysResult.journeysToDelete, + journeysToTransfer: journeysResult.journeysToTransfer, + journeysToRemove: journeysResult.journeysToRemove, + teamsToDelete: journeysResult.teamsToDelete, + teamsToTransfer: journeysResult.teamsToTransfer, + teamsToRemove: journeysResult.teamsToRemove, + logs: allLogs + } + } + }) +) + +export { UserDeleteIdType, UserDeleteLogEntry } diff --git a/apis/api-users/src/schema/userDelete/userDeleteConfirm.ts b/apis/api-users/src/schema/userDelete/userDeleteConfirm.ts new file mode 100644 index 00000000000..5d48331e21d --- /dev/null +++ b/apis/api-users/src/schema/userDelete/userDeleteConfirm.ts @@ -0,0 +1,241 @@ +import { prisma } from '@core/prisma/users/client' + +import { builder } from '../builder' + +import { + type LogEntry, + callJourneysConfirm, + createLog, + deleteFirebaseUser, + deleteUserData, + lookupUser +} from './service' +import { UserDeleteIdType, UserDeleteLogEntry } from './userDeleteCheck' + +interface UserDeleteConfirmProgressShape { + log: LogEntry + done: boolean + success: boolean | null +} + +const UserDeleteConfirmProgress = + builder.objectRef('UserDeleteConfirmProgress') + +builder.objectType(UserDeleteConfirmProgress, { + fields: (t) => ({ + log: t.field({ + type: UserDeleteLogEntry, + nullable: false, + resolve: (parent) => parent.log + }), + done: t.exposeBoolean('done', { nullable: false }), + success: t.boolean({ + nullable: true, + resolve: (parent) => parent.success + }) + }) +}) + +builder.subscriptionField('userDeleteConfirm', (t) => + t.withAuth({ isSuperAdmin: true }).field({ + type: UserDeleteConfirmProgress, + nullable: false, + args: { + idType: t.arg({ type: UserDeleteIdType, required: true }), + id: t.arg.string({ required: true }) + }, + subscribe: async function* (_parent, { idType, id }, ctx) { + try { + const [{ user, firebase, logs: lookupLogs }, caller] = + await Promise.all([ + lookupUser(idType, id), + prisma.user.findUnique({ where: { userId: ctx.currentUser.id } }) + ]) + for (const log of lookupLogs) { + yield { log, done: false, success: null } + } + if (caller == null) { + yield { + log: createLog('❌ Caller user not found', 'error'), + done: true, + success: false + } + return + } + + // Firebase-only account — no DB user, just delete Firebase auth + if (user == null) { + if (!firebase.exists || firebase.uid == null) { + yield { + log: createLog('❌ No Firebase account found to delete', 'error'), + done: true, + success: false + } + return + } + + // Create audit log before deletion so there is always a durable + // record, matching the behaviour of the full-user path. + let auditLog: { id: string } | null = null + try { + auditLog = await prisma.userDeleteAuditLog.create({ + data: { + deletedUserId: firebase.uid, + callerUserId: caller.id, + callerEmail: caller.email, + deletedJourneyIds: [], + deletedTeamIds: [], + deletedUserJourneyIds: [], + deletedUserTeamIds: [], + success: false + } + }) + yield { + log: createLog('📝 Audit log created'), + done: false, + success: null + } + } catch (auditError) { + console.error( + 'Failed to create audit log for firebase-only deletion:', + auditError + ) + yield { + log: createLog( + '❌ Failed to create audit log. Aborting deletion.', + 'error' + ), + done: true, + success: false + } + return + } + + yield { + log: createLog('🔥 Deleting Firebase-only account...'), + done: false, + success: null + } + + const fbLogs = await deleteFirebaseUser( + firebase.uid, + null, + firebase.email + ) + for (const log of fbLogs) { + yield { log, done: false, success: null } + } + + const hasError = fbLogs.some((log) => log.level === 'error') + + // Best-effort audit log update + try { + await prisma.userDeleteAuditLog.update({ + where: { id: auditLog.id }, + data: { + success: !hasError, + ...(hasError + ? { errorMessage: 'Firebase deletion failed' } + : {}) + } + }) + } catch { + // best-effort + } + + yield { + log: createLog( + hasError + ? '❌ Firebase deletion failed' + : '✅ Firebase-only account deleted successfully', + hasError ? 'error' : 'info' + ), + done: true, + success: !hasError + } + return + } + + // Prevent self-deletion + if (user.userId === ctx.currentUser.id) { + yield { + log: createLog('❌ Cannot delete your own account', 'error'), + done: true, + success: false + } + return + } + + // Phase 1: Journeys DB cleanup (via interop) + yield { + log: createLog('🔄 Starting journeys database cleanup...'), + done: false, + success: null + } + + const journeysResult = await callJourneysConfirm(user.userId) + for (const log of journeysResult.logs) { + yield { log, done: false, success: null } + } + + if (!journeysResult.success) { + yield { + log: createLog('❌ Journeys cleanup failed, aborting', 'error'), + done: true, + success: false + } + return + } + + // Phase 2: Users DB deletion + Firebase cleanup + yield { + log: createLog('🔄 Starting user record deletion...'), + done: false, + success: null + } + + const firebaseUidOverride = + firebase.uid != null && firebase.uid !== user.userId + ? firebase.uid + : null + + const userResult = await deleteUserData({ + userDbId: user.id, + firebaseUserId: user.userId, + firebaseUidOverride, + userEmail: user.email, + callerUserId: caller.id, + callerEmail: caller.email, + deletedJourneyIds: journeysResult.deletedJourneyIds, + deletedTeamIds: journeysResult.deletedTeamIds, + deletedUserJourneyIds: journeysResult.deletedUserJourneyIds, + deletedUserTeamIds: journeysResult.deletedUserTeamIds + }) + + for (const log of userResult.logs) { + yield { log, done: false, success: null } + } + + yield { + log: createLog( + userResult.success + ? '✅ User deletion completed successfully' + : '❌ User deletion failed', + userResult.success ? 'info' : 'error' + ), + done: true, + success: userResult.success + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('userDeleteConfirm subscription error:', error) + yield { + log: createLog(`❌ Unexpected error: ${message}`, 'error'), + done: true, + success: false + } + } + }, + resolve: (progress) => progress + }) +) diff --git a/apps/journeys-admin/__generated__/globalTypes.ts b/apps/journeys-admin/__generated__/globalTypes.ts index 53e64f186bc..e3474fe6b6c 100644 --- a/apps/journeys-admin/__generated__/globalTypes.ts +++ b/apps/journeys-admin/__generated__/globalTypes.ts @@ -494,6 +494,11 @@ export interface CustomDomainUpdateInput { routeAllTeamJourneys?: boolean | null; } +export interface DateTimeFilter { + gte?: any | null; + lte?: any | null; +} + export interface EmailActionInput { gtmEventName?: string | null; email: string; @@ -723,7 +728,7 @@ export interface LanguagesFilter { ids?: string[] | null; bcp47?: string[] | null; iso3?: string[] | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } export interface LinkActionInput { diff --git a/apps/journeys/__generated__/globalTypes.ts b/apps/journeys/__generated__/globalTypes.ts index 7f25b1ca4e2..7c7621e6f75 100644 --- a/apps/journeys/__generated__/globalTypes.ts +++ b/apps/journeys/__generated__/globalTypes.ts @@ -292,6 +292,11 @@ export interface ChatOpenEventCreateInput { value?: MessagePlatform | null; } +export interface DateTimeFilter { + gte?: any | null; + lte?: any | null; +} + export interface JourneyProfileUpdateInput { lastActiveTeamId?: string | null; journeyFlowBackButtonClicked?: boolean | null; @@ -330,7 +335,7 @@ export interface LanguagesFilter { ids?: string[] | null; bcp47?: string[] | null; iso3?: string[] | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } export interface MultiselectSubmissionEventCreateInput { diff --git a/apps/resources/__generated__/globalTypes.ts b/apps/resources/__generated__/globalTypes.ts index 2e022a4f4d9..57cc571cd1e 100644 --- a/apps/resources/__generated__/globalTypes.ts +++ b/apps/resources/__generated__/globalTypes.ts @@ -298,6 +298,11 @@ export interface ChatOpenEventCreateInput { value?: MessagePlatform | null; } +export interface DateTimeFilter { + gte?: any | null; + lte?: any | null; +} + export interface JourneyProfileUpdateInput { lastActiveTeamId?: string | null; journeyFlowBackButtonClicked?: boolean | null; @@ -329,7 +334,7 @@ export interface LanguagesFilter { ids?: string[] | null; bcp47?: string[] | null; iso3?: string[] | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } export interface MultiselectSubmissionEventCreateInput { @@ -459,7 +464,7 @@ export interface VideosFilter { subtitleLanguageIds?: string[] | null; published?: boolean | null; locked?: boolean | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } //============================================================== diff --git a/apps/watch/__generated__/globalTypes.ts b/apps/watch/__generated__/globalTypes.ts index 2e022a4f4d9..57cc571cd1e 100644 --- a/apps/watch/__generated__/globalTypes.ts +++ b/apps/watch/__generated__/globalTypes.ts @@ -298,6 +298,11 @@ export interface ChatOpenEventCreateInput { value?: MessagePlatform | null; } +export interface DateTimeFilter { + gte?: any | null; + lte?: any | null; +} + export interface JourneyProfileUpdateInput { lastActiveTeamId?: string | null; journeyFlowBackButtonClicked?: boolean | null; @@ -329,7 +334,7 @@ export interface LanguagesFilter { ids?: string[] | null; bcp47?: string[] | null; iso3?: string[] | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } export interface MultiselectSubmissionEventCreateInput { @@ -459,7 +464,7 @@ export interface VideosFilter { subtitleLanguageIds?: string[] | null; published?: boolean | null; locked?: boolean | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } //============================================================== diff --git a/libs/journeys/ui/__generated__/globalTypes.ts b/libs/journeys/ui/__generated__/globalTypes.ts index 62ca8e780be..cdfff8331c9 100644 --- a/libs/journeys/ui/__generated__/globalTypes.ts +++ b/libs/journeys/ui/__generated__/globalTypes.ts @@ -274,6 +274,11 @@ export interface ChatOpenEventCreateInput { value?: MessagePlatform | null; } +export interface DateTimeFilter { + gte?: any | null; + lte?: any | null; +} + export interface JourneyProfileUpdateInput { lastActiveTeamId?: string | null; journeyFlowBackButtonClicked?: boolean | null; @@ -305,7 +310,7 @@ export interface LanguagesFilter { ids?: string[] | null; bcp47?: string[] | null; iso3?: string[] | null; - updatedAt?: any | null; + updatedAt?: DateTimeFilter | null; } export interface MultiselectSubmissionEventCreateInput { diff --git a/libs/prisma/users/db/migrations/20260317034905_20260317034904/migration.sql b/libs/prisma/users/db/migrations/20260317034905_20260317034904/migration.sql new file mode 100644 index 00000000000..a569f075eec --- /dev/null +++ b/libs/prisma/users/db/migrations/20260317034905_20260317034904/migration.sql @@ -0,0 +1,30 @@ +-- CreateTable +CREATE TABLE "UserDeleteAuditLog" ( + "id" TEXT NOT NULL, + "deletedUserId" TEXT NOT NULL, + "deletedUserEmail" TEXT, + "deletedUserFirstName" TEXT NOT NULL, + "deletedUserLastName" TEXT, + "callerUserId" TEXT NOT NULL, + "callerEmail" TEXT, + "callerFirstName" TEXT NOT NULL, + "callerLastName" TEXT, + "deletedJourneyIds" TEXT[], + "deletedTeamIds" TEXT[], + "deletedUserJourneyIds" TEXT[], + "deletedUserTeamIds" TEXT[], + "success" BOOLEAN NOT NULL DEFAULT false, + "errorMessage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserDeleteAuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "UserDeleteAuditLog_deletedUserId_idx" ON "UserDeleteAuditLog"("deletedUserId"); + +-- CreateIndex +CREATE INDEX "UserDeleteAuditLog_callerUserId_idx" ON "UserDeleteAuditLog"("callerUserId"); + +-- CreateIndex +CREATE INDEX "UserDeleteAuditLog_createdAt_idx" ON "UserDeleteAuditLog"("createdAt"); diff --git a/libs/prisma/users/db/migrations/20260330000000_remove_pii_from_user_delete_audit_log/migration.sql b/libs/prisma/users/db/migrations/20260330000000_remove_pii_from_user_delete_audit_log/migration.sql new file mode 100644 index 00000000000..dc1be9ac9af --- /dev/null +++ b/libs/prisma/users/db/migrations/20260330000000_remove_pii_from_user_delete_audit_log/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "UserDeleteAuditLog" +DROP COLUMN "deletedUserEmail", +DROP COLUMN "deletedUserFirstName", +DROP COLUMN "deletedUserLastName", +DROP COLUMN "callerFirstName", +DROP COLUMN "callerLastName"; diff --git a/libs/prisma/users/db/migrations/migration_lock.toml b/libs/prisma/users/db/migrations/migration_lock.toml index fbffa92c2bb..044d57cdb0d 100644 --- a/libs/prisma/users/db/migrations/migration_lock.toml +++ b/libs/prisma/users/db/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/libs/prisma/users/db/schema.prisma b/libs/prisma/users/db/schema.prisma index 61f00ba3b1f..4f4cbf7b4f9 100644 --- a/libs/prisma/users/db/schema.prisma +++ b/libs/prisma/users/db/schema.prisma @@ -27,3 +27,21 @@ model User { superAdmin Boolean @default(false) emailVerified Boolean @default(true) } + +model UserDeleteAuditLog { + id String @id @default(uuid()) + deletedUserId String + callerUserId String + callerEmail String? + deletedJourneyIds String[] + deletedTeamIds String[] + deletedUserJourneyIds String[] + deletedUserTeamIds String[] + success Boolean @default(false) + errorMessage String? + createdAt DateTime @default(now()) + + @@index(deletedUserId) + @@index(callerUserId) + @@index(createdAt) +} diff --git a/libs/prisma/users/src/__generated__/pothos-types.ts b/libs/prisma/users/src/__generated__/pothos-types.ts index b53d874e40e..5d901aa1559 100644 --- a/libs/prisma/users/src/__generated__/pothos-types.ts +++ b/libs/prisma/users/src/__generated__/pothos-types.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import type { Prisma, User } from "./client/client.js"; +import type { Prisma, User, UserDeleteAuditLog } from "./client/client.js"; import type { PothosPrismaDatamodel } from "@pothos/plugin-prisma"; export default interface PrismaTypes { User: { @@ -16,5 +16,19 @@ export default interface PrismaTypes { ListRelations: never; Relations: {}; }; + UserDeleteAuditLog: { + Name: "UserDeleteAuditLog"; + Shape: UserDeleteAuditLog; + Include: never; + Select: Prisma.UserDeleteAuditLogSelect; + OrderBy: Prisma.UserDeleteAuditLogOrderByWithRelationInput; + WhereUnique: Prisma.UserDeleteAuditLogWhereUniqueInput; + Where: Prisma.UserDeleteAuditLogWhereInput; + Create: {}; + Update: {}; + RelationName: never; + ListRelations: never; + Relations: {}; + }; } -export function getDatamodel(): PothosPrismaDatamodel { return JSON.parse("{\"datamodel\":{\"models\":{\"User\":{\"fields\":[{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"id\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":true,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"userId\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":true,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"firstName\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"lastName\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"email\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":true,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"imageUrl\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"DateTime\",\"kind\":\"scalar\",\"name\":\"createdAt\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"Boolean\",\"kind\":\"scalar\",\"name\":\"superAdmin\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"Boolean\",\"kind\":\"scalar\",\"name\":\"emailVerified\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueIndexes\":[]}}}}"); } \ No newline at end of file +export function getDatamodel(): PothosPrismaDatamodel { return JSON.parse("{\"datamodel\":{\"models\":{\"User\":{\"fields\":[{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"id\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":true,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"userId\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":true,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"firstName\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"lastName\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"email\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":true,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"imageUrl\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"DateTime\",\"kind\":\"scalar\",\"name\":\"createdAt\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"Boolean\",\"kind\":\"scalar\",\"name\":\"superAdmin\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"Boolean\",\"kind\":\"scalar\",\"name\":\"emailVerified\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueIndexes\":[]},\"UserDeleteAuditLog\":{\"fields\":[{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"id\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":true,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"deletedUserId\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"callerUserId\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"callerEmail\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"deletedJourneyIds\",\"isRequired\":true,\"isList\":true,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"deletedTeamIds\",\"isRequired\":true,\"isList\":true,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"deletedUserJourneyIds\",\"isRequired\":true,\"isList\":true,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"deletedUserTeamIds\",\"isRequired\":true,\"isList\":true,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"Boolean\",\"kind\":\"scalar\",\"name\":\"success\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"String\",\"kind\":\"scalar\",\"name\":\"errorMessage\",\"isRequired\":false,\"isList\":false,\"hasDefaultValue\":false,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false},{\"type\":\"DateTime\",\"kind\":\"scalar\",\"name\":\"createdAt\",\"isRequired\":true,\"isList\":false,\"hasDefaultValue\":true,\"isUnique\":false,\"isId\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueIndexes\":[]}}}}"); } \ No newline at end of file diff --git a/libs/shared/gql/src/__generated__/graphql-env.d.ts b/libs/shared/gql/src/__generated__/graphql-env.d.ts index 9d3dc4dc1e8..0030a4ef083 100644 --- a/libs/shared/gql/src/__generated__/graphql-env.d.ts +++ b/libs/shared/gql/src/__generated__/graphql-env.d.ts @@ -185,7 +185,7 @@ export type introspection_types = { 'MultiselectOptionBlockUpdateInput': { kind: 'INPUT_OBJECT'; name: 'MultiselectOptionBlockUpdateInput'; isOneOf: false; inputFields: [{ name: 'parentBlockId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'MultiselectSubmissionEvent': { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; fields: { 'createdAt': { name: 'createdAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'label': { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'value': { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'MultiselectSubmissionEventCreateInput': { kind: 'INPUT_OBJECT'; name: 'MultiselectSubmissionEventCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'blockId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'stepId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'values'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; }; defaultValue: null }]; }; - 'Mutation': { kind: 'OBJECT'; name: 'Mutation'; fields: { 'audioPreviewCreate': { name: 'audioPreviewCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewDelete': { name: 'audioPreviewDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewUpdate': { name: 'audioPreviewUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'bibleCitationCreate': { name: 'bibleCitationCreate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'bibleCitationDelete': { name: 'bibleCitationDelete'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'bibleCitationUpdate': { name: 'bibleCitationUpdate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'blockDelete': { name: 'blockDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockDeleteAction': { name: 'blockDeleteAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; } }; 'blockDuplicate': { name: 'blockDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockOrderUpdate': { name: 'blockOrderUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockRestore': { name: 'blockRestore'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockUpdateAction': { name: 'blockUpdateAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Action'; ofType: null; }; } }; 'blockUpdateChatAction': { name: 'blockUpdateChatAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatAction'; ofType: null; }; } }; 'blockUpdateEmailAction': { name: 'blockUpdateEmailAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'EmailAction'; ofType: null; }; } }; 'blockUpdateLinkAction': { name: 'blockUpdateLinkAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'LinkAction'; ofType: null; }; } }; 'blockUpdateNavigateToBlockAction': { name: 'blockUpdateNavigateToBlockAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NavigateToBlockAction'; ofType: null; }; } }; 'blockUpdatePhoneAction': { name: 'blockUpdatePhoneAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'PhoneAction'; ofType: null; }; } }; 'buttonBlockCreate': { name: 'buttonBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; }; } }; 'buttonBlockUpdate': { name: 'buttonBlockUpdate'; type: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; } }; 'buttonClickEventCreate': { name: 'buttonClickEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonClickEvent'; ofType: null; }; } }; 'cardBlockCreate': { name: 'cardBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'cardBlockUpdate': { name: 'cardBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'chatButtonCreate': { name: 'chatButtonCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonRemove': { name: 'chatButtonRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonUpdate': { name: 'chatButtonUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatOpenEventCreate': { name: 'chatOpenEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatOpenEvent'; ofType: null; }; } }; 'cloudflareR2CompleteMultipart': { name: 'cloudflareR2CompleteMultipart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Create': { name: 'cloudflareR2Create'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Delete': { name: 'cloudflareR2Delete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2MultipartPrepare': { name: 'cloudflareR2MultipartPrepare'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2MultipartPrepared'; ofType: null; }; } }; 'cloudflareUploadComplete': { name: 'cloudflareUploadComplete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'createCloudflareImageFromPrompt': { name: 'createCloudflareImageFromPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByFile': { name: 'createCloudflareUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByUrl': { name: 'createCloudflareUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createImageBySegmindPrompt': { name: 'createImageBySegmindPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createJourneyEventsExportLog': { name: 'createJourneyEventsExportLog'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyEventsExportLog'; ofType: null; }; } }; 'createKeyword': { name: 'createKeyword'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Keyword'; ofType: null; }; } }; 'createMuxVideoAndQueueUpload': { name: 'createMuxVideoAndQueueUpload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByFile': { name: 'createMuxVideoUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByUrl': { name: 'createMuxVideoUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createVerificationRequest': { name: 'createVerificationRequest'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'customDomainCheck': { name: 'customDomainCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomainCheck'; ofType: null; }; } }; 'customDomainCreate': { name: 'customDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainDelete': { name: 'customDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainUpdate': { name: 'customDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'deleteCloudflareImage': { name: 'deleteCloudflareImage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'deleteMuxVideo': { name: 'deleteMuxVideo'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'enableMuxDownload': { name: 'enableMuxDownload'; type: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; } }; 'fixVideoLanguages': { name: 'fixVideoLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'googleSheetsSyncBackfill': { name: 'googleSheetsSyncBackfill'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncCreate': { name: 'googleSheetsSyncCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncDelete': { name: 'googleSheetsSyncDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'hostCreate': { name: 'hostCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostDelete': { name: 'hostDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostUpdate': { name: 'hostUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'iconBlockCreate': { name: 'iconBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'iconBlockUpdate': { name: 'iconBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'imageBlockCreate': { name: 'imageBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'imageBlockUpdate': { name: 'imageBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'integrationDelete': { name: 'integrationDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Integration'; ofType: null; }; } }; 'integrationGoogleCreate': { name: 'integrationGoogleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGoogleUpdate': { name: 'integrationGoogleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGrowthSpacesCreate': { name: 'integrationGrowthSpacesCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'integrationGrowthSpacesUpdate': { name: 'integrationGrowthSpacesUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'journeyAiTranslateCreate': { name: 'journeyAiTranslateCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCollectionCreate': { name: 'journeyCollectionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionDelete': { name: 'journeyCollectionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionUpdate': { name: 'journeyCollectionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCreate': { name: 'journeyCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCustomizationFieldPublisherUpdate': { name: 'journeyCustomizationFieldPublisherUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyCustomizationFieldUserUpdate': { name: 'journeyCustomizationFieldUserUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyDuplicate': { name: 'journeyDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyFeature': { name: 'journeyFeature'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeyLanguageAiDetect': { name: 'journeyLanguageAiDetect'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'journeyNotificationUpdate': { name: 'journeyNotificationUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyNotification'; ofType: null; }; } }; 'journeyProfileCreate': { name: 'journeyProfileCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyProfileUpdate': { name: 'journeyProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyPublish': { name: 'journeyPublish'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeySimpleUpdate': { name: 'journeySimpleUpdate'; type: { kind: 'SCALAR'; name: 'Json'; ofType: null; } }; 'journeyTemplate': { name: 'journeyTemplate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyThemeCreate': { name: 'journeyThemeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeDelete': { name: 'journeyThemeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeUpdate': { name: 'journeyThemeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyUpdate': { name: 'journeyUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyViewEventCreate': { name: 'journeyViewEventCreate'; type: { kind: 'OBJECT'; name: 'JourneyViewEvent'; ofType: null; } }; 'journeyVisitorExportToGoogleSheet': { name: 'journeyVisitorExportToGoogleSheet'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyVisitorGoogleSheetExportResult'; ofType: null; }; } }; 'journeysArchive': { name: 'journeysArchive'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysDelete': { name: 'journeysDelete'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysRestore': { name: 'journeysRestore'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysTrash': { name: 'journeysTrash'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'multiselectBlockCreate': { name: 'multiselectBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectBlockUpdate': { name: 'multiselectBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectOptionBlockCreate': { name: 'multiselectOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectOptionBlockUpdate': { name: 'multiselectOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectSubmissionEventCreate': { name: 'multiselectSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; ofType: null; }; } }; 'playlistCreate': { name: 'playlistCreate'; type: { kind: 'UNION'; name: 'MutationPlaylistCreateResult'; ofType: null; } }; 'playlistDelete': { name: 'playlistDelete'; type: { kind: 'UNION'; name: 'MutationPlaylistDeleteResult'; ofType: null; } }; 'playlistItemAdd': { name: 'playlistItemAdd'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddResult'; ofType: null; } }; 'playlistItemAddWithVideoAndLanguageIds': { name: 'playlistItemAddWithVideoAndLanguageIds'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddWithVideoAndLanguageIdsResult'; ofType: null; } }; 'playlistItemRemove': { name: 'playlistItemRemove'; type: { kind: 'UNION'; name: 'MutationPlaylistItemRemoveResult'; ofType: null; } }; 'playlistItemsReorder': { name: 'playlistItemsReorder'; type: { kind: 'UNION'; name: 'MutationPlaylistItemsReorderResult'; ofType: null; } }; 'playlistUpdate': { name: 'playlistUpdate'; type: { kind: 'UNION'; name: 'MutationPlaylistUpdateResult'; ofType: null; } }; 'qrCodeCreate': { name: 'qrCodeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeDelete': { name: 'qrCodeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeUpdate': { name: 'qrCodeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'radioOptionBlockCreate': { name: 'radioOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioOptionBlockUpdate': { name: 'radioOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioQuestionBlockCreate': { name: 'radioQuestionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionBlockUpdate': { name: 'radioQuestionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionSubmissionEventCreate': { name: 'radioQuestionSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionSubmissionEvent'; ofType: null; }; } }; 'shortLinkCreate': { name: 'shortLinkCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkCreateResult'; ofType: null; }; } }; 'shortLinkDelete': { name: 'shortLinkDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDeleteResult'; ofType: null; }; } }; 'shortLinkDomainCreate': { name: 'shortLinkDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainCreateResult'; ofType: null; }; } }; 'shortLinkDomainDelete': { name: 'shortLinkDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainDeleteResult'; ofType: null; }; } }; 'shortLinkDomainUpdate': { name: 'shortLinkDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainUpdateResult'; ofType: null; }; } }; 'shortLinkUpdate': { name: 'shortLinkUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkUpdateResult'; ofType: null; }; } }; 'signUpBlockCreate': { name: 'signUpBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; }; } }; 'signUpBlockUpdate': { name: 'signUpBlockUpdate'; type: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; } }; 'signUpSubmissionEventCreate': { name: 'signUpSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpSubmissionEvent'; ofType: null; }; } }; 'siteCreate': { name: 'siteCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationSiteCreateResult'; ofType: null; }; } }; 'spacerBlockCreate': { name: 'spacerBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'spacerBlockUpdate': { name: 'spacerBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'stepBlockCreate': { name: 'stepBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepBlockPositionUpdate': { name: 'stepBlockPositionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; }; }; } }; 'stepBlockUpdate': { name: 'stepBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepNextEventCreate': { name: 'stepNextEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepNextEvent'; ofType: null; }; } }; 'stepPreviousEventCreate': { name: 'stepPreviousEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepPreviousEvent'; ofType: null; }; } }; 'stepViewEventCreate': { name: 'stepViewEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepViewEvent'; ofType: null; }; } }; 'teamCreate': { name: 'teamCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'teamUpdate': { name: 'teamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'textResponseBlockCreate': { name: 'textResponseBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; }; } }; 'textResponseBlockUpdate': { name: 'textResponseBlockUpdate'; type: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; } }; 'textResponseSubmissionEventCreate': { name: 'textResponseSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseSubmissionEvent'; ofType: null; }; } }; 'triggerUnsplashDownload': { name: 'triggerUnsplashDownload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'typographyBlockCreate': { name: 'typographyBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'typographyBlockUpdate': { name: 'typographyBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'updateJourneysEmailPreference': { name: 'updateJourneysEmailPreference'; type: { kind: 'OBJECT'; name: 'JourneysEmailPreference'; ofType: null; } }; 'updateVideoAlgoliaIndex': { name: 'updateVideoAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'updateVideoVariantAlgoliaIndex': { name: 'updateVideoVariantAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'userImpersonate': { name: 'userImpersonate'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'userInviteAcceptAll': { name: 'userInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; }; }; } }; 'userInviteCreate': { name: 'userInviteCreate'; type: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; } }; 'userInviteRemove': { name: 'userInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; } }; 'userJourneyApprove': { name: 'userJourneyApprove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyOpen': { name: 'userJourneyOpen'; type: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; } }; 'userJourneyPromote': { name: 'userJourneyPromote'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemove': { name: 'userJourneyRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemoveAll': { name: 'userJourneyRemoveAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; }; }; } }; 'userJourneyRequest': { name: 'userJourneyRequest'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userMediaProfileUpdate': { name: 'userMediaProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserMediaProfile'; ofType: null; }; } }; 'userTeamDelete': { name: 'userTeamDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'userTeamInviteAcceptAll': { name: 'userTeamInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; }; }; } }; 'userTeamInviteCreate': { name: 'userTeamInviteCreate'; type: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; } }; 'userTeamInviteRemove': { name: 'userTeamInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; } }; 'userTeamUpdate': { name: 'userTeamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'validateEmail': { name: 'validateEmail'; type: { kind: 'OBJECT'; name: 'AuthenticatedUser'; ofType: null; } }; 'videoBlockCreate': { name: 'videoBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoBlockUpdate': { name: 'videoBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoCollapseEventCreate': { name: 'videoCollapseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCollapseEvent'; ofType: null; }; } }; 'videoCompleteEventCreate': { name: 'videoCompleteEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCompleteEvent'; ofType: null; }; } }; 'videoCreate': { name: 'videoCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDelete': { name: 'videoDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDescriptionCreate': { name: 'videoDescriptionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionDelete': { name: 'videoDescriptionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionUpdate': { name: 'videoDescriptionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoEditionCreate': { name: 'videoEditionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionDelete': { name: 'videoEditionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionUpdate': { name: 'videoEditionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoExpandEventCreate': { name: 'videoExpandEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoExpandEvent'; ofType: null; }; } }; 'videoImageAltCreate': { name: 'videoImageAltCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltDelete': { name: 'videoImageAltDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltUpdate': { name: 'videoImageAltUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoOriginCreate': { name: 'videoOriginCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginDelete': { name: 'videoOriginDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginUpdate': { name: 'videoOriginUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoPauseEventCreate': { name: 'videoPauseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPauseEvent'; ofType: null; }; } }; 'videoPlayEventCreate': { name: 'videoPlayEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPlayEvent'; ofType: null; }; } }; 'videoProgressEventCreate': { name: 'videoProgressEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoProgressEvent'; ofType: null; }; } }; 'videoPublishChildren': { name: 'videoPublishChildren'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenResult'; ofType: null; }; } }; 'videoPublishChildrenAndLanguages': { name: 'videoPublishChildrenAndLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenAndLanguagesResult'; ofType: null; }; } }; 'videoSnippetCreate': { name: 'videoSnippetCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetDelete': { name: 'videoSnippetDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetUpdate': { name: 'videoSnippetUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoStartEventCreate': { name: 'videoStartEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStartEvent'; ofType: null; }; } }; 'videoStudyQuestionCreate': { name: 'videoStudyQuestionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionDelete': { name: 'videoStudyQuestionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionUpdate': { name: 'videoStudyQuestionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoSubtitleCreate': { name: 'videoSubtitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleDelete': { name: 'videoSubtitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleUpdate': { name: 'videoSubtitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoTitleCreate': { name: 'videoTitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleDelete': { name: 'videoTitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleUpdate': { name: 'videoTitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoUpdate': { name: 'videoUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoVariantCreate': { name: 'videoVariantCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDelete': { name: 'videoVariantDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDownloadCreate': { name: 'videoVariantDownloadCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadDelete': { name: 'videoVariantDownloadDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadUpdate': { name: 'videoVariantDownloadUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantUpdate': { name: 'videoVariantUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'visitorUpdate': { name: 'visitorUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; 'visitorUpdateForCurrentUser': { name: 'visitorUpdateForCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; }; }; + 'Mutation': { kind: 'OBJECT'; name: 'Mutation'; fields: { 'audioPreviewCreate': { name: 'audioPreviewCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewDelete': { name: 'audioPreviewDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'audioPreviewUpdate': { name: 'audioPreviewUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'AudioPreview'; ofType: null; }; } }; 'bibleCitationCreate': { name: 'bibleCitationCreate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'bibleCitationDelete': { name: 'bibleCitationDelete'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'bibleCitationUpdate': { name: 'bibleCitationUpdate'; type: { kind: 'OBJECT'; name: 'BibleCitation'; ofType: null; } }; 'blockDelete': { name: 'blockDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockDeleteAction': { name: 'blockDeleteAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; } }; 'blockDuplicate': { name: 'blockDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockOrderUpdate': { name: 'blockOrderUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockRestore': { name: 'blockRestore'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Block'; ofType: null; }; }; }; } }; 'blockUpdateAction': { name: 'blockUpdateAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Action'; ofType: null; }; } }; 'blockUpdateChatAction': { name: 'blockUpdateChatAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatAction'; ofType: null; }; } }; 'blockUpdateEmailAction': { name: 'blockUpdateEmailAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'EmailAction'; ofType: null; }; } }; 'blockUpdateLinkAction': { name: 'blockUpdateLinkAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'LinkAction'; ofType: null; }; } }; 'blockUpdateNavigateToBlockAction': { name: 'blockUpdateNavigateToBlockAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NavigateToBlockAction'; ofType: null; }; } }; 'blockUpdatePhoneAction': { name: 'blockUpdatePhoneAction'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'PhoneAction'; ofType: null; }; } }; 'buttonBlockCreate': { name: 'buttonBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; }; } }; 'buttonBlockUpdate': { name: 'buttonBlockUpdate'; type: { kind: 'OBJECT'; name: 'ButtonBlock'; ofType: null; } }; 'buttonClickEventCreate': { name: 'buttonClickEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ButtonClickEvent'; ofType: null; }; } }; 'cardBlockCreate': { name: 'cardBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'cardBlockUpdate': { name: 'cardBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CardBlock'; ofType: null; }; } }; 'chatButtonCreate': { name: 'chatButtonCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonRemove': { name: 'chatButtonRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatButtonUpdate': { name: 'chatButtonUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatButton'; ofType: null; }; } }; 'chatOpenEventCreate': { name: 'chatOpenEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ChatOpenEvent'; ofType: null; }; } }; 'cloudflareR2CompleteMultipart': { name: 'cloudflareR2CompleteMultipart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Create': { name: 'cloudflareR2Create'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2Delete': { name: 'cloudflareR2Delete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2'; ofType: null; }; } }; 'cloudflareR2MultipartPrepare': { name: 'cloudflareR2MultipartPrepare'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareR2MultipartPrepared'; ofType: null; }; } }; 'cloudflareUploadComplete': { name: 'cloudflareUploadComplete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'createCloudflareImageFromPrompt': { name: 'createCloudflareImageFromPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByFile': { name: 'createCloudflareUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createCloudflareUploadByUrl': { name: 'createCloudflareUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createImageBySegmindPrompt': { name: 'createImageBySegmindPrompt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CloudflareImage'; ofType: null; }; } }; 'createJourneyEventsExportLog': { name: 'createJourneyEventsExportLog'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyEventsExportLog'; ofType: null; }; } }; 'createKeyword': { name: 'createKeyword'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Keyword'; ofType: null; }; } }; 'createMuxVideoAndQueueUpload': { name: 'createMuxVideoAndQueueUpload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByFile': { name: 'createMuxVideoUploadByFile'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createMuxVideoUploadByUrl': { name: 'createMuxVideoUploadByUrl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; }; } }; 'createVerificationRequest': { name: 'createVerificationRequest'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; 'customDomainCheck': { name: 'customDomainCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomainCheck'; ofType: null; }; } }; 'customDomainCreate': { name: 'customDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainDelete': { name: 'customDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'customDomainUpdate': { name: 'customDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'CustomDomain'; ofType: null; }; } }; 'deleteCloudflareImage': { name: 'deleteCloudflareImage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'deleteMuxVideo': { name: 'deleteMuxVideo'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'enableMuxDownload': { name: 'enableMuxDownload'; type: { kind: 'OBJECT'; name: 'MuxVideo'; ofType: null; } }; 'fixVideoLanguages': { name: 'fixVideoLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'googleSheetsSyncBackfill': { name: 'googleSheetsSyncBackfill'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncCreate': { name: 'googleSheetsSyncCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'googleSheetsSyncDelete': { name: 'googleSheetsSyncDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'GoogleSheetsSync'; ofType: null; }; } }; 'hostCreate': { name: 'hostCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostDelete': { name: 'hostDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'hostUpdate': { name: 'hostUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Host'; ofType: null; }; } }; 'iconBlockCreate': { name: 'iconBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'iconBlockUpdate': { name: 'iconBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IconBlock'; ofType: null; }; } }; 'imageBlockCreate': { name: 'imageBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'imageBlockUpdate': { name: 'imageBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'ImageBlock'; ofType: null; }; } }; 'integrationDelete': { name: 'integrationDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'INTERFACE'; name: 'Integration'; ofType: null; }; } }; 'integrationGoogleCreate': { name: 'integrationGoogleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGoogleUpdate': { name: 'integrationGoogleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGoogle'; ofType: null; }; } }; 'integrationGrowthSpacesCreate': { name: 'integrationGrowthSpacesCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'integrationGrowthSpacesUpdate': { name: 'integrationGrowthSpacesUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'IntegrationGrowthSpaces'; ofType: null; }; } }; 'journeyAiTranslateCreate': { name: 'journeyAiTranslateCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCollectionCreate': { name: 'journeyCollectionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionDelete': { name: 'journeyCollectionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCollectionUpdate': { name: 'journeyCollectionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCollection'; ofType: null; }; } }; 'journeyCreate': { name: 'journeyCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyCustomizationFieldPublisherUpdate': { name: 'journeyCustomizationFieldPublisherUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyCustomizationFieldUserUpdate': { name: 'journeyCustomizationFieldUserUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyCustomizationField'; ofType: null; }; }; }; } }; 'journeyDuplicate': { name: 'journeyDuplicate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyFeature': { name: 'journeyFeature'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeyLanguageAiDetect': { name: 'journeyLanguageAiDetect'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'journeyNotificationUpdate': { name: 'journeyNotificationUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyNotification'; ofType: null; }; } }; 'journeyProfileCreate': { name: 'journeyProfileCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyProfileUpdate': { name: 'journeyProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyProfile'; ofType: null; }; } }; 'journeyPublish': { name: 'journeyPublish'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeySimpleUpdate': { name: 'journeySimpleUpdate'; type: { kind: 'SCALAR'; name: 'Json'; ofType: null; } }; 'journeyTemplate': { name: 'journeyTemplate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyThemeCreate': { name: 'journeyThemeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeDelete': { name: 'journeyThemeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyThemeUpdate': { name: 'journeyThemeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyTheme'; ofType: null; }; } }; 'journeyUpdate': { name: 'journeyUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeyViewEventCreate': { name: 'journeyViewEventCreate'; type: { kind: 'OBJECT'; name: 'JourneyViewEvent'; ofType: null; } }; 'journeyVisitorExportToGoogleSheet': { name: 'journeyVisitorExportToGoogleSheet'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyVisitorGoogleSheetExportResult'; ofType: null; }; } }; 'journeysArchive': { name: 'journeysArchive'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysDelete': { name: 'journeysDelete'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysRestore': { name: 'journeysRestore'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'journeysTrash': { name: 'journeysTrash'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Journey'; ofType: null; }; } }; 'multiselectBlockCreate': { name: 'multiselectBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectBlockUpdate': { name: 'multiselectBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectBlock'; ofType: null; }; } }; 'multiselectOptionBlockCreate': { name: 'multiselectOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectOptionBlockUpdate': { name: 'multiselectOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectOptionBlock'; ofType: null; }; } }; 'multiselectSubmissionEventCreate': { name: 'multiselectSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'MultiselectSubmissionEvent'; ofType: null; }; } }; 'playlistCreate': { name: 'playlistCreate'; type: { kind: 'UNION'; name: 'MutationPlaylistCreateResult'; ofType: null; } }; 'playlistDelete': { name: 'playlistDelete'; type: { kind: 'UNION'; name: 'MutationPlaylistDeleteResult'; ofType: null; } }; 'playlistItemAdd': { name: 'playlistItemAdd'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddResult'; ofType: null; } }; 'playlistItemAddWithVideoAndLanguageIds': { name: 'playlistItemAddWithVideoAndLanguageIds'; type: { kind: 'UNION'; name: 'MutationPlaylistItemAddWithVideoAndLanguageIdsResult'; ofType: null; } }; 'playlistItemRemove': { name: 'playlistItemRemove'; type: { kind: 'UNION'; name: 'MutationPlaylistItemRemoveResult'; ofType: null; } }; 'playlistItemsReorder': { name: 'playlistItemsReorder'; type: { kind: 'UNION'; name: 'MutationPlaylistItemsReorderResult'; ofType: null; } }; 'playlistUpdate': { name: 'playlistUpdate'; type: { kind: 'UNION'; name: 'MutationPlaylistUpdateResult'; ofType: null; } }; 'qrCodeCreate': { name: 'qrCodeCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeDelete': { name: 'qrCodeDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'qrCodeUpdate': { name: 'qrCodeUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'QrCode'; ofType: null; }; } }; 'radioOptionBlockCreate': { name: 'radioOptionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioOptionBlockUpdate': { name: 'radioOptionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioOptionBlock'; ofType: null; }; } }; 'radioQuestionBlockCreate': { name: 'radioQuestionBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionBlockUpdate': { name: 'radioQuestionBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionBlock'; ofType: null; }; } }; 'radioQuestionSubmissionEventCreate': { name: 'radioQuestionSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RadioQuestionSubmissionEvent'; ofType: null; }; } }; 'shortLinkCreate': { name: 'shortLinkCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkCreateResult'; ofType: null; }; } }; 'shortLinkDelete': { name: 'shortLinkDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDeleteResult'; ofType: null; }; } }; 'shortLinkDomainCreate': { name: 'shortLinkDomainCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainCreateResult'; ofType: null; }; } }; 'shortLinkDomainDelete': { name: 'shortLinkDomainDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainDeleteResult'; ofType: null; }; } }; 'shortLinkDomainUpdate': { name: 'shortLinkDomainUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkDomainUpdateResult'; ofType: null; }; } }; 'shortLinkUpdate': { name: 'shortLinkUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationShortLinkUpdateResult'; ofType: null; }; } }; 'signUpBlockCreate': { name: 'signUpBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; }; } }; 'signUpBlockUpdate': { name: 'signUpBlockUpdate'; type: { kind: 'OBJECT'; name: 'SignUpBlock'; ofType: null; } }; 'signUpSubmissionEventCreate': { name: 'signUpSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SignUpSubmissionEvent'; ofType: null; }; } }; 'siteCreate': { name: 'siteCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'UNION'; name: 'MutationSiteCreateResult'; ofType: null; }; } }; 'spacerBlockCreate': { name: 'spacerBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'spacerBlockUpdate': { name: 'spacerBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpacerBlock'; ofType: null; }; } }; 'stepBlockCreate': { name: 'stepBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepBlockPositionUpdate': { name: 'stepBlockPositionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; }; }; } }; 'stepBlockUpdate': { name: 'stepBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepBlock'; ofType: null; }; } }; 'stepNextEventCreate': { name: 'stepNextEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepNextEvent'; ofType: null; }; } }; 'stepPreviousEventCreate': { name: 'stepPreviousEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepPreviousEvent'; ofType: null; }; } }; 'stepViewEventCreate': { name: 'stepViewEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'StepViewEvent'; ofType: null; }; } }; 'teamCreate': { name: 'teamCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'teamUpdate': { name: 'teamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Team'; ofType: null; }; } }; 'textResponseBlockCreate': { name: 'textResponseBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; }; } }; 'textResponseBlockUpdate': { name: 'textResponseBlockUpdate'; type: { kind: 'OBJECT'; name: 'TextResponseBlock'; ofType: null; } }; 'textResponseSubmissionEventCreate': { name: 'textResponseSubmissionEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TextResponseSubmissionEvent'; ofType: null; }; } }; 'triggerUnsplashDownload': { name: 'triggerUnsplashDownload'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'typographyBlockCreate': { name: 'typographyBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'typographyBlockUpdate': { name: 'typographyBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TypographyBlock'; ofType: null; }; } }; 'updateJourneysEmailPreference': { name: 'updateJourneysEmailPreference'; type: { kind: 'OBJECT'; name: 'JourneysEmailPreference'; ofType: null; } }; 'updateVideoAlgoliaIndex': { name: 'updateVideoAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'updateVideoVariantAlgoliaIndex': { name: 'updateVideoVariantAlgoliaIndex'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'userDeleteCheck': { name: 'userDeleteCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteCheckResult'; ofType: null; }; } }; 'userDeleteJourneysCheck': { name: 'userDeleteJourneysCheck'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteJourneysCheckResult'; ofType: null; }; } }; 'userDeleteJourneysConfirm': { name: 'userDeleteJourneysConfirm'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteJourneysConfirmResult'; ofType: null; }; } }; 'userImpersonate': { name: 'userImpersonate'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'userInviteAcceptAll': { name: 'userInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; }; }; } }; 'userInviteCreate': { name: 'userInviteCreate'; type: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; } }; 'userInviteRemove': { name: 'userInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserInvite'; ofType: null; }; } }; 'userJourneyApprove': { name: 'userJourneyApprove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyOpen': { name: 'userJourneyOpen'; type: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; } }; 'userJourneyPromote': { name: 'userJourneyPromote'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemove': { name: 'userJourneyRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userJourneyRemoveAll': { name: 'userJourneyRemoveAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; }; }; } }; 'userJourneyRequest': { name: 'userJourneyRequest'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserJourney'; ofType: null; }; } }; 'userMediaProfileUpdate': { name: 'userMediaProfileUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserMediaProfile'; ofType: null; }; } }; 'userTeamDelete': { name: 'userTeamDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'userTeamInviteAcceptAll': { name: 'userTeamInviteAcceptAll'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; }; }; } }; 'userTeamInviteCreate': { name: 'userTeamInviteCreate'; type: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; } }; 'userTeamInviteRemove': { name: 'userTeamInviteRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeamInvite'; ofType: null; }; } }; 'userTeamUpdate': { name: 'userTeamUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserTeam'; ofType: null; }; } }; 'validateEmail': { name: 'validateEmail'; type: { kind: 'OBJECT'; name: 'AuthenticatedUser'; ofType: null; } }; 'videoBlockCreate': { name: 'videoBlockCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoBlockUpdate': { name: 'videoBlockUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoBlock'; ofType: null; }; } }; 'videoCollapseEventCreate': { name: 'videoCollapseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCollapseEvent'; ofType: null; }; } }; 'videoCompleteEventCreate': { name: 'videoCompleteEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoCompleteEvent'; ofType: null; }; } }; 'videoCreate': { name: 'videoCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDelete': { name: 'videoDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoDescriptionCreate': { name: 'videoDescriptionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionDelete': { name: 'videoDescriptionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoDescriptionUpdate': { name: 'videoDescriptionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoDescription'; ofType: null; }; } }; 'videoEditionCreate': { name: 'videoEditionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionDelete': { name: 'videoEditionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoEditionUpdate': { name: 'videoEditionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoEdition'; ofType: null; }; } }; 'videoExpandEventCreate': { name: 'videoExpandEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoExpandEvent'; ofType: null; }; } }; 'videoImageAltCreate': { name: 'videoImageAltCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltDelete': { name: 'videoImageAltDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoImageAltUpdate': { name: 'videoImageAltUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoImageAlt'; ofType: null; }; } }; 'videoOriginCreate': { name: 'videoOriginCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginDelete': { name: 'videoOriginDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoOriginUpdate': { name: 'videoOriginUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoOrigin'; ofType: null; }; } }; 'videoPauseEventCreate': { name: 'videoPauseEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPauseEvent'; ofType: null; }; } }; 'videoPlayEventCreate': { name: 'videoPlayEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPlayEvent'; ofType: null; }; } }; 'videoProgressEventCreate': { name: 'videoProgressEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoProgressEvent'; ofType: null; }; } }; 'videoPublishChildren': { name: 'videoPublishChildren'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenResult'; ofType: null; }; } }; 'videoPublishChildrenAndLanguages': { name: 'videoPublishChildrenAndLanguages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoPublishChildrenAndLanguagesResult'; ofType: null; }; } }; 'videoSnippetCreate': { name: 'videoSnippetCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetDelete': { name: 'videoSnippetDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoSnippetUpdate': { name: 'videoSnippetUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSnippet'; ofType: null; }; } }; 'videoStartEventCreate': { name: 'videoStartEventCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStartEvent'; ofType: null; }; } }; 'videoStudyQuestionCreate': { name: 'videoStudyQuestionCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionDelete': { name: 'videoStudyQuestionDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoStudyQuestionUpdate': { name: 'videoStudyQuestionUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoStudyQuestion'; ofType: null; }; } }; 'videoSubtitleCreate': { name: 'videoSubtitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleDelete': { name: 'videoSubtitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoSubtitleUpdate': { name: 'videoSubtitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoSubtitle'; ofType: null; }; } }; 'videoTitleCreate': { name: 'videoTitleCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleDelete': { name: 'videoTitleDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoTitleUpdate': { name: 'videoTitleUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoTitle'; ofType: null; }; } }; 'videoUpdate': { name: 'videoUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Video'; ofType: null; }; } }; 'videoVariantCreate': { name: 'videoVariantCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDelete': { name: 'videoVariantDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'videoVariantDownloadCreate': { name: 'videoVariantDownloadCreate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadDelete': { name: 'videoVariantDownloadDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantDownloadUpdate': { name: 'videoVariantDownloadUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariantDownload'; ofType: null; }; } }; 'videoVariantUpdate': { name: 'videoVariantUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'VideoVariant'; ofType: null; }; } }; 'visitorUpdate': { name: 'visitorUpdate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; 'visitorUpdateForCurrentUser': { name: 'visitorUpdateForCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Visitor'; ofType: null; }; } }; }; }; 'MutationAudioPreviewCreateInput': { kind: 'INPUT_OBJECT'; name: 'MutationAudioPreviewCreateInput'; isOneOf: false; inputFields: [{ name: 'languageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'value'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'duration'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'size'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'bitrate'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'codec'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'MutationAudioPreviewUpdateInput': { kind: 'INPUT_OBJECT'; name: 'MutationAudioPreviewUpdateInput'; isOneOf: false; inputFields: [{ name: 'languageId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'duration'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'size'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'bitrate'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'codec'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'MutationBibleCitationCreateInput': { kind: 'INPUT_OBJECT'; name: 'MutationBibleCitationCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'osisId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'videoId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'bibleBookId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'chapterStart'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }, { name: 'chapterEnd'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'verseStart'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'verseEnd'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; defaultValue: null }, { name: 'order'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; }; defaultValue: null }]; }; @@ -312,7 +312,7 @@ export type introspection_types = { 'StepViewEvent': { kind: 'OBJECT'; name: 'StepViewEvent'; fields: { 'createdAt': { name: 'createdAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'label': { name: 'label'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'value': { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'StepViewEventCreateInput': { kind: 'INPUT_OBJECT'; name: 'StepViewEventCreateInput'; isOneOf: false; inputFields: [{ name: 'id'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; defaultValue: null }, { name: 'blockId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'value'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'String': unknown; - 'Subscription': { kind: 'OBJECT'; name: 'Subscription'; fields: { 'journeyAiTranslateCreateSubscription': { name: 'journeyAiTranslateCreateSubscription'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyAiTranslateProgress'; ofType: null; }; } }; }; }; + 'Subscription': { kind: 'OBJECT'; name: 'Subscription'; fields: { 'journeyAiTranslateCreateSubscription': { name: 'journeyAiTranslateCreateSubscription'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'JourneyAiTranslateProgress'; ofType: null; }; } }; 'userDeleteConfirm': { name: 'userDeleteConfirm'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteConfirmProgress'; ofType: null; }; } }; }; }; 'Tag': { kind: 'OBJECT'; name: 'Tag'; fields: { 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TagName'; ofType: null; }; }; }; } }; 'parentId': { name: 'parentId'; type: { kind: 'SCALAR'; name: 'ID'; ofType: null; } }; 'service': { name: 'service'; type: { kind: 'ENUM'; name: 'Service'; ofType: null; } }; }; }; 'TagName': { kind: 'OBJECT'; name: 'TagName'; fields: { 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'language': { name: 'language'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Language'; ofType: null; }; } }; 'primary': { name: 'primary'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'value': { name: 'value'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; 'Taxonomy': { kind: 'OBJECT'; name: 'Taxonomy'; fields: { 'category': { name: 'category'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TaxonomyName'; ofType: null; }; }; }; } }; 'term': { name: 'term'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; @@ -353,6 +353,13 @@ export type introspection_types = { 'UnsplashUserLinks': { kind: 'OBJECT'; name: 'UnsplashUserLinks'; fields: { 'followers': { name: 'followers'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'following': { name: 'following'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'html': { name: 'html'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'likes': { name: 'likes'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'photos': { name: 'photos'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'portfolio': { name: 'portfolio'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'self': { name: 'self'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; 'User': { kind: 'INTERFACE'; name: 'User'; fields: { 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; }; possibleTypes: 'AnonymousUser' | 'AuthenticatedUser'; }; 'UserAgent': { kind: 'OBJECT'; name: 'UserAgent'; fields: { 'browser': { name: 'browser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Browser'; ofType: null; }; } }; 'device': { name: 'device'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Device'; ofType: null; }; } }; 'os': { name: 'os'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'OperatingSystem'; ofType: null; }; } }; }; }; + 'UserDeleteCheckResult': { kind: 'OBJECT'; name: 'UserDeleteCheckResult'; fields: { 'journeysToDelete': { name: 'journeysToDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'journeysToRemove': { name: 'journeysToRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'journeysToTransfer': { name: 'journeysToTransfer'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'logs': { name: 'logs'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteLogEntry'; ofType: null; }; }; }; } }; 'teamsToDelete': { name: 'teamsToDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'teamsToRemove': { name: 'teamsToRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'teamsToTransfer': { name: 'teamsToTransfer'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'userEmail': { name: 'userEmail'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'userFirstName': { name: 'userFirstName'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'userId': { name: 'userId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; + 'UserDeleteConfirmProgress': { kind: 'OBJECT'; name: 'UserDeleteConfirmProgress'; fields: { 'done': { name: 'done'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'log': { name: 'log'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteLogEntry'; ofType: null; }; } }; 'success': { name: 'success'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; }; }; + 'UserDeleteIdType': { name: 'UserDeleteIdType'; enumValues: 'databaseId' | 'email'; }; + 'UserDeleteJourneysCheckResult': { kind: 'OBJECT'; name: 'UserDeleteJourneysCheckResult'; fields: { 'journeysToDelete': { name: 'journeysToDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'journeysToRemove': { name: 'journeysToRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'journeysToTransfer': { name: 'journeysToTransfer'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'logs': { name: 'logs'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteJourneysLogEntry'; ofType: null; }; }; }; } }; 'teamsToDelete': { name: 'teamsToDelete'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'teamsToRemove': { name: 'teamsToRemove'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'teamsToTransfer': { name: 'teamsToTransfer'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; }; }; + 'UserDeleteJourneysConfirmResult': { kind: 'OBJECT'; name: 'UserDeleteJourneysConfirmResult'; fields: { 'deletedJourneyIds': { name: 'deletedJourneyIds'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'deletedTeamIds': { name: 'deletedTeamIds'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'deletedUserJourneyIds': { name: 'deletedUserJourneyIds'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'deletedUserTeamIds': { name: 'deletedUserTeamIds'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'logs': { name: 'logs'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserDeleteJourneysLogEntry'; ofType: null; }; }; }; } }; 'success': { name: 'success'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; }; }; + 'UserDeleteJourneysLogEntry': { kind: 'OBJECT'; name: 'UserDeleteJourneysLogEntry'; fields: { 'level': { name: 'level'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'message': { name: 'message'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'timestamp': { name: 'timestamp'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; + 'UserDeleteLogEntry': { kind: 'OBJECT'; name: 'UserDeleteLogEntry'; fields: { 'level': { name: 'level'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'message': { name: 'message'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'timestamp': { name: 'timestamp'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; 'UserInvite': { kind: 'OBJECT'; name: 'UserInvite'; fields: { 'acceptedAt': { name: 'acceptedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'email': { name: 'email'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'removedAt': { name: 'removedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'senderId': { name: 'senderId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; }; }; 'UserInviteCreateInput': { kind: 'INPUT_OBJECT'; name: 'UserInviteCreateInput'; isOneOf: false; inputFields: [{ name: 'email'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; 'UserJourney': { kind: 'OBJECT'; name: 'UserJourney'; fields: { 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journey': { name: 'journey'; type: { kind: 'OBJECT'; name: 'Journey'; ofType: null; } }; 'journeyId': { name: 'journeyId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'journeyNotification': { name: 'journeyNotification'; type: { kind: 'OBJECT'; name: 'JourneyNotification'; ofType: null; } }; 'openedAt': { name: 'openedAt'; type: { kind: 'SCALAR'; name: 'DateTime'; ofType: null; } }; 'role': { name: 'role'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'ENUM'; name: 'UserJourneyRole'; ofType: null; }; } }; 'user': { name: 'user'; type: { kind: 'INTERFACE'; name: 'User'; ofType: null; } }; 'userId': { name: 'userId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; }; };