feat(strategic-initiative): link initiatives to teams directly, team visibility through them (G-P2-4)

Merged
EV
10d ago
+783-43

What

G-P2-4 (backend): strategic initiatives can be linked to teams directly. Part of the Gauzy ↔ Ever Works integration plan (06-gauzy-implementation-plan.md G-P2-4, owner decision D3b, mapping 01-domain-mapping.md §9 in ever-co/homelab). An Ever Works Mission scoped to a Team maps to a strategic initiative linked to that team. The Angular "Initiatives" pages follow in a separate PR.

Additive. An initiative with no direct team behaves exactly as before. All existing rows have none.

AreaChange
EntityNew M:N teams on OrganizationStrategicInitiative, owned by the initiative. The pivot is organization_strategic_initiative_team (organizationStrategicInitiativeId, organizationTeamId), with both columns as the primary key; each column is indexed and cascades with its row.
Migration 1791000000731-CreateOrganizationStrategicInitiativeTeamTableRuns on Postgres, SQLite and MySQL. Guarded by hasTable; down drops the pivot. Keys, indexes and FKs use TypeORM's own names, so a generated migration shows no drift.
WritesteamIds on the REST create/update DTO and on the GraphQL Create/UpdateOrganizationStrategicInitiativeInput. Each id must be a team of the initiative's organization in the caller's tenant; otherwise the write fails with 400 and nothing is saved. On update, stated teamIds replace the links (an empty list unlinks all), and omitted teamIds leave them alone.
ReadsREST and the GraphQL OrganizationStrategicInitiative.teamIds return the ids of the directly linked teams, on the list, single and by-project reads. The type is IOrganizationStrategicInitiativeRead in contracts. teamIds is kept off the entity interface because an id array there makes it an invalid TypeORM find condition for the entities that embed initiatives.
VisibilityvisibilityScope = 'team' now means a member of a team linked directly, or of a team of an aligned project, on all three reads. The second half is the previous rule, unchanged.
GraphQL snapshotRegenerated with graphql-snapshot; the changes are additive (one type field, two input fields).

Tests

  • organization-strategic-initiative.service.teams.spec.ts (new):
    • create links the stated teams (deduplicated) and returns teamIds;
    • a team of another organization is refused and nothing is saved;
    • an update without teamIds leaves the links, one with teamIds replaces them, and one with [] unlinks all;
    • team visibility through a direct team, through a project team, and for nobody else;
    • the single-row read checks direct teams too.
  • database/strategic-initiative-team-migration.spec.ts (new): the migration runs for real on SQLite. It checks one row per (initiative, team), cascades from either side, indexes, an idempotent re-run, down, and up again after down.
  • Postgres via PGlite (Postgres 17): PK refuses a duplicate, FK refuses an unknown team, cascades with the team and with the initiative, indexes, down, up after down (7/7 pass).
  • Core suites for organization-strategic-initiative, organization-project, goal and the migration: 16 suites, 330 tests green (Node 26, repo install). Core tsc passes with 0 errors.
  • The 30 node-only static gates of static-checks.yml pass, and so does the egress static-hostname audit.

🤖 Generated with Claude Code


Summary by cubic

Links strategic initiatives to teams directly (part of the Gauzy ↔ Ever Works integration plan, G-P2-4) so visibilityScope = 'team' also covers members of directly linked teams, not just teams of aligned projects. Additive: initiatives without direct teams behave exactly as before.

  • Adds an M:N teams relation with a new pivot-table migration (organization_strategic_initiative_team) for Postgres, SQLite, and MySQL; on MySQL the pivot is created in one statement under a named lock (within MySQL's 64-character lock-name limit) because MySQL commits each DDL separately.
  • Writes accept teamIds on the REST create/update DTO and GraphQL inputs; each id must name a team of the initiative's organization, otherwise the write fails with 400 and nothing is saved. A raw teams list in the body is dropped.
  • On update, stated teamIds replace the links (an empty list unlinks all) while omitted teamIds leave them alone; moving the initiative to another organization unlinks its previous teams.
  • Reads return teamIds on list, single, and by-project reads, and keep the caller's relations in either form (list or object).