Skip to content

feat: add journeyTransferFromAnonymous mutation to transfer journeys …#8946

Open
mikeallisonJS wants to merge 1 commit intomainfrom
26-00-MA-fix-guest-login-workflow-backend
Open

feat: add journeyTransferFromAnonymous mutation to transfer journeys …#8946
mikeallisonJS wants to merge 1 commit intomainfrom
26-00-MA-fix-guest-login-workflow-backend

Conversation

@mikeallisonJS
Copy link
Copy Markdown
Collaborator

@mikeallisonJS mikeallisonJS commented Apr 2, 2026

…from anonymous users

  • Introduced a new mutation journeyTransferFromAnonymous in the GraphQL schema to allow the transfer of journeys from anonymous users to a specified team.
  • Updated the schema and generated types to include the new mutation and its arguments.
  • Added corresponding tests to ensure the functionality works as expected.
  • Enhanced existing types with updatedAt fields for better tracking of changes.

Summary by CodeRabbit

  • New Features
    • Anonymous Journey Transfer: Journeys created under anonymous accounts can now be transferred to your teams. When transferring, specify a destination team explicitly or enable automatic assignment to have the system select the most appropriate team based on your current team memberships and roles.

…from anonymous users

- Introduced a new mutation `journeyTransferFromAnonymous` in the GraphQL schema to allow the transfer of journeys from anonymous users to a specified team.
- Updated the schema and generated types to include the new mutation and its arguments.
- Added corresponding tests to ensure the functionality works as expected.
- Enhanced existing types with `updatedAt` fields for better tracking of changes.
@mikeallisonJS mikeallisonJS requested a review from Kneesal April 2, 2026 01:36
@mikeallisonJS mikeallisonJS self-assigned this Apr 2, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Walkthrough

A new GraphQL mutation journeyTransferFromAnonymous is added to enable transferring ownership of anonymous journeys to authenticated users, with optional team specification and auto-resolution to the user's earliest managed team.

Changes

Cohort / File(s) Summary
GraphQL Schema Definitions
apis/api-gateway/schema.graphql, apis/api-journeys-modern/schema.graphql
Added new mutation field journeyTransferFromAnonymous(journeyId: ID!, teamId: ID): Journey! to expose the transfer capability on both gateway and modern journey API layers.
Mutation Implementation
apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts
Implemented the mutation resolver with journey validation, ownership checks, team resolution fallback logic (provided teamId → manager team → first team), Prisma transaction to update ownership and team assignment, and cleanup of empty source teams.
Schema Registration
apis/api-journeys-modern/src/schema/journey/index.ts
Added import to register the new mutation definition alongside existing journey schema modules.
Test Suite
apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts
Added comprehensive Jest test covering authentication enforcement, journey/team resolution, ownership transfer, no-op behavior for already-owned journeys, old team cleanup logic, and error scenarios (missing journey, no user teams, non-anonymous owners).

Sequence Diagram

