Skip to content
212 changes: 153 additions & 59 deletions backend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@
# Errors are thrown as GraphQL errors with a machine-readable
# `extensions.code`. The client switches on the code, never on the message.
#
# UNAUTHENTICATED The operation needs a logged-in coach.
# UNAUTHENTICATED The operation needs a logged-in coach, or the session token
# or the Google token is missing, invalid or expired.
# NOT_FOUND The id does not exist, or belongs to another coach.
# Both cases give the same answer, so that nobody can find
# out which ids exist (constraint 9 in docs/DATA_MODEL.md).
# NAME_TAKEN The coach already has a routine with this name for this
# gymnast and apparatus (constraint 6).
# INVALID_ITEMS The items do not fit the routine: a vault outside vault,
# an element on vault, more than one vault, or an item from
# another apparatus (constraints 7 and 8). Also a removed
# item added to a gymnast's list.
# BAD_USER_INPUT Empty name, malformed id, `first` out of range, a
# DIFFICULTY sort without an apparatus filter, or a
# difficulty filter together with vault, or a difficulty
# value that is not 0.1-1.0 in steps of 0.1.
# another apparatus (constraints 7 and 8). Also an item that
# has been removed from the Code of Points (`removed`) and
# is not already in the routine or the gymnast's list.
# BAD_USER_INPUT Empty name, name longer than 70 characters, malformed id,
# `first` out of range, a DIFFICULTY sort without an
# apparatus filter, a difficulty filter together with vault,
# or a difficulty value that is not 0.1-1.0 in steps of 0.1.
# LIMIT_REACHED The coach has reached a limit: 200 gymnasts, 50 routines
# per gymnast, or 50 items in a routine.
# per gymnast, 50 items in a routine, or 100 items in a
# gymnast's list.
#
# Apollo Server also adds codes of its own. The ones that matter here:
# GRAPHQL_VALIDATION_FAILED The query is invalid, or deeper than the limit.
# INTERNAL_SERVER_ERROR Unexpected error. The client shows a general
# message and no details.
#
# Why this way:
# - `me` is the only operation where "not logged in" is a normal answer. The
Expand All @@ -32,52 +40,67 @@
# - Failures that the user can fix (NAME_TAKEN, INVALID_ITEMS) carry a
# readable message that the UI can show as it is.

"""One of the four apparatus: VT, UB, BB or FX."""
"""
One of the four apparatus: VT, UB, BB or FX.
"""
type Apparatus {
id: ID!
name: String!
groups: [ElementGroup!]!
}

"""A group of elements or vaults on one apparatus, e.g. "1.000 Mounts"."""
"""
A group of elements or vaults on one apparatus, e.g. "1.000 Mounts".
"""
type ElementGroup {
id: ID!
number: Int!
name: String!
apparatus: Apparatus!
}

"""Fields shared by everything in the catalog: elements and vaults."""
"""
Fields shared by everything in the catalog: elements and vaults.
"""
interface CatalogItem {
id: ID!
code: String!
description: String!
"""The gymnast the item is named after, if any."""
"""
The gymnast the item is named after, if any.
"""
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
True when the item is no longer in the Code of Points. It can still be
fetched by id, and it stays in routines and gymnast lists, but it is not
found by the search and does not count in a D-value.
fetched by id, and it stays in routines and gymnast lists, so the UI should
warn about it there. It is not found by the search, a routine that has it
gets no D-score (null), and it cannot be added to a routine or a list.
"""
removed: Boolean!
}

"""One element on uneven bars, balance beam or floor."""
"""
One element on uneven bars, balance beam or floor.
"""
type Element implements CatalogItem {
id: ID!
code: String!
description: String!
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
True when the item is no longer in the Code of Points. It can still be
fetched by id, and it stays in routines and gymnast lists, but it is not
found by the search and does not count in a D-value.
found by the search, and a routine that has it gets no D-score.
"""
removed: Boolean!
"""
Expand All @@ -92,28 +115,38 @@ type Element implements CatalogItem {
variants: [Element!]!
}

"""One vault from the vault table."""
"""
One vault from the vault table.
"""
type Vault implements CatalogItem {
id: ID!
code: String!
description: String!
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
True when the item is no longer in the Code of Points. It can still be
fetched by id, and it stays in routines and gymnast lists, but it is not
found by the search and does not count in a D-value.
found by the search, and a routine that has it gets no D-score.
"""
removed: Boolean!
"""Fixed difficulty score, e.g. 2.6."""
"""
Fixed difficulty score, e.g. 2.6.
"""
dValue: Float!
"""Other ways of performing the same vault. Removed variants are left out."""
"""
Other ways of performing the same vault. Removed variants are left out.
"""
variants: [Vault!]!
}

