Integration Documentation

API Client

packages/api-client/src/endpoints/createChallengeSubscription.ts

import { request } from './util'
import { Challenge, ChallengeSubscription } from '../models'

export interface CreateChallengeSubscriptionBody {
  challengeSubscription: {
    challengeId: Challenge['id']
    startTomorrow: boolean
  }
}

export type CreateChallengeSubscriptionResponse =
  | { success: false; message: string }
  | { success: true; challengeSubscription: ChallengeSubscription }

/**
 * Starts a user on a new challenge, creating a challenge subscription.
 *
 * @param userId The identifier assigned by Diet ID for the user.
 * @param challengeId The identifier assigned by Diet ID for the challenge.
 * @param startTomorrow (optional) Whether the user should start the challenge tomorrow instead of today (default: false).
 *
 * @returns The challenge subscription (join record between user and challenge).
 */
export const createChallengeSubscription = async (
  challengeId: CreateChallengeSubscriptionBody['challengeSubscription']['challengeId'],
  startTomorrow: CreateChallengeSubscriptionBody['challengeSubscription']['startTomorrow'] = false
) =>
  request<CreateChallengeSubscriptionResponse>('POST', '/api/v1/challenge_subscriptions', {
    challengeSubscription: { challengeId, startTomorrow }
  })

packages/api-client/src/endpoints/createCheckin.ts

import { request } from './util'
import { ChallengeSubscription, Checkin } from '../models'

export interface CreateCheckinBody {
  checkin: {
    challengeSubscriptionId: ChallengeSubscription['id']
    successful: boolean
    forYesterday: boolean
    metadata?: object
  }
}

export type CreateCheckinResponse = { success: false; message: string } | { success: true; checkin: Checkin }

/**
 * Check in a user for a particular challenge.
 *
 * @param challengeSubscriptionId The id of the challenge subscription the user is checking in for.
 * @param successful Whether the user indicated success or failure in the checkin.
 * @param forYesterday (optional) Whether the checkin is for yesterday instead of today (default: false).
 * @param metadata (optional) Arbitrary additional data you'd like to attach to the checkin.
 *
 * @returns The checkin record.
 */
export const createCheckin = (
  challengeSubscriptionId: CreateCheckinBody['checkin']['challengeSubscriptionId'],
  successful: CreateCheckinBody['checkin']['successful'],
  forYesterday: CreateCheckinBody['checkin']['forYesterday'] = false,
  metadata?: CreateCheckinBody['checkin']['metadata']
) =>
  request<CreateCheckinResponse>('POST', '/api/v1/checkins', {
    checkin: {
      challengeSubscriptionId,
      successful,
      forYesterday,
      metadata
    }
  })

packages/api-client/src/endpoints/createSSO.ts (/api/v3)

import { request } from './util'
import { CreateUserBody } from './createUser'

export interface CreateSSOBody {
  user: {
    /**
     * The user's unique identifier within a partner system (assigned by you).
     */
    partnerUserId: User['partnerUserId']

    /**
     * The user's email address.
     */
    email?: string

    /**
     * The user's first name.
     */
    firstName?: string

    /**
     * The user's last name.
     */
    lastName?: string

    /**
     * The user's desired username (optional).
     */
    desiredUsername?: string

    /**
     * The user's gender (optional).
       - female
       - male
     */
    gender?: enum

    /**
     * The user's postal code (optional).
     */
    postalCode?: string

    /**
     * The user's date of birth formatted as YYYY-MM-DD (optional).
       - 1999-11-12
     */
    DOB?: string

    /**
     * The user's age in years (optional).
     */
    age?: integer

    /**
     * The user's height in inches (optional).
     */
    height?: integer

    /**
     * The user's weight in lbs (optional).
     */
    weight?: integer

    /**
     * The user's activity level (optional).
     */
    activityLevel?: enum:ActivityLevel

    /**
     * The user's pre-defined health goals (optional).
     */
    healthGoals?: array:HealthGoal

    /**
     * The user's initial patient alert (optional).
     */
    patientAlert?: string

    /**
     * The user's initial patient alert (optional).
     */
    parentalConsentGiven?: bool

    /**
     * Any addditional user params (optional).
     */
    additionalParams?: string - JSON blob

  }

export type CreateSSOResponse = { success: false; message: string } | { success: true; url: string }

/**
 * Returns a url the frontend can use to embed a Diet ID assessment in an iframe, webview, etc.
 * The user is automatically signed in and prompted to complete the assessment.
 *
 * @param user The fields for finding or creating a Diet ID user (id required; email, firstName, and lastName optional)
 * @returns A url that signs in a user and prompts them to complete an assessment.
 *
 */
export const createSSO = (user: CreateSSOBody['user']) => request<CreateSSOResponse>('POST', '/api/v3/sso', { user })

packages/api-client/src/endpoints/createUser.ts

import { request } from './util'
import { User } from '../models'

export interface CreateUserBody {
  user: {
    /**
     * The user's unique identifier within a partner system (assigned by you).
     */
    partnerUserId: User['partnerUserId']

    /**
     * The user's email address.
     */
    email?: string

    /**
     * The user's first name.
     */
    firstName?: string

    /**
     * The user's last name.
     */
    lastName?: string
  }
}

export type CreateUserResponse = { success: false; message: string } | { success: true; user: User }

/**
 * Creates a corresponding Diet ID user for a local user.
 *
 * @param user The required fields for creating a Diet ID user (id, email, first name, last name)
 *
 * @returns The newly created Diet ID user.
 */
export const createUser = async (user: CreateUserBody['user']) =>
  request<CreateUserResponse>('POST', '/api/v1/user', { user })

packages/api-client/src/endpoints/getCurrentUser.ts

import { request } from './util'
import { User } from '../models'

export type GetCurrentUserResponse = { success: false; message: string } | { success: true; user: User | null }

/**
 * Returns the current Diet ID user.
 *
 * @returns The Diet ID user record.
 */
export const getCurrentUser = async () => request<GetCurrentUserResponse>('GET', '/api/v1/user')

packages/api-client/src/endpoints/getWorkflow.ts

import { request } from './util'
import { Workflow } from '../models'

export interface GetWorkflowParams {
  workflowId: Workflow['id']
}

export type GetWorkflowResponse =
  | {
      success: false
      message: string
    }
  | { success: true; workflow: Workflow }

/**
 * Returns the data collected and results from an assessment (workflow).
 *
 * @param workflowId The identifier assigned by Diet ID for the workflow.
 *
 * @returns The workflow with the id.
 */
export const getWorkflow = (workflowId: GetWorkflowParams['workflowId']) =>
  request<GetWorkflowResponse>('GET', `/api/v1/workflows/${workflowId}`)

packages/api-client/src/endpoints/index.ts

export * from './createChallengeSubscription'
export * from './createCheckin'
export * from './createSSO'
export * from './createUser'
export * from './getCurrentUser'
export * from './getWorkflow'
export * from './util'

packages/api-client/src/endpoints/util.ts

let gateway = 'http://localhost:3500'

/**
 * Sets the url to the API Gateway all requests should route through.
 *
 * @param url The url to your API Gateway. e.g., https://yourapiserver.com/pathtodietidgateway
 */
export const setGateway = (url: string) => {
  gateway = url
}

/**
 * Possible HTTP verbs within Diet ID's API.
 */
type RequestMethod = 'GET' | 'POST'

/**
 * Utility method for sending API requests to Diet ID.
 *
 * @param method The HTTP verb for the request ("GET", "POST", etc.)
 * @param endpoint The url slug for a specific portion of the API
 * @param body Optional data to be sent with POST and PUT requests
 *
 * @returns The JSON response from Diet ID for the API request.
 */
export const request = async <T>(method: RequestMethod, endpoint: string, body?: object): Promise<T> => {
  if (!gateway) {
    throw new Error("Please configure your API Gateway before making an API request (with setGateway('...'))")
  }

  const url = `${gateway}${endpoint}`
  const headers = { Accepts: 'application/json', 'Content-Type': 'application/json' }
  const data = JSON.stringify(body)

  return (await fetch(url, { headers, method, body: data })).json()
}

packages/api-client/src/models/Challenge.ts

import { Opaque } from '../types'

export interface Challenge {
  id: Opaque<number, 'Challenge'>

  /**
   * The name of the challenge.
   */
  name: string
}

packages/api-client/src/models/ChallengeSubscription.ts

import { ChallengeSubscriptionState, Opaque } from '../types'
import { Challenge } from './Challenge'
import { Checkin } from './Checkin'
import { Tip } from './Tip'
import { User } from './User'

export interface ChallengeSubscription {
  id: Opaque<number, 'ChallengeSubscription'>

  /**
   * The user the challenge subscription belongs to.
   */
  userId: User['id']

  /**
   * The challenge the challenge subscription belongs to.
   */
  challengeId: Challenge['id']

  /**
   * @todo Add description
   */
  day: any

  /**
   * A helpful tip for the user to complete the challenge.
   */
  tip: Tip | null

  /**
   * The number of times the user should check in throughout the day.
   * For example, a "drink 8 glasses of water" challenge may be 8.
   */
  checkinsPerDay: number

  /**
   * The challenge subscription's state. Valid transitions are:
   *
   * StartingTomorrow -> Active
   *
   * Active -> MissedYesterday | Failed | Succeeded
   *
   * Failed -> StartingTomorrow | Active
   */
  state: ChallengeSubscriptionState

  /**
   * Past checkins the user has made for the challenge subscription.
   */
  checkins: Checkin[]
}

packages/api-client/src/models/Checkin.ts

import { Opaque } from '../types'
import { ChallengeSubscription } from './ChallengeSubscription'

export interface Checkin {
  id: Opaque<number, 'Checkin'>

  /**
   * The challenge subscription this checkin is for.
   */
  challengeSubscriptionId: ChallengeSubscription['id']

  /**
   * Arbitrary additional data passed to Diet ID when the user checked in.
   */
  data: object | null
}

packages/api-client/src/models/Diet.ts

import { Opaque } from '../types'

export interface Diet {
  id: Opaque<number, 'Diet'>

  /**
   * Human-readable label for the diet type (e.g., "American").
   */
  name: string

  /**
   * Unique code for the diet type (e.g., "AME")
   */
  code: string

  /**
   * The quality of the diet on a scale of 1 to 10 (e.g., 5).
   */
  quality: number

  /**
   * Human-readable label for the diet type and quality (e.g., "American quality 7 (7 out of 10)")
   */
  longName: string

  /**
   * A short sentence describing the diet (e.g., "American Quality 5 (Q5) is characterized by fresh and processed meat, ...")
   */
  description: string

  /**
   * The Healthy Eating Index (HEI) of the diet.
   */
  hei: number

  /**
   * A photo of the diet's typical food and drink.
   */
  fingerprintPhotoUrl: string

  /**
   * @todo Add description
   */
  foodGroupValues: { [foodGroupId: string]: number }

  /**
   * Url to a PDF of a mealplan
   */
  mealPlanUrl: string
}

packages/api-client/src/models/NutritionInfo.ts

import { Opaque } from '../types'
import { NutrientUnit } from '../types'

export interface NutritionInfo {
  /**
   * Unique identifier (e.g., "added-sugars-by-total-sugars")
   */
  key: Opaque<string, 'NutritionInfo'>

  /**
   * Quantitative value (e.g., 60.06351427805127)
   */
  value: number

  /**
   * Value's unit of measurement (e.g., "g")
   */
  unit: NutrientUnit

  /**
   * Human-readable label for display of the value and unit (e.g., "Added Sugars")
   */
  label: string

  /**
   * @todo Add description
   */
  isDefault: boolean
}

packages/api-client/src/models/Tip.ts

import { Opaque } from '../types'

export interface Tip {
  id: Opaque<number, 'Tip'>

  /**
   * The content of the tip.
   */
  content: string
}

packages/api-client/src/models/User.ts

import { Opaque } from '../types'
import { Challenge } from './Challenge'
import { ChallengeSubscription } from './ChallengeSubscription'
import { Workflow } from './Workflow'

/**
 * A user identifier provided by you, the partner
 */
export type PartnerUserID = Opaque<string, 'PartnerUserID'>

export interface User {
  /**
   * The user's unique identifier within Diet ID's system (assigned by Diet ID).
   */
  id: Opaque<number, 'User'>

  /**
   * The user's unique identifier within a partner system (assigned by you).
   */
  partnerUserId: PartnerUserID | null

  /**
   * The record of the assessment the user took to determine their ID diet.
   */
  dietIdWorkflowId: Workflow['id'] | null

  /**
   * The record of the assessment the user took to determine their Ideal diet.
   */
  dietIdealWorkflowId: Workflow['id'] | null

  /**
   * The challenge the user is currently participating in.
   */
  challenge: Challenge | null

  /**
   * The join record between the user and their current challenge, which contains additional info specific to that user.
   */
  challengeSubscription: ChallengeSubscription | null
}

packages/api-client/src/models/Workflow.ts

import {
  ActivityLevel,
  DietRestriction,
  Gender,
  HealthGoal,
  Opaque,
  ScreenerResponse,
  StyleScreenerResponse,
  WeightGoal,
  WeightTrend
} from '../types'
import { Challenge } from './Challenge'
import { Diet } from './Diet'
import { NutritionInfo } from './NutritionInfo'
import { User } from './User'

/**
 * A workflow identifier provided by you, the partner
 */
export type PartnerWorkflowID = Opaque<string, 'PartnerWorkflowID'>

export interface Workflow {
  id: Opaque<number, 'Workflow'>

  /**
   * The workflow's unique identifier within a partner system (assigned by you).
   */
  partnerWorkflowId: PartnerWorkflowID | null

  /**
   * The user the workflow belongs to.
   */
  userId: User['id']

  /**
   * A private url to view the workflow results.
   */
  shareUrl: string

  /**
   * Basic demographic and lifestyle info
   */
  userInfo?: {
    gender: Gender
    ageInYears: number
    weightInPounds: number
    heightInInches: number
    weightTrend: WeightTrend
    activityLevel: ActivityLevel
  }

  /**
   * Diet screeners describe what a user currently does and does not eat.
   * Note: Many of these will be null since we ask the user one screener question
   * at a time and often skip a number of them based on branching logic.
   */
  dietScreener: {
    meat: ScreenerResponse
    poultry: ScreenerResponse
    fish: ScreenerResponse
    grains: ScreenerResponse
    dairy: ScreenerResponse
    style: StyleScreenerResponse
  }

  /**
   * Diet restrictions are what a user cannot eat due to allergies, religious observance, etc.
   */
  dietRestrictions: DietRestriction[]

  /**
   * Ideal goals are selected by the user to help determine what ideal diets are optimal for their needs.
   */
  idealGoals: {
    /**
     * Health goals are specific things the user wants to accomplish with their change in diet.
     */
    forHealth: HealthGoal[]

    /**
     * A weight goal qualifies the "ManageWeight" health goal (whether to lose, maintain, or gain weight)
     */
    weightGoal: WeightGoal | null

    /**
     * The goal diet is a diet the user specifically wants to move towards apart from any health goals.
     */
    goalDiet: Diet['code'] | null
  }

  /**
   * The user's ID diet as determined by the assessment.
   */
  dietId: Diet | null

  /**
   * The user's Ideal diet as determined by the assessment.
   */
  dietIdeal: Diet | null

  /**
   * Nutrition info for the user's ID diet.
   * This is workflow-specific data, with baseline diet nutrients adjusted for height, weight, etc.
   */
  dietIdNutritionInfo: NutritionInfo[] | null

  /**
   * Nutrition info for the user's Ideal diet.
   * This is workflow-specific data, with baseline diet nutrients adjusted for height, weight, etc.
   */
  dietIdealNutritionInfo: NutritionInfo[] | null

  /**
   * The sequence of challenges a user is pre-determined to complete based on their ID and Ideal diets.
   */
  challengeIds?: Challenge['id'][]

  /**
   * Whether the workflow has been completed or is a work in progress.
   */
  completed: boolean

  /**
   * The UTC datetime when the workflow was started.
   */
  createdAt: Date
}

packages/api-client/src/models/index.ts

export * from './Challenge'
export * from './ChallengeSubscription'
export * from './Checkin'
export * from './Diet'
export * from './NutritionInfo'
export * from './Tip'
export * from './User'
export * from './Workflow'

packages/api-client/src/webhooks/Workflow.ts

import { User, Workflow } from '../models'

export interface WorkflowWebhook {
  /**
   * The user who completed the workflow
   */
  user: User

