Skip to content

identity hexagonal refactor - #159

Merged
pramodnarayana merged 4 commits into
developmentfrom
refactor/identity-hexagonal
Jun 12, 2026
Merged

pramodnarayana merged 4 commits into
developmentfrom
refactor/identity-hexagonal

Conversation

@pramodnarayana

@pramodnarayana pramodnarayana commented Jun 12, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Global encryption configuration added (local keys + KMS) for API and worker.
    • New user-management use cases: create, get profile, get by id, list (merged with invitations), and remove.
  • Bug Fixes

    • Users endpoints consistently enforce tenant scoping and return clearer not-found/authorization errors.
  • Chores

    • Migration to repository-based identity integrations and package export reorganization.
    • Updated tests and package wiring to align with the refactor.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@pramodnarayana, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 30 minutes and 27 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 85c1d225-7885-4f24-850a-dd8abf1c4a8a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f19e94 and fea7d47.

📒 Files selected for processing (7)
  • apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-user.repository.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-user.repository.ts
  • packages/identity/src/core/ports/outbound/errors.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts
  • packages/identity/src/identity.module.spec.ts
  • packages/identity/src/index.spec.ts
📝 Walkthrough

Walkthrough

This PR migrates identity outbound contracts from provider to repository ports, renames adapters and tokens, adds user orchestration use-cases and tests, reshapes package exports, and wires a global EncryptionModule into API and worker apps.

Changes

Provider-to-Repository Pattern Migration with Use Cases

