  scalar Date

  directive @sanitize(
    allowedTags: [String]
    maxLength: Int
    minLength: Int
    pattern: String
  ) on INPUT_FIELD_DEFINITION

  enum SortOrder {
    ASCENDING
    DESCENDING
  }

  enum ReactionType {
    LIKE
    HEART
    SMILE
    HUSHED
    CRYING
    POUT
  }

  enum SortBy {
    UPDATED_TIME
    SCORE
    SAVED_AT
    PUBLISHED_AT
  }

  enum ContentReader {
    WEB
    PDF
    EPUB
  }

  input SortParams {
    order: SortOrder
    by: SortBy!
  }

  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
    totalCount: Int
  }

  type ArticleEdge {
    cursor: String!
    node: Article!
  }

  type FeedArticleEdge {
    cursor: String!
    node: FeedArticle!
  }

  type ArticlesSuccess {
    edges: [ArticleEdge!]!
    pageInfo: PageInfo!
  }

  # User
  type User {
    id: ID!
    name: String!
    isFullUser: Boolean
    viewerIsFollowing: Boolean
    isFriend: Boolean
      @deprecated(reason: "isFriend has been replaced with viewerIsFollowing")
    # temporary; same as profile.pictureUrl for backward compatibility purposes
    picture: String
    profile: Profile!
    sharedArticles: [FeedArticle!]!
    sharedArticlesCount: Int
    sharedHighlightsCount: Int
    sharedNotesCount: Int
    friendsCount: Int
    followersCount: Int
  }

  type Profile {
    id: ID!
    username: String!
    private: Boolean!
    bio: String
    pictureUrl: String
  }

  # Query: User
  union UserResult = UserSuccess | UserError
  enum UserErrorCode {
    UNAUTHORIZED
    USER_NOT_FOUND
    BAD_REQUEST
  }
  type UserError {
    errorCodes: [UserErrorCode!]!
  }
  type UserSuccess {
    user: User!
  }

  # Query: Users
  union UsersResult = UsersSuccess | UsersError
  enum UsersErrorCode {
    UNAUTHORIZED
  }
  type UsersError {
    errorCodes: [UsersErrorCode!]!
  }
  type UsersSuccess {
    users: [User!]!
  }

  # Mutations: googleLogin
  union LoginResult = LoginSuccess | LoginError
  type LoginSuccess {
    me: User!
  }
  type LoginError {
    errorCodes: [LoginErrorCode!]!
  }
  enum LoginErrorCode {
    # Authorization failed due to 3rd party errors
    AUTH_FAILED
    # User already exists
    USER_ALREADY_EXISTS
    # Invalid credentials provided
    INVALID_CREDENTIALS
    # User not found
    USER_NOT_FOUND
    # Mismatching source
    WRONG_SOURCE
    # Access denied
    ACCESS_DENIED
  }

  input GoogleLoginInput {
    secret: String!
    email: String!
  }

  input GoogleSignupInput {
    secret: String!
    email: String!
    username: String!
    name: String!
    pictureUrl: String!
    sourceUserId: String!
    bio: String
  }

  type GoogleSignupSuccess {
    me: User!
  }

  enum SignupErrorCode {
    UNKNOWN
    ACCESS_DENIED
    GOOGLE_AUTH_ERROR
    INVALID_USERNAME
    USER_EXISTS
    EXPIRED_TOKEN
    INVALID_PASSWORD
    INVALID_EMAIL
  }

  type GoogleSignupError {
    errorCodes: [SignupErrorCode]!
  }

  union GoogleSignupResult = GoogleSignupSuccess | GoogleSignupError

  # Mutation: logOut
  union LogOutResult = LogOutSuccess | LogOutError

  enum LogOutErrorCode {
    LOG_OUT_FAILED
  }
  type LogOutError {
    errorCodes: [LogOutErrorCode!]!
  }
  type LogOutSuccess {
    message: String
  }

  enum DeleteAccountErrorCode {
    USER_NOT_FOUND
    UNAUTHORIZED
    FORBIDDEN
  }

  type DeleteAccountError {
    errorCodes: [DeleteAccountErrorCode!]!
  }

  type DeleteAccountSuccess {
    userID: ID!
  }

  union DeleteAccountResult = DeleteAccountSuccess | DeleteAccountError

  union UpdateUserResult = UpdateUserSuccess | UpdateUserError
  input UpdateUserInput {
    name: String! @sanitize(maxLength: 50)
    bio: String @sanitize(maxLength: 400)
  }

  enum UpdateUserErrorCode {
    EMPTY_NAME
    BIO_TOO_LONG
    USER_NOT_FOUND
    UNAUTHORIZED
  }
  type UpdateUserError {
    errorCodes: [UpdateUserErrorCode!]!
  }
  type UpdateUserSuccess {
    user: User!
  }

  # Mutation: updateUserProfile
  union UpdateUserProfileResult =
      UpdateUserProfileSuccess
    | UpdateUserProfileError
  input UpdateUserProfileInput {
    userId: ID!
    username: String @sanitize(maxLength: 15)
    bio: String @sanitize(maxLength: 400)
    pictureUrl: String @sanitize
  }

  type UpdateUserProfileSuccess {
    user: User!
  }

  enum UpdateUserProfileErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    BAD_DATA
    BAD_USERNAME
    USERNAME_EXISTS
  }

  type UpdateUserProfileError {
    errorCodes: [UpdateUserProfileErrorCode!]!
  }

  # Article
  input ArticleHighlightsInput {
    includeFriends: Boolean
  }

  type ReadState {
    reading: Boolean
    readingTime: Int

    progressPercent: Float!
    progressAnchorIndex: Int!
  }

  type HighlightStats {
    highlightCount: Int!
  }

  type ShareStats {
    viewCount: Int!
    saveCount: Int!
    readDuration: Int!
  }

  type LinkShareInfo {
    title: String!
    description: String!
    imageUrl: String!
  }

  type Link {
    id: ID!
    url: String!
    slug: String!
    savedBy: User!
    savedAt: Date!
    updatedAt: Date!
    savedByViewer: Boolean!
    postedByViewer: Boolean!

    readState: ReadState!
    highlightStats: HighlightStats!

    shareInfo: LinkShareInfo!
    shareStats: ShareStats!

    page: Page!
  }

  enum PageType {
    ARTICLE
    BOOK
    FILE
    PROFILE
    WEBSITE
    HIGHLIGHTS
    UNKNOWN
    TWEET
    VIDEO
    IMAGE
  }

  type Page {
    id: ID!
    url: String!
    hash: String!
    originalUrl: String!

    type: PageType!
    image: String!
    title: String!
    author: String
    description: String
    publishedAt: Date

    originalHtml: String!
    readableHtml: String!
    createdAt: Date!
    updatedAt: Date!
  }

  type RecommendingUser {
    userId: String!
    name: String!
    username: String!
    profileImageURL: String
  }

  type Recommendation {
    id: ID!
    name: String!
    user: RecommendingUser
    recommendedAt: Date!
    note: String
  }

  type Article {
    id: ID!
    title: String!
    slug: String!
    # for uploaded file articles (PDFs), the URL here is the saved omnivore link in GCS
    url: String!
    hash: String!
    content: String!
    pageType: PageType
    contentReader: ContentReader!
    hasContent: Boolean
    author: String
    image: String
    description: String
    originalHtml: String
    createdAt: Date!
    savedAt: Date!
    updatedAt: Date!
    publishedAt: Date
    readingProgressTopPercent: Float
    readingProgressPercent: Float!
    readingProgressAnchorIndex: Int!
    sharedComment: String
    savedByViewer: Boolean
    postedByViewer: Boolean
    # for uploaded file articles (PDFs), we track the original article URL separately!
    originalArticleUrl: String
    highlights(input: ArticleHighlightsInput): [Highlight!]!
    shareInfo: LinkShareInfo
    isArchived: Boolean!
    linkId: ID
    labels: [Label!]
    uploadFileId: ID
    siteName: String
    siteIcon: String
    subscription: String
    unsubMailTo: String
    unsubHttpUrl: String
    state: ArticleSavingRequestStatus
    language: String
    readAt: Date
    recommendations: [Recommendation!]
    wordsCount: Int
  }

  # Query: article
  union ArticleResult = ArticleSuccess | ArticleError
  enum ArticleErrorCode {
    NOT_FOUND
    BAD_DATA
    UNAUTHORIZED
  }
  type ArticleError {
    errorCodes: [ArticleErrorCode!]!
  }
  type ArticleSuccess {
    article: Article!
  }

  # Query: sharedArticle
  union SharedArticleResult = SharedArticleSuccess | SharedArticleError

  enum SharedArticleErrorCode {
    NOT_FOUND
  }

  type SharedArticleError {
    errorCodes: [SharedArticleErrorCode!]!
  }

  type SharedArticleSuccess {
    article: Article!
  }

  # Query: articles
  union ArticlesResult = ArticlesSuccess | ArticlesError
  enum ArticlesErrorCode {
    UNAUTHORIZED
  }
  type ArticlesError {
    errorCodes: [ArticlesErrorCode!]!
  }

  # PageInfo
  input PageInfoInput {
    title: String
    author: String @sanitize
    description: String @sanitize
    previewImage: String @sanitize
    canonicalUrl: String @sanitize
    publishedAt: Date
    contentType: String
  }

  input PreparedDocumentInput {
    document: String!
    pageInfo: PageInfoInput!
  }

  # Mutation: uploadFileRequest
  enum UploadFileStatus {
    INITIALIZED
    COMPLETED
  }
  union UploadFileRequestResult =
      UploadFileRequestSuccess
    | UploadFileRequestError

  input UploadFileRequestInput {
    url: String!
    contentType: String!
    createPageEntry: Boolean
    clientRequestId: String
  }

  enum UploadFileRequestErrorCode {
    UNAUTHORIZED
    BAD_INPUT
    FAILED_CREATE
  }
  type UploadFileRequestError {
    errorCodes: [UploadFileRequestErrorCode!]!
  }
  type UploadFileRequestSuccess {
    id: ID!
    uploadSignedUrl: String
    uploadFileId: ID
    createdPageId: String
  }

  # Mutation: createArticle
  union CreateArticleResult = CreateArticleSuccess | CreateArticleError
  input CreateArticleInput {
    url: String!
    # can be used for saving an article without triggering puppeteer (for instance, from a browser extension)
    preparedDocument: PreparedDocumentInput
    articleSavingRequestId: ID
    uploadFileId: ID
    skipParsing: Boolean
    source: String
    state: ArticleSavingRequestStatus
    labels: [CreateLabelInput!]
  }
  enum CreateArticleErrorCode {
    UNABLE_TO_FETCH
    UNABLE_TO_PARSE
    UNAUTHORIZED
    NOT_ALLOWED_TO_PARSE
    PAYLOAD_TOO_LARGE
    UPLOAD_FILE_MISSING
    ELASTIC_ERROR
  }
  type CreateArticleError {
    errorCodes: [CreateArticleErrorCode!]!
  }
  type CreateArticleSuccess {
    createdArticle: Article!
    # user that saved the article
    user: User!
    created: Boolean! # Indicates if an article has been created or updated
  }

  # Used for all save operations
  enum SaveErrorCode {
    UNKNOWN
    UNAUTHORIZED
    EMBEDDED_HIGHLIGHT_FAILED
  }

  type SaveError {
    errorCodes: [SaveErrorCode!]!
    message: String
  }

  type SaveSuccess {
    url: String!
    clientRequestId: ID!
  }

  input SaveFileInput {
    url: String!
    source: String!
    clientRequestId: ID!
    uploadFileId: ID!
    state: ArticleSavingRequestStatus
    labels: [CreateLabelInput!]
  }

  input ParseResult {
    title: String!
    byline: String
    dir: String
    content: String!
    textContent: String!
    length: Int!
    excerpt: String!
    siteName: String
    siteIcon: String
    previewImage: String
    publishedDate: Date
    language: String
  }

  input SavePageInput {
    url: String!
    source: String!
    clientRequestId: ID!
    title: String
    originalContent: String!
    parseResult: ParseResult
    state: ArticleSavingRequestStatus
    labels: [CreateLabelInput!]
  }

  input SaveUrlInput {
    url: String!
    source: String!
    clientRequestId: ID!
    state: ArticleSavingRequestStatus
    labels: [CreateLabelInput!]
  }

  union SaveResult = SaveSuccess | SaveError

  input UpdatePageInput {
    pageId: ID!
    title: String
    description: String
    byline: String
    savedAt: Date
    publishedAt: Date
    previewImage: String @sanitize
  }

  type UpdatePageSuccess {
    updatedPage: Article!
  }

  enum UpdatePageErrorCode {
    UPDATE_FAILED
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    FORBIDDEN
  }

  type UpdatePageError {
    errorCodes: [UpdatePageErrorCode!]!
  }

  union UpdatePageResult = UpdatePageSuccess | UpdatePageError

  # Mutation: setFollow
  union SetFollowResult = SetFollowSuccess | SetFollowError
  input SetFollowInput {
    userId: ID!
    follow: Boolean!
  }
  enum SetFollowErrorCode {
    NOT_FOUND
    UNAUTHORIZED
  }
  type SetFollowError {
    errorCodes: [SetFollowErrorCode!]!
  }
  type SetFollowSuccess {
    updatedUser: User!
  }

  # Mutation: saveArticleReadingProgress
  union SaveArticleReadingProgressResult =
      SaveArticleReadingProgressSuccess
    | SaveArticleReadingProgressError
  input SaveArticleReadingProgressInput {
    id: ID!
    readingProgressTopPercent: Float
    readingProgressPercent: Float!
    readingProgressAnchorIndex: Int!
  }
  enum SaveArticleReadingProgressErrorCode {
    NOT_FOUND
    BAD_DATA
    UNAUTHORIZED
  }
  type SaveArticleReadingProgressError {
    errorCodes: [SaveArticleReadingProgressErrorCode!]!
  }
  type SaveArticleReadingProgressSuccess {
    updatedArticle: Article!
  }

  # Mutation: setBookmark
  union SetBookmarkArticleResult =
      SetBookmarkArticleSuccess
    | SetBookmarkArticleError
  input SetBookmarkArticleInput {
    articleID: ID!
    bookmark: Boolean!
  }
  enum SetBookmarkArticleErrorCode {
    NOT_FOUND
    BOOKMARK_EXISTS
  }
  type SetBookmarkArticleError {
    errorCodes: [SetBookmarkArticleErrorCode!]!
  }
  type SetBookmarkArticleSuccess {
    bookmarkedArticle: Article!
  }

  # Feed Article
  type FeedArticle {
    id: ID!
    article: Article!
    sharedBy: User!
    sharedAt: Date!
    sharedComment: String
    sharedWithHighlights: Boolean
    highlightsCount: Int
    annotationsCount: Int
    highlight: Highlight
    reactions: [Reaction!]!
  }

  enum HighlightType {
    HIGHLIGHT
    REDACTION
    NOTE
  }

  # Highlight
  type Highlight {
    id: ID!
    # used for simplified url format
    shortId: String!
    user: User!
    quote: String
    # piece of content before the quote
    prefix: String
    # piece of content after the quote
    suffix: String
    patch: String
    annotation: String
    replies: [HighlightReply!]!
    sharedAt: Date
    createdAt: Date!
    updatedAt: Date!
    reactions: [Reaction!]!
    createdByMe: Boolean!
    highlightPositionPercent: Float
    highlightPositionAnchorIndex: Int
    labels: [Label!]
    type: HighlightType!
    html: String
  }

  input CreateHighlightInput {
    id: ID!
    shortId: String!
    articleId: ID!
    patch: String
    quote: String @sanitize(maxLength: 12000, minLength: 1)
    prefix: String @sanitize
    suffix: String @sanitize
    annotation: String @sanitize(maxLength: 4000)
    sharedAt: Date
    highlightPositionPercent: Float
    highlightPositionAnchorIndex: Int
    type: HighlightType
    html: String
  }

  type CreateHighlightSuccess {
    highlight: Highlight!
  }

  enum CreateHighlightErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    BAD_DATA
    NOT_FOUND
    ALREADY_EXISTS
  }

  type CreateHighlightError {
    errorCodes: [CreateHighlightErrorCode!]!
  }

  union CreateHighlightResult = CreateHighlightSuccess | CreateHighlightError

  input MergeHighlightInput {
    id: ID!
    shortId: ID!
    articleId: ID!
    patch: String!
    quote: String! @sanitize(maxLength: 12000, minLength: 1)
    prefix: String @sanitize
    suffix: String @sanitize
    annotation: String @sanitize(maxLength: 8000)
    overlapHighlightIdList: [String!]!
    highlightPositionPercent: Float
    highlightPositionAnchorIndex: Int
    html: String
  }

  type MergeHighlightSuccess {
    highlight: Highlight!
    overlapHighlightIdList: [String!]!
  }

  enum MergeHighlightErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    BAD_DATA
    NOT_FOUND
    ALREADY_EXISTS
  }

  type MergeHighlightError {
    errorCodes: [MergeHighlightErrorCode!]!
  }

  union MergeHighlightResult = MergeHighlightSuccess | MergeHighlightError

  input UpdateHighlightInput {
    highlightId: ID!
    annotation: String @sanitize(maxLength: 4000)
    sharedAt: Date
    quote: String @sanitize(maxLength: 12000, minLength: 1)
    html: String
  }

  type UpdateHighlightSuccess {
    highlight: Highlight!
  }

  enum UpdateHighlightErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
    BAD_DATA
  }

  type UpdateHighlightError {
    errorCodes: [UpdateHighlightErrorCode!]!
  }

  union UpdateHighlightResult = UpdateHighlightSuccess | UpdateHighlightError

  type DeleteHighlightSuccess {
    highlight: Highlight!
  }

  enum DeleteHighlightErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
  }

  type DeleteHighlightError {
    errorCodes: [DeleteHighlightErrorCode!]!
  }

  union DeleteHighlightResult = DeleteHighlightSuccess | DeleteHighlightError

  # HighlightReply
  type HighlightReply {
    id: ID!
    user: User!
    highlight: Highlight!
    text: String!
    createdAt: Date!
    updatedAt: Date!
  }

  input CreateHighlightReplyInput {
    highlightId: ID!
    text: String!
  }

  type CreateHighlightReplySuccess {
    highlightReply: HighlightReply!
  }

  enum CreateHighlightReplyErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
    EMPTY_ANNOTATION
  }

  type CreateHighlightReplyError {
    errorCodes: [CreateHighlightReplyErrorCode!]!
  }

  union CreateHighlightReplyResult =
      CreateHighlightReplySuccess
    | CreateHighlightReplyError

  input UpdateHighlightReplyInput {
    highlightReplyId: ID!
    text: String!
  }

  type UpdateHighlightReplySuccess {
    highlightReply: HighlightReply!
  }

  enum UpdateHighlightReplyErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
  }

  type UpdateHighlightReplyError {
    errorCodes: [UpdateHighlightReplyErrorCode!]!
  }

  union UpdateHighlightReplyResult =
      UpdateHighlightReplySuccess
    | UpdateHighlightReplyError

  type DeleteHighlightReplySuccess {
    highlightReply: HighlightReply!
  }

  enum DeleteHighlightReplyErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
  }

  type DeleteHighlightReplyError {
    errorCodes: [DeleteHighlightReplyErrorCode!]!
  }

  union DeleteHighlightReplyResult =
      DeleteHighlightReplySuccess
    | DeleteHighlightReplyError

  # Reaction
  type Reaction {
    id: ID!
    user: User!
    code: ReactionType!
    createdAt: Date!
    updatedAt: Date
  }

  input CreateReactionInput {
    highlightId: ID
    userArticleId: ID
    code: ReactionType!
  }

  type CreateReactionSuccess {
    reaction: Reaction!
  }

  enum CreateReactionErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    BAD_TARGET
    BAD_CODE
    NOT_FOUND
  }

  type CreateReactionError {
    errorCodes: [CreateReactionErrorCode!]!
  }

  union CreateReactionResult = CreateReactionSuccess | CreateReactionError

  type DeleteReactionSuccess {
    reaction: Reaction!
  }

  enum DeleteReactionErrorCode {
    UNAUTHORIZED
    FORBIDDEN
    NOT_FOUND
  }

  type DeleteReactionError {
    errorCodes: [DeleteReactionErrorCode!]!
  }

  union DeleteReactionResult = DeleteReactionSuccess | DeleteReactionError

  # Query: Feed
  union FeedArticlesResult = FeedArticlesSuccess | FeedArticlesError
  enum FeedArticlesErrorCode {
    UNAUTHORIZED
  }
  type FeedArticlesError {
    errorCodes: [FeedArticlesErrorCode!]!
  }

  type FeedArticlesSuccess {
    edges: [FeedArticleEdge!]!
    pageInfo: PageInfo!
  }

  #  Mutation: setShareArticle
  enum SetShareArticleErrorCode {
    NOT_FOUND
    UNAUTHORIZED
  }

  type SetShareArticleSuccess {
    updatedFeedArticleId: String
    updatedFeedArticle: FeedArticle
    updatedArticle: Article!
  }

  type SetShareArticleError {
    errorCodes: [SetShareArticleErrorCode!]!
  }

  union SetShareArticleResult = SetShareArticleSuccess | SetShareArticleError
  input SetShareArticleInput {
    articleID: ID!
    share: Boolean!
    sharedComment: String @sanitize(maxLength: 4000)
    sharedWithHighlights: Boolean
  }

  #  Mutation: updateSharedComment
  enum UpdateSharedCommentErrorCode {
    NOT_FOUND
    UNAUTHORIZED
  }

  type UpdateSharedCommentSuccess {
    articleID: ID!
    sharedComment: String!
  }

  type UpdateSharedCommentError {
    errorCodes: [UpdateSharedCommentErrorCode!]!
  }

  union UpdateSharedCommentResult =
      UpdateSharedCommentSuccess
    | UpdateSharedCommentError
  input UpdateSharedCommentInput {
    articleID: ID!
    sharedComment: String! @sanitize(maxLength: 4000)
  }

  # Query: Followers
  union GetFollowersResult = GetFollowersSuccess | GetFollowersError
  enum GetFollowersErrorCode {
    UNAUTHORIZED
  }
  type GetFollowersError {
    errorCodes: [GetFollowersErrorCode!]!
  }
  type GetFollowersSuccess {
    followers: [User!]!
  }

  union GetFollowingResult = GetFollowingSuccess | GetFollowingError
  enum GetFollowingErrorCode {
    UNAUTHORIZED
  }
  type GetFollowingError {
    errorCodes: [GetFollowingErrorCode!]!
  }
  type GetFollowingSuccess {
    following: [User!]!
  }

  type UserPersonalization {
    id: ID
    theme: String
    fontSize: Int
    fontFamily: String
    margin: Int
    libraryLayoutType: String
    librarySortOrder: SortOrder
    speechVoice: String
    speechSecondaryVoice: String
    speechRate: String
    speechVolume: String
  }

  # Query: UserPersonalization
  union GetUserPersonalizationResult =
      GetUserPersonalizationSuccess
    | GetUserPersonalizationError
  enum GetUserPersonalizationErrorCode {
    UNAUTHORIZED
  }
  type GetUserPersonalizationError {
    errorCodes: [GetUserPersonalizationErrorCode!]!
  }
  type GetUserPersonalizationSuccess {
    userPersonalization: UserPersonalization
  }

  # Mutation: SetUserPersonalizationResult
  union SetUserPersonalizationResult =
      SetUserPersonalizationSuccess
    | SetUserPersonalizationError
  enum SetUserPersonalizationErrorCode {
    UNAUTHORIZED
    NOT_FOUND
  }
  type SetUserPersonalizationError {
    errorCodes: [SetUserPersonalizationErrorCode!]!
  }
  type SetUserPersonalizationSuccess {
    updatedUserPersonalization: UserPersonalization!
  }
  input SetUserPersonalizationInput {
    theme: String @sanitize
    fontSize: Int
    fontFamily: String @sanitize
    margin: Int
    libraryLayoutType: String @sanitize
    librarySortOrder: SortOrder
    speechVoice: String
    speechSecondaryVoice: String
    speechRate: String
    speechVolume: String
  }

  # Type: ArticleSavingRequest
  enum ArticleSavingRequestStatus {
    PROCESSING
    SUCCEEDED
    FAILED
    DELETED
    ARCHIVED
  }

  type ArticleSavingRequest {
    id: ID!
    userId: ID! @deprecated(reason: "userId has been replaced with user")
    user: User!
    article: Article @deprecated(reason: "article has been replaced with slug")
    slug: String!
    status: ArticleSavingRequestStatus!
    errorCode: CreateArticleErrorCode
    createdAt: Date!
    updatedAt: Date!
    url: String!
  }

  # Query: ArticleSavingRequest
  union ArticleSavingRequestResult =
      ArticleSavingRequestSuccess
    | ArticleSavingRequestError
  enum ArticleSavingRequestErrorCode {
    UNAUTHORIZED
    NOT_FOUND
    BAD_DATA
  }
  type ArticleSavingRequestError {
    errorCodes: [ArticleSavingRequestErrorCode!]!
  }
  type ArticleSavingRequestSuccess {
    articleSavingRequest: ArticleSavingRequest!
  }

  # Mutation: CreateArticleSavingRequest
  union CreateArticleSavingRequestResult =
      CreateArticleSavingRequestSuccess
    | CreateArticleSavingRequestError
  enum CreateArticleSavingRequestErrorCode {
    UNAUTHORIZED
    BAD_DATA
  }
  type CreateArticleSavingRequestError {
    errorCodes: [CreateArticleSavingRequestErrorCode!]!
  }
  type CreateArticleSavingRequestSuccess {
    articleSavingRequest: ArticleSavingRequest!
  }
  input CreateArticleSavingRequestInput {
    url: String!
  }

  # Mutation: SetShareHighlightRequest
  union SetShareHighlightResult =
      SetShareHighlightSuccess
    | SetShareHighlightError
  enum SetShareHighlightErrorCode {
    UNAUTHORIZED
    NOT_FOUND
    FORBIDDEN
  }
  type SetShareHighlightError {
    errorCodes: [SetShareHighlightErrorCode!]!
  }
  type SetShareHighlightSuccess {
    highlight: Highlight!
  }
  input SetShareHighlightInput {
    id: ID!
    share: Boolean!
  }

  enum ReportType {
    SPAM
    ABUSIVE
    CONTENT_DISPLAY
    CONTENT_VIOLATION
  }

  input ReportItemInput {
    pageId: ID!
    itemUrl: String!
    sharedBy: ID
    reportTypes: [ReportType!]!
    reportComment: String!
  }

  type ReportItemResult {
    message: String!
  }

  input UpdateLinkShareInfoInput {
    linkId: ID!
    title: String! @sanitize(maxLength: 95)
    description: String! @sanitize(maxLength: 300)
  }

  union UpdateLinkShareInfoResult =
      UpdateLinkShareInfoSuccess
    | UpdateLinkShareInfoError
  enum UpdateLinkShareInfoErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  type UpdateLinkShareInfoError {
    errorCodes: [UpdateLinkShareInfoErrorCode!]!
  }

  type UpdateLinkShareInfoSuccess {
    message: String!
  }

  input ArchiveLinkInput {
    linkId: ID!
    archived: Boolean!
  }

  enum ArchiveLinkErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  type ArchiveLinkError {
    message: String!
    errorCodes: [ArchiveLinkErrorCode!]!
  }

  type ArchiveLinkSuccess {
    linkId: String!
    message: String!
  }
  union ArchiveLinkResult = ArchiveLinkSuccess | ArchiveLinkError

  # Query: NewsletterEmails
  enum NewsletterEmailsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  type NewsletterEmail {
    id: ID!
    address: String!
    confirmationCode: String
    createdAt: Date!
    subscriptionCount: Int!
  }

  type NewsletterEmailsSuccess {
    newsletterEmails: [NewsletterEmail!]!
  }

  type NewsletterEmailsError {
    errorCodes: [NewsletterEmailsErrorCode!]!
  }

  union NewsletterEmailsResult = NewsletterEmailsSuccess | NewsletterEmailsError

  # Mutation: CreateNewsletterEmail
  enum CreateNewsletterEmailErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  type CreateNewsletterEmailSuccess {
    newsletterEmail: NewsletterEmail!
  }

  type CreateNewsletterEmailError {
    errorCodes: [CreateNewsletterEmailErrorCode!]!
  }

  union CreateNewsletterEmailResult =
      CreateNewsletterEmailSuccess
    | CreateNewsletterEmailError

  # Mutation: DeleteNewsletterEmail
  enum DeleteNewsletterEmailErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type DeleteNewsletterEmailSuccess {
    newsletterEmail: NewsletterEmail!
  }

  type DeleteNewsletterEmailError {
    errorCodes: [DeleteNewsletterEmailErrorCode!]!
  }

  union DeleteNewsletterEmailResult =
      DeleteNewsletterEmailSuccess
    | DeleteNewsletterEmailError

  # Query: Reminder

  type Reminder {
    id: ID!
    archiveUntil: Boolean!
    sendNotification: Boolean!
    remindAt: Date!
  }

  type ReminderSuccess {
    reminder: Reminder!
  }

  enum ReminderErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type ReminderError {
    errorCodes: [ReminderErrorCode!]!
  }

  union ReminderResult = ReminderSuccess | ReminderError

  # Mutation: CreateReminder
  input CreateReminderInput {
    linkId: ID
    clientRequestId: ID
    archiveUntil: Boolean!
    sendNotification: Boolean!
    remindAt: Date!
  }

  type CreateReminderSuccess {
    reminder: Reminder!
  }

  enum CreateReminderErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type CreateReminderError {
    errorCodes: [CreateReminderErrorCode!]!
  }

  union CreateReminderResult = CreateReminderSuccess | CreateReminderError

  input UpdateReminderInput {
    id: ID!
    archiveUntil: Boolean!
    sendNotification: Boolean!
    remindAt: Date!
  }

  union UpdateReminderResult = UpdateReminderSuccess | UpdateReminderError

  type UpdateReminderSuccess {
    reminder: Reminder!
  }

  enum UpdateReminderErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type UpdateReminderError {
    errorCodes: [UpdateReminderErrorCode!]!
  }

  union DeleteReminderResult = DeleteReminderSuccess | DeleteReminderError

  type DeleteReminderSuccess {
    reminder: Reminder!
  }

  enum DeleteReminderErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type DeleteReminderError {
    errorCodes: [DeleteReminderErrorCode!]!
  }

  type SendInstallInstructionsSuccess {
    sent: Boolean!
  }

  enum SendInstallInstructionsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    FORBIDDEN
  }

  type SendInstallInstructionsError {
    errorCodes: [SendInstallInstructionsErrorCode!]!
  }

  union SendInstallInstructionsResult =
      SendInstallInstructionsSuccess
    | SendInstallInstructionsError

  input SetDeviceTokenInput {
    id: ID
    token: String
  }

  type DeviceToken {
    id: ID!
    token: String!
    createdAt: Date!
  }

  type SetDeviceTokenSuccess {
    deviceToken: DeviceToken!
  }

  enum SetDeviceTokenErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type SetDeviceTokenError {
    errorCodes: [SetDeviceTokenErrorCode!]!
  }

  union SetDeviceTokenResult = SetDeviceTokenSuccess | SetDeviceTokenError

  type Label {
    id: ID!
    name: String!
    color: String!
    description: String
    createdAt: Date
    position: Int
    internal: Boolean
  }

  type LabelsSuccess {
    labels: [Label!]!
  }

  enum LabelsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  type LabelsError {
    errorCodes: [LabelsErrorCode!]!
  }

  union LabelsResult = LabelsSuccess | LabelsError

  input CreateLabelInput {
    name: String! @sanitize(maxLength: 64)
    color: String @sanitize(pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
    description: String @sanitize(maxLength: 100)
  }

  type CreateLabelSuccess {
    label: Label!
  }

  enum CreateLabelErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    LABEL_ALREADY_EXISTS
  }

  type CreateLabelError {
    errorCodes: [CreateLabelErrorCode!]!
  }

  union CreateLabelResult = CreateLabelSuccess | CreateLabelError

  type DeleteLabelSuccess {
    label: Label!
  }

  enum DeleteLabelErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    FORBIDDEN
  }

  type DeleteLabelError {
    errorCodes: [DeleteLabelErrorCode!]!
  }

  union DeleteLabelResult = DeleteLabelSuccess | DeleteLabelError

  input UpdateLabelInput {
    labelId: ID!
    color: String!
    description: String
    name: String!
  }

  type UpdateLabelSuccess {
    label: Label!
  }

  enum UpdateLabelErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    FORBIDDEN
  }

  type UpdateLabelError {
    errorCodes: [UpdateLabelErrorCode!]!
  }

  union UpdateLabelResult = UpdateLabelSuccess | UpdateLabelError

  input SetLabelsInput {
    pageId: ID!
    labelIds: [ID!]!
  }

  union SetLabelsResult = SetLabelsSuccess | SetLabelsError

  type SetLabelsSuccess {
    labels: [Label!]!
  }

  type SetLabelsError {
    errorCodes: [SetLabelsErrorCode!]!
  }

  enum SetLabelsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input GenerateApiKeyInput {
    name: String!
    scopes: [String!]
    expiresAt: Date!
  }

  union GenerateApiKeyResult = GenerateApiKeySuccess | GenerateApiKeyError

  type GenerateApiKeySuccess {
    apiKey: ApiKey!
  }

  type GenerateApiKeyError {
    errorCodes: [GenerateApiKeyErrorCode!]!
  }

  enum GenerateApiKeyErrorCode {
    BAD_REQUEST
    ALREADY_EXISTS
    UNAUTHORIZED
  }

  # Query: search
  union SearchResult = SearchSuccess | SearchError

  type SearchItem {
    # used for pages
    id: ID!
    title: String!
    slug: String!
    # for uploaded file articles (PDFs), the URL here is the saved omnivore link in GCS
    url: String!
    pageType: PageType!
    contentReader: ContentReader!
    createdAt: Date!
    updatedAt: Date
    isArchived: Boolean!
    readingProgressTopPercent: Float
    readingProgressPercent: Float!
    readingProgressAnchorIndex: Int!
    author: String
    image: String
    description: String
    publishedAt: Date
    ownedByViewer: Boolean
    # for uploaded file articles (PDFs), we track the original article URL separately!
    originalArticleUrl: String
    uploadFileId: ID
    # used for highlights
    pageId: ID
    shortId: String
    quote: String
    annotation: String
    labels: [Label!]
    subscription: String
    unsubMailTo: String
    unsubHttpUrl: String
    state: ArticleSavingRequestStatus
    siteName: String
    language: String
    readAt: Date
    savedAt: Date!
    highlights: [Highlight!]
    siteIcon: String
    recommendations: [Recommendation!]
    wordsCount: Int
    content: String
    archivedAt: Date
  }

  type SearchItemEdge {
    cursor: String!
    node: SearchItem!
  }

  type SearchSuccess {
    edges: [SearchItemEdge!]!
    pageInfo: PageInfo!
  }

  enum SearchErrorCode {
    UNAUTHORIZED
    QUERY_TOO_LONG
  }

  type SearchError {
    errorCodes: [SearchErrorCode!]!
  }

  union SubscriptionsResult = SubscriptionsSuccess | SubscriptionsError

  type SubscriptionsSuccess {
    subscriptions: [Subscription!]!
  }

  type Subscription {
    id: ID!
    name: String!
    newsletterEmail: String!
    url: String
    description: String
    status: SubscriptionStatus!
    unsubscribeMailTo: String
    unsubscribeHttpUrl: String
    icon: String
    createdAt: Date!
    updatedAt: Date!
  }

  enum SubscriptionStatus {
    ACTIVE
    UNSUBSCRIBED
    DELETED
  }

  type SubscriptionsError {
    errorCodes: [SubscriptionsErrorCode!]!
  }

  enum SubscriptionsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union UnsubscribeResult = UnsubscribeSuccess | UnsubscribeError

  type UnsubscribeSuccess {
    subscription: Subscription!
  }

  type UnsubscribeError {
    errorCodes: [UnsubscribeErrorCode!]!
  }

  enum UnsubscribeErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    ALREADY_UNSUBSCRIBED
    UNSUBSCRIBE_METHOD_NOT_FOUND
  }

  union SubscribeResult = SubscribeSuccess | SubscribeError

  type SubscribeSuccess {
    subscriptions: [Subscription!]!
  }

  type SubscribeError {
    errorCodes: [SubscribeErrorCode!]!
  }

  enum SubscribeErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    ALREADY_SUBSCRIBED
  }

  union AddPopularReadResult = AddPopularReadSuccess | AddPopularReadError

  type AddPopularReadSuccess {
    pageId: String!
  }

  type AddPopularReadError {
    errorCodes: [AddPopularReadErrorCode!]!
  }

  enum AddPopularReadErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input SetWebhookInput {
    id: ID
    url: String!
    eventTypes: [WebhookEvent!]!
    contentType: String
    method: String
    enabled: Boolean
  }

  enum WebhookEvent {
    PAGE_CREATED
    PAGE_UPDATED
    PAGE_DELETED
    HIGHLIGHT_CREATED
    HIGHLIGHT_UPDATED
    HIGHLIGHT_DELETED
    LABEL_CREATED
    LABEL_UPDATED
    LABEL_DELETED
  }

  union SetWebhookResult = SetWebhookSuccess | SetWebhookError

  type SetWebhookSuccess {
    webhook: Webhook!
  }

  type Webhook {
    id: ID!
    url: String!
    eventTypes: [WebhookEvent!]!
    contentType: String!
    method: String!
    enabled: Boolean!
    createdAt: Date!
    updatedAt: Date!
  }

  type SetWebhookError {
    errorCodes: [SetWebhookErrorCode!]!
  }

  enum SetWebhookErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    ALREADY_EXISTS
    NOT_FOUND
  }

  union DeleteWebhookResult = DeleteWebhookSuccess | DeleteWebhookError

  type DeleteWebhookSuccess {
    webhook: Webhook!
  }

  type DeleteWebhookError {
    errorCodes: [DeleteWebhookErrorCode!]!
  }

  enum DeleteWebhookErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union WebhookResult = WebhookSuccess | WebhookError

  type WebhookSuccess {
    webhook: Webhook!
  }

  type WebhookError {
    errorCodes: [WebhookErrorCode!]!
  }

  enum WebhookErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union WebhooksResult = WebhooksSuccess | WebhooksError

  type WebhooksSuccess {
    webhooks: [Webhook!]!
  }

  type WebhooksError {
    errorCodes: [WebhooksErrorCode!]!
  }

  enum WebhooksErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union ApiKeysResult = ApiKeysSuccess | ApiKeysError

  type ApiKeysSuccess {
    apiKeys: [ApiKey!]!
  }

  type ApiKey {
    id: ID!
    name: String!
    key: String
    scopes: [String!]
    createdAt: Date!
    expiresAt: Date!
    usedAt: Date
  }

  type ApiKeysError {
    errorCodes: [ApiKeysErrorCode!]!
  }

  enum ApiKeysErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union RevokeApiKeyResult = RevokeApiKeySuccess | RevokeApiKeyError

  type RevokeApiKeySuccess {
    apiKey: ApiKey!
  }

  type RevokeApiKeyError {
    errorCodes: [RevokeApiKeyErrorCode!]!
  }

  enum RevokeApiKeyErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input SetLabelsForHighlightInput {
    highlightId: ID!
    labelIds: [ID!]!
  }

  union TypeaheadSearchResult = TypeaheadSearchSuccess | TypeaheadSearchError

  type TypeaheadSearchSuccess {
    items: [TypeaheadSearchItem!]!
  }

  type TypeaheadSearchError {
    errorCodes: [TypeaheadSearchErrorCode!]!
  }

  enum TypeaheadSearchErrorCode {
    UNAUTHORIZED
  }

  type TypeaheadSearchItem {
    id: ID!
    title: String!
    slug: String!
    siteName: String
    contentReader: ContentReader!
  }

  union UpdatesSinceResult = UpdatesSinceSuccess | UpdatesSinceError

  type UpdatesSinceSuccess {
    edges: [SyncUpdatedItemEdge!]!
    pageInfo: PageInfo!
  }

  type SyncUpdatedItemEdge {
    cursor: String!
    updateReason: UpdateReason!
    itemID: ID!
    node: SearchItem # for created or updated items, null for deletions */
  }

  enum UpdateReason {
    CREATED
    UPDATED
    DELETED
  }

  type UpdatesSinceError {
    errorCodes: [UpdatesSinceErrorCode!]!
  }

  enum UpdatesSinceErrorCode {
    UNAUTHORIZED
  }

  input MoveLabelInput {
    labelId: ID!
    afterLabelId: ID # null to move to the top
  }

  union MoveLabelResult = MoveLabelSuccess | MoveLabelError

  type MoveLabelSuccess {
    label: Label!
  }

  type MoveLabelError {
    errorCodes: [MoveLabelErrorCode!]!
  }

  enum MoveLabelErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union SetIntegrationResult = SetIntegrationSuccess | SetIntegrationError

  type SetIntegrationSuccess {
    integration: Integration!
  }

  type Integration {
    id: ID!
    name: String!
    type: IntegrationType!
    token: String!
    enabled: Boolean!
    createdAt: Date!
    updatedAt: Date!
  }

  enum IntegrationType {
    EXPORT
    IMPORT
  }

  type SetIntegrationError {
    errorCodes: [SetIntegrationErrorCode!]!
  }

  enum SetIntegrationErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    INVALID_TOKEN
    ALREADY_EXISTS
  }

  input SetIntegrationInput {
    id: ID
    name: String!
    type: IntegrationType
    token: String!
    enabled: Boolean!
  }

  union IntegrationsResult = IntegrationsSuccess | IntegrationsError

  type IntegrationsSuccess {
    integrations: [Integration!]!
  }

  type IntegrationsError {
    errorCodes: [IntegrationsErrorCode!]!
  }

  enum IntegrationsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union DeleteIntegrationResult =
      DeleteIntegrationSuccess
    | DeleteIntegrationError

  type DeleteIntegrationSuccess {
    integration: Integration!
  }

  type DeleteIntegrationError {
    errorCodes: [DeleteIntegrationErrorCode!]!
  }

  enum DeleteIntegrationErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union RecentSearchesResult = RecentSearchesSuccess | RecentSearchesError

  type RecentSearchesSuccess {
    searches: [RecentSearch!]!
  }

  type RecentSearch {
    id: ID!
    term: String!
    createdAt: Date!
  }

  type RecentSearchesError {
    errorCodes: [RecentSearchesErrorCode!]!
  }

  enum RecentSearchesErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  input OptInFeatureInput {
    name: String!
  }

  union OptInFeatureResult = OptInFeatureSuccess | OptInFeatureError

  type OptInFeatureSuccess {
    feature: Feature!
  }

  type Feature {
    id: ID!
    name: String!
    token: String!
    createdAt: Date!
    updatedAt: Date!
    grantedAt: Date
    expiresAt: Date
  }

  type OptInFeatureError {
    errorCodes: [OptInFeatureErrorCode!]!
  }

  enum OptInFeatureErrorCode {
    BAD_REQUEST
    NOT_FOUND
  }

  union RulesResult = RulesSuccess | RulesError

  type RulesSuccess {
    rules: [Rule!]!
  }

  type Rule {
    id: ID!
    name: String!
    filter: String!
    actions: [RuleAction!]!
    enabled: Boolean!
    createdAt: Date!
    updatedAt: Date!
  }

  type RuleAction {
    type: RuleActionType!
    params: [String!]!
  }

  enum RuleActionType {
    ADD_LABEL
    ARCHIVE
    MARK_AS_READ
    SEND_NOTIFICATION
  }

  type RulesError {
    errorCodes: [RulesErrorCode!]!
  }

  enum RulesErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  input SetRuleInput {
    id: ID
    name: String!
    description: String
    filter: String!
    actions: [RuleActionInput!]!
    enabled: Boolean!
  }

  input RuleActionInput {
    type: RuleActionType!
    params: [String!]!
  }

  union SetRuleResult = SetRuleSuccess | SetRuleError

  type SetRuleSuccess {
    rule: Rule!
  }

  type SetRuleError {
    errorCodes: [SetRuleErrorCode!]!
  }

  enum SetRuleErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union DeleteRuleResult = DeleteRuleSuccess | DeleteRuleError

  type DeleteRuleSuccess {
    rule: Rule!
  }

  type DeleteRuleError {
    errorCodes: [DeleteRuleErrorCode!]!
  }

  enum DeleteRuleErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union DeviceTokensResult = DeviceTokensSuccess | DeviceTokensError

  type DeviceTokensSuccess {
    deviceTokens: [DeviceToken!]!
  }

  type DeviceTokensError {
    errorCodes: [DeviceTokensErrorCode!]!
  }

  enum DeviceTokensErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  input SaveFilterInput {
    id: ID
    name: String!
    filter: String!
    description: String
  }

  union SaveFilterResult = SaveFilterSuccess | SaveFilterError

  type SaveFilterSuccess {
    filter: Filter!
  }

  type Filter {
    id: ID!
    name: String!
    filter: String!
    position: Int!
    description: String
    createdAt: Date!
    updatedAt: Date!
  }

  type SaveFilterError {
    errorCodes: [SaveFilterErrorCode!]!
  }

  enum SaveFilterErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union FiltersResult = FiltersSuccess | FiltersError

  type FiltersSuccess {
    filters: [Filter!]!
  }

  type FiltersError {
    errorCodes: [FiltersErrorCode!]!
  }

  enum FiltersErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union DeleteFilterResult = DeleteFilterSuccess | DeleteFilterError

  type DeleteFilterSuccess {
    filter: Filter!
  }

  type DeleteFilterError {
    errorCodes: [DeleteFilterErrorCode!]!
  }

  enum DeleteFilterErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input MoveFilterInput {
    filterId: ID!
    afterFilterId: ID # null to move to the top
  }

  union MoveFilterResult = MoveFilterSuccess | MoveFilterError

  type MoveFilterSuccess {
    filter: Filter!
  }

  type MoveFilterError {
    errorCodes: [MoveFilterErrorCode!]!
  }

  enum MoveFilterErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input CreateGroupInput {
    name: String! @sanitize(maxLength: 140)
    maxMembers: Int
    expiresInDays: Int
    description: String
    topics: [String!]
    onlyAdminCanPost: Boolean
    onlyAdminCanSeeMembers: Boolean
  }

  union CreateGroupResult = CreateGroupSuccess | CreateGroupError

  type CreateGroupSuccess {
    group: RecommendationGroup!
  }

  type RecommendationGroup {
    id: ID!
    name: String!
    inviteUrl: String!
    admins: [User!]!
    members: [User!]!
    createdAt: Date!
    updatedAt: Date!
    canPost: Boolean!
    description: String
    topics: [String!]
    canSeeMembers: Boolean!
  }

  type CreateGroupError {
    errorCodes: [CreateGroupErrorCode!]!
  }

  enum CreateGroupErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union GroupsResult = GroupsSuccess | GroupsError

  type GroupsSuccess {
    groups: [RecommendationGroup!]!
  }

  type GroupsError {
    errorCodes: [GroupsErrorCode!]!
  }

  enum GroupsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  input RecommendInput {
    pageId: ID!
    groupIds: [ID!]!
    note: String
    recommendedWithHighlights: Boolean
  }

  union RecommendResult = RecommendSuccess | RecommendError

  type RecommendSuccess {
    success: Boolean!
  }

  type RecommendError {
    errorCodes: [RecommendErrorCode!]!
  }

  enum RecommendErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union JoinGroupResult = JoinGroupSuccess | JoinGroupError

  type JoinGroupSuccess {
    group: RecommendationGroup!
  }

  type JoinGroupError {
    errorCodes: [JoinGroupErrorCode!]!
  }

  enum JoinGroupErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  input RecommendHighlightsInput {
    pageId: ID!
    highlightIds: [ID!]!
    groupIds: [ID!]!
    note: String
  }

  union RecommendHighlightsResult =
      RecommendHighlightsSuccess
    | RecommendHighlightsError

  type RecommendHighlightsSuccess {
    success: Boolean!
  }

  type RecommendHighlightsError {
    errorCodes: [RecommendHighlightsErrorCode!]!
  }

  enum RecommendHighlightsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  union LeaveGroupResult = LeaveGroupSuccess | LeaveGroupError

  type LeaveGroupSuccess {
    success: Boolean!
  }

  type LeaveGroupError {
    errorCodes: [LeaveGroupErrorCode!]!
  }

  enum LeaveGroupErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  enum UploadImportFileType {
    URL_LIST
    POCKET
    MATTER
  }

  enum UploadImportFileErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    UPLOAD_DAILY_LIMIT_EXCEEDED
  }

  union UploadImportFileResult = UploadImportFileSuccess | UploadImportFileError

  type UploadImportFileError {
    errorCodes: [UploadImportFileErrorCode!]!
  }

  type UploadImportFileSuccess {
    uploadSignedUrl: String
  }

  union RecentEmailsResult = RecentEmailsSuccess | RecentEmailsError

  type RecentEmailsSuccess {
    recentEmails: [RecentEmail!]!
  }

  type RecentEmailsError {
    errorCodes: [RecentEmailsErrorCode!]!
  }

  enum RecentEmailsErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  type RecentEmail {
    id: ID!
    from: String!
    to: String!
    subject: String!
    type: String!
    text: String!
    html: String
    createdAt: Date!
  }

  union MarkEmailAsItemResult = MarkEmailAsItemSuccess | MarkEmailAsItemError

  type MarkEmailAsItemSuccess {
    success: Boolean!
  }

  type MarkEmailAsItemError {
    errorCodes: [MarkEmailAsItemErrorCode!]!
  }

  enum MarkEmailAsItemErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
  }

  enum BulkActionType {
    DELETE
    ARCHIVE
    MARK_AS_READ
    ADD_LABELS
  }

  union BulkActionResult = BulkActionSuccess | BulkActionError

  type BulkActionSuccess {
    success: Boolean!
  }

  type BulkActionError {
    errorCodes: [BulkActionErrorCode!]!
  }

  enum BulkActionErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union ImportFromIntegrationResult =
      ImportFromIntegrationSuccess
    | ImportFromIntegrationError

  type ImportFromIntegrationSuccess {
    success: Boolean!
  }

  type ImportFromIntegrationError {
    errorCodes: [ImportFromIntegrationErrorCode!]!
  }

  enum ImportFromIntegrationErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
  }

  union SetFavoriteArticleResult =
      SetFavoriteArticleSuccess
    | SetFavoriteArticleError

  type SetFavoriteArticleSuccess {
    favoriteArticle: Article!
  }

  type SetFavoriteArticleError {
    errorCodes: [SetFavoriteArticleErrorCode!]!
  }

  enum SetFavoriteArticleErrorCode {
    UNAUTHORIZED
    BAD_REQUEST
    NOT_FOUND
    ALREADY_EXISTS
  }

  # Mutations
  type Mutation {
    googleLogin(input: GoogleLoginInput!): LoginResult!
    googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
    logOut: LogOutResult!
    deleteAccount(userID: ID!): DeleteAccountResult!
    updateUser(input: UpdateUserInput!): UpdateUserResult!
    updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult!
    createArticle(input: CreateArticleInput!): CreateArticleResult!
    createHighlight(input: CreateHighlightInput!): CreateHighlightResult!
    mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
    updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
    deleteHighlight(highlightId: ID!): DeleteHighlightResult!
    createHighlightReply(
      input: CreateHighlightReplyInput!
    ): CreateHighlightReplyResult!
    updateHighlightReply(
      input: UpdateHighlightReplyInput!
    ): UpdateHighlightReplyResult!
    deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
    createReaction(input: CreateReactionInput!): CreateReactionResult!
    deleteReaction(id: ID!): DeleteReactionResult!
    uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult!
    saveArticleReadingProgress(
      input: SaveArticleReadingProgressInput!
    ): SaveArticleReadingProgressResult!
    setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
    updateSharedComment(
      input: UpdateSharedCommentInput!
    ): UpdateSharedCommentResult!
    setFollow(input: SetFollowInput!): SetFollowResult!
    setBookmarkArticle(
      input: SetBookmarkArticleInput!
    ): SetBookmarkArticleResult!
    setUserPersonalization(
      input: SetUserPersonalizationInput!
    ): SetUserPersonalizationResult!
    createArticleSavingRequest(
      input: CreateArticleSavingRequestInput!
    ): CreateArticleSavingRequestResult!
    setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
    reportItem(input: ReportItemInput!): ReportItemResult!
    updateLinkShareInfo(
      input: UpdateLinkShareInfoInput!
    ): UpdateLinkShareInfoResult!
    setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult!
    createNewsletterEmail: CreateNewsletterEmailResult!
    deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
    saveUrl(input: SaveUrlInput!): SaveResult!
    savePage(input: SavePageInput!): SaveResult!
    updatePage(input: UpdatePageInput!): UpdatePageResult!
    saveFile(input: SaveFileInput!): SaveResult!
    createReminder(input: CreateReminderInput!): CreateReminderResult!
    updateReminder(input: UpdateReminderInput!): UpdateReminderResult!
    deleteReminder(id: ID!): DeleteReminderResult!
    setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult!
    createLabel(input: CreateLabelInput!): CreateLabelResult!
    updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
    deleteLabel(id: ID!): DeleteLabelResult!
    setLabels(input: SetLabelsInput!): SetLabelsResult!
    generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult!
    unsubscribe(name: String!): UnsubscribeResult!
    subscribe(name: String!): SubscribeResult!
    addPopularRead(name: String!): AddPopularReadResult!
    setWebhook(input: SetWebhookInput!): SetWebhookResult!
    deleteWebhook(id: ID!): DeleteWebhookResult!
    revokeApiKey(id: ID!): RevokeApiKeyResult!
    setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult!
    moveLabel(input: MoveLabelInput!): MoveLabelResult!
    setIntegration(input: SetIntegrationInput!): SetIntegrationResult!
    deleteIntegration(id: ID!): DeleteIntegrationResult!
    optInFeature(input: OptInFeatureInput!): OptInFeatureResult!
    setRule(input: SetRuleInput!): SetRuleResult!
    deleteRule(id: ID!): DeleteRuleResult!
    saveFilter(input: SaveFilterInput!): SaveFilterResult!
    deleteFilter(id: ID!): DeleteFilterResult!
    moveFilter(input: MoveFilterInput!): MoveFilterResult!
    createGroup(input: CreateGroupInput!): CreateGroupResult!
    recommend(input: RecommendInput!): RecommendResult!
    joinGroup(inviteCode: String!): JoinGroupResult!
    recommendHighlights(
      input: RecommendHighlightsInput!
    ): RecommendHighlightsResult!
    leaveGroup(groupId: ID!): LeaveGroupResult!
    uploadImportFile(
      type: UploadImportFileType!
      contentType: String!
    ): UploadImportFileResult!
    markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult!
    bulkAction(
      query: String!
      action: BulkActionType!
      labelIds: [ID!]
      expectedCount: Int # max number of items to process
      async: Boolean # if true, return immediately and process in the background
    ): BulkActionResult!
    importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
    setFavoriteArticle(id: ID!): SetFavoriteArticleResult!
  }

  # FIXME: remove sort from feedArticles after all cached tabs are closed
  # FIXME: sharedOnly is legacy
  type Query {
    hello: String
    me: User
    user(userId: ID, username: String): UserResult!
    articles(
      sharedOnly: Boolean
      sort: SortParams
      after: String
      first: Int
      query: String
      includePending: Boolean
    ): ArticlesResult!
    article(username: String!, slug: String!, format: String): ArticleResult!
    sharedArticle(
      username: String!
      slug: String!
      selectedHighlightId: String
    ): SharedArticleResult!
    feedArticles(
      after: String
      first: Int
      sort: SortParams
      sharedByUser: ID
    ): FeedArticlesResult!
    users: UsersResult!
    validateUsername(username: String!): Boolean!
    getFollowers(userId: ID): GetFollowersResult!
    getFollowing(userId: ID): GetFollowingResult!
    getUserPersonalization: GetUserPersonalizationResult!
    articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult!
    newsletterEmails: NewsletterEmailsResult!
    reminder(linkId: ID!): ReminderResult!
    labels: LabelsResult!
    search(
      after: String
      first: Int
      query: String
      includeContent: Boolean
      format: String
    ): SearchResult!
    subscriptions(sort: SortParams): SubscriptionsResult!
    sendInstallInstructions: SendInstallInstructionsResult!
    webhooks: WebhooksResult!
    webhook(id: ID!): WebhookResult!
    apiKeys: ApiKeysResult!
    typeaheadSearch(query: String!, first: Int): TypeaheadSearchResult!
    updatesSince(
      after: String
      first: Int
      since: Date!
      sort: SortParams
    ): UpdatesSinceResult!
    integrations: IntegrationsResult!
    recentSearches: RecentSearchesResult!
    rules(enabled: Boolean): RulesResult!
    deviceTokens: DeviceTokensResult!
    filters: FiltersResult!
    groups: GroupsResult!
    recentEmails: RecentEmailsResult!
  }