End to End React with Prisma 2
Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.
13. Add Create Saved Article operations
Objective: Create mutation to allow us to make saved articles.
The last model that we haven't talked about yet, is the SavedArticles. This will allow us to like articles which then get saved to a separate list which we can read at our leisure. A savedArticle is linked to the feed that it appeared in and the owner who liked the article. Let's start in the typeDefs file. Also, while we are adding operations, we will also add a me
operation which will allow us to fetch the current user's information stored in the database. The me
query is pretty straightforward, we just fetch the current user based on the context.
utils/api/typeDefs.ts
ts
// update the feed to add savedArticlestype Feed {id: Stringname: Stringurl: Stringbundles: [Bundle]author: Usertags: [FeedTag]likes: [User]savedArticles: [SavedArticle]}scalar JSONtype SavedArticle {id: Stringauthor: Userurl: Stringcontents: JSONfeed: Feed}input SavedArticleInput {url: String}input SavedArticleCreateInput {id: Stringfeed: NestedFeedCreateInputcontents: JSONurl: String}input NestedFeedCreateInput {connect: FeedWhereUniqueInput}type Query {savedArticle(data: SavedArticleInput): SavedArticlesavedArticles: [SavedArticle]me: User}type Mutation {createSavedArticle(data: SavedArticleCreateInput): SavedArticle}
You'll see that we are using a new primitive called JSON
which we haven't used before. We need this because we will actually store the entire item from the RSS feed in the savedArticles item so that we can have quick access to things like the article's title, description and url. Since items in an RSS feed are generally unstructured from feed to feed, we can't rely on a specific set of fields so a good comprimise is to store it as a JSON blob.
In practice there is a reason why graphQL servers are so strict in their types- its possible that someone could embed nasty things inside their RSS feed. For example what if one of the fields actually had a database command to drop a table? It might be possible that it gets handled in a way that actually causes the string to execute as a command- yikes! The solution is that proper scrubbing is important for a enterprise-grade website before we store anything to our databases. In our case, since RSS feeds are generally pretty benign data sources, the risk is low.
In any case, we will be adding a package called graphql-type-json
to support this new JSON
type.
npm install --save graphql-type-json
utils/api/resolvers.ts
ts
import { GraphQLJSONObject } from 'graphql-type-json';// other imports and definitionsexport const resolvers = {JSON: GraphQLJSONObject,Feed: {...createFieldResolver('feed', 'author'),...createFieldResolver('feed', 'bundles'),...createFieldResolver('feed', 'likes'),...createFieldResolver('feed', 'tags'),...createFieldResolver('feed', 'savedArticles'),},// other resolver definitionsSavedArticle: {...createFieldResolver('savedArticle', 'author'),...createFieldResolver('savedArticle', 'feed'),},// other resolver definitionsQuery: {// other queriessavedArticle: async (parent, { data: { url } }, { prisma, user: { id: authorId } }) => {const result = await prisma.savedArticle.findMany({ where: { url, authorId } });return result[0];},savedArticles: (parent, args, { prisma, user: { id: authorId } }) =>prisma.savedArticle.findMany({ where: { authorId: authorId ? authorId : null } }),me: (parent, args, { prisma, user: { id } }) => prisma.user.findOne({ where: { id } }),},Mutation: {// other mutationscreateSavedArticle: (parent, { data }, { prisma, user }) => {const author = { author: { connect: { id: user.id } } };return prisma.savedArticle.create({ data: { ...data, ...author } });},}
You can see that we add savedArticles
to the Feed
resolver definition so that we'll be able to look up any savedArticles when requested by the client. In the queries section, savedArticle
takes a parameter of url
and we add the authorId along with it to ensure we are only looking up the current user's articles. In the UI we will only ever allow a user to like an article which does not already have a savedArticle record associated with it, so when we perform this findMany
it should only ever return 1 entry. We will then just take that entry, which will be the [0]
position in the findMany
request and return it to the user. If nothing is found to match that url for that user, we return null to the user just like with a feed
or bundle
query.
The savedArticles
query is simpler and we only return savedArticles by the current user. Finally, the createSavedArticle
mutation will take the article information, associate the user and save it to the database.
Let's make sure this createSavedArticle
mutation works. Send the following mutation:
graphql
mutation createSavedArticleMutation($data: SavedArticleCreateInput) {createSavedArticle(data: $data) {...SavedArticleFragment}}fragment SavedArticleFragment on SavedArticle {idcontentsurlauthor {...AuthorFragment}feed {...FeedFragment}}fragment FeedFragment on Feed {idnameurllikes {...AuthorFragment}tags {...FeedTagFragment}author {...AuthorFragment}}fragment AuthorFragment on User {idauth0picturenickname}fragment FeedTagFragment on FeedTag {idname}
With the following parameters that I pulled from an article from the NY Times Technology RSS feed.
json
{"data": {"id": "1000","feed": { "connect": { "id": "1" } },"url": "https://www.nytimes.com/2020/10/28/technology/section-230-hearing.html","contents": {"title": "We Need Policy, Not WrestleMania","pubDate": "2020-10-28 16:55:31","link": "https://www.nytimes.com/2020/10/28/technology/section-230-hearing.html","guid": "https://www.nytimes.com/2020/10/28/technology/section-230-hearing.html","author": "Shira Ovide","description": "Lawmakers could debate an important law that affects speech on the internet. Or they could yell.","content": "Lawmakers could debate an important law that affects speech on the internet. Or they could yell."}}}
When you run the savedArticle
query with the url that matches above, or a savedArticles
query we should see this as the single returned record.