Layer / File(s) Summary
Port interfaces and public surface
packages/identity/src/core/ports/outbound/*, packages/identity/src/index.ts
Renames outbound port interfaces to repository ports, adds UserListItem type, consolidates outbound exports, and updates package entry-point exports.
Adapter implementations and specs
packages/identity/src/adapters/outbound/drizzle-*-repository.ts, packages/identity/src/adapters/outbound/*.spec.ts
Renames Drizzle adapters to repository adapters, repoints imports to core/ports/outbound, and updates tests to instantiate and assert the new repository adapters.
IdentityModule wiring & tests
packages/identity/src/identity.module.ts, packages/identity/src/identity.module.spec.ts
Switches module provider bindings and exports to *_REPOSITORY tokens, adds user use-case providers/exports, and tightens async option typing.
Controller/service DI updates
apps/api/src/modules/identity/{auth,roles,system-admin,tenants}/*, packages/auth/src/services/auth.service.ts
Controllers and auth service now inject repository tokens/types (IUserRepository, ITenantRepository, etc.); corresponding tests register mocks under new tokens.
User use-cases
packages/identity/src/core/use-cases/users/*.ts, *.spec.ts
Adds use-cases: CreateUserUseCase, GetUserProfileUseCase, GetUserByIdUseCase, ListUsersWithInvitationsUseCase, RemoveUserUseCase, with tests covering repository calls and error mapping.
UsersController delegation
apps/api/src/modules/identity/users/users.controller.ts, users.controller.spec.ts
Refactors controller to delegate to use-cases, imports shared UserListItem, and updates tests to assert use-case invocations and exception propagation.
Ports and config updates for Better Auth
packages/identity/src/better-auth.config.ts, packages/identity/src/adapters/outbound/better-auth.*
Switches Better Auth tenant dependency to ITenantRepository/TENANT_REPOSITORY and updates imports to use outbound core ports and shared utils.
Public exports and barrel cleanup
packages/identity/src/index.ts, packages/identity/src/interfaces/index.ts, packages/identity/src/services/identity-event-publisher.service.ts
Replaces interfaces barrel with core/ports/outbound exports, adds use-case and repository adapter exports, and updates internal imports to the new paths.
Type-only import refinements
packages/credentials/src/oauth/*, packages/credentials/src/index.ts
Split IEncryptionService into type-only imports while keeping ENCRYPTION_SERVICE as a runtime value import; update docs to point crypto utilities to @soopa/security.

Encryption Module Integration

Layer / File(s) Summary
API & worker encryption setup
apps/api/src/app/app.module.ts, apps/worker/src/app.module.ts, apps/worker/package.json
Adds EncryptionModule.forRootAsync to AppModules, configuring mode from INFRA_MODE and wiring ENCRYPTION_KEY, KMS_KEY_ID, KMS_ENDPOINT, and KMS_REGION via ConfigService; worker workspace adds @soopa/security dependency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🐰 From providers old to repositories new,
Use-cases now handle what controllers once knew.
Encryption arrives, configs set true,
Ports and adapters shifted into view. 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'identity hexagonal refactor' directly describes the main architectural change across the changeset: moving from provider-based to repository-based abstractions (hexagonal architecture pattern) in the identity module.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/identity-hexagonal

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.

@coderabbitai coderabbitai Bot left a comment •

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.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/identity/src/index.spec.ts (1)

55-66: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Cover the newly exported barrel surface here.

This smoke test still only asserts adapter symbols, so it will not catch regressions in the new root exports added in index.ts for the outbound ports and user use cases. Add a few representative assertions for those new exports so the package-surface refactor is actually guarded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/index.spec.ts` around lines 55 - 66, The test only
asserts adapter symbols; update the spec to also assert the new root exports for
the outbound ports and user use cases so the package surface change is covered:
open the package's index.ts, identify the newly exported symbols (e.g., the
outbound port interfaces and user use case classes/functions such as
CreateUserUseCase, UserOutboundPort — replace with the exact names you find) and
add expect((IdentityPackage as any).<ExportName>).toBeDefined() assertions for
each representative export alongside the existing adapter assertions (reference
IdentityPackage and the adapter names already in the test to locate where to add
them).
packages/identity/src/adapters/outbound/drizzle-user.repository.ts (1)

300-302: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Throw the repository contract’s NotFoundException here.

IUserRepository.deleteIfNotLastAdmin() documents a NotFoundException when the user is missing or not a member of the tenant, but this branch now throws a generic Error. That turns a normal not-found/masked-access path into an untyped failure for every repository consumer behind the new token wiring.

Suggested fix
-import { Inject, Injectable, Logger } from "`@nestjs/common`";
+import { Inject, Injectable, Logger, NotFoundException } from "`@nestjs/common`";
...
       if (!membershipWithRole.length) {
-        throw new Error("User is not a member of this organization");
+        throw new NotFoundException("User not found");
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/adapters/outbound/drizzle-user.repository.ts` around
lines 300 - 302, Replace the generic Error thrown when membershipWithRole is
empty with the repository contract's NotFoundException so callers receive the
documented not-found behavior; locate the check in deleteIfNotLastAdmin (the
branch that currently does if (!membershipWithRole.length) throw new Error("User
is not a member of this organization")) and throw NotFoundException (or
construct the repository's NotFoundException type) with an appropriate message
instead of Error.
apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts (1)

207-219: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stub the role lookup in the create-user happy path.

SystemAdminController.createUser() now rejects any truthy role when roleProvider.findById() returns nothing, but this test passes "member" without arranging that lookup. The current expectation fails before mockUserProvider.create() is reached.

Suggested fix
   it('should create user via provider', async () => {
       mockUserProvider.findByEmail.mockResolvedValue(null);
+      mockRoleProvider.findById.mockResolvedValue({ id: 'member' });
       const mockUser = {
         id: 'u1',
         email: 'new@example.com',
       };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts`
around lines 207 - 219, The test calls SystemAdminController.createUser with
role 'member' but doesn't stub the role lookup, causing createUser to reject
when roleProvider.findById returns undefined; update the test to arrange/mock
the role lookup before calling controller.createUser by having the mock role
provider's findById (e.g., mockRoleProvider.findById) return a valid role object
(an object containing at least id/name) so the happy path continues to
mockUserProvider.create and the test exercises the create flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/app/app.module.ts`:
- Around line 52-60: The EncryptionModule.forRootAsync useFactory is missing the
KMS region parameter; update the factory (the useFactory function that receives
ConfigService / cfg) to include region: cfg.get('KMS_REGION') alongside kmsKeyId
and kmsEndpoint so the returned config object supplies region to the shared
EncryptionModule; make this change in the app.module where
EncryptionModule.forRootAsync is registered for both the API and the worker.

In `@packages/identity/src/core/ports/outbound/types.ts`:
- Around line 60-78: Update the UserListItem union's invitation variant to make
the name property optional: change the invitation branch (the object with kind:
"invitation") so that name?: string instead of name: string; this keeps all
other fields (id, email, role, status, emailVerified, createdAt, updatedAt,
isInvitation, permissions, image, banned, banReason, banExpires) intact and
aligns the invitation shape with cases that map name to an empty string.

In `@packages/identity/src/core/use-cases/users/create-user.use-case.ts`:
- Around line 14-16: The execute method in CreateUserUseCase is a pure
pass-through to userRepository.create(input) so either add meaningful logic
(validation, error mapping, and side-effects) or remove the indirection: if you
keep it, validate CreateUserInput inside execute (e.g., required fields, email
format), call this.userRepository.create(input), wrap repository errors into
domain errors, and emit a UserCreated event via the event bus (e.g.,
eventBus.publish('UserCreated', user)); if you remove it, delete
CreateUserUseCase and update callers/controllers to inject and call
userRepository.create(...) directly, removing the thin wrapper.

In `@packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts`:
- Around line 30-31: Replace the expensive tenant list load in
get-user-by-id.use-case: instead of calling
tenantRepository.findAllForUser(user.id) and using userTenants.some(...) to set
isMember, call tenantRepository.findOneForUser(user.id, tenantId) and set
isMember = Boolean(result) (or check for null/undefined). Update the code paths
that reference userTenants/isMember to use the new lookup so authorization is
based on the single tenant-scoped query.

In
`@packages/identity/src/core/use-cases/users/get-user-profile.use-case.spec.ts`:
- Around line 16-24: Add a unit test that covers the NotFoundException path:
mock userRepository.findById to resolve to null and assert that
useCase.execute("user-id") rejects/throws a NotFoundException. Locate the spec
file testing GetUserProfileUseCase (references: GetUserProfileUseCase,
useCase.execute, userRepository.findById) and add a new it/test block that
verifies the thrown error is an instance (or has the expected name/message) of
NotFoundException when findById returns null.

In
`@packages/identity/src/core/use-cases/users/list-users-with-invitations.use-case.ts`:
- Around line 33-48: The invitedUsers array maps pendingInvitations into
UserListItem objects and currently sets banned: false which is incorrect for
invitations; update the mapping (in the invitedUsers creation where
pendingInvitations.map is used) to set banned: undefined so the optional banned
field correctly reflects that invitees have no ban status (leave all other
fields unchanged).

In `@packages/identity/src/core/use-cases/users/remove-user.use-case.ts`:
- Around line 38-44: The current catch in remove-user.use-case.ts relies on
matching an English error string from deleteIfNotLastAdmin(), which is brittle;
instead either (A) add an explicit membership check before deletion (call a
repository method like usersRepository.isMember or
organizationRepository.hasMember with the userId and organizationId and throw
NotFoundException if false) or (B) change the repository contract so
deleteIfNotLastAdmin() throws a typed/unique error (e.g. MembershipError or
NotMemberError) and catch that specific class here to rethrow NotFoundException;
update the catch to test the specific error type
(MembershipError/NotMemberError) rather than error.message.includes(...).

In `@packages/identity/src/identity.module.spec.ts`:
- Around line 3-9: The test is missing the ROLE_REPOSITORY token in its wiring
assertions; update the import list and the assertions in identity.module.spec.ts
to include ROLE_REPOSITORY so the suite verifies that IdentityModule
registers/exports the role repository and that RolesController can inject it;
specifically add ROLE_REPOSITORY to the imported constants and include it in the
same assertion blocks where AUTH_PROVIDER, USER_REPOSITORY, TENANT_REPOSITORY,
PERMISSION_REPOSITORY, and IDENTITY_OPTIONS are checked.

In `@packages/identity/src/identity.module.ts`:
- Around line 43-48: registerAsync() is currently ignoring the
eventPublisherToken returned by the async options factory (so callers that set
eventPublisherToken in the resolved IdentityModuleOptions silently get the
default IdentityEventPublisher), causing inconsistent behavior vs register();
update the registerAsync implementation (the function named registerAsync and
its options handling) to read and honor eventPublisherToken from the resolved
options object returned by useFactory (and from any passed-in AsyncOptions
interface), and wire that token into the providers the same way register() does
(also update the other async-registration branches where options are mapped —
the blocks around the other async provider builds that currently only check
top-level tokens — to select options.eventPublisherToken when present). Ensure
you reference IdentityModuleOptions.eventPublisherToken and the
eventPublisherToken variable used in provider registration so the async path
exposes the same contract as the sync path.

---

Outside diff comments:
In `@apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts`:
- Around line 207-219: The test calls SystemAdminController.createUser with role
'member' but doesn't stub the role lookup, causing createUser to reject when
roleProvider.findById returns undefined; update the test to arrange/mock the
role lookup before calling controller.createUser by having the mock role
provider's findById (e.g., mockRoleProvider.findById) return a valid role object
(an object containing at least id/name) so the happy path continues to
mockUserProvider.create and the test exercises the create flow.

In `@packages/identity/src/adapters/outbound/drizzle-user.repository.ts`:
- Around line 300-302: Replace the generic Error thrown when membershipWithRole
is empty with the repository contract's NotFoundException so callers receive the
documented not-found behavior; locate the check in deleteIfNotLastAdmin (the
branch that currently does if (!membershipWithRole.length) throw new Error("User
is not a member of this organization")) and throw NotFoundException (or
construct the repository's NotFoundException type) with an appropriate message
instead of Error.

In `@packages/identity/src/index.spec.ts`:
- Around line 55-66: The test only asserts adapter symbols; update the spec to
also assert the new root exports for the outbound ports and user use cases so
the package surface change is covered: open the package's index.ts, identify the
newly exported symbols (e.g., the outbound port interfaces and user use case
classes/functions such as CreateUserUseCase, UserOutboundPort — replace with the
exact names you find) and add expect((IdentityPackage as
any).<ExportName>).toBeDefined() assertions for each representative export
alongside the existing adapter assertions (reference IdentityPackage and the
adapter names already in the test to locate where to add them).
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e925e46-c16b-4fe3-9d8f-be2b22437698

📥 Commits

Reviewing files that changed from the base of the PR and between 6c0e7eb and 66f56e3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (60)
  • apps/api/src/app/app.module.ts
  • apps/api/src/modules/identity/auth/auth.controller.coverage.spec.ts
  • apps/api/src/modules/identity/auth/auth.controller.spec.ts
  • apps/api/src/modules/identity/auth/auth.controller.ts
  • apps/api/src/modules/identity/auth/platform.guard.spec.ts
  • apps/api/src/modules/identity/roles/roles.controller.spec.ts
  • apps/api/src/modules/identity/roles/roles.controller.ts
  • apps/api/src/modules/identity/roles/roles.controller.visibility.spec.ts
  • apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts
  • apps/api/src/modules/identity/system-admin/system-admin.controller.ts
  • apps/api/src/modules/identity/tenants/tenants.controller.spec.ts
  • apps/api/src/modules/identity/tenants/tenants.controller.ts
  • apps/api/src/modules/identity/users/users.controller.spec.ts
  • apps/api/src/modules/identity/users/users.controller.ts
  • apps/worker/package.json
  • apps/worker/src/app.module.ts
  • packages/auth/src/services/auth.service.ts
  • packages/credentials/src/index.ts
  • packages/credentials/src/oauth/token-manager.service.ts
  • packages/credentials/src/oauth/token-refresh.service.ts
  • packages/identity/src/adapters/outbound/better-auth.abac.spec.ts
  • packages/identity/src/adapters/outbound/better-auth.adapter.spec.ts
  • packages/identity/src/adapters/outbound/better-auth.adapter.ts
  • packages/identity/src/adapters/outbound/drizzle-permission.repository.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-permission.repository.ts
  • packages/identity/src/adapters/outbound/drizzle-role.repository.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-role.repository.ts
  • packages/identity/src/adapters/outbound/drizzle-tenant.repository.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-tenant.repository.ts
  • packages/identity/src/adapters/outbound/drizzle-user.repository.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-user.repository.ts
  • packages/identity/src/better-auth.config.ts
  • packages/identity/src/constants.ts
  • packages/identity/src/core/ports/outbound/auth-provider.port.ts
  • packages/identity/src/core/ports/outbound/better-auth-config.port.ts
  • packages/identity/src/core/ports/outbound/email-provider.port.ts
  • packages/identity/src/core/ports/outbound/errors.ts
  • packages/identity/src/core/ports/outbound/event-publisher.port.ts
  • packages/identity/src/core/ports/outbound/index.ts
  • packages/identity/src/core/ports/outbound/permission-repository.port.ts
  • packages/identity/src/core/ports/outbound/role-repository.port.ts
  • packages/identity/src/core/ports/outbound/tenant-repository.port.ts
  • packages/identity/src/core/ports/outbound/types.ts
  • packages/identity/src/core/ports/outbound/user-repository.port.ts
  • packages/identity/src/core/use-cases/users/create-user.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/create-user.use-case.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts
  • packages/identity/src/core/use-cases/users/get-user-profile.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/get-user-profile.use-case.ts
  • packages/identity/src/core/use-cases/users/list-users-with-invitations.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/list-users-with-invitations.use-case.ts
  • packages/identity/src/core/use-cases/users/remove-user.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/remove-user.use-case.ts
  • packages/identity/src/identity.module.spec.ts
  • packages/identity/src/identity.module.ts
  • packages/identity/src/index.spec.ts
  • packages/identity/src/index.ts
  • packages/identity/src/interfaces/index.ts
  • packages/identity/src/services/identity-event-publisher.service.ts
💤 Files with no reviewable changes (1)
  • packages/identity/src/interfaces/index.ts

Comment thread apps/api/src/app/app.module.ts
Comment thread packages/identity/src/core/ports/outbound/types.ts
Comment on lines +14 to +16
async execute(input: CreateUserInput) {
return this.userRepository.create(input);
}

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.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider whether this thin wrapper adds sufficient value.

The execute method is a pure pass-through to userRepository.create(input) with no validation, orchestration, or error transformation. If future orchestration (e.g., event publishing, multi-step workflows) is planned, this structure is appropriate. Otherwise, controllers could inject the repository directly, reducing indirection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/core/use-cases/users/create-user.use-case.ts` around
lines 14 - 16, The execute method in CreateUserUseCase is a pure pass-through to
userRepository.create(input) so either add meaningful logic (validation, error
mapping, and side-effects) or remove the indirection: if you keep it, validate
CreateUserInput inside execute (e.g., required fields, email format), call
this.userRepository.create(input), wrap repository errors into domain errors,
and emit a UserCreated event via the event bus (e.g.,
eventBus.publish('UserCreated', user)); if you remove it, delete
CreateUserUseCase and update callers/controllers to inject and call
userRepository.create(...) directly, removing the thin wrapper.

Comment thread packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts Outdated
Comment on lines +38 to +44
} catch (error) {
if (
error instanceof Error &&
error.message.includes("not a member of this organization")
) {
throw new NotFoundException("User not found in this organization");
}

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 | 🏗️ Heavy lift

Don’t couple the not-found path to an adapter error string.

This branch only returns NotFoundException when deleteIfNotLastAdmin() throws an English message containing "not a member of this organization". Any adapter wording change will turn the tenant-scoped 404 into a 500. Move this to a structured repository contract (for example a typed failure reason) or perform an explicit membership lookup before delete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/core/use-cases/users/remove-user.use-case.ts` around
lines 38 - 44, The current catch in remove-user.use-case.ts relies on matching
an English error string from deleteIfNotLastAdmin(), which is brittle; instead
either (A) add an explicit membership check before deletion (call a repository
method like usersRepository.isMember or organizationRepository.hasMember with
the userId and organizationId and throw NotFoundException if false) or (B)
change the repository contract so deleteIfNotLastAdmin() throws a typed/unique
error (e.g. MembershipError or NotMemberError) and catch that specific class
here to rethrow NotFoundException; update the catch to test the specific error
type (MembershipError/NotMemberError) rather than error.message.includes(...).

Comment thread packages/identity/src/identity.module.spec.ts
Comment thread packages/identity/src/identity.module.ts
@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 8 file(s) based on 9 unresolved review comments.

Files modified:

  • apps/api/src/app/app.module.ts
  • apps/worker/src/app.module.ts
  • packages/identity/src/core/ports/outbound/types.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts
  • packages/identity/src/core/use-cases/users/get-user-profile.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/list-users-with-invitations.use-case.ts
  • packages/identity/src/identity.module.spec.ts
  • packages/identity/src/identity.module.ts

Commit: 2abda65a8d746a9f0131551821440fa729475299

The changes have been pushed to the refactor/identity-hexagonal branch.

Time taken: 10m 33s

Fixed 8 file(s) based on 9 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/identity/src/identity.module.ts (1)

201-212: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor eventPublisherToken in registerAsync().

register() supports a custom IdentityModuleOptions.eventPublisherToken, but Lines 205-209 ignore the resolved option and always return defaultPublisher. That silently changes behavior for async callers that provide a custom publisher from useFactory.

Suggested fix
         IdentityEventPublisher,
         {
           provide: IDENTITY_EVENT_PUBLISHER,
-          useFactory: (identityOptions: IdentityModuleOptions, defaultPublisher: IdentityEventPublisher) => {
-            // Honor eventPublisherToken from resolved options
-            // If a custom token was provided, it should be injected at index 2
-            // Otherwise, use the default IdentityEventPublisher
-            return defaultPublisher;
-          },
-          inject: [IDENTITY_OPTIONS, IdentityEventPublisher],
+          useFactory: (
+            identityOptions: IdentityModuleOptions,
+            moduleRef: ModuleRef,
+            defaultPublisher: IdentityEventPublisher,
+          ) => {
+            if (!identityOptions.eventPublisherToken) {
+              return defaultPublisher;
+            }
+
+            return moduleRef.get(identityOptions.eventPublisherToken, {
+              strict: false,
+            });
+          },
+          inject: [IDENTITY_OPTIONS, ModuleRef, IdentityEventPublisher],
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/identity.module.ts` around lines 201 - 212, The
provider for IDENTITY_EVENT_PUBLISHER currently ignores
IdentityModuleOptions.eventPublisherToken and always returns defaultPublisher;
update the provider's useFactory to accept (identityOptions:
IdentityModuleOptions, defaultPublisher: IdentityEventPublisher, moduleRef:
ModuleRef) and, if identityOptions.eventPublisherToken is set, resolve and
return moduleRef.get(identityOptions.eventPublisherToken, { strict: false })
otherwise return defaultPublisher; also update the inject array to
[IDENTITY_OPTIONS, IdentityEventPublisher, ModuleRef] and keep the provider name
IDENTITY_EVENT_PUBLISHER so async register callers who supply a custom token are
honored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/identity/src/identity.module.ts`:
- Around line 6-7: The file imports unused symbols Inject and ModuleRef which
cause lint failures; remove Inject and ModuleRef from the import list in
identity.module.ts (keep Global and Module) unless you later use ModuleRef for
the async publisher fix—update the import statement to only import the actually
used symbols so the linter passes.

---

Duplicate comments:
In `@packages/identity/src/identity.module.ts`:
- Around line 201-212: The provider for IDENTITY_EVENT_PUBLISHER currently
ignores IdentityModuleOptions.eventPublisherToken and always returns
defaultPublisher; update the provider's useFactory to accept (identityOptions:
IdentityModuleOptions, defaultPublisher: IdentityEventPublisher, moduleRef:
ModuleRef) and, if identityOptions.eventPublisherToken is set, resolve and
return moduleRef.get(identityOptions.eventPublisherToken, { strict: false })
otherwise return defaultPublisher; also update the inject array to
[IDENTITY_OPTIONS, IdentityEventPublisher, ModuleRef] and keep the provider name
IDENTITY_EVENT_PUBLISHER so async register callers who supply a custom token are
honored.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67382ae0-ad1e-412f-8953-60547c485387

📥 Commits

Reviewing files that changed from the base of the PR and between 66f56e3 and 2abda65.

📒 Files selected for processing (8)
  • apps/api/src/app/app.module.ts
  • apps/worker/src/app.module.ts
  • packages/identity/src/core/ports/outbound/types.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts
  • packages/identity/src/core/use-cases/users/get-user-profile.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/list-users-with-invitations.use-case.ts
  • packages/identity/src/identity.module.spec.ts
  • packages/identity/src/identity.module.ts

Comment thread packages/identity/src/identity.module.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts (2)

42-49: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add mock call assertions to verify repository method invocations.

The test verifies the user is returned but doesn't confirm that findById and findOneForUser were called with the expected arguments. Following the pattern in get-user-profile.use-case.spec.ts (line 24), add assertions to ensure the use case invokes repositories correctly.

🧪 Suggested assertions
   const result = await useCase.execute("user1", "tenant1");
   expect(result).toEqual(user);
+  expect(userRepository.findById).toHaveBeenCalledWith("user1");
+  expect(tenantRepository.findOneForUser).toHaveBeenCalledWith("user1", "tenant1");
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts`
around lines 42 - 49, The test is missing verifications that the repositories
were called with the expected arguments; after calling useCase.execute("user1",
"tenant1") add assertions that userRepository.findById was invoked with "user1"
and tenantRepository.findOneForUser was invoked with { userId: "user1",
tenantId: "tenant1" } (or the exact shape your use case passes) to mirror the
pattern in get-user-profile.use-case.spec.ts; use
expect(userRepository.findById).toHaveBeenCalledWith("user1") and
expect(tenantRepository.findOneForUser).toHaveBeenCalledWith(/* args your
useCase uses */) and also consider toHaveBeenCalledTimes(1) where appropriate.

