Skip to content
94 changes: 82 additions & 12 deletions backend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@
# 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).
# Also removeGymnastItem for an item that is not in the
# gymnast's list.
# ALREADY_EXISTS addGymnastItem for an item that is already in the
# gymnast's list.
# 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).
# BAD_USER_INPUT Empty name, malformed id, `first` out of range.
# 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.
# 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 Down Expand Up @@ -53,6 +67,13 @@ interface CatalogItem {
"""Full URL of the drawing, ready to use as the src of an <img>."""
imageUrl: String!
elementGroup: ElementGroup!
"""
True when the item has been removed from the Code of Points. It is not found
by the catalog search, but it can still be in routines and gymnast lists, so
the UI should warn about it there. It does not count in the D-score, and it
cannot be added to a routine or a list.
"""
removed: Boolean!
}

"""One element on uneven bars, balance beam or floor."""
Expand All @@ -64,11 +85,16 @@ type Element implements CatalogItem {
"""Full URL of the drawing, ready to use as the src of an <img>."""
imageUrl: String!
elementGroup: ElementGroup!
"""True when the element has been removed from the Code of Points."""
removed: Boolean!
"""Difficulty value, 0.1-1.0."""
value: Float!
"""Difficulty letter A-J, calculated from value."""
difficultyLetter: String!
"""Other ways of performing the same element (same code, same value)."""
"""
Other ways of performing the same element (same code, same value) that are
still in the Code of Points.
"""
variants: [Element!]!
}

Expand All @@ -81,8 +107,11 @@ type Vault implements CatalogItem {
"""Full URL of the drawing, ready to use as the src of an <img>."""
imageUrl: String!
elementGroup: ElementGroup!
"""True when the vault has been removed from the Code of Points."""
removed: Boolean!
"""Fixed difficulty score, e.g. 2.6."""
dValue: Float!
"""Other ways of performing the same vault that are still in the Code of Points."""
variants: [Vault!]!
}

Expand Down Expand Up @@ -138,8 +167,11 @@ 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."""
Expand All @@ -150,8 +182,9 @@ type Query {
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.
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 Down Expand Up @@ -191,6 +224,12 @@ type Gymnast {
by the backend.
"""
routines(apparatusId: ID): [Routine!]!
"""
The elements and vaults the gymnast can do or is practicing, in the order
they were added. Optionally only the items on one apparatus. Can contain
removed items (see `removed`). Limited to 100 per gymnast by the backend.
"""
items(apparatusId: ID): [CatalogItem!]!
}

type Routine {
Expand All @@ -200,8 +239,14 @@ type Routine {
apparatus: Apparatus!
"""The elements, or the single vault, in the order of the routine."""
items: [CatalogItem!]!
"""Sum of the eight highest values, each code counted once. Calculated."""
"""
Sum of the eight highest values, each code counted once. Calculated. Removed
items (see `removed`) do not count.
"""
dScore: Float!
createdAt: DateTime!
"""When the routine, its name or its items were last changed."""
updatedAt: DateTime!
}

type AuthPayload {
Expand All @@ -220,20 +265,45 @@ type Mutation {
"""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."""
"""
Adds an element or vault to the gymnast's list. Throws NOT_FOUND,
ALREADY_EXISTS (the item is already in the list), INVALID_ITEMS (a removed
item) or LIMIT_REACHED.
"""
addGymnastItem(gymnastId: ID!, itemId: ID!): Gymnast!
"""
Removes an item from the gymnast's list, also when it has been removed from
the Code of Points. Throws NOT_FOUND when the gymnast does not exist or the
item is not in the list.
"""
removeGymnastItem(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!
"""Throws NAME_TAKEN."""
"""
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.
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!
}
19 changes: 19 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,22 @@
workspace, TypeScript, Prisma and the ESLint configuration for the backend,
wrote the "Database (local setup)" section in the README, and checked that
the install, lint, build and the connection to a local database work.

## Missing fields in the GraphQL schema (`backend/src/schema.graphql`, `docs/API.md`)

- **Tool:** Claude Code, and Claude for the research.
- **What the team did:** Wrote issue #37, which lists the fields that the
database has and the schema does not expose: the gymnast's list of elements
and vaults with mutations, `createdAt` and `updatedAt` on a routine, and a
flag for removed items. The team made the decisions: adding an item that is
already in the list, or removing one that is not, gives an error and is
never silent. A gymnast's list is limited to 100 items. A removed item does
not count in the D-score. The team also defined what "removed" means (an
item that is no longer in the Code of Points edition the app uses).
- **What Claude did:** Researched how others handle this (idempotency in RFC
9110, errors as data in Apollo's guide, the gRPC error names, and soft
delete and warnings in the UI) and proposed the error names `ALREADY_EXISTS`
and `NOT_FOUND`. Added the fields, mutations and error codes to the schema,
and described them in `docs/API.md`, including the limits, the warning in
the UI and the tests. Checked that the schema parses with the `graphql`
library.
Loading