feat(strategic-initiative): link initiatives to teams directly, team visibility through them (G-P2-4)
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.
| Area | Change |
|---|---|
| Entity | New 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-CreateOrganizationStrategicInitiativeTeamTable | Runs 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. |
| Writes | teamIds 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. |
| Reads | REST 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. |
| Visibility | visibilityScope = '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 snapshot | Regenerated 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
teamIdsleaves the links, one withteamIdsreplaces them, and one with[]unlinks all; teamvisibility through a direct team, through a project team, and for nobody else;- the single-row read checks direct teams too.
- create links the stated teams (deduplicated) and returns
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 afterdown.- 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 afterdown(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
tscpasses with 0 errors. - The 30 node-only static gates of
static-checks.ymlpass, 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
teamsrelation 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
teamIdson 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 rawteamslist in the body is dropped. - On update, stated
teamIdsreplace the links (an empty list unlinks all) while omittedteamIdsleave them alone; moving the initiative to another organization unlinks its previous teams. - Reads return
teamIdson list, single, and by-project reads, and keep the caller's relations in either form (list or object).