32-40: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add mock call assertions to verify repository method invocations.

The test verifies the exception is thrown but doesn't confirm that findById and findOneForUser were called with the expected arguments. Following the pattern in get-user-profile.use-case.spec.ts (lines 24, 31), add assertions to strengthen test confidence.

🧪 Suggested assertions
   await expect(useCase.execute("user1", "tenant1")).rejects.toThrow(
     NotFoundException,
   );
+  expect(userRepository.findById).toHaveBeenCalledWith("user1");
+  expect(tenantRepository.findOneForUser).toHaveBeenCalledWith("user1", "tenant1");
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts`
around lines 32 - 40, The test "should throw NotFoundException if user is not
member of tenant" currently asserts the error but not that repository methods
were invoked; add mock call assertions after the expect to verify
userRepository.findById was called with "user1" and
tenantRepository.findOneForUser was called with the returned user and "tenant1"
(or with the user id per your implementation), using the mocked methods'
toHaveBeenCalledWith/toHaveBeenCalledTimes assertions so the test confirms both
calls occurred as expected when useCase.execute("user1", "tenant1") is invoked.
apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts (1)

207-230: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Assert that the new role-repository dependency is actually used.

This test now stubs mockRoleProvider.findById(), but it never verifies that SystemAdminController.createUser() consulted ROLE_REPOSITORY before creating the user. If that validation call disappears, the test still passes.

✅ Tighten the happy-path assertion
       const result = await controller.createUser({
         name: 'Test',
         email: 'new@example.com',
         role: 'member',
       });

       expect(result).toEqual(mockUser);
+      expect(mockRoleProvider.findById).toHaveBeenCalled();
       expect(mockUserProvider.create).toHaveBeenCalledWith({
         name: 'Test',
         email: 'new@example.com',
         role: 'member',
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts`
around lines 207 - 230, The test currently stubs mockRoleProvider.findById but
never asserts it was used; update the test for SystemAdminController.createUser
to explicitly assert that mockRoleProvider.findById was called with the
requested role (e.g.,
expect(mockRoleProvider.findById).toHaveBeenCalledWith('member')) before
asserting mockUserProvider.create was invoked, so the test fails if createUser
stops consulting ROLE_REPOSITORY (check references to createUser,
mockRoleProvider.findById, and mockUserProvider.create).
packages/identity/src/identity.module.spec.ts (1)

