{
"meta": {
"agent": "planner",
"task_id": "545",
"title": "BJ-5: DB migration — atlassian_oauth_state",
"created_at": "2026-04-23T17:15:00Z"
},
"inputs": [
{
"name": "issue",
"type": "link",
"ref": "AgentSDE/agent-core#545",
"notes": "BJ-5 Wave 1 — persist rotating Atlassian OAuth tokens"
},
{
"name": "epic",
"type": "link",
"ref": "AgentSDE/agent-core#539",
"notes": "Atlassian integration epic; branch target rc/atlassian-integration"
},
{
"name": "dep",
"type": "link",
"ref": "AgentSDE/agent-core#540",
"notes": "BJ-0 platform union audit (Wave 0 serial blocker)"
},
{
"name": "db-module",
"type": "file",
"ref": "src/database/database.module.ts",
"notes": "Uses better-sqlite3 + synchronize:true; no migration runner"
},
{
"name": "entities-barrel",
"type": "file",
"ref": "src/database/entities/index.ts",
"notes": "Pattern: entity files + barrel export"
}
],
"outputs": [
{
"name": "plan",
"type": "spec",
"format": "md",
"content": "plan.md"
}
],
"files": [
{
"path": "src/database/entities/atlassian-oauth-state.entity.ts",
"action": "create",
"reason": "TypeORM entity with unique (platform, tenantId)"
},
{
"path": "src/database/entities/index.ts",
"action": "modify",
"reason": "Export new entity"
},
{
"path": "src/database/database.module.ts",
"action": "modify",
"reason": "Register entity in forRoot + forFeature"
},
{
"path": "src/atlassian/atlassian-oauth-state.repo.ts",
"action": "create",
"reason": "Injectable repo with get + upsert"
},
{
"path": "src/atlassian/atlassian-oauth-state.repo.spec.ts",
"action": "create",
"reason": "Unit tests against in-memory SQLite"
},
{
"path": "src/atlassian/atlassian.module.ts",
"action": "create",
"reason": "NestJS module wiring forFeature + repo provider"
},
{
"path": "src/atlassian/index.ts",
"action": "create",
"reason": "Barrel export for module/repo/entity"
}
],
"steps": [
{
"id": "S1",
"summary": "Create AtlassianOauthStateEntity and register it in DatabaseModule + entities barrel",
"acceptance": [
"Entity file declares @Entity('atlassian_oauth_state') with columns id (uuid pk), platform, tenant_id, access_token (nullable), refresh_token, expires_at (nullable), updated_at",
"@Unique(['platform', 'tenantId']) present",
"Entity exported from src/database/entities/index.ts",
"Entity added to both forRoot and forFeature arrays in database.module.ts"
],
"depends_on": []
},
{
"id": "S2",
"summary": "Create AtlassianModule, AtlassianOauthStateRepo, and barrel with get/upsert API",
"acceptance": [
"AtlassianOauthStateRepo is @Injectable with InjectRepository(AtlassianOauthStateEntity)",
"get(platform, tenantId) returns entity or null",
"upsert(platform, tenantId, { accessToken, refreshToken, expiresAt }) writes row and updates on conflict without duplicating",
"AtlassianModule imports TypeOrmModule.forFeature([entity]) and exports the repo",
"src/atlassian/index.ts exports module, repo, and entity"
],
"depends_on": [
"S1"
]
},
{
"id": "S3",
"summary": "Add unit tests for the repo using in-memory SQLite",
"acceptance": [
"Spec uses Test.createTestingModule with DatabaseModule under NODE_ENV=test",
"Covers: insert via upsert, read via get, idempotent upsert on same (platform, tenantId), null accessToken/expiresAt round-trip",
"npm run test passes; npm run lint zero warnings; npm run build clean"
],
"depends_on": [
"S2"
]
}
],
"risks": [
{
"risk": "Issue references PG types (timestamptz, uuid) and a raw migration file, but repo uses better-sqlite3 + synchronize:true with no migration runner",
"mitigation": "Deliver a TypeORM entity (acts as migration via synchronize); map types to SQLite equivalents; document deviation in PR body"
},
{
"risk": "Repository.upsert may lack full ON CONFLICT support on older better-sqlite3 builds",
"mitigation": "If driver unsupported, fall back to findOne + save in a single transaction; spec asserts no duplicates"
}
],
"assumptions": [
"Unit tests follow existing convention: in-memory SQLite via NODE_ENV=test, no fs mocking",
"PR targets rc/atlassian-integration per EPIC #539, not master"
],
"open_questions": []
}