Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 6691ca7e8d fix: add index on appToken type and context email for invitation lookups
https://sonarly.com/issue/20167?type=bug

The `GetCurrentUser` GraphQL query takes ~1.9 seconds due to an unindexed full table scan on the `appToken` table's JSONB `context->>'email'` column during workspace invitation lookup, causing noticeable UI lag on every page load.

Fix: Added a TypeORM core migration that creates a partial composite index on the `core.appToken` table:

```sql
CREATE INDEX "IDX_APP_TOKEN_TYPE_CONTEXT_EMAIL"
  ON "core"."appToken" ("type", (context->>'email'))
  WHERE "deletedAt" IS NULL
```

This index directly targets the `findInvitationsByEmail` query in `workspace-invitation.service.ts` (lines 96-108) which filters by `type = 'INVITATION_TOKEN'` and `context->>'email' = :email` with `deletedAt IS NULL`. Without this index, PostgreSQL performs a full sequential scan of the `appToken` table, extracting JSONB text for every row — taking 1469ms in the traced transaction.

The index is:
- **Composite** on `(type, context->>'email')` to match the two-column filter pattern
- **Partial** with `WHERE deletedAt IS NULL` to exclude soft-deleted rows and keep the index small
- **Expression-based** using `context->>'email'` to index the JSONB text extraction directly

The migration follows the team's existing convention (seen in `foreignKeyIndexStandardization` and other migrations) with proper `up`/`down` methods.

**Note for large deployments**: If the `appToken` table is very large, consider running the index creation manually with `CONCURRENTLY` before the migration to avoid table locking during deployment.
2026-03-31 15:15:33 +00:00
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddAppTokenInvitationLookupIndex1774969800000
implements MigrationInterface
{
name = 'AddAppTokenInvitationLookupIndex1774969800000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE INDEX "IDX_APP_TOKEN_TYPE_CONTEXT_EMAIL" ON "core"."appToken" ("type", (context->>'email')) WHERE "deletedAt" IS NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "core"."IDX_APP_TOKEN_TYPE_CONTEXT_EMAIL"`,
);
}
}