95-115: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Assert the concrete adapter classes, not just that useClass exists.

These checks still pass if the wrong adapter is bound to a token. For this migration, the spec should compare each provider’s useClass to the expected repository/auth adapter so cross-token wiring regressions fail loudly.

✅ Tighten the wiring assertions
 import {
   AUTH_PROVIDER,
   USER_REPOSITORY,
   TENANT_REPOSITORY,
   PERMISSION_REPOSITORY,
   ROLE_REPOSITORY,
   IDENTITY_OPTIONS,
 } from "./constants.js";
+import { BetterAuthAdapter } from "./adapters/outbound/better-auth.adapter.js";
+import { DrizzleUserRepositoryAdapter } from "./adapters/outbound/drizzle-user.repository.js";
+import { DrizzleTenantRepositoryAdapter } from "./adapters/outbound/drizzle-tenant.repository.js";
+import { DrizzlePermissionRepositoryAdapter } from "./adapters/outbound/drizzle-permission.repository.js";
+import { DrizzleRoleRepositoryAdapter } from "./adapters/outbound/drizzle-role.repository.js";
...
-    expect(authProv.useClass).toBeDefined();
+    expect(authProv.useClass).toBe(BetterAuthAdapter);
...
-    expect(userProv.useClass).toBeDefined();
+    expect(userProv.useClass).toBe(DrizzleUserRepositoryAdapter);
...
-    expect(tenantProv.useClass).toBeDefined();
+    expect(tenantProv.useClass).toBe(DrizzleTenantRepositoryAdapter);
...
-    expect(permProv.useClass).toBeDefined();
+    expect(permProv.useClass).toBe(DrizzlePermissionRepositoryAdapter);
...
-    expect(roleProv.useClass).toBeDefined();
+    expect(roleProv.useClass).toBe(DrizzleRoleRepositoryAdapter);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/identity/src/identity.module.spec.ts` around lines 95 - 115, The
tests currently only assert useClass is defined; change them to assert the exact
concrete adapter classes are wired by replacing the loose checks in the
assertClassProvider results (for AUTH_PROVIDER, USER_REPOSITORY,
TENANT_REPOSITORY, PERMISSION_REPOSITORY, ROLE_REPOSITORY) with strict equality
checks against the expected adapter classes (e.g. the concrete Auth adapter and
the concrete repository adapter classes used in your module) so that
assertClassProvider(...).useClass is compared to the specific class (use
expect(...useClass).toBe(ExpectedAuthAdapter / ExpectedUserRepositoryAdapter /
ExpectedTenantRepositoryAdapter / ExpectedPermissionRepositoryAdapter /
ExpectedRoleRepositoryAdapter).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/identity/src/adapters/outbound/drizzle-user.repository.ts`:
- Around line 300-303: The empty membershipWithRole result is being mapped to
UserNotFoundError even when the tenantId does not exist; update the logic in the
repository (around the FOR UPDATE select and the membershipWithRole check) to
first verify the tenant/organization exists (e.g., inspect the result of the FOR
UPDATE select or run an existence check for tenantId) and throw a
TenantNotFoundError (or another tenant-specific error) when the tenant is
missing, otherwise keep throwing UserNotFoundError when the tenant exists but
the membershipWithRole array is empty; reference membershipWithRole,
UserNotFoundError, tenantId and the earlier FOR UPDATE select to locate where to
add the tenant existence check and adjust the thrown error accordingly.

