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 savedArticles
    type Feed {
    id: String
    name: String
    url: String
    bundles: [Bundle]
    author: User
    tags: [FeedTag]
    likes: [User]
    savedArticles: [SavedArticle]
    }
    scalar JSON
    type SavedArticle {
    id: String
    author: User
    url: String
    contents: JSON
    feed: Feed
    }
    input SavedArticleInput {
    url: String
    }
    input SavedArticleCreateInput {
    id: String
    feed: NestedFeedCreateInput
    contents: JSON
    url: String
    }
    input NestedFeedCreateInput {
    connect: FeedWhereUniqueInput
    }
    type Query {
    savedArticle(data: SavedArticleInput): SavedArticle
    savedArticles: [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 definitions
    export const resolvers = {
    JSON: GraphQLJSONObject,
    Feed: {
    ...createFieldResolver('feed', 'author'),
    ...createFieldResolver('feed', 'bundles'),
    ...createFieldResolver('feed', 'likes'),
    ...createFieldResolver('feed', 'tags'),
    ...createFieldResolver('feed', 'savedArticles'),
    },
    // other resolver definitions
    SavedArticle: {
    ...createFieldResolver('savedArticle', 'author'),
    ...createFieldResolver('savedArticle', 'feed'),
    },
    // other resolver definitions
    Query: {
    // other queries
    savedArticle: 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 mutations
    createSavedArticle: (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 {
    id
    contents
    url
    author {
    ...AuthorFragment
    }
    feed {
    ...FeedFragment
    }
    }
    fragment FeedFragment on Feed {
    id
    name
    url
    likes {
    ...AuthorFragment
    }
    tags {
    ...FeedTagFragment
    }
    author {
    ...AuthorFragment
    }
    }
    fragment AuthorFragment on User {
    id
    auth0
    picture
    nickname
    }
    fragment FeedTagFragment on FeedTag {
    id
    name
    }

    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.