From fbb0d13e445a533cc0defb2b5a19096daa45b9c3 Mon Sep 17 00:00:00 2001 From: Mike Allison Date: Thu, 2 Apr 2026 00:45:58 +0000 Subject: [PATCH 1/4] fix: login after guest flow --- apis/api-gateway/schema.graphql | 1 + apis/api-journeys-modern/schema.graphql | 1 + .../src/schema/journey/index.ts | 1 + ...rneyTransferFromAnonymous.mutation.spec.ts | 347 ++++++++++++++++++ .../journeyTransferFromAnonymous.mutation.ts | 149 ++++++++ .../api-journeys/src/__generated__/graphql.ts | 79 +++- .../JourneyTransferFromAnonymous.ts | 21 ++ .../pages/templates/[journeyId]/customize.tsx | 58 ++- apps/journeys-admin/pages/users/verify.tsx | 13 +- .../SignIn/PasswordPage/PasswordPage.spec.tsx | 62 +++- .../SignIn/PasswordPage/PasswordPage.tsx | 30 +- .../SignIn/RegisterPage/RegisterPage.spec.tsx | 67 ++++ .../SignIn/RegisterPage/RegisterPage.tsx | 57 ++- .../SignInServiceButton.spec.tsx | 101 ++++- .../SignInServiceButton.tsx | 51 ++- .../addTransferParam/addTransferParam.spec.ts | 29 ++ .../addTransferParam/addTransferParam.ts | 9 + .../SignIn/utils/addTransferParam/index.ts | 1 + .../GuestPreviewScreen/GuestPreviewScreen.tsx | 8 + .../src/libs/pendingGuestJourney/index.ts | 5 + .../pendingGuestJourney.spec.ts | 75 ++++ .../pendingGuestJourney.ts | 36 ++ .../gql/src/__generated__/graphql-env.d.ts | 2 +- 23 files changed, 1146 insertions(+), 57 deletions(-) create mode 100644 apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts create mode 100644 apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts create mode 100644 apps/journeys-admin/__generated__/JourneyTransferFromAnonymous.ts create mode 100644 apps/journeys-admin/src/components/SignIn/utils/addTransferParam/addTransferParam.spec.ts create mode 100644 apps/journeys-admin/src/components/SignIn/utils/addTransferParam/addTransferParam.ts create mode 100644 apps/journeys-admin/src/components/SignIn/utils/addTransferParam/index.ts create mode 100644 apps/journeys-admin/src/libs/pendingGuestJourney/index.ts create mode 100644 apps/journeys-admin/src/libs/pendingGuestJourney/pendingGuestJourney.spec.ts create mode 100644 apps/journeys-admin/src/libs/pendingGuestJourney/pendingGuestJourney.ts diff --git a/apis/api-gateway/schema.graphql b/apis/api-gateway/schema.graphql index 3829001aa22..827f4d15eaa 100644 --- a/apis/api-gateway/schema.graphql +++ b/apis/api-gateway/schema.graphql @@ -410,6 +410,7 @@ type Mutation @join__type(graph: API_ANALYTICS) @join__type(graph: API_JOURNEYS integrationGoogleCreate(input: IntegrationGoogleCreateInput!) : IntegrationGoogle! @join__field(graph: API_JOURNEYS_MODERN) integrationGoogleUpdate(id: ID!, input: IntegrationGoogleUpdateInput!) : IntegrationGoogle! @join__field(graph: API_JOURNEYS_MODERN) integrationDelete(id: ID!) : Integration! @join__field(graph: API_JOURNEYS_MODERN) + journeyTransferFromAnonymous(journeyId: ID!, teamId: ID) : Journey! @join__field(graph: API_JOURNEYS_MODERN) journeyAiTranslateCreate(input: JourneyAiTranslateInput!) : Journey! @join__field(graph: API_JOURNEYS_MODERN) createJourneyEventsExportLog(input: JourneyEventsExportLogInput!) : JourneyEventsExportLog! @join__field(graph: API_JOURNEYS_MODERN) journeyLanguageAiDetect(input: MutationJourneyLanguageAiDetectInput!) : Boolean! @join__field(graph: API_JOURNEYS_MODERN) diff --git a/apis/api-journeys-modern/schema.graphql b/apis/api-journeys-modern/schema.graphql index 6936136c8a8..582abf595b3 100644 --- a/apis/api-journeys-modern/schema.graphql +++ b/apis/api-journeys-modern/schema.graphql @@ -1414,6 +1414,7 @@ type Mutation { integrationGoogleCreate(input: IntegrationGoogleCreateInput!): IntegrationGoogle! integrationGoogleUpdate(id: ID!, input: IntegrationGoogleUpdateInput!): IntegrationGoogle! integrationDelete(id: ID!): Integration! + journeyTransferFromAnonymous(journeyId: ID!, teamId: ID): Journey! journeyAiTranslateCreate(input: JourneyAiTranslateInput!): Journey! createJourneyEventsExportLog(input: JourneyEventsExportLogInput!): JourneyEventsExportLog! journeyLanguageAiDetect(input: MutationJourneyLanguageAiDetectInput!): Boolean! diff --git a/apis/api-journeys-modern/src/schema/journey/index.ts b/apis/api-journeys-modern/src/schema/journey/index.ts index 090bdf99fb8..e30e9ff6f19 100644 --- a/apis/api-journeys-modern/src/schema/journey/index.ts +++ b/apis/api-journeys-modern/src/schema/journey/index.ts @@ -1,5 +1,6 @@ import './adminJourney.query' import './adminJourneys.query' import './journey' +import './journeyTransferFromAnonymous.mutation' import './inputs' import './enums' diff --git a/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts b/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts new file mode 100644 index 00000000000..87aea14ab18 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts @@ -0,0 +1,347 @@ +import { ExecutionResult } from 'graphql' + +import { UserJourneyRole, UserTeamRole } from '@core/prisma/journeys/client' +import { getUserFromPayload } from '@core/yoga/firebaseClient' + +import { getClient } from '../../../test/client' +import { prismaMock } from '../../../test/prismaMock' +import { graphql } from '../../lib/graphql/subgraphGraphql' + +jest.mock('@core/yoga/firebaseClient', () => ({ + getUserFromPayload: jest.fn() +})) + +jest.mock('@core/prisma/users/client', () => ({ + prisma: { + user: { + findFirst: jest.fn() + } + } +})) + +const { prisma: mockPrismaUsers } = jest.requireMock( + '@core/prisma/users/client' +) + +const mockGetUserFromPayload = getUserFromPayload as jest.MockedFunction< + typeof getUserFromPayload +> + +const JOURNEY_TRANSFER_MUTATION = graphql(` + mutation JourneyTransferFromAnonymous($journeyId: ID!, $teamId: ID) { + journeyTransferFromAnonymous(journeyId: $journeyId, teamId: $teamId) { + id + } + } +`) + +describe('journeyTransferFromAnonymous', () => { + const mockUser = { + id: 'authUserId', + email: 'test@example.com', + emailVerified: true, + firstName: 'Test', + lastName: 'User', + imageUrl: null, + roles: [] + } + + const authClient = getClient({ + headers: { authorization: 'token' }, + context: { currentUser: mockUser } + }) + + const mockJourney = { + id: 'journeyId', + teamId: 'anonTeamId', + userJourneys: [ + { + id: 'ujId', + userId: 'anonUserId', + journeyId: 'journeyId', + role: UserJourneyRole.owner, + updatedAt: new Date() + } + ], + team: { + id: 'anonTeamId', + userTeams: [ + { + id: 'utId', + userId: 'anonUserId', + teamId: 'anonTeamId', + role: UserTeamRole.manager + } + ] + } + } + + const mockUserTeam = { + id: 'targetUtId', + userId: mockUser.id, + teamId: 'targetTeamId', + role: UserTeamRole.manager, + createdAt: new Date(), + updatedAt: new Date() + } + + beforeEach(() => { + jest.clearAllMocks() + mockGetUserFromPayload.mockReturnValue(mockUser) + prismaMock.userRole.findUnique.mockResolvedValue({ + id: 'userRoleId', + userId: mockUser.id, + roles: [] + }) + }) + + function makeTxMock(targetTeamId = 'targetTeamId') { + return { + userJourney: { + deleteMany: jest.fn().mockResolvedValue({ count: 1 }), + create: jest.fn().mockResolvedValue({ + id: 'newUjId', + userId: mockUser.id, + journeyId: 'journeyId', + role: UserJourneyRole.owner + }) + }, + journey: { + update: jest.fn().mockResolvedValue({ + id: 'journeyId', + teamId: targetTeamId + }), + count: jest.fn().mockResolvedValue(0) + }, + userTeam: { + deleteMany: jest.fn().mockResolvedValue({ count: 1 }) + }, + team: { + delete: jest.fn().mockResolvedValue({}) + } + } + } + + it('should transfer journey with explicit teamId', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ email: null }) + prismaMock.userTeam.findFirst.mockResolvedValue(mockUserTeam) + + const txMock = makeTxMock() + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId', teamId: 'targetTeamId' } + })) as ExecutionResult<{ + journeyTransferFromAnonymous: { id: string } + }> + + expect(result.errors).toBeUndefined() + expect(result.data?.journeyTransferFromAnonymous.id).toBe('journeyId') + expect(txMock.journey.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: 'journeyId' }, + data: { teamId: 'targetTeamId' } + }) + ) + }) + + it('should auto-resolve to first managed team when teamId is omitted', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ email: null }) + + prismaMock.userTeam.findFirst + .mockResolvedValueOnce(mockUserTeam) + .mockResolvedValueOnce(mockUserTeam) + + const txMock = makeTxMock() + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult<{ + journeyTransferFromAnonymous: { id: string } + }> + + expect(result.errors).toBeUndefined() + expect(result.data?.journeyTransferFromAnonymous.id).toBe('journeyId') + expect(prismaMock.userTeam.findFirst).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + userId: mockUser.id, + role: UserTeamRole.manager + }, + orderBy: { createdAt: 'asc' } + }) + ) + expect(txMock.journey.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: { teamId: 'targetTeamId' } + }) + ) + }) + + it('should return error if journey is not found', async () => { + prismaMock.journey.findUnique.mockResolvedValue(null) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'nonExistent' } + })) as ExecutionResult + + expect(result.errors).toBeDefined() + expect(result.errors?.[0].message).toBe('Journey not found') + }) + + it('should move team without changing ownership when user already owns (linked account)', async () => { + const ownedJourney = { + ...mockJourney, + userJourneys: [ + { + ...mockJourney.userJourneys[0], + userId: mockUser.id + } + ] + } + prismaMock.journey.findUnique.mockResolvedValue(ownedJourney as any) + prismaMock.userTeam.findFirst.mockResolvedValue(mockUserTeam) + + const txMock = makeTxMock() + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult<{ + journeyTransferFromAnonymous: { id: string } + }> + + expect(result.errors).toBeUndefined() + expect(result.data?.journeyTransferFromAnonymous.id).toBe('journeyId') + expect(txMock.userJourney.deleteMany).not.toHaveBeenCalled() + expect(txMock.userJourney.create).not.toHaveBeenCalled() + expect(txMock.journey.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: { teamId: 'targetTeamId' } + }) + ) + }) + + it('should no-op when user already owns and team is already correct', async () => { + const ownedJourney = { + ...mockJourney, + teamId: 'targetTeamId', + userJourneys: [ + { + ...mockJourney.userJourneys[0], + userId: mockUser.id + } + ] + } + prismaMock.journey.findUnique.mockResolvedValue(ownedJourney as any) + prismaMock.userTeam.findFirst.mockResolvedValue(mockUserTeam) + prismaMock.journey.findUniqueOrThrow.mockResolvedValue({ + id: 'journeyId', + teamId: 'targetTeamId' + } as any) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult<{ + journeyTransferFromAnonymous: { id: string } + }> + + expect(result.errors).toBeUndefined() + expect(result.data?.journeyTransferFromAnonymous.id).toBe('journeyId') + expect(prismaMock.$transaction).not.toHaveBeenCalled() + }) + + it('should return error if journey owner is not anonymous', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ + email: 'owner@example.com' + }) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult + + expect(result.errors).toBeDefined() + expect(result.errors?.[0].message).toContain('not an anonymous user') + }) + + it('should fall through to auto-resolve when provided teamId user is not a member of', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ email: null }) + + prismaMock.userTeam.findFirst + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockUserTeam) + .mockResolvedValueOnce(mockUserTeam) + + const txMock = makeTxMock() + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId', teamId: 'invalidTeamId' } + })) as ExecutionResult<{ + journeyTransferFromAnonymous: { id: string } + }> + + expect(result.errors).toBeUndefined() + expect(result.data?.journeyTransferFromAnonymous.id).toBe('journeyId') + expect(txMock.journey.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: { teamId: 'targetTeamId' } + }) + ) + }) + + it('should return error if user has no teams', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ email: null }) + prismaMock.userTeam.findFirst.mockResolvedValue(null) + + const result = (await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult + + expect(result.errors).toBeDefined() + expect(result.errors?.[0].message).toContain('No team found') + }) + + it('should not delete old team if other journeys remain', async () => { + prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any) + mockPrismaUsers.user.findFirst.mockResolvedValue({ email: null }) + prismaMock.userTeam.findFirst.mockResolvedValue(mockUserTeam) + + const txMock = makeTxMock() + txMock.journey.count.mockResolvedValue(2) + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock)) + + await authClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId', teamId: 'targetTeamId' } + }) + + expect(txMock.userTeam.deleteMany).not.toHaveBeenCalled() + expect(txMock.team.delete).not.toHaveBeenCalled() + }) + + it('should require authentication', async () => { + const publicClient = getClient() + + const result = (await publicClient({ + document: JOURNEY_TRANSFER_MUTATION, + variables: { journeyId: 'journeyId' } + })) as ExecutionResult + + expect(result.errors).toBeDefined() + }) +}) diff --git a/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts b/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts new file mode 100644 index 00000000000..eef3cc7c9d9 --- /dev/null +++ b/apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts @@ -0,0 +1,149 @@ +import { GraphQLError } from 'graphql' + +import { + UserJourneyRole, + UserTeamRole, + prisma +} from '@core/prisma/journeys/client' +import { prisma as prismaUsers } from '@core/prisma/users/client' + +import { builder } from '../builder' + +import { JourneyRef } from './journey' + +builder.mutationField('journeyTransferFromAnonymous', (t) => + t.withAuth({ isAuthenticated: true }).prismaField({ + type: JourneyRef, + nullable: false, + args: { + journeyId: t.arg({ type: 'ID', required: true }), + teamId: t.arg({ type: 'ID', required: false }) + }, + resolve: async (query, _parent, { journeyId, teamId }, context) => { + const journey = await prisma.journey.findUnique({ + where: { id: journeyId }, + include: { + userJourneys: true, + team: { include: { userTeams: true } } + } + }) + + if (journey == null) { + throw new GraphQLError('Journey not found', { + extensions: { code: 'NOT_FOUND' } + }) + } + + const ownerUserJourney = journey.userJourneys.find( + (uj) => uj.role === UserJourneyRole.owner + ) + if (ownerUserJourney == null) { + throw new GraphQLError('Journey has no owner', { + extensions: { code: 'BAD_REQUEST' } + }) + } + + const alreadyOwns = ownerUserJourney.userId === context.user.id + + if (!alreadyOwns) { + const ownerUser = await prismaUsers.user.findFirst({ + where: { userId: ownerUserJourney.userId }, + select: { email: true } + }) + if (ownerUser != null && ownerUser.email != null) { + throw new GraphQLError( + 'Journey owner is not an anonymous user; transfer is not permitted', + { extensions: { code: 'FORBIDDEN' } } + ) + } + } + + let resolvedTeamId: string | null = null + + if (teamId != null) { + const membership = await prisma.userTeam.findFirst({ + where: { + teamId, + userId: context.user.id, + role: { in: [UserTeamRole.manager, UserTeamRole.member] } + } + }) + if (membership != null) { + resolvedTeamId = teamId + } + } + + if (resolvedTeamId == null) { + const ownedTeam = await prisma.userTeam.findFirst({ + where: { + userId: context.user.id, + role: UserTeamRole.manager + }, + orderBy: { createdAt: 'asc' } + }) + if (ownedTeam != null) { + resolvedTeamId = ownedTeam.teamId + } + } + + if (resolvedTeamId == null) { + const anyTeam = await prisma.userTeam.findFirst({ + where: { userId: context.user.id }, + orderBy: { createdAt: 'asc' } + }) + if (anyTeam != null) { + resolvedTeamId = anyTeam.teamId + } + } + + if (resolvedTeamId == null) { + throw new GraphQLError('No team found for the current user', { + extensions: { code: 'BAD_REQUEST' } + }) + } + + if (journey.teamId === resolvedTeamId) { + return await prisma.journey.findUniqueOrThrow({ + ...query, + where: { id: journeyId } + }) + } + + const oldTeamId = journey.teamId + + return await prisma.$transaction(async (tx) => { + if (!alreadyOwns) { + await tx.userJourney.deleteMany({ + where: { journeyId } + }) + + await tx.userJourney.create({ + data: { + userId: context.user.id, + journeyId, + role: UserJourneyRole.owner + } + }) + } + + const updated = await tx.journey.update({ + ...query, + where: { id: journeyId }, + data: { teamId: resolvedTeamId } + }) + + const remainingJourneys = await tx.journey.count({ + where: { teamId: oldTeamId } + }) + if (remainingJourneys === 0) { + await tx.userTeam.deleteMany({ where: { teamId: oldTeamId } }) + await tx.team + .delete({ where: { id: oldTeamId } }) + .catch(() => undefined) + } + + return updated + }) + } + }) +) diff --git a/apis/api-journeys/src/__generated__/graphql.ts b/apis/api-journeys/src/__generated__/graphql.ts index e0385f5df7f..c5f99195cbe 100644 --- a/apis/api-journeys/src/__generated__/graphql.ts +++ b/apis/api-journeys/src/__generated__/graphql.ts @@ -88,6 +88,7 @@ export type BibleBook = { order: Scalars['Int']['output']; osisId: Scalars['String']['output']; paratextAbbreviation: Scalars['String']['output']; + updatedAt: Scalars['DateTime']['output']; }; @@ -103,6 +104,10 @@ export type BibleBookName = { value: Scalars['String']['output']; }; +export type BibleBooksFilter = { + updatedAt?: InputMaybe; +}; + export type BibleCitation = { __typename?: 'BibleCitation'; bibleBook: BibleBook; @@ -569,6 +574,10 @@ export type ContinentName = { value: Scalars['String']['output']; }; +export type CountriesFilter = { + updatedAt?: InputMaybe; +}; + export type Country = { __typename?: 'Country'; continent: Continent; @@ -583,6 +592,7 @@ export type Country = { longitude?: Maybe; name: Array; population?: Maybe; + updatedAt: Scalars['DateTime']['output']; }; @@ -678,6 +688,11 @@ export type CustomDomainVerificationResponse = { message: Scalars['String']['output']; }; +export type DateTimeFilter = { + gte?: InputMaybe; + lte?: InputMaybe; +}; + export enum DefaultPlatform { Android = 'android', Ios = 'ios', @@ -1603,9 +1618,14 @@ export type Keyword = { __typename?: 'Keyword'; id: Scalars['ID']['output']; language: Language; + updatedAt: Scalars['DateTime']['output']; value: Scalars['String']['output']; }; +export type KeywordsFilter = { + updatedAt?: InputMaybe; +}; + export type LabeledVideoCounts = { __typename?: 'LabeledVideoCounts'; featureFilmCount: Scalars['Int']['output']; @@ -1623,6 +1643,7 @@ export type Language = { labeledVideoCounts: LabeledVideoCounts; name: Array; slug?: Maybe; + updatedAt: Scalars['DateTime']['output']; }; @@ -1657,6 +1678,7 @@ export type LanguagesFilter = { bcp47?: InputMaybe>; ids?: InputMaybe>; iso3?: InputMaybe>; + updatedAt?: InputMaybe; }; export type LinkAction = Action & { @@ -1895,6 +1917,7 @@ export type Mutation = { journeyThemeCreate: JourneyTheme; journeyThemeDelete: JourneyTheme; journeyThemeUpdate: JourneyTheme; + journeyTransferFromAnonymous: Journey; journeyUpdate: Journey; /** * Creates a JourneyViewEvent, returns null if attempting to create another @@ -2007,7 +2030,6 @@ export type Mutation = { videoPlayEventCreate: VideoPlayEvent; videoProgressEventCreate: VideoProgressEvent; videoPublishChildren: VideoPublishChildrenResult; - videoPublishChildrenAndLanguages: VideoPublishChildrenAndLanguagesResult; videoSnippetCreate: VideoSnippet; videoSnippetDelete: VideoSnippet; videoSnippetUpdate: VideoSnippet; @@ -2519,6 +2541,12 @@ export type MutationJourneyThemeUpdateArgs = { }; +export type MutationJourneyTransferFromAnonymousArgs = { + journeyId: Scalars['ID']['input']; + teamId?: InputMaybe; +}; + + export type MutationJourneyUpdateArgs = { id: Scalars['ID']['input']; input: JourneyUpdateInput; @@ -3023,12 +3051,9 @@ export type MutationVideoProgressEventCreateArgs = { export type MutationVideoPublishChildrenArgs = { + dryRun: Scalars['Boolean']['input']; id: Scalars['ID']['input']; -}; - - -export type MutationVideoPublishChildrenAndLanguagesArgs = { - id: Scalars['ID']['input']; + mode: VideoPublishMode; }; @@ -3881,6 +3906,11 @@ export type QueryArclightApiKeyByKeyArgs = { }; +export type QueryBibleBooksArgs = { + where?: InputMaybe; +}; + + export type QueryBibleCitationArgs = { id: Scalars['ID']['input']; }; @@ -3914,6 +3944,7 @@ export type QueryCheckVideoVariantsInAlgoliaArgs = { export type QueryCountriesArgs = { ids?: InputMaybe>; term?: InputMaybe; + where?: InputMaybe; }; @@ -4088,6 +4119,11 @@ export type QueryJourneysPlausibleStatsTimeseriesArgs = { }; +export type QueryKeywordsArgs = { + where?: InputMaybe; +}; + + export type QueryLanguageArgs = { id: Scalars['ID']['input']; idType?: InputMaybe; @@ -5292,6 +5328,7 @@ export type Video = { studyQuestions: Array; subtitles: Array; title: Array; + updatedAt: Scalars['DateTime']['output']; /** @deprecated Use variants instead */ variant?: Maybe; variantLanguages: Array; @@ -5626,6 +5663,7 @@ export type VideoEdition = { __typename?: 'VideoEdition'; id: Scalars['ID']['output']; name?: Maybe; + updatedAt: Scalars['DateTime']['output']; videoSubtitles: Array; videoVariants: Array; }; @@ -5795,22 +5833,30 @@ export type VideoProgressEventCreateInput = { value?: InputMaybe; }; -export type VideoPublishChildrenAndLanguagesResult = { - __typename?: 'VideoPublishChildrenAndLanguagesResult'; +export type VideoPublishChildrenResult = { + __typename?: 'VideoPublishChildrenResult'; + dryRun?: Maybe; parentId?: Maybe; - publishedChildIds?: Maybe>; - publishedChildrenCount?: Maybe; publishedVariantIds?: Maybe>; publishedVariantsCount?: Maybe; + publishedVideoCount?: Maybe; + publishedVideoIds?: Maybe>; + videosFailedValidation: Array; }; -export type VideoPublishChildrenResult = { - __typename?: 'VideoPublishChildrenResult'; - parentId?: Maybe; - publishedChildIds?: Maybe>; - publishedChildrenCount?: Maybe; +export type VideoPublishChildrenUnpublishedVideo = { + __typename?: 'VideoPublishChildrenUnpublishedVideo'; + message?: Maybe; + missingFields?: Maybe>; + videoId?: Maybe; }; +export enum VideoPublishMode { + ChildrenVideosAndVariants = 'childrenVideosAndVariants', + ChildrenVideosOnly = 'childrenVideosOnly', + VariantsOnly = 'variantsOnly' +} + export enum VideoRedirectType { Dh = 'dh', Dl = 'dl', @@ -6010,6 +6056,7 @@ export type VideoVariant = { slug: Scalars['String']['output']; subtitle: Array; subtitleCount: Scalars['Int']['output']; + updatedAt: Scalars['DateTime']['output']; /** version control for master video file */ version: Scalars['Int']['output']; video?: Maybe