---

Outside diff comments:
In `@apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts`:
- Around line 207-230: The test currently stubs mockRoleProvider.findById but
never asserts it was used; update the test for SystemAdminController.createUser
to explicitly assert that mockRoleProvider.findById was called with the
requested role (e.g.,
expect(mockRoleProvider.findById).toHaveBeenCalledWith('member')) before
asserting mockUserProvider.create was invoked, so the test fails if createUser
stops consulting ROLE_REPOSITORY (check references to createUser,
mockRoleProvider.findById, and mockUserProvider.create).

In `@packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts`:
- Around line 42-49: The test is missing verifications that the repositories
were called with the expected arguments; after calling useCase.execute("user1",
"tenant1") add assertions that userRepository.findById was invoked with "user1"
and tenantRepository.findOneForUser was invoked with { userId: "user1",
tenantId: "tenant1" } (or the exact shape your use case passes) to mirror the
pattern in get-user-profile.use-case.spec.ts; use
expect(userRepository.findById).toHaveBeenCalledWith("user1") and
expect(tenantRepository.findOneForUser).toHaveBeenCalledWith(/* args your
useCase uses */) and also consider toHaveBeenCalledTimes(1) where appropriate.
- Around line 32-40: The test "should throw NotFoundException if user is not
member of tenant" currently asserts the error but not that repository methods
were invoked; add mock call assertions after the expect to verify
userRepository.findById was called with "user1" and
tenantRepository.findOneForUser was called with the returned user and "tenant1"
(or with the user id per your implementation), using the mocked methods'
toHaveBeenCalledWith/toHaveBeenCalledTimes assertions so the test confirms both
calls occurred as expected when useCase.execute("user1", "tenant1") is invoked.