sequenceDiagram
    actor User
    participant GraphQL
    participant Mutation as journeyTransferFromAnonymous<br/>(Resolver)
    participant Prisma as Prisma<br/>(Database)
    
    User->>GraphQL: journeyTransferFromAnonymous(journeyId, teamId?)
    GraphQL->>Mutation: Execute with auth context
    Mutation->>Mutation: Validate authenticated user
    Mutation->>Prisma: Load journey + userJourneys + teams
    Mutation->>Mutation: Validate journey exists & has owner
    Mutation->>Mutation: Check if user already owns journey
    alt User does not own journey
        Mutation->>Mutation: Verify owner has no email (anonymous)
        Mutation->>Mutation: Resolve destination teamId<br/>(provided → manager team → first team)
    else User owns journey
        Mutation->>Mutation: Use existing team or resolve
    end
    alt Team changed or ownership needs update
        Mutation->>Prisma: Begin transaction
        Mutation->>Prisma: Delete old userJourney records
        Mutation->>Prisma: Create new owner userJourney<br/>(if user didn't own)
        Mutation->>Prisma: Update journey.teamId
        Mutation->>Mutation: Check if old team empty
        alt Old team has no remaining journeys
            Mutation->>Prisma: Delete userTeam rows for old team
            Mutation->>Prisma: Delete team record
        end
        Mutation->>Prisma: Commit transaction
    else Already on correct team & owned
        Mutation->>Mutation: Return journey (no-op)
    end
    Mutation->>GraphQL: Return transferred Journey
    GraphQL->>User: Journey
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a new journeyTransferFromAnonymous mutation to transfer journeys from anonymous users.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 26-00-MA-fix-guest-login-workflow-backend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 2, 2026

View your CI Pipeline Execution ↗ for commit d695765

Command Status Duration Result
nx run watch-e2e:e2e ✅ Succeeded 20s View ↗
nx run resources-e2e:e2e ✅ Succeeded 15s View ↗
nx run journeys-admin-e2e:e2e ✅ Succeeded 28s View ↗
nx run journeys-e2e:e2e ✅ Succeeded 19s View ↗
nx run videos-admin-e2e:e2e ✅ Succeeded 5s View ↗
nx run player-e2e:e2e ✅ Succeeded 3s View ↗
nx run short-links-e2e:e2e ✅ Succeeded 3s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
Additional runs (20) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2026-04-02 01:47:16 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
player ✅ Ready player preview Thu Apr 2 14:39:59 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
short-links ✅ Ready short-links preview Thu Apr 2 14:40:06 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
videos-admin ✅ Ready videos-admin preview Thu Apr 2 14:40:15 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Thu Apr 2 14:40:34 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
resources ✅ Ready resources preview Thu Apr 2 14:40:36 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys ✅ Ready journeys preview Thu Apr 2 14:41:00 NZDT 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Thu Apr 2 14:42:39 NZDT 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts (1)

126-326: Reduce any casts in mocks to keep this spec type-safe.

The repeated as any and fn: any patterns can hide contract drift between resolver queries and fixtures.

♻️ Suggested typed pattern
+type JourneyFindUniqueResult = Awaited<
+  ReturnType<typeof prismaMock.journey.findUnique>
+>
+
+type TxCallback = Parameters<typeof prismaMock.$transaction>[0]
+
- prismaMock.journey.findUnique.mockResolvedValue(mockJourney as any)
+ prismaMock.journey.findUnique.mockResolvedValue(
+   mockJourney as JourneyFindUniqueResult
+ )
 
- prismaMock.$transaction.mockImplementation(async (fn: any) => fn(txMock))
+ prismaMock.$transaction.mockImplementation(async (fn: TxCallback) => fn(txMock))

As per coding guidelines: **/*.{ts,tsx}: Define a type if possible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts`
around lines 126 - 326, Tests use broad any casts (e.g., mockJourney as any, fn:
any, txMock returned by makeTxMock), which weakens type safety; replace those
with concrete types by typing fixtures and mocks to Prisma/GraphQL types and the
transaction helper — e.g., type mockJourney and mockUserTeam as the appropriate
Prisma model payloads, have makeTxMock return a typed TxMock interface, and type
the $transaction implementation parameter as (tx: TxMock) => Promise<any>
instead of any; update calls like
prismaMock.journey.findUnique.mockResolvedValue(...) and
mockPrismaUsers.user.findFirst.mockResolvedValue(...) to use the properly typed
fixtures so the compiler catches contract drift.
apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts (3)

140-142: Silent .catch(() => undefined) may mask unexpected errors.

While catching errors during team deletion handles FK constraint violations gracefully, catching and ignoring all errors could mask unexpected database issues or misconfigurations.

Consider logging the error or being more selective about which errors to suppress.

💡 Suggested improvement
-          await tx.team
-            .delete({ where: { id: oldTeamId } })
-            .catch(() => undefined)
+          await tx.team
+            .delete({ where: { id: oldTeamId } })
+            .catch((error) => {
+              // Expected: FK constraints if related records still exist
+              // Log unexpected errors for observability
+              if (!error.code || error.code !== 'P2003') {
+                // Consider logging: logger.warn({ error, oldTeamId }, 'Unexpected error deleting team')
+              }
+            })

Prisma error code P2003 indicates foreign key constraint violation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`
around lines 140 - 142, The silent .catch on tx.team.delete({ where: { id:
oldTeamId } }) hides all errors; change it to handle only foreign-key constraint
errors (Prisma error code P2003) by detecting
Prisma.PrismaClientKnownRequestError (e.g., instanceof
Prisma.PrismaClientKnownRequestError and err.code === 'P2003') and swallow that
case, but for any other error log it (use the existing logger such as
processLogger or transaction logger) and rethrow so unexpected DB issues aren't
masked. Ensure the fix references tx.team.delete and oldTeamId and imports
Prisma types if needed.

48-59: Consider explicit handling when owner user record is missing.

If ownerUser is null (user record not found in users DB), the current logic silently allows the transfer. While this may be intentional (treating missing users as anonymous), the implicit behavior could mask data inconsistencies.

The anonymous check correctly uses email != null per the canonical definition in apis/api-users/src/schema/builder.ts.

💡 Optional: Make missing user handling explicit
       if (!alreadyOwns) {
         const ownerUser = await prismaUsers.user.findFirst({
           where: { userId: ownerUserJourney.userId },
           select: { email: true }
         })
-        if (ownerUser != null && ownerUser.email != null) {
+        // If user record exists and has an email, they are not anonymous
+        if (ownerUser?.email != null) {
           throw new GraphQLError(
             'Journey owner is not an anonymous user; transfer is not permitted',
             { extensions: { code: 'FORBIDDEN' } }
           )
         }
+        // ownerUser == null or ownerUser.email == null means anonymous or missing user record
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`
around lines 48 - 59, The current transfer logic silently treats a missing user
record as anonymous; update the check around prismaUsers.user.findFirst (used to
fetch ownerUser for ownerUserJourney) to explicitly handle ownerUser === null:
either reject the transfer with a GraphQLError (e.g., same FORBIDDEN code) or
log/throw a distinct error indicating a missing owner record before allowing
transfer; modify the block that uses alreadyOwns and ownerUser to explicitly
branch on ownerUser === null (instead of implicitly permitting transfer) so the
intent is clear and data inconsistencies are not silently ignored.

129-133: Type safety: resolvedTeamId is string | null but guaranteed non-null here.

At line 132, resolvedTeamId has type string | null, but we've guaranteed it's non-null by the check at lines 99-103. Consider using a non-null assertion or restructuring to help TypeScript understand this invariant.

💡 Optional: Use non-null assertion for clarity
         const updated = await tx.journey.update({
           ...query,
           where: { id: journeyId },
-          data: { teamId: resolvedTeamId }
+          data: { teamId: resolvedTeamId! }
         })

Alternatively, throw immediately after resolution and use as string:

// After line 97
if (resolvedTeamId == null) {
  throw new GraphQLError('No team found for the current user', {
    extensions: { code: 'BAD_REQUEST' }
  })
}
const finalTeamId: string = resolvedTeamId
// Use finalTeamId below
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`
around lines 129 - 133, The variable resolvedTeamId is typed as string | null
but is guaranteed non-null by your earlier check; update the code so TypeScript
knows it cannot be null before calling tx.journey.update. Either assert non-null
when passing it (resolvedTeamId!) or assign it to a non-null typed constant
(e.g., const finalTeamId: string = resolvedTeamId) after the null-check and use
finalTeamId in the tx.journey.update call; reference resolvedTeamId and the
tx.journey.update call to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`:
- Around line 23-29: The Prisma query that fetches the journey via
prisma.journey.findUnique does not respect soft deletes; update the query
referenced (prisma.journey.findUnique used when resolving
journeyTransferFromAnonymous) to filter out deleted journeys by adding
deletedAt: null to the where clause (e.g. change to a composite where/AND or use
findFirst with where: { id: journeyId, deletedAt: null }) so transfers cannot
act on soft-deleted Journey records; keep the existing include (userJourneys,
team) unchanged.

---

Nitpick comments:
In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts`:
- Around line 126-326: Tests use broad any casts (e.g., mockJourney as any, fn:
any, txMock returned by makeTxMock), which weakens type safety; replace those
with concrete types by typing fixtures and mocks to Prisma/GraphQL types and the
transaction helper — e.g., type mockJourney and mockUserTeam as the appropriate
Prisma model payloads, have makeTxMock return a typed TxMock interface, and type
the $transaction implementation parameter as (tx: TxMock) => Promise<any>
instead of any; update calls like
prismaMock.journey.findUnique.mockResolvedValue(...) and
mockPrismaUsers.user.findFirst.mockResolvedValue(...) to use the properly typed
fixtures so the compiler catches contract drift.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`:
- Around line 140-142: The silent .catch on tx.team.delete({ where: { id:
oldTeamId } }) hides all errors; change it to handle only foreign-key constraint
errors (Prisma error code P2003) by detecting
Prisma.PrismaClientKnownRequestError (e.g., instanceof
Prisma.PrismaClientKnownRequestError and err.code === 'P2003') and swallow that
case, but for any other error log it (use the existing logger such as
processLogger or transaction logger) and rethrow so unexpected DB issues aren't
masked. Ensure the fix references tx.team.delete and oldTeamId and imports
Prisma types if needed.
- Around line 48-59: The current transfer logic silently treats a missing user
record as anonymous; update the check around prismaUsers.user.findFirst (used to
fetch ownerUser for ownerUserJourney) to explicitly handle ownerUser === null:
either reject the transfer with a GraphQLError (e.g., same FORBIDDEN code) or
log/throw a distinct error indicating a missing owner record before allowing
transfer; modify the block that uses alreadyOwns and ownerUser to explicitly
branch on ownerUser === null (instead of implicitly permitting transfer) so the
intent is clear and data inconsistencies are not silently ignored.
- Around line 129-133: The variable resolvedTeamId is typed as string | null but
is guaranteed non-null by your earlier check; update the code so TypeScript
knows it cannot be null before calling tx.journey.update. Either assert non-null
when passing it (resolvedTeamId!) or assign it to a non-null typed constant
(e.g., const finalTeamId: string = resolvedTeamId) after the null-check and use
finalTeamId in the tx.journey.update call; reference resolvedTeamId and the
tx.journey.update call to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3ef1cf00-3d05-4b11-88d9-eaafd5ea48a4

📥 Commits

Reviewing files that changed from the base of the PR and between 97c5d50 and d695765.

⛔ Files ignored due to path filters (2)
  • apis/api-journeys/src/__generated__/graphql.ts is excluded by !**/__generated__/**
  • libs/shared/gql/src/__generated__/graphql-env.d.ts is excluded by !**/__generated__/**
📒 Files selected for processing (5)
  • apis/api-gateway/schema.graphql
  • apis/api-journeys-modern/schema.graphql
  • apis/api-journeys-modern/src/schema/journey/index.ts
  • apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.spec.ts
  • apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts

Comment on lines +23 to +29
const journey = await prisma.journey.findUnique({
where: { id: journeyId },
include: {
userJourneys: true,
team: { include: { userTeams: true } }
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add deletedAt: null filter to respect soft deletes.

The journey fetch does not filter out soft-deleted journeys. This could allow transferring ownership of a deleted journey, which is likely unintended.

As per coding guidelines: "Always filter where: { deletedAt: null } in Prisma queries to respect soft deletes on blocks, journeys, and other entities."

🔧 Proposed fix
       const journey = await prisma.journey.findUnique({
-        where: { id: journeyId },
+        where: { id: journeyId, deletedAt: null },
         include: {
           userJourneys: true,
           team: { include: { userTeams: true } }
         }
       })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const journey = await prisma.journey.findUnique({
where: { id: journeyId },
include: {
userJourneys: true,
team: { include: { userTeams: true } }
}
})
const journey = await prisma.journey.findFirst({
where: { id: journeyId, deletedAt: null },
include: {
userJourneys: true,
team: { include: { userTeams: true } }
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apis/api-journeys-modern/src/schema/journey/journeyTransferFromAnonymous.mutation.ts`
around lines 23 - 29, The Prisma query that fetches the journey via
prisma.journey.findUnique does not respect soft deletes; update the query
referenced (prisma.journey.findUnique used when resolving
journeyTransferFromAnonymous) to filter out deleted journeys by adding
deletedAt: null to the where clause (e.g. change to a composite where/AND or use
findFirst with where: { id: journeyId, deletedAt: null }) so transfers cannot
act on soft-deleted Journey records; keep the existing include (userJourneys,
team) unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants