Skip to content

Calendar Platform (Idea)

The following are some example usage queries to how I think a GraphQL Backend-API should work for a Calendar App. Properties and names are not fixed, and feedback on any of this is welcome!

graphql
{
  profile {
    id
    name
    emailAddress
    avatar
  }

  calendar {
    id
    type
    title
    color
  }

  events(where: { startAt: "2020-04-01", endAt: "2020-04-30" }, order: STARTING_ASC) {
    id
    title
    location {
      name
    }
    startAt
    endAt
    color
  }

  event(where: { id: "SOME-LONG-EVENT-ID" }) {
    id
    title
    location {
      name
      address
      latlon
    }
    startAt
    endAt
    color # Event could have a color? Fall back to calendar color by default?
    url
    notes
    repeats

    calendar {
      id
      title
      color
    }

    attendees {
      name
      emailAddress
      avatar

      member {
        id
        name
        emailAddress
        avatar
      }

      isOrganiser
      rsvp # ATTENDING, TENTATIVE, DECLINED
    }

    thread {
      # One
      id
    }
  }

  threads {
    id

    event {
      id
    }

    messages(limit: 20, order: CREATED_DESC) {
      id
    }
  }

  message(where: { id: "SOME-MESSAGE-ID" }) {
    id
    author {
      emailAddress
      avatar
    }
    text
    createdAt
  }
}
  • Events listed by week with latest message
graphql
{
  events(where: { startAt: "2020-04-06", endAt: "2020-04-12" }, order: STARTING_ASC) {
    id
    title
    startAt
    endAt
    isAllDay
    isManyDays
    location {
      name
    }
    color

    thread {
      id
      messages(limit: 1, order: CREATED_DESC) {
        id
        text
        author {
          avatar
        }
      }
    }
  }
}
  • Events listed by month
graphql
{
  events(where: { startAt: "2020-04-01", endAt: "2020-04-30" }, order: STARTING_ASC) {
    id
    title
    startAt
    endAt
    location {
      name
    }
    color
  }
}
  • Message thread with event
graphql
{
  thread(where: { id: "SOME-THREAD-ID" }) {
    id
    startAt
    total

    event {
      id
      title
      startAt
      endAt
      location {
        name
      }
      color
    }

    messages(page: 1, limit: 20, order: CREATED_DESC) {
      id
      author {
        # One
        emailAddress
        avatar
      }
      text
      createdAt
    }
  }
}
  • Event details, ready to edit the event
graphql
query EditEvent {
  event(where: { id: "SOME-EVENT-ID" }) {
    id
    title
    startAt
    endAt

    location {
      # One
      name
      address
      latlon
    }

    color # Inherit from calendar, but could an event have it's own color?
    repeats # NEVER (null), DAILY, WEEKLY, FORTNIGHTLY, MONTHLY, YEARLY
    url # Clever support for URLs, MAILTO or TEL prefixes in-app?
    notes

    calendar {
      # One
      id # Compare with calendars[*].id to choose a calendar
    }

    attendees {
      # Many
      name
      emailAddress
      avatar # Pulled from Gravatar?
      member {
        id
        name
        emailAddress
      }

      isOrganiser # Boolean to determine if this attendee created the event?
      rsvp # ATTENDING, TENTATIVE, DECLINED
    }
  }

  profile {
    id # Compare with events.attendees.id to find "YOU"
  }

  calendars {
    # Many
    id
    title
    color
  }
}
  • Activity view (list of threads ordered by last updated)
graphql
query Activity {
  threads(order: UPDATED_DESC) {
    id
    startAt
    total

    event {
      id
      title
      location {
        name
      }
      color
    }

    messages(page: 1, limit: 20, order: CREATED_DESC) {
      id
      author {
        # One
        emailAddress
        avatar
      }
      text
      createdAt
    }
  }
}
  • Searching for "Coffee"
graphql
query Search {
  events(where: { title: "%coffee%" }) {
    id
    title
    startAt
    endAt
    location {
      name
    }
    color
  }
}
  • Creating an event
graphql
mutation createEvent {
  createEvent(
    create: {
      title: "Tuesday stand-up"
      # 8AM UTC, 9AM BST
      startDate: "2020-04-07T08:00:00.000Z"
      # 9AM UTC, 10AM BST
      endDate: "2020-04-07T09:00:00.000Z"
      # Perhaps right now, single-calendar setup, we don't need this?
      # I think we should, even if we're not displaying multiple calendars in the app
      calendar: { id: "496baec6-cc5f-4c54-944d-18c2bfe18598" }
      # Manual location
      location: { name: "Office", address: "WeWork, Aldgate Tower, London E1 8FA", latlon: "51.513582,-0.0762422" }
      # Just throw a latlon at the API, and it could lookup a location from it?
      # Unsure which 3rd party service we'd need to support this feature though..
      location: { latlon: "51.513582,-0.0762422" }
      # Or perhaps an online meet?
      conference: {
        # TBD, a unified way of creating/generating conference-call data!
        type: "hangouts"
        # Ideally the app would make a request to fetch "new" conference data & send it up to the API
      }
      attendees: [
        { id: "0220743c-c9b2-40a6-a478-e168cfc005c4", isOrganiser: true }
        { name: "Alex", emailAddress: "alex@hotmail.com" }
        { name: "James", emailAddress: "james@gmail.com" }
      ]
    }
    options: {
      # In this case the app wants to control whether or not to send emails
      # Obviously this would default to true!
      sendCreateEmail: false
    }
  ) {
    id
    title
    startDate
    endDate
    location {
      name
    }
  }
}
  • Updating events
graphql
mutation updateEvent {
  updateEvent(
    update: {
      # Ideally the App would know what properties of the form have been updated
      # And just send the diff, but if you send an entire event object we can work with that too
      location: { name: "Office", address: "WeWork, Aldgate Tower, London E1 8FA" }
    }
    where: {
      # The where & updates are seperate for full flexibility
      id: "SOME-EVENT-ID"
    }
  ) {
    id
  }
}

mutation updateLotsOfEvents {
  updateManyEvents(
    update: {
      # This would "unset" the location property of events
      location: null
    }
    where: {
      # IMO this would be a neat "trick" to select an entire day
      startDate: ">= 2020-04-02"
      # If no TIME is present on the startDate then assume start-of-day
      endDate: "<= 2020-04-02"
      # If no TIME is present on the endDate then assume end-of-day
    }
  ) {
    # Array of Events affected returned, so this would be [{ id: 22 }]
    id
    # Returning IDs of those events affected
  }
}

mutation rsvpToEvent {
  replyToEvent(
    update: {
      # Simply replying YES
      rsvp: ATTENDING
      # What if we want to suggest a new start time?
      rsvp: TENTATIVE
      suggestStartDate: "2020-04-02T09:00:00.000Z"
      suggestEndDate: "2020-04-02T10:00:00.000Z"
      # Really though, this should mark your attendance but not edit the event
      # Instead, it should put a message in the chat to suggest a new time
      # With some sort of one-click approve? TBD
    }
    where: {
      # The where & updates are seperate for full flexibility
      id: "SOME-EVENT-ID"
    }
  ) {
    id
  }
}
  • Write a new message
graphql
mutation createTextMessage {
  createMessage(
    create: {
      # Attach a message to a thread
      thread: { id: "SOME-THREAD-ID" }
      # Or why not to a specific event
      thread: { event: { id: "SOME-EVENT-ID" } }
      # Because an event has one thread right?

      # A text message just needs text
      text: "Quick coffee to discuss my YC application?"
    }
  ) {
    id
    thread {
      # To get event information about the thread we just added to
      event {
        id
        title
      }
    }
    text
  }
}

query GetThreadMessages {
  thread(where: { id: "SOME-THREAD-ID" }) {
    messages(page: 1, limit: 20, order: CREATED_DESC) {
      id
      author {
        name
        emailAddress
        avatar
      }
      text
      createdAt
    }
  }
}

In the future we'll have more types of chat, so I see this format above being flexible enough to support other types:

graphql
# GraphQL cannot handle incoming file types, so instead we need two mutations:
# First to get a set of upload credentials, so the app can upload direct to S3
# Second to mark an upload as complete, and get back an assetID to use in other mutations

mutation getImageUploadCredentials {
  createAsset(
    create: {
      type: IMAGE
      # The name of the image
      fileName: "IMG_0932.JPG"
      # The mimetype of the image
      fileMime: "image/jpeg"
      # The file size of the image, in bytes
      fileSize: 20420492
      # Sha1 hash so we can verify the image was uploaded correctly
      fileSha1: "mkldsfv8jq32rfjkarnvkq34nf9u"
    }
  ) {
    token # Used to confirm the upload was finished
    url
    headers
    body
  }
}

# Then the client uploads the image to upload.url with upload.headers & upload.body
# Once done, the client notifies the server that the image has been uploaded, so the server can perform final checks
# Use `fileSha1` to verify the image is correct, trigger moderation, etc. etc.