  /**
   * The workflow that was completed
   */
  workflow: Workflow
}

packages/api-client/src/webhooks/index.ts

export * from './Workflow'

packages/api-client/src/fixtures.ts

import * as Models from './models'

// necessary to efficiently avoid "Block-scoped variable used before its declaraton" ts(2448) errors on foreign keys
const id = {
  challenge: 1 as Models.Challenge['id'],
  challengeSubscription: 2 as Models.ChallengeSubscription['id'],
  checkin: 1 as Models.Checkin['id'],
  dietId: 76 as Models.Diet['id'],
  dietIdeal: 29 as Models.Diet['id'],
  tip: 1 as Models.Tip['id'],
  user: 575 as Models.User['id'],
  workflowWithoutPHI: 1928 as Models.Workflow['id'],
  workflowWithPHI: 2048 as Models.Workflow['id']
}

export const challenge: Models.Challenge = {
  id: id.challenge,
  name: 'Avoid Soda'
}

export const challengeSubscription: Models.ChallengeSubscription = {
  id: id.challengeSubscription,
  userId: id.user,
  challengeId: id.challenge,
  day: null,
  // @ts-ignore
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
  tip: tip,
  checkinsPerDay: 1,
  state: 'Active',
  // @ts-ignore
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
  checkins: [checkin]
}

export const checkin = {
  id: id.checkin,
  challengeSubscriptionId: id.challengeSubscription,
  data: { someData: 'youPassedIn' }
}

export const dietId: Models.Diet = {
  id: id.dietId,
  name: 'Mediterranean',
  code: 'MED',
  quality: 6,
  longName: 'Mediterranean quality 6 (6 out of 10)',
  description:
    'This diet pattern includes lean meats, poultry, eggs and dairy, baked/grilled seafood, vegetables and fruits, some beans and nuts, mainly refined and some whole grains, some processed foods, olives, olive oil, and red wine in moderation (optional).',
  hei: 59,
  fingerprintPhotoUrl:
    'https://dqpn.imgix.net/assets/diet-images/76_MED/6/fingerprint_photo_CldNusqDTzpZeHFhShWrOHXzdEjnBi.png?w=960&auto=format,compression&txtalign=bottom%2Cright&txtclr=c1cdbd&txt=%C2%A9%202020%20Diet%20ID%20Inc&txtsize=36&txtfont=Arial%2CBoldMT&txtpad=15&txtalign=center',
  mealPlanUrl: 'http://dietid.com',
  foodGroupValues: {
    FRU0100: 0.03133333333333333,
    FRU0200: 0.0,
    FRU0300: 0.0,
    FRU0400: 0.12
  }
}

export const dietIdeal: Models.Diet = {
  id: id.dietIdeal,
  name: 'Flexitarian',
  code: 'FLX',
  quality: 9,
  longName: 'Flexitarian quality 9 (9 out of 10)',
  description:
    'This diet pattern includes a variety of fresh vegetables and fruits, predominantly whole grains, beans, nuts and seeds, eggs, limited amounts of high-quality poultry and/or fish, and non-fat dairy products and plant-based dairy alternatives.',
  hei: 92,
  fingerprintPhotoUrl:
    'https://dqpn.imgix.net/assets/diet-images/29_FLX/9/fingerprint_photo_AbZPPZtzJVVIpfSPrLUBiYHPdoteBM.png?w=960&auto=format,compression&txtalign=bottom%2Cright&txtclr=c1cdbd&txt=%C2%A9%202020%20Diet%20ID%20Inc&txtsize=36&txtfont=Arial%2CBoldMT&txtpad=15&txtalign=center',
  mealPlanUrl: 'http://dietid.com',
  foodGroupValues: {
    FRU0100: 0.142,
    FRU0200: 0.0,
    FRU0300: 0.7240000000000001,
    FRU0400: 3.2693333333333334
  }
}

export const nutritionInfo: Models.NutritionInfo = {
  key: 'added-sugars-by-total-sugars' as Models.NutritionInfo['key'],
  value: 60.06351427805127,
  unit: 'g',
  label: 'Added Sugars',
  isDefault: false
}

export const tip = {
  id: id.tip,
  content: 'Some tip'
}

export const user = {
  id: id.user,
  partnerUserId: '71' as Models.PartnerUserID,
  dietIdWorkflowId: id.workflowWithPHI,
  dietIdealWorkflowId: id.workflowWithPHI,
  challenge: challenge,
  challengeSubscription: challengeSubscription
}

export const workflowWithoutPHI: Models.Workflow = {
  id: id.workflowWithoutPHI,
  partnerWorkflowId: '86' as Models.PartnerWorkflowID,
  userId: id.user,
  dietScreener: {
    meat: 'Yes',
    poultry: null,
    fish: null,
    grains: 'Yes',
    dairy: null,
    style: null
  },
  dietRestrictions: ['DairyFree', 'Organic', 'PeanutFree'],
  idealGoals: {
    forHealth: ['ManageWeight'],
    weightGoal: 'Lose',
    goalDiet: 'FLX' as Models.Diet['code']
  },
  dietId: dietId,
  dietIdeal: dietIdeal,
  dietIdNutritionInfo: [nutritionInfo],
  dietIdealNutritionInfo: [nutritionInfo],
  challengeIds: [id.challenge],
  completed: true,
  createdAt: new Date()
}

export const workflowWithPHI: Models.Workflow = {
  ...workflowWithoutPHI,
  id: id.workflowWithPHI,
  partnerWorkflowId: '85' as Models.PartnerWorkflowID,
  userInfo: {
    gender: 'Female',
    ageInYears: 20,
    weightInPounds: 185,
    heightInInches: 71,
    weightTrend: 'Rising',
    activityLevel: 'HighlyActive'
  }
}

packages/api-client/src/index.ts

import * as fixtures from './fixtures'

export * from './endpoints'
export * from './models'
export * from './webhooks'
export { fixtures }

packages/api-client/src/types.ts

import { Opaque as _Opaque } from 'type-fest'

export type Opaque<Type, Token = unknown> = _Opaque<Type, Token>

/**
 * Screener questions that are not shown to the user are sent as null.
 */
export type ScreenerResponse = 'Yes' | 'No' | 'Sometimes' | null

/**
 * If during the screener the user indicates that they consume meat and grains
 * then they are also asked if they follow a particular style of diet. Otherwise this value is null.
 * The string type is only necessary for when the user specifies a style other than what's listed.
 * Free text responses to "Other ethnic style" and "Other pattern" are sent as "OtherEthnic:foo" and
 * "OtherPattern:foo" respectively where foo is what the user typed in the textbox.
 */
export type StyleScreenerResponse = 'None' | 'LOC' | 'LOF' | 'MED' | 'MEX' | 'SOU' | string | null

export type Gender = 'Male' | 'Female'

export type WeightTrend = 'Rising' | 'Constant' | 'Falling'

export type ActivityLevel = 'Sedentary' | 'BelowGuidelines' | 'MeetsGuidelines' | 'AboveGuidelines' | 'HighlyActive'

export type DietRestriction =
  | 'DairyFree'
  | 'NutFree'
  | 'GlutenFree'
  | 'WheatFree'
  | 'ShellfishFree'
  | 'SoyFree'
  | 'PeanutFree'
  | 'AlcoholFree'
  | 'EggFree'
  | 'Halal'
  | 'Kosher'
  | 'Organic'

/**
 * The string type is only necessary for when the user specifies a goal other than what's listed.
 * "Other" goals are sent as "other:foo" where foo is what the user typed in the textbox.
 */
export type HealthGoal =
  | 'ReduceCardioRisk'
  | 'ControlBloodPressure'
  | 'PreventOrControlDiabetes'
  | 'ManageHeartFailure'
  | 'DecreaseInflammation'
  | 'ImproveOverall'
  | 'ManageWeight'
  | 'ManageHighCholesterol'
  | 'ManageFoodSensitivities'
  | 'ImproveGutHealth'
  | string

export type WeightGoal = 'Lose' | 'Maintain' | 'Gain'

export type ChallengeSubscriptionState = 'StartingTomorrow' | 'Active' | 'MissedYesterday' | 'Succeeded' | 'Failed'

export type NutrientUnit = 'g' | 'mg' | 'mcg' | '%' | 'kcal'