@@ -130,3 +130,93 @@ Located in `backend/python/plugins/`. Use Poetry for dependencies. See [backend/
130130- Migration scripts must be added to ` All() ` in ` register.go `
131131- API changes require running ` make swag ` to update Swagger docs
132132- Python plugins require ` libgit2 ` for gitextractor functionality
133+
134+ ## Asana Plugin Implementation (2025-02-03)
135+
136+ ### Overview
137+ Implemented a complete Asana plugin (` backend/plugins/asana/ ` ) to collect projects (boards), sections, and tasks from Asana's REST API and map them to DevLake's ticket/board domain model.
138+
139+ ### Architecture Decisions
140+ - ** Scope Model** : Asana ** Project** = DevLake ** Board** (scope)
141+ - ** Authentication** : Personal Access Token (PAT) via Bearer token (` Authorization: Bearer <token> ` )
142+ - ** API Base URL** : ` https://app.asana.com/api/1.0/ ` (default endpoint)
143+ - ** Pagination** : Asana uses offset-based pagination via ` next_page.offset ` in response; implemented sequential fetching with ` GetNextPageCustomData `
144+
145+ ### Implementation Details
146+
147+ #### Models (` backend/plugins/asana/models/ ` )
148+ - ** Connection** (` connection.go ` ): ` AsanaConn ` (Token, RestConnection), ` AsanaConnection ` (BaseConnection + AsanaConn)
149+ - ** Scope** (` project.go ` ): ` AsanaProject ` implements ` ToolLayerScope ` ; primary key: ` (ConnectionId, Gid) `
150+ - ** Scope Config** (` scope_config.go ` ): ` AsanaScopeConfig ` embeds ` common.ScopeConfig ` with ` Entities: ["TICKET"] `
151+ - ** Tool Layer** :
152+ - ` task.go ` : ` AsanaTask ` (Gid, Name, Notes, Completed, DueOn, ProjectGid, SectionGid, AssigneeGid, CreatorGid, etc.)
153+ - ` section.go ` : ` AsanaSection ` (Gid, Name, ProjectGid)
154+ - ` user.go ` : ` AsanaUser ` (Gid, Name, Email) - optional, for assignee/creator enrichment
155+ - ** Migration** : ` 20250203_add_init_tables.go ` creates all tables via ` migrationhelper.AutoMigrateTables `
156+
157+ #### API Layer (` backend/plugins/asana/api/ ` )
158+ - ** Connection API** (` connection_api.go ` ): Test connection via ` GET users/me ` , CRUD operations, default endpoint handling
159+ - ** Scope API** (` scope_api.go ` ): PutScopes, GetScopeList, GetScope, PatchScope, DeleteScope (uses ` :scopeId ` path param, not ` :projectId ` )
160+ - ** Scope Config API** (` scope_config_api.go ` ): Full CRUD for scope configs
161+ - ** Blueprint V200** (` blueprint_v200.go ` ): Maps Asana projects to ` ticket.Board ` domain entities when scope config includes ` DOMAIN_TYPE_TICKET `
162+ - ** Remote API** (` remote_api.go ` ): Proxy endpoint for direct API access
163+ - ** Init** (` init.go ` ): Sets up ` DsHelper[AsanaConnection, AsanaProject, AsanaScopeConfig] ` with default endpoint
164+
165+ #### Tasks (` backend/plugins/asana/tasks/ ` )
166+ - ** Project** : ` project_collector.go ` (GET ` /projects/{gid} ` ), ` project_extractor.go ` (extracts to ` _tool_asana_projects ` )
167+ - ** Section** : ` section_collector.go ` (GET ` /projects/{gid}/sections ` ), ` section_extractor.go ` (extracts to ` _tool_asana_sections ` )
168+ - ** Task** :
169+ - ` task_collector.go ` : Collects tasks with offset pagination (limit=100, offset from ` next_page.offset ` )
170+ - ` task_extractor.go ` : Extracts task data including memberships (project/section), assignee, creator, parent
171+ - ` task_convertor.go ` : Converts ` AsanaTask ` → ` ticket.Issue ` + ` ticket.BoardIssue ` using ` didgen ` for domain IDs
172+ - ** API Client** (` api_client.go ` ): Creates ` ApiAsyncClient ` with Bearer auth, sets default endpoint if missing
173+ - ** Task Data** (` task_data.go ` ): ` AsanaOptions ` (ConnectionId, ProjectId, ScopeConfigId), ` AsanaTaskData ` , ` CreateRawDataSubTaskArgs ` helper
174+
175+ #### Plugin Entry (` backend/plugins/asana/ ` )
176+ - ** Main** (` asana.go ` ): ` PluginEntry impl.Asana ` for plugin loading
177+ - ** Impl** (` impl/impl.go ` ): Implements all required interfaces:
178+ - ` PluginMeta ` : Name="asana", Description, RootPkgPath
179+ - ` PluginTask ` : SubTaskMetas (CollectProject, ExtractProject, CollectSection, ExtractSection, CollectTask, ExtractTask, ConvertTask)
180+ - ` PluginModel ` : GetTablesInfo() returns all 6 models
181+ - ` PluginMigration ` : MigrationScripts() from migrationscripts.All()
182+ - ` PluginApi ` : ApiResources() with connections, scopes, scope-configs, test, proxy routes
183+ - ` PluginSource ` : Connection(), Scope(), ScopeConfig()
184+ - ` DataSourcePluginBlueprintV200 ` : MakeDataSourcePipelinePlanV200()
185+ - ` CloseablePluginTask ` : Close() releases ApiClient
186+
187+ #### Config UI (` config-ui/src/plugins/register/asana/ ` )
188+ - ** Config** (` config.tsx ` ): ` AsanaConfig ` with:
189+ - Connection fields: name, endpoint (default: ` https://app.asana.com/api/1.0/ ` ), token, proxy, rateLimitPerHour (default: 150)
190+ - Data scope title: "Projects"
191+ - Scope config entities: ` ['TICKET'] `
192+ - ** Icon** (` assets/icon.svg ` ): Placeholder SVG icon
193+ - ** Registration** (` index.ts ` ): Exports ` AsanaConfig `
194+ - ** Plugin Registry** (` config-ui/src/plugins/register/index.ts ` ): Added ` AsanaConfig ` to ` pluginConfigs ` array
195+
196+ #### Testing
197+ - ** Table Info Test** (` backend/plugins/table_info_test.go ` ): Added ` asana ` import and ` checker.FeedIn("asana/models", asana.Asana{}.GetTablesInfo) `
198+ - ** E2E Test** (` backend/plugins/asana/e2e/task_test.go ` ):
199+ - Imports raw task CSV (` e2e/raw_tables/_raw_asana_tasks.csv ` )
200+ - Runs ` ExtractTaskMeta ` subtask
201+ - Verifies ` _tool_asana_tasks ` against snapshot (` e2e/snapshot_tables/_tool_asana_tasks.csv ` )
202+
203+ ### Key Implementation Notes
204+ 1 . ** Scope ID** : Uses ` :scopeId ` path param (not ` :projectId ` ) to match generic scope helper expectations; scope ID is Asana project GID
205+ 2 . ** Response Parsing** : Asana API wraps responses in ` {"data": {...}} ` or ` {"data": [...], "next_page": {...}} ` ; collectors unwrap ` data ` field
206+ 3 . ** Date Parsing** : ` due_on ` is date-only string (` YYYY-MM-DD ` ); ` parseAsanaDate() ` helper converts to ` *time.Time `
207+ 4 . ** Task Memberships** : Tasks can belong to multiple projects/sections; extractor uses first section from memberships array
208+ 5 . ** Domain Conversion** : Task status maps: ` completed=true ` → ` ticket.DONE ` , ` completed=false ` → ` ticket.TODO `
209+ 6 . ** Default Endpoint** : Set in ` api/init.go ` constant and applied in ` PostConnections ` if missing from request
210+
211+ ### Files Created
212+ - ** Backend** : 25+ Go files across ` models/ ` , ` api/ ` , ` tasks/ ` , ` impl/ ` , ` e2e/ `
213+ - ** Config UI** : 3 TypeScript files (` config.tsx ` , ` index.ts ` , ` assets/icon.svg ` )
214+ - ** Test** : 1 CSV fixture pair (raw + snapshot), 1 E2E test file
215+ - ** CI** : Updated ` table_info_test.go `
216+
217+ ### Next Steps (Optional Enhancements)
218+ - Add user collection/enrichment for assignee/creator names
219+ - Support OAuth 2.0 authentication (currently PAT only)
220+ - Add task comments/subtasks collection
221+ - Implement incremental collection with time-based bookmarking
222+ - Add transformation rules for custom field mappings
0 commit comments