MOTIR-2262Done
`projectAccessService.getPermissions` — resolve an actor's permission set for a project in one round trip, and its DTO
Give the rest of the product one way to ask "what may this actor do in this project?" and get a permission SET back, instead of asking a different capability method per domain. projectAccessService already carries five such methods — getCapabilities, getCommentCapabilities, getAttachmentCapabilities, getWatcherCapabilities, getSettingsCapabilities — each spending its own resolveInputs round trip to answer three or four booleans. This card adds the general read those specialise.
The shape
getPermissions(projectId, ctx, tx?)onlib/services/projectAccessService.ts— oneresolveInputscall, thenresolvePermissions, returning the actor's set.txstays optional exactly as every sibling method has it, so a caller already inside a transaction shares the snapshot and the RLS workspace GUC.getRoleCatalog(projectId, ctx, tx?)— the read the settings page renders: the built-in roles with their permission sets, plus the catalog's domain grouping. It is a project-scoped read (not a static import) because the custom-roles story makes the answer project-dependent, and a page that reads a constant today would need its data source replaced then.- A serialisable DTO in
lib/dto/permissions.ts— the set as a sorted array of keys, plus the roles and domains, in the mapper-layer shape the repo's other DTOs follow (lib/mappers/). ASetdoes not cross the server/client boundary; an array that is sorted crosses it deterministically, which matters for render stability. - The five existing capability methods keep working unchanged. They are called from ~40 places and this card does not re-point them; where each already computes its booleans from the same inputs, it may compute them through
hasPermissioninternally, but its return shape is untouched.
Acceptance criteria
projectAccessService.getPermissionsandgetRoleCatalogexist, take the optionaltxin the same position as their siblings, and throwProjectNotFoundError— never a 403-shaped error — for a project id in another workspace, preserving the shipped no-existence-leak posture.getPermissionsfor an actor with no workspace membership on a non-public project returns the empty set, and for a workspace owner returns the full catalog.- The DTO in
lib/dto/permissions.tsis JSON-serialisable, and its permission array is sorted by catalog order, not by insertion. - An integration test drives a real Postgres row set — a project with an actual
ProjectMembershipat each of the three roles — throughgetPermissionsand asserts the resolved sets, rather than mockingresolveInputs. - The five existing capability methods' return shapes are unchanged, asserted by their existing tests continuing to pass untouched.
- Per-file coverage on every new and changed module meets the ≥90% floor.
Context refs
lib/services/projectAccessService.ts—resolveInputs, the five capability methods this generalises, and the optional-txconvention.lib/dto/projectMembers.tsandlib/mappers/projectMemberMappers.ts— the DTO + mapper layering this follows.lib/projects/errors.ts—ProjectNotFoundError, the cross-workspace posture.- The resolution card —
resolvePermissions, which this wraps with IO.