mutation finallyFinishedUploadingImage($token: String!) {
  uploadedAsset(token: $token) {
    id # The ID of the image
    fileName
    fileLocation # The final URL this image will be available at
    fileMime
    fileSize
    fileSha1
  }
}

mutation createImageMessage {
  createMessage(
    create: {
      thread: { id: "SOME-THREAD-ID" }
      text: "Sat downstairs at table 15, see you in bit!"
      image: { id: $imageID }
    }
  ) {
    id
    author {
      emailAddress
      avatar
    }
    text
    image {
      fileName
      fileLocation
      fileSize # "${fileName} is greater than 2MB, tap to show"
    }
    createdAt
  }
}

mutation createEmbedMessage {
  createMessage(
    create: {
      thread: { id: "SOME-THREAD-ID" }
      text: "Lol, what about this"
      # Provide a URL to expand
      url: "https://vm.tiktok.com/nCGxS3/"
    }
  ) {
    id
    author {
      emailAddress
      avatar
    }
    text
    # For an example, check out https://github.com/someimportantcompany/yoem
    embed {
      title # Due to the wide-ranging nature of embeds
      description # Literally any one of these fields could be NULL
      url # Except `url`, that guy is super reliable 🤦‍♂️
      provider {
        name
        url
      }
      author {
        name
        url
      }
      thumbnail {
        url
        height
        width
      }
      iframe {
        html
        height
        width
      }
    }
    createdAt
  }
}

mutation getDocumentUploadCredentials {
  createAsset(
    create: {
      type: DOCUMENT
      # The name of the document
      fileName: "Paperwork.docx"
      # The mimetype of the document
      fileMime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
      # The file size of the document, in bytes
      fileSize: 20420492
      # Sha1 hash so we can verify the document was uploaded correctly
      fileSha1: "dfvsdfvwrfsfvqrfafsv4wdec"
    }
  ) {
    token # Used to confirm the upload was finished
    url
    headers
    body
  }
}

# Then the client uploads the document to upload.url with upload.headers & upload.body
# Once done, the client notifies the server that the document has been uploaded, so the server can perform final checks
# Use `fileSha1` to verify the document is correct, trigger moderation, etc. etc.

mutation finallyFinishedUploadingImage($token: String!) {
  uploadedAsset(token: $token) {
    id # The ID of the document
    fileName
    fileLocation # The final URL this document will be available at
    fileMime
    fileSize
    fileSha1
  }
}

mutation createDocumentMessage($documentID: ID!) {
  createMessage(
    create: {
      thread: { id: "SOME-THREAD-ID" }
      text: "Could you please read over this before we catch-up? Thanks"
      document: { id: $documentID }
    }
  ) {
    id
    author {
      emailAddress
      avatar
    }
    text
    document {
      fileName
      fileLocation
      fileSize # "${fileName} is greater than 2MB, tap to show"
    }
    createdAt
  }
}

mutation createLocationMessage {
  createMessage(
    create: {
      thread: { id: "SOME-THREAD-ID" }
      text: "How about we meet here?"
      location: { name: "Pret, Aldgate East", latlon: "51.513582,-0.0762422" }
    }
  ) {
    id
    author {
      emailAddress
      avatar
    }
    text
    location {
      latlon
    }
    createdAt
  }
}

query EverythingIsNamespacedSoItAllFitsTogether {
  thread(where: { id: "SOME-THREAD-ID" }) {
    messages(page: 1, limit: 20, order: CREATED_DESC) {
      id
      author {
        emailAddress
        avatar
      }
      text
      image {
        fileName
        fileLocation
        fileSize # "${fileName} is greater than 2MB, tap to show"
      }
      embed {
        title
        description
        url
      }
      document {
        fileName
        fileLocation
        fileSize # "${fileName} is greater than 2MB, tap to show"
      }
      location {
        name
        latlon
      }
      createdAt
    }
  }
}

Potential Subscriptions

graphql
{
  createdEvent {
    # EVENT_CREATED
    id
  }
  updatedEvent {
    # EVENT_UPDATED
    id
  }
  updatedThread {
    # THREAD_UPDATED
    id
  }
  updatedMessage {
    # MESSAGE_UPDATED
    id
  }
}

Questions

  • Event locations need to consider online meetups, Google Meet or Microsoft Teams
    • Support more types? Slack? Zoom?
  • Event alerts? Email alerts?
  • Event timezone support?
  • Only time when on-device account management fails is when you want to switch an event between calendars... that would technically involve deleting from one provider & creating in another?
  • Messages have color?
  • When a message is sent, the thread should be updated. But not the event?
  • When the event is updated, a new context/status message should be generated so the thread should get an update triggered too?