diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..8aebd5b --- /dev/null +++ b/docs/data-model.md @@ -0,0 +1,37 @@ +# Crate data model + +This document describes the data the application will need. `schema.graphql` +is the GraphQL contract built from this model. Database tables, resolvers and +queries are deliberately left to M2. + +## Core catalog + +- An **album** has a title, an optional release year, one or more artists, zero + or more genres and an ordered list of tracks. +- An **artist** can belong to many albums, and an **album** can have many + artists. +- A **genre** can belong to many albums, and an **album** can have many genres. +- A **track** belongs to one album and has a title, track number and optional + duration. + +The two many-to-many relationships will use join tables in PostgreSQL in M2. + +## User-generated data + +- A **review** belongs to one album and one simulated user. A user can have one + review per album, so saving another review updates the existing one. Album + details can also return the current user's review to prefill an edit form. +- A **crate item** belongs to one simulated user and one album. It represents + the user's personal wantlist. The schema has separate add and remove actions + so repeated requests cannot accidentally switch the album back to the wrong + state. + +The user identity and persistence of reviews and crate items are M2 work. The +schema already describes the operations the client will need. + +## Search results + +`searchAlbums` accepts one search term, optional genre filters and a sort +choice. It returns a cursor-based connection so the client can load results in +pages. The connection also contains the total number of matches and genre +facet counts for the filter UI. diff --git a/schema.graphql b/schema.graphql new file mode 100644 index 0000000..5900c6b --- /dev/null +++ b/schema.graphql @@ -0,0 +1,142 @@ +scalar DateTime + +type Query { + searchAlbums(input: AlbumSearchInput!, first: Int!, after: String): AlbumConnection! + album(id: ID!): Album + myCrate(first: Int!, after: String): CrateConnection! +} + +type Mutation { + upsertReview(input: UpsertReviewInput!): UpsertReviewPayload! + deleteReview(id: ID!): DeleteReviewPayload! + addToCrate(albumId: ID!): AddToCratePayload! + removeFromCrate(albumId: ID!): RemoveFromCratePayload! +} + +type Album { + id: ID! + title: String! + releaseYear: Int + artists: [Artist!]! + tracks: [Track!]! + genres: [Genre!]! + averageRating: Float + reviewCount: Int! + reviews(first: Int!, after: String): ReviewConnection! + myReview: Review + isInCrate: Boolean! +} + +type Artist { + id: ID! + name: String! +} + +type Track { + id: ID! + title: String! + trackNumber: Int! + durationSeconds: Int +} + +type Genre { + id: ID! + name: String! +} + +type Review { + id: ID! + rating: Int! + comment: String! + createdAt: DateTime! + updatedAt: DateTime! +} + +type CrateItem { + id: ID! + album: Album! + addedAt: DateTime! +} + +type AlbumConnection { + edges: [AlbumEdge!]! + pageInfo: PageInfo! + totalCount: Int! + genreFacets: [GenreFacet!]! +} + +type AlbumEdge { + cursor: String! + node: Album! +} + +type ReviewConnection { + edges: [ReviewEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type ReviewEdge { + cursor: String! + node: Review! +} + +type CrateConnection { + edges: [CrateEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CrateEdge { + cursor: String! + node: CrateItem! +} + +type PageInfo { + hasNextPage: Boolean! + endCursor: String +} + +type GenreFacet { + genre: Genre! + count: Int! +} + +type DeleteReviewPayload { + deletedReviewId: ID! + album: Album! +} + +type UpsertReviewPayload { + review: Review! + album: Album! +} + +type AddToCratePayload { + item: CrateItem! + album: Album! +} + +type RemoveFromCratePayload { + removedAlbumId: ID! + album: Album! +} + +input AlbumSearchInput { + query: String! + genreIds: [ID!] + sort: AlbumSort! +} + +input UpsertReviewInput { + albumId: ID! + rating: Int! + comment: String! +} + +enum AlbumSort { + RELEVANCE + TITLE_ASC + RELEASE_YEAR_DESC + AVERAGE_RATING_DESC +}