In `@packages/identity/src/identity.module.spec.ts`:
- Around line 95-115: The tests currently only assert useClass is defined;
change them to assert the exact concrete adapter classes are wired by replacing
the loose checks in the assertClassProvider results (for AUTH_PROVIDER,
USER_REPOSITORY, TENANT_REPOSITORY, PERMISSION_REPOSITORY, ROLE_REPOSITORY) with
strict equality checks against the expected adapter classes (e.g. the concrete
Auth adapter and the concrete repository adapter classes used in your module) so
that assertClassProvider(...).useClass is compared to the specific class (use
expect(...useClass).toBe(ExpectedAuthAdapter / ExpectedUserRepositoryAdapter /
ExpectedTenantRepositoryAdapter / ExpectedPermissionRepositoryAdapter /
ExpectedRoleRepositoryAdapter).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbfa3012-2ba0-41f6-865c-95cb03b8e825

📥 Commits

Reviewing files that changed from the base of the PR and between 2abda65 and 7f19e94.

📒 Files selected for processing (8)
  • apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts
  • packages/identity/src/adapters/outbound/drizzle-user.repository.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.spec.ts
  • packages/identity/src/core/use-cases/users/get-user-by-id.use-case.ts
  • packages/identity/src/core/use-cases/users/get-user-profile.use-case.spec.ts
  • packages/identity/src/identity.module.spec.ts
  • packages/identity/src/identity.module.ts
  • packages/identity/src/index.spec.ts

Comment thread packages/identity/src/adapters/outbound/drizzle-user.repository.ts
@pramodnarayana
pramodnarayana merged commit 3604010 into development Jun 12, 2026
2 checks passed
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.

1 participant