"""What the user can sort the catalog by."""
"""
What the user can sort the catalog by.
"""
enum CatalogSortField {
CODE
"""
Expand Down Expand Up @@ -147,12 +180,16 @@ input CatalogFilter {
it throws BAD_USER_INPUT. Empty or missing means no difficulty filter.
"""
difficultyValues: [Float!]
"""Only items named after a gymnast."""
"""
Only items named after a gymnast.
"""
namedOnly: Boolean
}

type CatalogEdge {
"""Opaque position marker. Pass it as `after` to get the next page."""
"""
Opaque position marker. Pass it as `after` to get the next page.
"""
cursor: String!
node: CatalogItem!
}
Expand All @@ -176,20 +213,36 @@ type CatalogConnection {
type Query {
apparatus: [Apparatus!]!
elementGroup(id: ID!): ElementGroup
"""
Also finds removed elements (see `removed`), so routines can show them.
"""
element(id: ID!): Element
"""
Also finds removed vaults (see `removed`), so routines can show them.
"""
vault(id: ID!): Vault
"""
Also finds removed items (see `removed`), so routines can show them.
"""
catalogItem(id: ID!): CatalogItem

"""The logged-in coach, or null when nobody is logged in."""
"""
The logged-in coach, or null when nobody is logged in.
"""
me: Coach
"""Throws UNAUTHENTICATED or NOT_FOUND."""
"""
Throws UNAUTHENTICATED or NOT_FOUND.
"""
gymnast(id: ID!): Gymnast
"""Throws UNAUTHENTICATED or NOT_FOUND."""
"""
Throws UNAUTHENTICATED or NOT_FOUND.
"""
routine(id: ID!): Routine
"""
The D-score of a list of items, for the calculator. Open to everyone and
nothing is saved. Throws INVALID_ITEMS when the items do not fit together.
Removed items do not count. At most 50 ids, otherwise BAD_USER_INPUT.
nothing is saved. Throws INVALID_ITEMS when the items do not fit together, or
when an item has been removed (see `removed`). At most 50 ids, otherwise
BAD_USER_INPUT.
"""
calculateDScore(itemIds: [ID!]!): Float!

Expand All @@ -198,27 +251,38 @@ type Query {
One or more words, case-insensitive. All the words must match: an item is
found if, for every search word, it has a word that starts with it, in the
code, name or description. A trailing "s" is removed from each search
word. Empty or missing means no text filter.
word. At most 80 characters and 8 words, otherwise BAD_USER_INPUT. Text
that has no searchable words (such as "ø" or "%") finds nothing. Empty or
only spaces means no text filter. A client should limit its input field to
80 characters.
"""
search: String
filter: CatalogFilter
sort: CatalogSort
"""Page size, 1-100. A value outside the range gives BAD_USER_INPUT."""
"""
Page size, 1-100. A value outside the range gives BAD_USER_INPUT.
"""
first: Int! = 20
after: String
): CatalogConnection!
}

"""An ISO 8601 date and time, e.g. 2026-10-01T12:00:00Z."""
"""
An ISO 8601 date and time, e.g. 2026-10-01T12:00:00Z.
"""
scalar DateTime

"""A logged-in user. Google's own id is never exposed."""
"""
A logged-in user. Google's own id is never exposed.
"""
type Coach {
id: ID!
name: String!
email: String!
createdAt: DateTime!
"""All the coach's gymnasts. Limited to 200 per coach by the backend."""
"""
All the coach's gymnasts. Limited to 200 per coach by the backend.
"""
gymnasts: [Gymnast!]!
}

Expand All @@ -232,9 +296,11 @@ type Gymnast {
routines(apparatusId: ID): [Routine!]!
"""
The elements and vaults the gymnast can do or is practicing, in the order
they were added. Removed items stay in the list, with removed: true.
they were added. Optionally only the elements on one apparatus. Removed
items stay in the list, with removed: true. Limited to 100 per gymnast by
the backend.
"""
elements: [CatalogItem!]!
elements(apparatusId: ID): [CatalogItem!]!
}

type Routine {
Expand All @@ -249,14 +315,23 @@ type Routine {
"""
items: [CatalogItem!]!
"""
Sum of the eight highest values, each code counted once. Removed items do
not count. Calculated.
Sum of the eight highest values, each code counted once. Calculated. null
when the routine has a removed item (see `removed`), the same rule as
`calculateDScore`, which rejects removed items. Remove the item to get a
score again.
"""
dScore: Float
createdAt: DateTime!
"""
When the routine, its name or its items were last changed.
"""
dScore: Float!
updatedAt: DateTime!
}

type AuthPayload {
"""Session token the client sends in the Authorization header."""
"""
Session token the client sends in the Authorization header.
"""
token: String!
coach: Coach!
}
Expand All @@ -268,35 +343,54 @@ input CreateRoutineInput {
}

type Mutation {
"""Verifies the Google token and creates the coach on the first login."""
"""
Verifies the Google token and creates the coach on the first login.
"""
login(googleToken: String!): AuthPayload!

"""Throws LIMIT_REACHED."""
"""
The name is trimmed and must be 1-70 characters (BAD_USER_INPUT otherwise).
Throws LIMIT_REACHED.
"""
createGymnast(name: String!): Gymnast!
"""
The name is trimmed and must be 1-70 characters (BAD_USER_INPUT otherwise).
"""
renameGymnast(id: ID!, name: String!): Gymnast!
"""Also deletes the gymnast's routines. Returns the id of the deleted gymnast."""
deleteGymnast(id: ID!): ID!

"""Throws NAME_TAKEN or LIMIT_REACHED."""
createRoutine(input: CreateRoutineInput!): Routine!
"""Throws NAME_TAKEN."""
renameRoutine(id: ID!, name: String!): Routine!
deleteRoutine(id: ID!): ID!
"""
Replaces the whole list. The client sends the ids in the wanted order.
More than 50 ids throws LIMIT_REACHED. Throws INVALID_ITEMS.
Also deletes the gymnast's routines. Returns the id of the deleted gymnast.
"""
setRoutineItems(routineId: ID!, itemIds: [ID!]!): Routine!
deleteGymnast(id: ID!): ID!

"""
Adds an element or vault to the gymnast's list. Adding an item that is
already in the list does nothing. Throws NOT_FOUND for an unknown gymnast
or item, and INVALID_ITEMS for a removed item.
already in the list does nothing. Throws NOT_FOUND (unknown gymnast or
item), INVALID_ITEMS (a removed item) or LIMIT_REACHED.
"""
addGymnastElement(gymnastId: ID!, itemId: ID!): Gymnast!
"""
Removes an element or vault from the gymnast's list. Removing an item that
is not in the list does nothing. Throws NOT_FOUND for an unknown gymnast.
Removes an item from the gymnast's list, also when it has been removed from
the Code of Points. Removing an item that is not in the list does nothing.
Throws NOT_FOUND when the gymnast does not exist.
"""
removeGymnastElement(gymnastId: ID!, itemId: ID!): Gymnast!

"""
The name is trimmed and must be 1-70 characters (BAD_USER_INPUT otherwise).
Throws NAME_TAKEN or LIMIT_REACHED.
"""
createRoutine(input: CreateRoutineInput!): Routine!
"""
The name is trimmed and must be 1-70 characters (BAD_USER_INPUT otherwise).
Throws NAME_TAKEN.
"""
renameRoutine(id: ID!, name: String!): Routine!
deleteRoutine(id: ID!): ID!
"""
Replaces the whole list. The client sends the ids in the wanted order.
More than 50 ids throws LIMIT_REACHED. Throws INVALID_ITEMS. A removed item
can stay in the list, but it cannot be added to it. The last save wins: the
mutation does not check if the routine was changed since the client loaded it.
"""
setRoutineItems(routineId: ID!, itemIds: [ID!]!): Routine!
}
Loading