Files are uploaded over REST, stored on disk (a volume in Docker) and recorded on the ticket as events; the bytes are written first, so no attachment ever points at a missing file. Only images, video, audio, PDF and plain text are shown inline, everything with nosniff and a sandbox CSP. The web shows thumbnails, uploads by drag and drop or by pasting a screenshot, and attaches files to new comments. Also adds `nx run api:contracts`, which writes schema.gql and openapi.json without starting the API or any database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
682 lines
13 KiB
GraphQL
682 lines
13 KiB
GraphQL
# ------------------------------------------------------
|
|
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
|
# ------------------------------------------------------
|
|
|
|
type Activity {
|
|
actor: UserRef!
|
|
id: ID!
|
|
issueId: ID
|
|
issueKey: String
|
|
occurredAt: String!
|
|
projectId: ID!
|
|
summary: String!
|
|
type: String!
|
|
}
|
|
|
|
type AreaOption {
|
|
archived: Boolean!
|
|
id: String!
|
|
name: String!
|
|
}
|
|
|
|
input AreaOptionInput {
|
|
archived: Boolean! = false
|
|
id: String!
|
|
name: String!
|
|
}
|
|
|
|
"""
|
|
A file on a ticket. Download it from the REST API: GET /api/issues/{issueId}/attachments/{id}
|
|
"""
|
|
type Attachment {
|
|
"""The comment it was attached to, if any"""
|
|
commentId: ID
|
|
fileName: String!
|
|
id: ID!
|
|
mimeType: String!
|
|
|
|
"""Bytes"""
|
|
size: Int!
|
|
uploadedAt: String!
|
|
uploadedBy: UserRef!
|
|
}
|
|
|
|
type BoardColumn {
|
|
issues: [Issue!]!
|
|
status: String!
|
|
total: Int!
|
|
}
|
|
|
|
input ChangeIssueStatusInput {
|
|
issueId: ID!
|
|
|
|
"""Required by statuses with requiresReason"""
|
|
reason: String
|
|
|
|
"""A status id"""
|
|
status: String!
|
|
}
|
|
|
|
"""
|
|
Writes are acknowledged immediately; the read model catches up asynchronously. Wait for a subscription event with version >= this version to see the change.
|
|
"""
|
|
type CommandResult {
|
|
id: ID!
|
|
key: String
|
|
version: Int!
|
|
}
|
|
|
|
type Comment {
|
|
author: UserRef!
|
|
body: String!
|
|
createdAt: String!
|
|
editedAt: String
|
|
id: ID!
|
|
issueId: ID!
|
|
version: Int!
|
|
}
|
|
|
|
type CommentChange {
|
|
action: String!
|
|
actorId: ID!
|
|
|
|
"""Null when deleted"""
|
|
comment: Comment
|
|
commentId: ID!
|
|
issueId: ID!
|
|
version: Int!
|
|
}
|
|
|
|
type CommentPage {
|
|
items: [Comment!]!
|
|
total: Int!
|
|
}
|
|
|
|
type CountByAssignee {
|
|
count: Int!
|
|
|
|
"""Null = unassigned"""
|
|
user: UserRef
|
|
}
|
|
|
|
type CountByKey {
|
|
count: Int!
|
|
key: String!
|
|
}
|
|
|
|
type CountByStatus {
|
|
count: Int!
|
|
|
|
"""Status id"""
|
|
key: String!
|
|
}
|
|
|
|
input CreateIssueInput {
|
|
"""An area id"""
|
|
area: String
|
|
assigneeId: ID
|
|
description: String
|
|
dueDate: String
|
|
|
|
"""Figma frame for UI changes"""
|
|
figmaUrl: String
|
|
labels: [String!]
|
|
priority: IssuePriority
|
|
projectId: ID!
|
|
releaseId: ID
|
|
storyPoints: Int
|
|
title: String!
|
|
|
|
"""A ticket type id"""
|
|
type: String!
|
|
}
|
|
|
|
input CreateProjectInput {
|
|
description: String
|
|
|
|
"""Readable by anyone, also without signing in"""
|
|
isPublic: Boolean
|
|
|
|
"""2-10 chars, e.g. "TS" → tickets TS-1, TS-2..."""
|
|
key: String!
|
|
name: String!
|
|
}
|
|
|
|
input CreateReleaseInput {
|
|
description: String
|
|
|
|
"""e.g. v1.4.0"""
|
|
name: String!
|
|
projectId: ID!
|
|
targetDate: String
|
|
}
|
|
|
|
type CreatedResolved {
|
|
created: Int!
|
|
resolved: Int!
|
|
|
|
"""Monday of the week (YYYY-MM-DD)"""
|
|
week: String!
|
|
}
|
|
|
|
type CumulativeFlow {
|
|
dates: [String!]!
|
|
series: [FlowSeries!]!
|
|
}
|
|
|
|
type DurationStats {
|
|
averageDays: Float
|
|
count: Int!
|
|
|
|
"""85% of tickets take at most this long"""
|
|
p85Days: Float
|
|
}
|
|
|
|
type FlowSeries {
|
|
counts: [Int!]!
|
|
status: String!
|
|
}
|
|
|
|
type Issue {
|
|
"""Statuses this ticket can move to next (project's workflow)"""
|
|
allowedTransitions: [String!]!
|
|
|
|
"""An area id"""
|
|
area: String
|
|
assignee: UserRef
|
|
attachments: [Attachment!]!
|
|
commentCount: Int!
|
|
comments(limit: Int! = 50, offset: Int! = 0): CommentPage!
|
|
createdAt: String!
|
|
description: String
|
|
dueDate: String
|
|
figmaUrl: String
|
|
|
|
"""History of this ticket"""
|
|
history(limit: Int! = 50): [Activity!]!
|
|
id: ID!
|
|
|
|
"""Human key, e.g. TS-42"""
|
|
key: String!
|
|
labels: [String!]!
|
|
|
|
"""Linked tickets I can see"""
|
|
linkedIssues: [Issue!]!
|
|
links: [IssueLink!]!
|
|
number: Int!
|
|
priority: IssuePriority!
|
|
projectId: ID!
|
|
projectKey: String!
|
|
release: ReleaseRef
|
|
reporter: UserRef!
|
|
|
|
"""A status id of the project's settings"""
|
|
status: String!
|
|
statusCategory: StatusCategory!
|
|
storyPoints: Int
|
|
title: String!
|
|
|
|
"""A ticket type id of the project's settings"""
|
|
type: String!
|
|
updatedAt: String!
|
|
version: Int!
|
|
}
|
|
|
|
type IssueChange {
|
|
action: String!
|
|
actorId: ID!
|
|
issue: Issue!
|
|
version: Int!
|
|
}
|
|
|
|
input IssueFilterInput {
|
|
areas: [String!]
|
|
assigneeId: ID
|
|
categories: [StatusCategory!]
|
|
labels: [String!]
|
|
priorities: [IssuePriority!]
|
|
projectId: ID!
|
|
releaseId: ID
|
|
|
|
"""Matches key, title or description"""
|
|
search: String
|
|
statuses: [String!]
|
|
types: [String!]
|
|
unassigned: Boolean
|
|
}
|
|
|
|
type IssueLink {
|
|
"""OUTWARD: this issue → other. INWARD: other → this issue."""
|
|
direction: LinkDirection!
|
|
issueId: ID!
|
|
issueKey: String!
|
|
type: IssueLinkType!
|
|
}
|
|
|
|
enum IssueLinkType {
|
|
BLOCKS
|
|
DEPENDS_ON
|
|
DUPLICATES
|
|
RELATES_TO
|
|
TESTS
|
|
}
|
|
|
|
type IssuePage {
|
|
items: [Issue!]!
|
|
total: Int!
|
|
}
|
|
|
|
enum IssuePriority {
|
|
CRITICAL
|
|
HIGH
|
|
LOW
|
|
LOWEST
|
|
MEDIUM
|
|
}
|
|
|
|
type IssueTypeOption {
|
|
archived: Boolean!
|
|
color: String!
|
|
icon: String!
|
|
id: String!
|
|
name: String!
|
|
}
|
|
|
|
input IssueTypeOptionInput {
|
|
archived: Boolean! = false
|
|
color: String!
|
|
icon: String!
|
|
id: String!
|
|
name: String!
|
|
}
|
|
|
|
enum LinkDirection {
|
|
INWARD
|
|
OUTWARD
|
|
}
|
|
|
|
input LinkIssuesInput {
|
|
issueId: ID!
|
|
targetIssueId: ID!
|
|
type: IssueLinkType!
|
|
}
|
|
|
|
type Me {
|
|
"""Roles inside the task-stream OAuth client"""
|
|
appRoles: [String!]!
|
|
email: String!
|
|
id: ID!
|
|
isActive: Boolean!
|
|
name: String!
|
|
|
|
"""Global roles in Pireon"""
|
|
roles: [String!]!
|
|
}
|
|
|
|
type Mutation {
|
|
addComment(body: String!, issueId: ID!): CommandResult!
|
|
|
|
"""Add a Pireon user to the project"""
|
|
addProjectMember(input: ProjectMemberInput!): CommandResult!
|
|
archiveRelease(releaseId: ID!): CommandResult!
|
|
|
|
"""Assign (or unassign with null) a project member"""
|
|
assignIssue(assigneeId: ID, issueId: ID!): CommandResult!
|
|
|
|
"""Move a ticket through the workflow (incl. QA)"""
|
|
changeIssueStatus(input: ChangeIssueStatusInput!): CommandResult!
|
|
changeProjectMemberRole(input: ProjectMemberInput!): CommandResult!
|
|
createIssue(input: CreateIssueInput!): CommandResult!
|
|
createProject(input: CreateProjectInput!): CommandResult!
|
|
createRelease(input: CreateReleaseInput!): CommandResult!
|
|
|
|
"""Author or project admin"""
|
|
deleteComment(commentId: ID!): CommandResult!
|
|
|
|
"""
|
|
Delete the project (owner only), with its tickets, comments and releases. `confirmKey` must be the project key.
|
|
"""
|
|
deleteProject(confirmKey: String!, projectId: ID!): CommandResult!
|
|
editComment(body: String!, commentId: ID!): CommandResult!
|
|
|
|
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
|
|
linkIssues(input: LinkIssuesInput!): CommandResult!
|
|
|
|
"""
|
|
Ship the release. Fails while it has unfinished tickets unless force = true.
|
|
"""
|
|
publishRelease(force: Boolean! = false, releaseId: ID!): CommandResult!
|
|
|
|
"""
|
|
Remove a file from a ticket (whoever uploaded it, or an owner/admin). Upload with the REST API.
|
|
"""
|
|
removeAttachment(attachmentId: ID!, issueId: ID!): CommandResult!
|
|
removeProjectMember(projectId: ID!, userId: ID!): CommandResult!
|
|
|
|
"""Put a ticket in a release (null removes it)"""
|
|
setIssueRelease(issueId: ID!, releaseId: ID): CommandResult!
|
|
unlinkIssues(input: LinkIssuesInput!): CommandResult!
|
|
updateIssue(input: UpdateIssueInput!): CommandResult!
|
|
updateProject(input: UpdateProjectInput!): CommandResult!
|
|
|
|
"""
|
|
Replace statuses, transitions, ticket types and areas (owners and admins). Values can be archived, not deleted.
|
|
"""
|
|
updateProjectSettings(projectId: ID!, settings: ProjectSettingsInput!): CommandResult!
|
|
updateRelease(input: UpdateReleaseInput!): CommandResult!
|
|
}
|
|
|
|
type Project {
|
|
"""
|
|
Statistics of the last `days` days (7-365): created vs resolved, cumulative flow, cycle/lead time, throughput and QA
|
|
"""
|
|
analytics(days: Int! = 90): ProjectAnalytics!
|
|
|
|
"""Kanban board, one column per status"""
|
|
board(area: String, assigneeId: ID, perColumn: Int! = 50, releaseId: ID): [BoardColumn!]!
|
|
createdAt: String!
|
|
description: String
|
|
id: ID!
|
|
|
|
"""Anyone can read it, also without signing in"""
|
|
isPublic: Boolean!
|
|
key: String!
|
|
members: [ProjectMember!]!
|
|
name: String!
|
|
owner: UserRef!
|
|
releases: [Release!]!
|
|
settings: ProjectSettings!
|
|
|
|
"""Project tracking dashboard"""
|
|
stats: ProjectStats!
|
|
updatedAt: String!
|
|
version: Int!
|
|
}
|
|
|
|
type ProjectAnalytics {
|
|
createdVsResolved: [CreatedResolved!]!
|
|
|
|
"""Tickets in each status at the end of every day"""
|
|
cumulativeFlow: CumulativeFlow!
|
|
|
|
"""Start of work → done"""
|
|
cycleTime: DurationStats!
|
|
from: String!
|
|
|
|
"""Creation → done"""
|
|
leadTime: DurationStats!
|
|
qa: QaStats!
|
|
|
|
"""Finished per person and week"""
|
|
throughput: [Throughput!]!
|
|
to: String!
|
|
}
|
|
|
|
type ProjectChange {
|
|
action: String!
|
|
actorId: ID!
|
|
project: Project!
|
|
version: Int!
|
|
}
|
|
|
|
type ProjectMember {
|
|
role: ProjectRole!
|
|
user: UserRef!
|
|
}
|
|
|
|
input ProjectMemberInput {
|
|
projectId: ID!
|
|
role: ProjectRole! = MEMBER
|
|
|
|
"""Pireon user id"""
|
|
userId: ID!
|
|
}
|
|
|
|
enum ProjectRole {
|
|
ADMIN
|
|
MEMBER
|
|
OWNER
|
|
VIEWER
|
|
}
|
|
|
|
"""Statuses (board order), transitions, ticket types and areas"""
|
|
type ProjectSettings {
|
|
areas: [AreaOption!]!
|
|
issueTypes: [IssueTypeOption!]!
|
|
statuses: [WorkflowStatus!]!
|
|
transitions: [StatusTransitions!]!
|
|
}
|
|
|
|
"""
|
|
The whole configuration (replaces the previous one). Statuses go in board order.
|
|
"""
|
|
input ProjectSettingsInput {
|
|
areas: [AreaOptionInput!]!
|
|
issueTypes: [IssueTypeOptionInput!]!
|
|
statuses: [WorkflowStatusInput!]!
|
|
transitions: [StatusTransitionsInput!]!
|
|
}
|
|
|
|
type ProjectStats {
|
|
byArea: [CountByKey!]!
|
|
|
|
"""Open tickets per person"""
|
|
byAssignee: [CountByAssignee!]!
|
|
byPriority: [CountByKey!]!
|
|
byStatus: [CountByStatus!]!
|
|
byType: [CountByKey!]!
|
|
open: Int!
|
|
total: Int!
|
|
}
|
|
|
|
type QaStats {
|
|
failed: Int!
|
|
failureRate: Float
|
|
passed: Int!
|
|
}
|
|
|
|
type Query {
|
|
"""Project activity feed (newest first)"""
|
|
activity(
|
|
"""ISO date cursor"""
|
|
before: String
|
|
issueId: ID
|
|
limit: Int! = 50
|
|
projectId: ID!
|
|
): [Activity!]!
|
|
issue(
|
|
"""Issue id or key (e.g. "TS-42")"""
|
|
idOrKey: String!
|
|
): Issue!
|
|
issues(filter: IssueFilterInput!, limit: Int! = 50, offset: Int! = 0): IssuePage!
|
|
|
|
"""The authenticated user"""
|
|
me: Me!
|
|
|
|
"""
|
|
Tickets across all my projects: assigned to me (default) or any (e.g. categories [QA])
|
|
"""
|
|
myIssues(assignedToMe: Boolean! = true, categories: [StatusCategory!], limit: Int! = 50, offset: Int! = 0, statuses: [String!]): IssuePage!
|
|
|
|
"""Projects where I am a member"""
|
|
myProjects: [Project!]!
|
|
|
|
"""A project of mine, or any public one (no sign-in needed)"""
|
|
project(
|
|
"""Project id or key (e.g. "TS")"""
|
|
idOrKey: String!
|
|
): Project!
|
|
|
|
"""Projects anyone can read, also without signing in"""
|
|
publicProjects: [Project!]!
|
|
release(id: ID!): Release!
|
|
|
|
"""Planned releases of all my projects, soonest target date first"""
|
|
upcomingReleases(limit: Int! = 10): [Release!]!
|
|
user(id: String!): User
|
|
|
|
"""Search users by name or email (to assign / add members)"""
|
|
users(limit: Int! = 20, search: String! = ""): [User!]!
|
|
}
|
|
|
|
type Release {
|
|
"""Scope vs finished tickets, day by day"""
|
|
burnup: ReleaseBurnup!
|
|
createdAt: String!
|
|
description: String
|
|
id: ID!
|
|
|
|
"""Tickets included in this release"""
|
|
issues(limit: Int! = 100, offset: Int! = 0): IssuePage!
|
|
name: String!
|
|
progress: ReleaseProgress!
|
|
projectId: ID!
|
|
releasedAt: String
|
|
status: ReleaseStatus!
|
|
targetDate: String
|
|
version: Int!
|
|
}
|
|
|
|
type ReleaseBurnup {
|
|
dates: [String!]!
|
|
done: [Int!]!
|
|
releaseId: ID!
|
|
|
|
"""Tickets in the release (not cancelled)"""
|
|
scope: [Int!]!
|
|
targetDate: String
|
|
}
|
|
|
|
type ReleaseChange {
|
|
action: String!
|
|
actorId: ID!
|
|
release: Release!
|
|
version: Int!
|
|
}
|
|
|
|
type ReleaseProgress {
|
|
byStatus: [CountByStatus!]!
|
|
finished: Int!
|
|
percent: Int!
|
|
total: Int!
|
|
}
|
|
|
|
type ReleaseRef {
|
|
id: ID!
|
|
name: String!
|
|
}
|
|
|
|
enum ReleaseStatus {
|
|
ARCHIVED
|
|
PLANNED
|
|
RELEASED
|
|
}
|
|
|
|
"""
|
|
What a status means, whatever each project calls it. DONE and CANCELLED are finished.
|
|
"""
|
|
enum StatusCategory {
|
|
CANCELLED
|
|
DONE
|
|
IN_PROGRESS
|
|
QA
|
|
TODO
|
|
}
|
|
|
|
type StatusTransitions {
|
|
from: String!
|
|
to: [String!]!
|
|
}
|
|
|
|
input StatusTransitionsInput {
|
|
from: String!
|
|
to: [String!]!
|
|
}
|
|
|
|
type Subscription {
|
|
"""Live activity feed"""
|
|
activityAdded(projectId: ID!): Activity!
|
|
commentChanged(issueId: ID!): CommentChange!
|
|
|
|
"""Tickets created/updated/moved in a project (board in realtime)"""
|
|
issueChanged(projectId: ID!): IssueChange!
|
|
|
|
"""Projects I am added to / removed from"""
|
|
myProjectsChanged: ProjectChange!
|
|
projectChanged(projectId: ID!): ProjectChange!
|
|
releaseChanged(projectId: ID!): ReleaseChange!
|
|
}
|
|
|
|
type Throughput {
|
|
count: Int!
|
|
user: UserRef
|
|
week: String!
|
|
}
|
|
|
|
input UpdateIssueInput {
|
|
"""An area id"""
|
|
area: String
|
|
description: String
|
|
dueDate: String
|
|
|
|
"""Figma frame for UI changes"""
|
|
figmaUrl: String
|
|
issueId: ID!
|
|
labels: [String!]
|
|
priority: IssuePriority
|
|
storyPoints: Int
|
|
title: String
|
|
type: String
|
|
}
|
|
|
|
input UpdateProjectInput {
|
|
description: String
|
|
isPublic: Boolean
|
|
name: String
|
|
projectId: ID!
|
|
}
|
|
|
|
input UpdateReleaseInput {
|
|
description: String
|
|
name: String
|
|
releaseId: ID!
|
|
targetDate: String
|
|
}
|
|
|
|
"""User managed by the Pireon identity server"""
|
|
type User {
|
|
email: String!
|
|
id: ID!
|
|
isActive: Boolean!
|
|
name: String!
|
|
}
|
|
|
|
type UserRef {
|
|
id: ID!
|
|
name: String!
|
|
}
|
|
|
|
type WorkflowStatus {
|
|
"""No new tickets or transitions into it"""
|
|
archived: Boolean!
|
|
category: StatusCategory!
|
|
|
|
"""Colour token: slate, violet, green…"""
|
|
color: String!
|
|
|
|
"""Stable id stored on tickets, e.g. IN_REVIEW"""
|
|
id: String!
|
|
name: String!
|
|
|
|
"""Entering it needs a reason"""
|
|
requiresReason: Boolean!
|
|
}
|
|
|
|
input WorkflowStatusInput {
|
|
archived: Boolean! = false
|
|
category: StatusCategory!
|
|
color: String!
|
|
id: String!
|
|
name: String!
|
|
requiresReason: Boolean! = false
|
|
} |