6.9 KiB
6.9 KiB
id, title, status, wave, depends_on, plan, spec
| id | title | status | wave | depends_on | plan | spec |
|---|---|---|---|---|---|---|
| 01-backend-data-model | Backend data model: automation_repositories join table | pending | 1 | plan.md | ../../specs/office/requirements/automations-settings.md |
Task 01: Backend data model — automation_repositories
Inputs
docs/specs/office/requirements/automations-settings.md— Data model, API surface, Failure modes sections (multi-repository additions).docs/plans/automation-multi-repository/plan.md— Backend section.apps/backend/internal/automation/models.go—Automation,CreateAutomationRequest,UpdateAutomationRequest.apps/backend/internal/automation/store.go— schema, CRUD, migration pattern (initSchema,migrateRepositoryIDSQL).apps/backend/internal/automation/service.go—CreateAutomation,UpdateAutomation.apps/backend/internal/task/repository/sqlite/base_migrations.go—migrateTaskRepositoriesAllowMultiBranchas the recreate-table migration pattern to mirror for droppingautomations.repository_id.- Open question to resolve first: find where
automation.NewServiceis constructed (likelyapps/backend/cmd/...or an app-wiring file) and determine whether a repository-lookup dependency (something satisfyingGetRepository(ctx, id) (*models.Repository, error)scoped by workspace) is already reachable there. If not, add a small interface +SetXxxsetter onServicefollowing the existingSetTaskDeleter/SetWorkspaceAuthorizerpattern, and wire it at construction time.
Change
- Schema (
store.go):- Add
automation_repositoriestable + index tocreateTablesSQL. - Remove
repository_idfrom the canonicalautomationsCREATE TABLE. - Replace
migrateRepositoryIDSQLwith a migration that: (a) createsautomation_repositories(already covered bycreateTablesSQLrunning first), (b) ifautomations.repository_idstill exists (check viaPRAGMA table_info(automations)), backfills every non-emptyrepository_idintoautomation_repositoriesat position 0, then (c) recreates theautomationstable without that column. - FK hazard:
automation_triggers.automation_idandautomation_runs.automation_idbothREFERENCES automations(id) ON DELETE CASCADE, and this DB opens with_foreign_keys=on(seeapps/backend/internal/db/sqlite.go). Recreatingautomations(rename-out/create-new/copy/drop-old) with FK enforcement on will fail or orphan those two children's constraints. Mirrorapps/backend/internal/task/repository/sqlite/base_migrations.go'srecreateTablehelper exactly: wrap the whole rename/create/copy/drop sequence inPRAGMA foreign_keys=OFFbefore the transaction anddeferaPRAGMA foreign_keys=ONafter — that helper already proves the pattern works for a table with FK children (migrateTasksRemoveWorkflowFKrecreatestasks, whichtask_sessions/task_repositories/etc. reference the same way). Theautomationpackage doesn't have this helper yet; port a minimal equivalent rather than reaching across packages. - Must be idempotent — safe to run
initSchemarepeatedly against an already-migrated DB.
- Add
- Model (
models.go):Automation.RepositoryID string→RepositoryIDs []string json:"repository_ids" db:"-".CreateAutomationRequest.RepositoryID string→RepositoryIDs []string json:"repository_ids".UpdateAutomationRequest.RepositoryID *string→RepositoryIDs []string json:"repository_ids,omitempty"(nil = untouched, non-nil = replace, matching thedto.TaskRepositoryInputconvention).
- Store CRUD (
store.go):CreateAutomation: insert theautomationsrow (norepository_idcolumn), then insert oneautomation_repositoriesrow per entry ina.RepositoryIDs(position = index), in the same transaction.- Add
listRepositoryIDsForAutomations(ctx, automationIDs []string) (map[string][]string, error)mirroringlistTriggersForAutomations; wire intoGetAutomation,ListAutomations,ListAllEnabledto hydrateRepositoryIDs. UpdateAutomation: whenreq.RepositoryIDs != nil, inside the same transaction as theautomationsUPDATE,DELETE FROM automation_repositories WHERE automation_id = ?then re-insert the new ordered list. Do not rely onON DELETE CASCADEfor this path (it only fires on automation deletion).applyAutomationUpdate: remove theRepositoryIDbranch.
- Service validation (
service.go):CreateAutomationandUpdateAutomation(whenRepositoryIDs != nil): reject duplicate IDs in the request, and reject any ID that isn't a repository belonging to the automation'sWorkspaceID. Return a sentinel error (e.g.ErrRepositoryNotInWorkspace) distinguishable from generic errors so callers can map it to a validation (not internal) error code.
- WS handler error mapping (
handlers.go): map the new sentinel error fromwsCreate/wsUpdatetows.ErrorCodeValidationinstead of falling through tows.ErrorCodeInternalError.
Acceptance
Automation,CreateAutomationRequest,UpdateAutomationRequestexposeRepositoryIDs []string(JSONrepository_ids); no remainingRepositoryID/repository_idsingular field ininternal/automation.- Creating/updating/listing/getting an automation round-trips an ordered
multi-repository list correctly, including through the batch-hydration path
used by
ListAutomations/ListAllEnabled. - A pre-existing DB with a non-empty legacy
repository_idcolumn value ends up with exactly one matchingautomation_repositoriesrow afterNewStore, and theautomationstable no longer has arepository_idcolumn. RunningNewStoreagain against the same (already migrated) DB file does not error and does not duplicate rows. CreateAutomation/UpdateAutomationreject arepository_idsentry that belongs to a different workspace, and reject a duplicate ID within one request, with a validation-class error (not an internal-error class one).
Verification
make -C apps/backend test
Files likely touched
apps/backend/internal/automation/models.goapps/backend/internal/automation/store.goapps/backend/internal/automation/store_test.goapps/backend/internal/automation/service.goapps/backend/internal/automation/service_test.goapps/backend/internal/automation/handlers.go- Whatever file constructs
automation.NewService(locate first)
Dependencies
None.
Parallelism
sequential (wave 1) — tasks 02 and 03 depend on this contract.
Output contract
Summary of changes, exact test command output, any new sentinel error names
and where the repository-lookup dependency was wired, plus a note updating
plan.md's Wave 1 checkbox and this file's status to done.