diff --git a/graphql/data/schema.js b/graphql/data/schema.js index 43d490b00..4b6488538 100755 --- a/graphql/data/schema.js +++ b/graphql/data/schema.js @@ -164,6 +164,42 @@ import GroupImpactLeaderboard from '../database/groupImpact/GroupImpactLeaderboa // Types import wildfireType from './types/wildfire' +import mutationType from './types/mutationType'; +import queryType from './types/queryType'; +import EncodedRevenueValueType from './types/EncodedRevenueValueType'; +import customErrorType from './types/customErrorType'; +import MissionType from './types/MissionType'; +import appType from './types/appType'; +import campaignType from './types/campaignType'; +import campaignThemeType from './types/campaignThemeType'; +import campaignSocialSharingType from './types/campaignSocialSharingType'; +import campaignGoalType from './types/campaignGoalType'; +import campaignTimeType from './types/campaignTimeType'; +import campaignContentType from './types/campaignContentType'; +import invitedUsersType from './types/invitedUsersType'; +import userImpactType from './types/userImpactType'; +import charityType from './types/charityType'; +import videoAdLogType from './types/videoAdLogType'; +import widgetType from './types/widgetType'; +import userRecruitType from './types/userRecruitType'; +import SearchEnginePersonalizedType from './types/SearchEnginePersonalizedType'; +import SearchEngineType from './types/SearchEngineType'; +import CauseType from './types/CauseType'; +import userGroupImpactMetricType from './types/userGroupImpactMetricType'; +import groupImpactMetricType from './types/groupImpactMetricType'; +import impactMetricType from './types/impactMetricType'; +import CauseOnboardingCopyType from './types/CauseOnboardingCopyType'; +import CauseSharingCopyType from './types/CauseSharingCopyType'; +import CauseThemeType from './types/CauseThemeType'; +import featureType from './types/featureType'; +import userType from './types/userType'; +import ExperimentActionsOutputType from './types/ExperimentActionsOutputType'; +import ExperimentActionsType from './types/ExperimentActionsType'; +import ExperimentGroupsType from './types/ExperimentGroupsType'; +import searchRateLimitType from './types/searchRateLimitType'; +import maxSearchesDayType from './types/maxSearchesDayType'; +import maxTabsDayType from './types/maxTabsDayType'; +import backgroundImageType from './types/backgroundImageType'; class App { constructor(id) { @@ -286,217 +322,15 @@ const { nodeInterface, nodeField } = nodeDefinitions( * Define your own types here */ -const backgroundImageType = new GraphQLObjectType({ - name: BACKGROUND_IMAGE, - description: 'A background image', - fields: () => ({ - id: globalIdField(BACKGROUND_IMAGE), - name: { - type: GraphQLString, - description: 'the background image name', - }, - image: { - type: GraphQLString, - description: 'The image filename', - }, - imageURL: { - type: GraphQLString, - description: 'The image file URL', - }, - category: { - type: new GraphQLNonNull(GraphQLString), - description: 'the category that the image falls into', - }, - thumbnail: { - type: GraphQLString, - description: 'The image thumbnail filename', - }, - thumbnailURL: { - type: GraphQLString, - description: 'The image thumbnail URL', - }, - timestamp: { - type: GraphQLString, - description: - 'ISO datetime string of when the background image was last set', - }, - }), - interfaces: [nodeInterface], -}) -const maxTabsDayType = new GraphQLObjectType({ - name: 'MaxTabsDay', - description: "Info about the user's day of most opened tabs", - fields: () => ({ - date: { - type: GraphQLString, - description: 'The day the most tabs were opened', - }, - numTabs: { - type: GraphQLInt, - description: 'The number of tabs opened on that day', - }, - }), -}) -const maxSearchesDayType = new GraphQLObjectType({ - name: 'MaxSearchesDay', - description: "Info about the user's day of most searches", - fields: () => ({ - date: { - type: GraphQLString, - description: 'The day (datetime)the most searches occurred', - }, - numSearches: { - type: GraphQLInt, - description: 'The number of searches made on that day', - }, - }), -}) -const searchRateLimitType = new GraphQLObjectType({ - name: 'SearchRateLimit', - description: 'Info about any rate-limiting for VC earned from search queries', - fields: () => ({ - limitReached: { - type: GraphQLBoolean, - description: - "Whether we are currently rate-limiting the user's VC earned from searches", - }, - reason: { - type: new GraphQLEnumType({ - name: 'SearchRateLimitReason', - description: - "Why we are rate-limiting the user's VC earned from searches", - values: { - NONE: { value: 'NONE' }, - ONE_MINUTE_MAX: { value: 'ONE_MINUTE_MAX' }, - FIVE_MINUTE_MAX: { value: 'FIVE_MINUTE_MAX' }, - DAILY_MAX: { value: 'DAILY_MAX' }, - }, - }), - }, - checkIfHuman: { - type: GraphQLBoolean, - description: 'Whether we should present the user with a CAPTCHA', - }, - }), -}) -const ExperimentGroupsType = new GraphQLInputObjectType({ - name: 'ExperimentGroups', - description: 'The experimental groups to which the user is assigned', - fields: { - anonSignIn: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupAnonSignIn', - description: 'The test of allowing anonymous user authentication', - values: { - NONE: { value: experimentConfig.anonSignIn.NONE }, - AUTHED_USER_ONLY: { - value: experimentConfig.anonSignIn.AUTHED_USER_ONLY, - }, - ANONYMOUS_ALLOWED: { - value: experimentConfig.anonSignIn.ANONYMOUS_ALLOWED, - }, - }, - }), - }, - variousAdSizes: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupVariousAdSizes', - description: 'The test of enabling many different ad sizes', - values: { - NONE: { value: experimentConfig.variousAdSizes.NONE }, - STANDARD: { value: experimentConfig.variousAdSizes.STANDARD }, - VARIOUS: { value: experimentConfig.variousAdSizes.VARIOUS }, - }, - }), - }, - thirdAd: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupThirdAd', - description: 'The test of enabling a third ad', - values: { - NONE: { value: experimentConfig.thirdAd.NONE }, - TWO_ADS: { value: experimentConfig.thirdAd.TWO_ADS }, - THREE_ADS: { value: experimentConfig.thirdAd.THREE_ADS }, - }, - }), - }, - oneAdForNewUsers: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupOneAdForNewUsers', - description: 'The test of showing only one ad to new users', - values: { - NONE: { value: experimentConfig.oneAdForNewUsers.NONE }, - DEFAULT: { value: experimentConfig.oneAdForNewUsers.DEFAULT }, - ONE_AD_AT_FIRST: { - value: experimentConfig.oneAdForNewUsers.ONE_AD_AT_FIRST, - }, - }, - }), - }, - adExplanation: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupAdExplanation', - description: 'The test of showing an explanation of why there are ads', - values: { - NONE: { value: experimentConfig.adExplanation.NONE }, - DEFAULT: { value: experimentConfig.adExplanation.DEFAULT }, - SHOW_EXPLANATION: { - value: experimentConfig.adExplanation.SHOW_EXPLANATION, - }, - }, - }), - }, - searchIntro: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupSearchIntro', - description: - 'The test of showing an introduction message to Search for a Cause', - values: { - NONE: { value: experimentConfig.searchIntro.NONE }, - NO_INTRO: { value: experimentConfig.searchIntro.NO_INTRO }, - INTRO_A: { - value: experimentConfig.searchIntro.INTRO_A, - }, - INTRO_HOMEPAGE: { - value: experimentConfig.searchIntro.INTRO_HOMEPAGE, - }, - }, - }), - }, - referralNotification: { - type: new GraphQLEnumType({ - name: 'ExperimentGroupReferralNotification', - description: - 'The test of showing a notification to ask users to recruit friends', - values: { - NONE: { value: experimentConfig.referralNotification.NONE }, - NO_NOTIFICATION: { - value: experimentConfig.referralNotification.NO_NOTIFICATION, - }, - COPY_A: { - value: experimentConfig.referralNotification.COPY_A, - }, - COPY_B: { - value: experimentConfig.referralNotification.COPY_B, - }, - COPY_C: { - value: experimentConfig.referralNotification.COPY_C, - }, - COPY_D: { - value: experimentConfig.referralNotification.COPY_D, - }, - COPY_E: { - value: experimentConfig.referralNotification.COPY_E, - }, - }, - }), - }, - }, -}) + + + + + const experimentActionsFields = { searchIntro: { @@ -524,1410 +358,124 @@ const experimentActionsFields = { }, } -const ExperimentActionsType = new GraphQLInputObjectType({ - name: 'ExperimentActions', - description: 'The actions a user may take in an experiment', - fields: experimentActionsFields, -}) -const ExperimentActionsOutputType = new GraphQLObjectType({ - name: 'ExperimentActionsOutput', - description: 'The actions a user has taken in an experiment', - fields: () => experimentActionsFields, -}) -// TODO: fetch only the fields we need: -// https://github.com/graphql/graphql-js/issues/19#issuecomment-272857189 -const userType = new GraphQLObjectType({ - name: USER, - description: 'A person who uses our app', - fields: () => ({ - id: globalIdField(USER), - userId: { - type: GraphQLString, - description: - "The users's Firebase ID (not Relay global ID, unlike the `id` field", - resolve: (user) => user.id, - }, - backgroundImage: { - type: backgroundImageType, - description: "Users's background image", - resolve: (user, _args, context) => getBackgroundImage(context.user, user), - }, - userImpact: { - type: userImpactType, - description: - "A user's cause-specific impact for the cause they are currently supporting", - resolve: async (user, _args, context) => - getUserImpact(context.user, user.id), - }, - username: { - type: GraphQLString, - description: "Users's username", - }, - email: { - type: GraphQLString, - description: "User's email", - }, - truexId: { - type: new GraphQLNonNull(GraphQLString), - description: 'a unique user ID sent to video ad partner truex', - resolve: (user, _args, context) => getOrCreateTruexId(context.user, user), - }, - causeId: { - type: GraphQLString, - description: - "The users's cause id. If empty the user does not have a cause.", - resolve: (user) => user.causeId, - }, - cause: { - type: CauseType, - description: 'cause type for the user', - resolve: (user, _args, context) => getCauseByUser(context.user, user.id), - }, - videoAdEligible: { - type: GraphQLBoolean, - description: - 'whether a user has completed 3 video ads in the last 24 hours', - resolve: (user, _args, context) => isVideoAdEligible(context.user, user), - }, - joined: { - type: GraphQLString, - description: 'ISO datetime string of when the user joined', - }, - justCreated: { - type: GraphQLBoolean, - description: - 'Whether or not the user was created during this request. Helpful for a "get or create" mutation', - resolve: (user) => - // The user will only have the 'justCreated' field when it's a - // brand new user item - !!user.justCreated, - }, - vcCurrent: { - type: GraphQLInt, - description: "User's current VC", - }, - vcAllTime: { - type: GraphQLInt, - description: "User's all time VC", - }, - tabs: { - type: GraphQLInt, - description: "User's all time tab count", - }, - tabsToday: { - type: GraphQLInt, - description: "User's tab count for today", - }, - maxTabsDay: { - type: maxTabsDayType, - description: "Info about the user's day of most opened tabs", - resolve: (user) => user.maxTabsDay.maxDay, - }, - level: { - type: GraphQLInt, - description: "User's vc", - }, - v4BetaEnabled: { - type: GraphQLBoolean, - description: 'If true, serve the new Tab V4 app.', - }, - hasViewedIntroFlow: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'if true, user has viewed intro flow in v4', - }, - // TODO: change to heartsForNextLevel to be able to get progress - heartsUntilNextLevel: { - type: GraphQLInt, - description: 'Remaing hearts until next level.', - }, - vcDonatedAllTime: { - type: GraphQLInt, - description: "User's total vc donated", - }, - recruits: { - type: userRecruitsConnection, - description: 'People recruited by this user', - args: { - ...connectionArgs, - startTime: { type: GraphQLString }, - endTime: { type: GraphQLString }, - }, - resolve: (user, args, context) => - connectionFromPromisedArray( - getRecruits(context.user, user.id, args.startTime, args.endTime), - args - ), - }, - numUsersRecruited: { - type: GraphQLInt, - description: 'The number of users this user has recruited', - }, - widgets: { - type: widgetConnection, - description: 'User widgets', - args: { - ...connectionArgs, - enabled: { type: GraphQLBoolean }, - }, - resolve: (user, args, context) => - connectionFromPromisedArray( - getWidgets(context.user, user.id, args.enabled), - args - ), - }, - activeWidget: { - type: GraphQLString, - description: "User's active widget id", - }, - currentMission: { - type: MissionType, - description: 'the current active mission for a user', - resolve: (user) => getCurrentUserMission(user), - }, - pastMissions: { - type: MissionsConnection, - description: 'gets all the past missions for a user', - args: connectionArgs, - resolve: (user, args) => { - return connectionFromPromisedArray(getPastUserMissions(user), args) - }, - }, - backgroundOption: { - type: GraphQLString, - description: "User's background option", - }, - customImage: { - type: GraphQLString, - description: "User's background custom image", - }, - backgroundColor: { - type: GraphQLString, - description: "User's background color", - }, - mergedIntoExistingUser: { - type: GraphQLBoolean, - description: - 'Whether this user was created by an existing user and then merged into the existing user', - }, - searches: { - type: GraphQLInt, - description: "User's all time search count", - }, - notifications: { - type: new GraphQLNonNull( - new GraphQLList( - new GraphQLObjectType({ - name: 'notifications', - description: 'user notifications to show on v4', - fields: () => ({ - code: { - type: GraphQLString, - description: 'the kind of notification it is', - }, - variation: { - type: new GraphQLNonNull(GraphQLString), - description: - 'the variation of the notification given to this user (e.g. for A/B testing)', - }, - }), - }) - ) - ), - description: 'notifications for the v4 user to see', - resolve: (user, args, context) => - getUserNotifications(context.user, user), - }, - searchesToday: { - type: GraphQLInt, - description: "User's search count for today", - }, - searchRateLimit: { - type: searchRateLimitType, - description: 'Info about any search query rate-limiting', - resolve: (user, args, context) => - checkSearchRateLimit(context.user, user.id), - }, - maxSearchesDay: { - type: maxSearchesDayType, - description: "Info about the user's day of most searches", - resolve: (user) => user.maxSearchesDay.maxDay, - }, - experimentActions: { - type: ExperimentActionsOutputType, - description: 'Actions the user has taken during experiments', - resolve: (user) => constructExperimentActionsType(user), - }, - pendingMissionInvites: { - type: new GraphQLNonNull( - new GraphQLList( - new GraphQLObjectType({ - name: 'PendingMissionInvite', - description: 'pending mission invites for user', - fields: () => ({ - missionId: { - type: new GraphQLNonNull(GraphQLString), - description: 'the mission id of the squad invite', - }, - invitingUser: { - type: new GraphQLObjectType({ - name: 'InvitingUser', - description: 'inviting user', - fields: () => ({ - name: { - type: new GraphQLNonNull(GraphQLString), - description: 'the name entered in invite', - }, - }), - }), - }, - }), - }) - ) - ), - }, - hasSeenSquads: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'whether a v4 user has been introduced to squads in the ui', - }, - features: { - type: new GraphQLNonNull(new GraphQLList(featureType)), - description: 'feature values for this specific user', - resolve: (user, args, context) => getUserFeatures(context.user, user), - }, - searchEngine: { - type: SearchEnginePersonalizedType, - description: 'the User’s search engine', - resolve: (user, args, context) => getUserSearchEngine(context.user, user), - }, - showYahooPrompt: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'whether to show the yahoo search prompt', - resolve: (user, _, context) => - getShouldShowYahooPrompt(context.user, user), - }, - showSfacExtensionPrompt: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'whether to show the SFAC extension prompt', - resolve: (user, _, context) => - getShouldShowSfacExtensionPrompt(context.user, user), - }, - showSfacIcon: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'whether to show the SFAC icon (and activity ui element)', - resolve: (user, _, context) => getShouldShowSfacIcon(context.user, user), - }, - sfacActivityState: { - type: new GraphQLNonNull( - new GraphQLEnumType({ - name: 'sfacActivityState', - description: 'what mode in which to show SFAC searches UI', - values: { - new: { value: 'new' }, - active: { value: 'active' }, - inactive: { value: 'inactive' }, - }, - }) - ), - resolve: (user, _, context) => getSfacActivityState(context.user, user), - }, - yahooPaidSearchRewardOptIn: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'whether or not the user has opted into searching for extra impact', - }, - userGroupImpactMetric: { - type: userGroupImpactMetricType, - description: 'Current UserGroupImpactMetric', - resolve: (user, _, context) => - UserGroupImpactMetricModel.getOrNull( - context, - user.userGroupImpactMetricId - ), - }, - leaderboard: { - type: new GraphQLList( - new GraphQLObjectType({ - name: 'leaderboardEntry', - description: 'content for each leaderboard', - fields: () => ({ - position: { - type: new GraphQLNonNull(GraphQLInt), - }, - userGroupImpactMetric: { - type: userGroupImpactMetricType, - description: 'UserGroupImpactMetric entity', - }, - user: { - type: userType, - description: 'User associated with this leaderboard entry', - }, - }), - }) - ), - description: 'Current UserGroupImpactMetrics leaderboard', - resolve: (user) => - user.userGroupImpactMetricId && - GroupImpactLeaderboard.getLeaderboardForUser( - user.userGroupImpactMetricId, - user.id - ), - }, - }), - interfaces: [nodeInterface], -}) -const featureType = new GraphQLObjectType({ - name: FEATURE, - description: 'Feature name and variation value pair applicable to a user.', - fields: () => ({ - featureName: { - type: new GraphQLNonNull(GraphQLString), - description: `Name of the Feature`, - }, - variation: { - type: new GraphQLNonNull(GraphQLString), - description: 'the value of the variation for this specific user', - }, - }), -}) -const CauseImpactCopy = new GraphQLObjectType({ - name: 'CauseSpecificImpactUI', - description: 'cause specific UI content around impact', - fields: () => ({ - impactCounterText: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown string: copy for ImpactCounter for normal case', - }, - referralRewardTitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: title copy for referralReward UserImpact modal', - }, - referralRewardSubtitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: subtitle copy for referralReward UserImpact modal', - }, - referralRewardNotification: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown string: copy for referral reward notification', - }, - claimImpactTitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: title for claimImpact notification in UserImpact', - }, - claimImpactSubtitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: subtitle for claimImpact notification in UserImpact', - }, - impactIcon: { - type: new GraphQLNonNull(GraphQLString), - description: 'string: name of the icon to use in impact counter', - }, - walkMeGif: { - type: GraphQLString, - description: 'file name of the cause GIF to use during onboarding', - }, - newlyReferredImpactWalkthroughText: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: copy for impact walkthrough notification in UserImpact when user is referred', - }, - impactWalkthroughText: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: copy for impact walkthrough notification in UserImpact', - }, - confirmImpactSubtitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'markdown string: copy for confirm impact modal in UserImpact', - }, - }), -}) - -const CauseThemeType = new GraphQLObjectType({ - name: 'CauseTheming', - description: 'css properties for a specific cause', - fields: () => ({ - primaryColor: { - type: new GraphQLNonNull(GraphQLString), - description: 'the primary color hex value', - }, - secondaryColor: { - type: new GraphQLNonNull(GraphQLString), - description: 'the secondary color hex value', - }, - }), -}) - -const CauseSharingCopyType = new GraphQLObjectType({ - name: 'SharingUICopy', - description: 'cause specific UI content around sharing', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown for modal title', - }, - subtitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown for modal subtitle', - }, - imgCategory: { - type: new GraphQLNonNull(GraphQLString), - description: `value to use for img switch statement on frontend, probably ‘cats’ or ‘seas’`, - }, - shareImage: { - type: new GraphQLNonNull(GraphQLString), - description: 'Image to use in email invite dialog', - }, - sentImage: { - type: new GraphQLNonNull(GraphQLString), - description: 'Image shown after email invite sent', - }, - redditButtonTitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'copy for reddit button', - }, - facebookButtonTitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'copy for facebook button', - }, - twitterButtonTitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'copy for twitter button', - }, - tumblrTitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'copy for tumblr button', - }, - tumblrCaption: { - type: new GraphQLNonNull(GraphQLString), - description: 'copy for tumblr caption', - }, - }), -}) -const CauseOnboardingCopyType = new GraphQLObjectType({ - name: 'OnboardingUICopy', - description: 'cause specific UI content around onboarding', - fields: () => ({ - steps: { - type: new GraphQLNonNull( - new GraphQLList( - new GraphQLObjectType({ - name: 'onboardingUIStep', - description: 'ui content for each onboarding step', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown title for onboarding step', - }, - subtitle: { - type: new GraphQLNonNull(GraphQLString), - description: 'markdown subtitle for onboarding step', - }, - imgName: { - type: new GraphQLNonNull(GraphQLString), - description: 'name of image to show', - }, - }), - }) - ) - ), - description: 'the steps array in onboarding', - resolve: (onboarding) => onboarding.steps, - }, - firstTabIntroDescription: { - type: new GraphQLNonNull(GraphQLString), - resolve: (onboarding) => onboarding.firstTabIntroDescription, - description: - 'markdown string shown when prompting the user to open their first tab, currently info about cat treats', - }, - }), -}) - -const impactMetricType = new GraphQLObjectType({ - name: IMPACT_METRIC, - description: 'An instance of ImpactMetric', - fields: () => ({ - id: globalIdField(IMPACT_METRIC), - charity: { - type: charityType, - description: 'Charity ID that this impact metric belongs to', - resolve: (gim, _args, context) => - CharityModel.get(context.user, gim.charityId), - }, - dollarAmount: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'Dollar amount (in micro USDs) required to achieve an instance of this ImpactMetric', - }, - description: { - type: new GraphQLNonNull(GraphQLString), - description: 'Markdown description of this ImpactMetric', - }, - whyValuableDescription: { - type: new GraphQLNonNull(GraphQLString), - description: - 'Markdown. A shorter version of the description that answers "why this impact matters".', - }, - metricTitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'Metric title. Should be placeable in a sentence. Example: "1 home visit"', - }, - impactTitle: { - type: new GraphQLNonNull(GraphQLString), - description: - 'Impact action title. Should be a longer title with a verb as well as a noun. Example: "Provide 1 visit from a community health worker"', - }, - active: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'Whether or not this GroupImpactMetric is still active', - }, - impactCountPerMetric: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'How many instances of the impact are provided per completion of a GroupImpactMetric run.', - }, - }), - interfaces: [nodeInterface], -}) - -const groupImpactMetricType = new GraphQLObjectType({ - name: GROUP_IMPACT_METRIC, - description: 'A specific instance of GroupImpactMetric', - fields: () => ({ - id: globalIdField(GROUP_IMPACT_METRIC), - cause: { - type: CauseType, - description: 'The cause ID this GroupImpactMetric belongs to', - resolve: (groupImpactMetric, _args, context) => - getCause(context.user, groupImpactMetric.causeId), - }, - impactMetric: { - type: impactMetricType, - description: 'Information about the ImpactMetric', - resolve: (groupImpactMetric) => - getImpactMetricById(groupImpactMetric.impactMetricId), - }, - dollarProgress: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far', - }, - dollarGoal: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far', - }, - dateStarted: { - type: GraphQLString, - description: - 'ISO datetime string of when this GroupImpactMetric was started', - }, - dateCompleted: { - type: GraphQLString, - description: - 'ISO datetime string of when this GroupImpactMetric was ended', - }, - dollarProgressFromTab: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far from tabs', - }, - dollarProgressFromSearch: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far from search', - }, - }), - interfaces: [nodeInterface], -}) - -const userGroupImpactMetricType = new GraphQLObjectType({ - name: USER_GROUP_IMPACT_METRIC, - description: 'A specific users contribution to a GroupImpactMetric', - fields: () => ({ - groupImpactMetric: { - type: groupImpactMetricType, - description: 'Information about the GroupImpactMetric', - resolve: (userGroupImpactMetric, _, context) => - GroupImpactMetricModel.get( - context, - userGroupImpactMetric.groupImpactMetricId - ), - }, - id: globalIdField(USER_GROUP_IMPACT_METRIC), - userId: { - type: new GraphQLNonNull(GraphQLString), - description: - 'The ID of the user which the UserGroupImpactMetric belongs to', - }, - dollarContribution: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user', - }, - tabDollarContribution: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from tabs', - }, - searchDollarContribution: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from search', - }, - shopDollarContribution: { - type: new GraphQLNonNull(GraphQLInt), - description: - 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from shopping', - }, - }), - interfaces: [nodeInterface], -}) - -const CauseType = new GraphQLObjectType({ - name: CAUSE, - description: 'all cause specific data and ui content', - fields: () => ({ - id: globalIdField(CAUSE), - about: { - type: new GraphQLNonNull(GraphQLString), - description: `Markdown - content that populates an "About the Cause" page`, - }, - name: { - type: new GraphQLNonNull(GraphQLString), - description: `String used to describe cause in account page`, - }, - nameForShop: { - type: new GraphQLNonNull(GraphQLString), - description: `String used to describe cause in the shop extension`, - }, - isAvailableToSelect: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'boolean if cause is available to select in ui', - }, - causeId: { - type: new GraphQLNonNull(GraphQLString), - description: "Cause's id", - resolve: (cause) => cause.id, - }, - icon: { - type: new GraphQLNonNull(GraphQLString), - description: `Name of an icon, mapping to an icon component on the frontend`, - }, - landingPagePath: { - type: new GraphQLNonNull(GraphQLString), - description: `URL path for the landing page belonging to this cause`, - }, - landingPagePhrase: { - type: new GraphQLNonNull(GraphQLString), - description: `Phrase for the landing page belonging to this cause`, - resolve: (cause, _, context) => getLandingPagePhrase(context.user, cause), - }, - individualImpactEnabled: { - type: new GraphQLNonNull(GraphQLBoolean), - description: `Whether or not the current cause supports individual impact`, - deprecationReason: 'Replaced by "impactType" field.', - }, - impactType: { - type: new GraphQLNonNull( - new GraphQLEnumType({ - name: 'causeImpactType', - description: - "The type of charitable impact that's enabled for this cause", - values: { - none: { value: CAUSE_IMPACT_TYPES.none }, - individual: { value: CAUSE_IMPACT_TYPES.individual }, - group: { value: CAUSE_IMPACT_TYPES.group }, - individual_and_group: { - value: CAUSE_IMPACT_TYPES.individual_and_group, - }, - }, - }) - ), - description: `Whether or not the current cause supports individual impact`, - }, - impactVisits: { - type: GraphQLInt, - description: `number of visits required for each impact unit (e.g. 14 for cat charity)`, - }, - impact: { - type: CauseImpactCopy, - description: 'the impact object on cause model', - resolve: (cause) => cause.impact, - }, - theme: { - type: new GraphQLNonNull(CauseThemeType), - description: 'the theme object on cause model', - resolve: (cause) => cause.theme, - }, - sharing: { - type: new GraphQLNonNull(CauseSharingCopyType), - description: 'the sharing object on cause model', - resolve: (cause) => cause.sharing, - }, - onboarding: { - type: new GraphQLNonNull(CauseOnboardingCopyType), - resolve: (cause) => cause.onboarding, - description: 'the onboarding object on cause model', - }, - groupImpactMetric: { - type: groupImpactMetricType, - description: - 'the group impact metric currently associated with this cause', - resolve: (cause, _args, context) => - getGroupImpactMetricForCause(context.user, cause.id), - }, - groupImpactMetricCount: { - type: GraphQLInt, - description: - 'how many times this particular group impact metric has been completed for this cause', - resolve: (cause, _args, context) => - getCauseImpactMetricCount(context.user, cause.id), - }, - charity: { - type: charityType, - description: 'Charity that this cause is currently generating impact for', - resolve: (cause, _, context) => - CharityModel.get(context.user, cause.charityId), - }, - charities: { - type: new GraphQLList(charityType), - description: 'Charity that this cause is currently generating impact for', - resolve: (cause, _, context) => - getCharitiesForCause(context.user, cause.charityIds, cause.charityId), - }, - }), - interfaces: [nodeInterface], -}) - -const searchEngineSharedFields = { - engineId: { - type: new GraphQLNonNull(GraphQLString), - description: "Engine's id", - resolve: (engine) => engine.id, - }, - name: { - type: new GraphQLNonNull(GraphQLString), - description: `Name of the Search Engine`, - }, - rank: { - type: new GraphQLNonNull(GraphQLInt), - description: 'what order to display the search engine in a list', - }, - isCharitable: { - type: new GraphQLNonNull(GraphQLBoolean), - description: `Whether or not the user can earn extra impact with this Search Engine`, - }, - inputPrompt: { - type: new GraphQLNonNull(GraphQLString), - description: `Display string to display in the search bar`, - }, -} -const SearchEngineType = new GraphQLObjectType({ - name: SEARCH_ENGINE, - description: 'all important data for a search engine.', - fields: () => ({ - ...searchEngineSharedFields, - id: globalIdField(SEARCH_ENGINE), - searchUrl: { - type: new GraphQLNonNull(GraphQLString), - description: - 'A search destination URL, with a {searchTerms} placeholder for the client to replace. Use `user.searchEngine` if the user is authenticated.', - }, - }), - interfaces: [nodeInterface], -}) -const SearchEnginePersonalizedType = new GraphQLObjectType({ - name: SEARCH_ENGINE_PERSONALIZED, - description: - 'SearchEngineType extended with fields potentially personalized to the user', - fields: () => ({ - ...searchEngineSharedFields, - searchUrlPersonalized: { - type: new GraphQLNonNull(GraphQLString), - description: - "Use this for the user's search behavior. A search destination URL, with a {searchTerms} placeholder for the client to replace. The URL might be personalized based on the user.", - }, - id: globalIdField(SEARCH_ENGINE_PERSONALIZED), - }), - interfaces: [nodeInterface], -}) - -const userRecruitType = new GraphQLObjectType({ - name: USER_RECRUITS, - description: 'Info about a user recruited by a referring user', - fields: () => ({ - // Ideally, we should build the global ID using the composite value of - // "referringUser" and "userID", which is guaranteed to be unique and can - // resolve back to one object via the nodeInterface. However, for privacy - // concerns, we should then also encrypt the userID because we don't want - // a referrer to know all the IDs of their recruits. For simplicity, we'll - // just use a compound value of "referringUser" and "recruitedAt", which is - // almost certainly unique, and just not implement nodeInterface now. - // https://github.com/graphql/graphql-relay-js/blob/4fdadd3bbf3d5aaf66f1799be3e4eb010c115a4a/src/node/node.js#L138 - id: globalIdField( - USER_RECRUITS, - (recruit) => `${recruit.referringUser}::${recruit.recruitedAt}` - ), - recruitedAt: { - type: GraphQLString, - description: 'ISO datetime string of when the recruited user joined', - }, - }), - // We haven't implemented nodeInterface here because a refetch is unlikely. See above. - // interfaces: [nodeInterface] -}) - -const widgetType = new GraphQLObjectType({ - name: WIDGET, - description: 'App widget', - fields: () => ({ - id: globalIdField(WIDGET), - name: { - type: GraphQLString, - description: 'Widget display name', - }, - type: { - type: GraphQLString, - description: 'Widget type', - }, - icon: { - type: GraphQLString, - description: 'Widget icon', - }, - enabled: { - type: GraphQLBoolean, - description: 'The Widget enabled state', - }, - visible: { - type: GraphQLBoolean, - description: 'The Widget visible state', - }, - data: { - type: GraphQLString, - description: 'Widget data.', - }, - config: { - type: GraphQLString, - description: 'Widget user specific configuration.', - }, - settings: { - type: GraphQLString, - description: 'Widget general configuration.', - }, - }), - interfaces: [nodeInterface], -}) - -const videoAdLogType = new GraphQLObjectType({ - name: VIDEO_AD_LOG, - description: 'Video Ad Log type', - fields: () => ({ - id: globalIdField(VIDEO_AD_LOG), - }), - interfaces: [nodeInterface], -}) -const charityType = new GraphQLObjectType({ - name: CHARITY, - description: 'A charitable charity', - fields: () => ({ - id: globalIdField(CHARITY), - name: { - type: GraphQLString, - description: 'the charity name', - }, - category: { - type: GraphQLString, - description: 'the charity category', - }, - website: { - type: GraphQLString, - description: 'the charity website', - }, - description: { - type: GraphQLString, - description: 'the charity description', - }, - impact: { - type: GraphQLString, - description: 'the charity impact message', - }, - logo: { - type: GraphQLString, - description: 'the charity logo image URI', - }, - image: { - type: GraphQLString, - description: 'the charity post-donation image URI', - }, - imageCaption: { - type: GraphQLString, - description: 'An optional caption for the post-donation image', - }, - vcReceived: { - type: GraphQLInt, - description: - 'The number of VC the charity has received in a given time period.', - args: { - startTime: { type: GraphQLString }, - endTime: { type: GraphQLString }, - }, - resolve: (charity, args, context) => - getCharityVcReceived( - context.user, - charity.id, - args.startTime, - args.endTime - ), - }, - impactMetrics: { - type: new GraphQLList(impactMetricType), - description: 'Impact Metrics that belong to this Charity', - resolve: (charity) => getImpactMetricsByCharityId(charity.id), - }, - longformDescription: { - type: GraphQLString, - description: 'the longform charity impact message', - }, - }), - interfaces: [nodeInterface], -}) -const userImpactType = new GraphQLObjectType({ - name: USER_IMPACT, - description: `a user's charity specific impact`, - fields: () => ({ - id: globalIdField( - USER_IMPACT, - (userImpact) => `${userImpact.userId}::${userImpact.charityId}` - ), - userId: { type: new GraphQLNonNull(GraphQLString) }, - charityId: { type: new GraphQLNonNull(GraphQLString) }, - userImpactMetric: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'a users impact for a specific charity', - }, - pendingUserReferralImpact: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'a users pending impact based on referrals', - }, - pendingUserReferralCount: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'pending user referral count', - }, - visitsUntilNextImpact: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'visits remaining until next recorded impact', - }, - confirmedImpact: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'enables a user to start accruing impact', - }, - hasClaimedLatestReward: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'flag that indicates if user has celebrated latest impact', - }, - }), -}) -const invitedUsersType = new GraphQLObjectType({ - name: INVITED_USERS, - description: `a record of a user email inviting someone`, - fields: () => ({ - id: globalIdField( - INVITED_USERS, - (invitedUsers) => - `${invitedUsers.inviterId}::${invitedUsers.invitedEmail}` - ), - inviterId: { type: new GraphQLNonNull(GraphQLString) }, - invitedEmail: { type: new GraphQLNonNull(GraphQLString) }, - invitedId: { - type: GraphQLString, - description: 'invited users id once user has successfully signed up', - }, - }), -}) -const campaignContentType = new GraphQLObjectType({ - name: 'CampaignContent', - description: 'Text content for campaigns', + + +// TODO: fetch only the fields we need: +// https://github.com/graphql/graphql-js/issues/19#issuecomment-272857189 +const CauseImpactCopy = new GraphQLObjectType({ + name: 'CauseSpecificImpactUI', + description: 'cause specific UI content around impact', fields: () => ({ - titleMarkdown: { + impactCounterText: { type: new GraphQLNonNull(GraphQLString), - description: 'The campaign title, using markdown', + description: 'markdown string: copy for ImpactCounter for normal case', }, - descriptionMarkdown: { + referralRewardTitle: { type: new GraphQLNonNull(GraphQLString), description: - 'The primary campaign text content (paragraphs, links, etc.), using markdown', + 'markdown string: title copy for referralReward UserImpact modal', }, - descriptionMarkdownTwo: { - type: GraphQLString, + referralRewardSubtitle: { + type: new GraphQLNonNull(GraphQLString), description: - 'Additional campaign text content (paragraphs, links, etc.), using markdown', + 'markdown string: subtitle copy for referralReward UserImpact modal', }, - }), -}) - -const campaignTimeType = new GraphQLObjectType({ - name: 'CampaignTime', - description: 'The start and end times (in ISO timestamps) for the campaign', - fields: () => ({ - start: { + referralRewardNotification: { type: new GraphQLNonNull(GraphQLString), - description: 'The start time of the campaign as an ISO timestamp', + description: 'markdown string: copy for referral reward notification', }, - end: { + claimImpactTitle: { type: new GraphQLNonNull(GraphQLString), - description: 'The end time of the campaign as an ISO timestamp', - }, - }), -}) - -const campaignGoalType = new GraphQLObjectType({ - name: 'CampaignGoal', - description: - 'Information on progress toward a target impact goal for the campaign', - fields: () => ({ - targetNumber: { - type: new GraphQLNonNull(GraphQLFloat), - description: - 'The goal number of whatever impact units the campaign is hoping to achieve', - }, - currentNumber: { - type: new GraphQLNonNull(GraphQLFloat), description: - 'The current number of whatever impact units the campaign is hoping to achieve', + 'markdown string: title for claimImpact notification in UserImpact', }, - impactUnitSingular: { + claimImpactSubtitle: { type: new GraphQLNonNull(GraphQLString), description: - 'The English word for the impact unit, singular (e.g. Heart, dollar, puppy)', + 'markdown string: subtitle for claimImpact notification in UserImpact', }, - impactUnitPlural: { + impactIcon: { type: new GraphQLNonNull(GraphQLString), - description: - 'The English word for the impact unit, plural (e.g. Hearts, dollars, puppies)', + description: 'string: name of the icon to use in impact counter', }, - impactVerbPastParticiple: { - type: new GraphQLNonNull(GraphQLString), - description: - 'The past-tense participle English verb that describes achieving the impact unit (e.g. given, raised, adopted)', + walkMeGif: { + type: GraphQLString, + description: 'file name of the cause GIF to use during onboarding', }, - impactVerbPastTense: { + newlyReferredImpactWalkthroughText: { type: new GraphQLNonNull(GraphQLString), description: - 'The simple past-tense English verb that describes achieving the impact unit (e.g. gave, raised, adopted)', - }, - limitProgressToTargetMax: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'If true, do not display a currentNumber greater than the targetNumber. Instead, limit goal progress to 100% of the target.', + 'markdown string: copy for impact walkthrough notification in UserImpact when user is referred', }, - showProgressBarLabel: { - type: new GraphQLNonNull(GraphQLBoolean), + impactWalkthroughText: { + type: new GraphQLNonNull(GraphQLString), description: - 'Whether the progress bar should have labels of the current number and goal number', + 'markdown string: copy for impact walkthrough notification in UserImpact', }, - showProgressBarEndText: { - type: new GraphQLNonNull(GraphQLBoolean), + confirmImpactSubtitle: { + type: new GraphQLNonNull(GraphQLString), description: - 'Whether the progress bar should have an end-of-campaign summary text of the progress', + 'markdown string: copy for confirm impact modal in UserImpact', }, }), }) -const campaignSocialSharingType = new GraphQLObjectType({ - name: 'CampaignSocialSharing', - description: - 'Information on progress toward a target impact goal for the campaign', - fields: () => ({ - url: { - type: new GraphQLNonNull(GraphQLString), - description: 'The URL to share', - }, - EmailShareButtonProps: { - type: new GraphQLObjectType({ - name: 'CampaignSocialSharingEmailProps', - description: 'Props for the email social sharing button', - fields: () => ({ - subject: { - type: new GraphQLNonNull(GraphQLString), - description: 'The email subject', - }, - body: { - type: new GraphQLNonNull(GraphQLString), - description: 'The email body', - }, - }), - }), - description: 'Props for the email social sharing button', - }, - FacebookShareButtonProps: { - type: new GraphQLObjectType({ - name: 'CampaignSocialSharingFacebookProps', - description: 'Props for the Facebook social sharing button', - fields: () => ({ - quote: { - type: new GraphQLNonNull(GraphQLString), - description: 'The text to share to Facebook', - }, - }), - }), - description: 'Props for the Facebook social sharing button', - }, - RedditShareButtonProps: { - type: new GraphQLObjectType({ - name: 'CampaignSocialSharingRedditProps', - description: 'Props for the Reddit social sharing button', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'The text to share to Reddit', - }, - }), - }), - description: 'Props for the Reddit social sharing button', - }, - TumblrShareButtonProps: { - type: new GraphQLObjectType({ - name: 'CampaignSocialSharingTumblrProps', - description: 'Props for the Tumblr social sharing button', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'The Tumblr title', - }, - caption: { - type: new GraphQLNonNull(GraphQLString), - description: 'The Tumblr caption', - }, - }), - }), - description: 'Props for the Tumblr social sharing button', - }, - TwitterShareButtonProps: { - type: new GraphQLObjectType({ - name: 'CampaignSocialSharingTwitterProps', - description: 'Props for the Twitter social sharing button', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'The text to share to Twitter', - }, - related: { - type: new GraphQLNonNull(new GraphQLList(GraphQLString)), - description: 'A list of Twitter handles that relate to the post', - }, - }), - }), - description: 'Props for the Twitter social sharing button', - }, - }), -}) -const campaignThemeType = new GraphQLObjectType({ - name: 'CampaignTheme', - description: 'Theming/styling for the campaign', - fields: () => ({ - color: { - type: new GraphQLObjectType({ - name: 'CampaignThemeColor', - description: 'Color theming for the campaign', - fields: () => ({ - main: { - type: new GraphQLNonNull(GraphQLString), - description: 'The primary color for the theme', - }, - light: { - type: new GraphQLNonNull(GraphQLString), - description: 'The lighter color for the theme', - }, - }), - }), - description: - 'The goal number of whatever impact units the campaign is hoping to achieve', - }, - }), -}) -const campaignType = new GraphQLObjectType({ - name: 'Campaign', - description: 'Campaigns (or "charity spotlights") shown to users.', - fields: () => ({ - campaignId: { - type: GraphQLString, - description: 'The ID of the campaign', - }, - charity: { - type: charityType, - description: 'The charity that this campaign features', - }, - content: { - type: new GraphQLNonNull(campaignContentType), - description: 'The text content for the campaign', - }, - // Deprecated. - endContent: { - type: campaignContentType, - deprecationReason: - 'The content returned by the server will automatically change when the campaign ends.', - description: - 'The text content for the campaign when it has finished (past the end time)', - }, - isLive: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'Whether or not the campaign should currently show to users', - }, - goal: { - type: campaignGoalType, - description: - 'Information on progress toward a target impact goal for the campaign', - }, - // Deprecated. - numNewUsers: { - type: GraphQLInt, - deprecationReason: 'Replaced by the generalized "goal" data.', - description: 'The number of new users who joined during this campaign.', - }, - showCountdownTimer: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'Whether to show a countdown timer for when the campaign will end', - }, - showHeartsDonationButton: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'Whether to show a button to donate hearts to the charity featured in the campaign -- which requires the "charity " field to be defined', - }, - showProgressBar: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'Whether to show a progress bar -- which requires the "goal" field to be defined', - }, - showSocialSharing: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'Whether to show social sharing buttons', - }, - socialSharing: { - type: campaignSocialSharingType, - description: 'Social sharing buttons', - }, - theme: { - type: campaignThemeType, - description: 'Theming/style for the campaign', - }, - time: { - type: new GraphQLNonNull(campaignTimeType), - description: - 'The start and end times (in ISO timestamps) for the campaign', - }, - }), -}) -const appType = new GraphQLObjectType({ - name: 'App', - description: 'Global app fields', - fields: () => ({ - id: globalIdField('App'), - moneyRaised: { - type: GraphQLFloat, - resolve: () => getMoneyRaised(), - }, - dollarsPerDayRate: { - type: GraphQLFloat, - resolve: () => getDollarsPerDayRate(), - }, - referralVcReward: { - type: GraphQLInt, - resolve: () => getReferralVcReward(), - }, - widgets: { - type: widgetConnection, - description: 'All the widgets', - args: connectionArgs, - resolve: (_, args, context) => - connectionFromPromisedArray(getAllBaseWidgets(context.user), args), - }, - charity: { - type: charityType, - description: 'One of the charities', - args: { - charityId: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: (_, { charityId }, context) => - CharityModel.get(context.user, charityId), - }, - charities: { - type: charityConnection, - description: 'All the charities', - args: { - ...connectionArgs, - filters: { - type: new GraphQLInputObjectType({ - name: 'CharitiesFilters', - description: 'Fields on which to filter the list of charities.', - fields: { - isPermanentPartner: { type: GraphQLBoolean }, - }, - }), - }, - }, - resolve: (_, args, context) => { - const { filters } = args - return connectionFromPromisedArray( - getCharities(context.user, filters), - args - ) - }, - }, - causes: { - type: causeConnection, - description: 'All the causes', - args: { - ...connectionArgs, - filters: { - type: new GraphQLInputObjectType({ - name: 'CausesFilters', - description: 'Fields on which to filter the list of causes.', - fields: { - isAvailableToSelect: { type: GraphQLBoolean }, - }, - }), - }, - }, - resolve: (_, args, context) => { - const { filters } = args - return connectionFromPromisedArray( - getCauses(context.user, filters), - args - ) - }, - }, - backgroundImages: { - type: backgroundImageConnection, - description: 'Get all the "legacy" (uncategorized) background Images', - args: connectionArgs, - resolve: (_, args, context) => - connectionFromPromisedArray(getBackgroundImages(context.user), args), - }, - campaign: { - type: campaignType, - description: 'Campaigns (or "charity spotlights") shown to users.', - resolve: (_, args, context) => getCampaign(context.user), - }, - searchEngines: { - type: searchEngineConnection, - description: 'All the search engines', - resolve: (_, args) => connectionFromArray(searchEngines, args), - }, - }), - interfaces: [nodeInterface], -}) + + + + + + + + + +const searchEngineSharedFields = { + engineId: { + type: new GraphQLNonNull(GraphQLString), + description: "Engine's id", + resolve: (engine) => engine.id, + }, + name: { + type: new GraphQLNonNull(GraphQLString), + description: `Name of the Search Engine`, + }, + rank: { + type: new GraphQLNonNull(GraphQLInt), + description: 'what order to display the search engine in a list', + }, + isCharitable: { + type: new GraphQLNonNull(GraphQLBoolean), + description: `Whether or not the user can earn extra impact with this Search Engine`, + }, + inputPrompt: { + type: new GraphQLNonNull(GraphQLString), + description: `Display string to display in the search bar`, + }, +} + + + + + + + + + + + + + + + + + + + // corresponds to UserMission table const SquadMemberInfo = new GraphQLObjectType({ @@ -2004,84 +552,7 @@ const EndOfMissionAward = new GraphQLObjectType({ }) // mostly corresponds to Mission table, rolls up stats -const MissionType = new GraphQLObjectType({ - name: 'Mission', - description: 'A goal that Tabbers complete with a group of friends', - fields: () => ({ - missionId: { - type: new GraphQLNonNull(GraphQLString), - description: 'Mission ID', - }, - status: { - type: new GraphQLNonNull( - new GraphQLEnumType({ - name: 'missionStatus', - description: - 'whether a user has accepted rejected or is pending invitation', - values: { - pending: { value: 'pending' }, - started: { value: 'started' }, - completed: { value: 'completed' }, - }, - }) - ), - description: - 'the current status of the current mission - pending, started, completed', - }, - squadName: { - type: new GraphQLNonNull(GraphQLString), - description: 'the name of the squad', - }, - // sending these both down and calculating on the front end so we can see percent move - tabGoal: { - type: new GraphQLNonNull(GraphQLInt), - description: 'the number of tabs to complete mission', - }, - tabCount: { - type: new GraphQLNonNull(GraphQLInt), - description: "the sum of users' number of tabs towards mission", - }, - acknowledgedMissionComplete: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'if a user has acknowledged mission complete', - }, - acknowledgedMissionStarted: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'if a user has acknowledged mission started', - }, - squadMembers: { - description: 'stats and state of each squad member', - type: new GraphQLNonNull(new GraphQLList(SquadMemberInfo)), - }, - endOfMissionAwards: { - type: new GraphQLNonNull(new GraphQLList(EndOfMissionAward)), - description: - 'the end of mission awards calculated when mission completes', - }, - started: { - type: GraphQLString, - description: 'ISO datetime string of when the mission started', - }, - completed: { - type: GraphQLString, - description: 'ISO datetime string of when the mission completed', - }, - }), -}) -const customErrorType = new GraphQLObjectType({ - name: 'CustomError', - description: 'For expected errors, such as during form validation', - fields: () => ({ - code: { - type: GraphQLString, - description: 'The error code', - }, - message: { - type: GraphQLString, - description: 'The error message', - }, - }), -}) + /** * Define your own connection types here. @@ -2359,30 +830,7 @@ const logReferralLinkClickMutation = mutationWithClientMutationId({ * Log earned revenue for a user */ -const EncodedRevenueValueType = new GraphQLInputObjectType({ - name: 'EncodedRevenueValue', - description: 'An object representing a single revenue value', - fields: { - encodingType: { - type: new GraphQLNonNull( - new GraphQLEnumType({ - name: 'EncodedRevenueValueTypeEnum', - description: - 'The type of transformation we should use to resolve the object into a revenue value', - values: { - AMAZON_CPM: { value: 'AMAZON_CPM' }, - }, - }) - ), - }, - encodedValue: { - description: - 'A string that we can decode to a revenue value (float) using the "encodingType" method', - type: new GraphQLNonNull(GraphQLString), - }, - adSize: { type: GraphQLString }, - }, -}) + const logUserRevenueMutation = mutationWithClientMutationId({ name: 'LogUserRevenue', @@ -3248,66 +1696,13 @@ const createUserExperimentMutation = mutationWithClientMutationId({ * This is the type that will be the root of our query, * and the entry point into our schema. */ -const queryType = new GraphQLObjectType({ - name: 'Query', - fields: () => ({ - node: nodeField, - // Add your own root fields here - app: { - type: appType, - resolve: () => App.getApp(1), - }, - user: { - type: userType, - args: { - userId: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: (_, args, context) => UserModel.get(context.user, args.userId), - }, - wildfire: { - type: wildfireType, - args: { - userId: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: (_, args) => getCauseForWildfire(args.userId), - }, - userImpact: { - type: userImpactType, - args: { - userId: { type: new GraphQLNonNull(GraphQLString) }, - charityId: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: async (_, args, context) => { - const { userId, charityId } = args - return ( - await UserImpactModel.getOrCreate(context.user, { - userId, - charityId, - }) - ).item - }, - }, - }), -}) + /** * This is the type that will be the root of our mutations, * and the entry point into performing writes in our schema. */ -const mutationType = new GraphQLObjectType({ - name: 'Mutation', - fields: () => ({ - logTab: logTabMutation, - updateImpact: updateImpactMutation, - createInvitedUsers: createInvitedUsersMutation, - createSquadInvites: createSquadInvitesMutation, - logSearch: logSearchMutation, - logUserRevenue: logUserRevenueMutation, - logUserDataConsent: logUserDataConsentMutation, - donateVc: donateVcMutation, - mergeIntoExistingUser: mergeIntoExistingUserMutation, - logEmailVerified: logEmailVerifiedMutation, - logReferralLinkClick: logReferralLinkClickMutation, + setUserBkgImage: setUserBkgImageMutation, setUserBkgColor: setUserBkgColorMutation, diff --git a/graphql/data/types/CauseOnboardingCopyType.js b/graphql/data/types/CauseOnboardingCopyType.js new file mode 100644 index 000000000..1a00ef376 --- /dev/null +++ b/graphql/data/types/CauseOnboardingCopyType.js @@ -0,0 +1,38 @@ +export default new GraphQLObjectType({ + name: 'OnboardingUICopy', + description: 'cause specific UI content around onboarding', + fields: () => ({ + steps: { + type: new GraphQLNonNull( + new GraphQLList( + new GraphQLObjectType({ + name: 'onboardingUIStep', + description: 'ui content for each onboarding step', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + description: 'markdown title for onboarding step', + }, + subtitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'markdown subtitle for onboarding step', + }, + imgName: { + type: new GraphQLNonNull(GraphQLString), + description: 'name of image to show', + }, + }), + }) + ) + ), + description: 'the steps array in onboarding', + resolve: (onboarding) => onboarding.steps, + }, + firstTabIntroDescription: { + type: new GraphQLNonNull(GraphQLString), + resolve: (onboarding) => onboarding.firstTabIntroDescription, + description: + 'markdown string shown when prompting the user to open their first tab, currently info about cat treats', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/CauseSharingCopyType.js b/graphql/data/types/CauseSharingCopyType.js new file mode 100644 index 000000000..d80374e0e --- /dev/null +++ b/graphql/data/types/CauseSharingCopyType.js @@ -0,0 +1,46 @@ +export default new GraphQLObjectType({ + name: 'SharingUICopy', + description: 'cause specific UI content around sharing', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + description: 'markdown for modal title', + }, + subtitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'markdown for modal subtitle', + }, + imgCategory: { + type: new GraphQLNonNull(GraphQLString), + description: `value to use for img switch statement on frontend, probably ‘cats’ or ‘seas’`, + }, + shareImage: { + type: new GraphQLNonNull(GraphQLString), + description: 'Image to use in email invite dialog', + }, + sentImage: { + type: new GraphQLNonNull(GraphQLString), + description: 'Image shown after email invite sent', + }, + redditButtonTitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'copy for reddit button', + }, + facebookButtonTitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'copy for facebook button', + }, + twitterButtonTitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'copy for twitter button', + }, + tumblrTitle: { + type: new GraphQLNonNull(GraphQLString), + description: 'copy for tumblr button', + }, + tumblrCaption: { + type: new GraphQLNonNull(GraphQLString), + description: 'copy for tumblr caption', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/CauseThemeType.js b/graphql/data/types/CauseThemeType.js new file mode 100644 index 000000000..49f7d4567 --- /dev/null +++ b/graphql/data/types/CauseThemeType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'CauseTheming', + description: 'css properties for a specific cause', + fields: () => ({ + primaryColor: { + type: new GraphQLNonNull(GraphQLString), + description: 'the primary color hex value', + }, + secondaryColor: { + type: new GraphQLNonNull(GraphQLString), + description: 'the secondary color hex value', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/CauseType.js b/graphql/data/types/CauseType.js new file mode 100644 index 000000000..6e9f59b3b --- /dev/null +++ b/graphql/data/types/CauseType.js @@ -0,0 +1,115 @@ +export default new GraphQLObjectType({ + name: CAUSE, + description: 'all cause specific data and ui content', + fields: () => ({ + id: globalIdField(CAUSE), + about: { + type: new GraphQLNonNull(GraphQLString), + description: `Markdown - content that populates an "About the Cause" page`, + }, + name: { + type: new GraphQLNonNull(GraphQLString), + description: `String used to describe cause in account page`, + }, + nameForShop: { + type: new GraphQLNonNull(GraphQLString), + description: `String used to describe cause in the shop extension`, + }, + isAvailableToSelect: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'boolean if cause is available to select in ui', + }, + causeId: { + type: new GraphQLNonNull(GraphQLString), + description: "Cause's id", + resolve: (cause) => cause.id, + }, + icon: { + type: new GraphQLNonNull(GraphQLString), + description: `Name of an icon, mapping to an icon component on the frontend`, + }, + landingPagePath: { + type: new GraphQLNonNull(GraphQLString), + description: `URL path for the landing page belonging to this cause`, + }, + landingPagePhrase: { + type: new GraphQLNonNull(GraphQLString), + description: `Phrase for the landing page belonging to this cause`, + resolve: (cause, _, context) => getLandingPagePhrase(context.user, cause), + }, + individualImpactEnabled: { + type: new GraphQLNonNull(GraphQLBoolean), + description: `Whether or not the current cause supports individual impact`, + deprecationReason: 'Replaced by "impactType" field.', + }, + impactType: { + type: new GraphQLNonNull( + new GraphQLEnumType({ + name: 'causeImpactType', + description: + "The type of charitable impact that's enabled for this cause", + values: { + none: { value: CAUSE_IMPACT_TYPES.none }, + individual: { value: CAUSE_IMPACT_TYPES.individual }, + group: { value: CAUSE_IMPACT_TYPES.group }, + individual_and_group: { + value: CAUSE_IMPACT_TYPES.individual_and_group, + }, + }, + }) + ), + description: `Whether or not the current cause supports individual impact`, + }, + impactVisits: { + type: GraphQLInt, + description: `number of visits required for each impact unit (e.g. 14 for cat charity)`, + }, + impact: { + type: CauseImpactCopy, + description: 'the impact object on cause model', + resolve: (cause) => cause.impact, + }, + theme: { + type: new GraphQLNonNull(CauseThemeType), + description: 'the theme object on cause model', + resolve: (cause) => cause.theme, + }, + sharing: { + type: new GraphQLNonNull(CauseSharingCopyType), + description: 'the sharing object on cause model', + resolve: (cause) => cause.sharing, + }, + onboarding: { + type: new GraphQLNonNull(CauseOnboardingCopyType), + resolve: (cause) => cause.onboarding, + description: 'the onboarding object on cause model', + }, + groupImpactMetric: { + type: groupImpactMetricType, + description: + 'the group impact metric currently associated with this cause', + resolve: (cause, _args, context) => + getGroupImpactMetricForCause(context.user, cause.id), + }, + groupImpactMetricCount: { + type: GraphQLInt, + description: + 'how many times this particular group impact metric has been completed for this cause', + resolve: (cause, _args, context) => + getCauseImpactMetricCount(context.user, cause.id), + }, + charity: { + type: charityType, + description: 'Charity that this cause is currently generating impact for', + resolve: (cause, _, context) => + CharityModel.get(context.user, cause.charityId), + }, + charities: { + type: new GraphQLList(charityType), + description: 'Charity that this cause is currently generating impact for', + resolve: (cause, _, context) => + getCharitiesForCause(context.user, cause.charityIds, cause.charityId), + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/EncodedRevenueValueType.js b/graphql/data/types/EncodedRevenueValueType.js new file mode 100644 index 000000000..68e6b5a78 --- /dev/null +++ b/graphql/data/types/EncodedRevenueValueType.js @@ -0,0 +1,24 @@ +export default new GraphQLInputObjectType({ + name: 'EncodedRevenueValue', + description: 'An object representing a single revenue value', + fields: { + encodingType: { + type: new GraphQLNonNull( + new GraphQLEnumType({ + name: 'EncodedRevenueValueTypeEnum', + description: + 'The type of transformation we should use to resolve the object into a revenue value', + values: { + AMAZON_CPM: { value: 'AMAZON_CPM' }, + }, + }) + ), + }, + encodedValue: { + description: + 'A string that we can decode to a revenue value (float) using the "encodingType" method', + type: new GraphQLNonNull(GraphQLString), + }, + adSize: { type: GraphQLString }, + }, +}); \ No newline at end of file diff --git a/graphql/data/types/ExperimentActionsOutputType.js b/graphql/data/types/ExperimentActionsOutputType.js new file mode 100644 index 000000000..291c09f81 --- /dev/null +++ b/graphql/data/types/ExperimentActionsOutputType.js @@ -0,0 +1,5 @@ +export default new GraphQLObjectType({ + name: 'ExperimentActionsOutput', + description: 'The actions a user has taken in an experiment', + fields: () => experimentActionsFields, +}); \ No newline at end of file diff --git a/graphql/data/types/ExperimentActionsType.js b/graphql/data/types/ExperimentActionsType.js new file mode 100644 index 000000000..e6f1cfec1 --- /dev/null +++ b/graphql/data/types/ExperimentActionsType.js @@ -0,0 +1,5 @@ +export default new GraphQLInputObjectType({ + name: 'ExperimentActions', + description: 'The actions a user may take in an experiment', + fields: experimentActionsFields, +}); \ No newline at end of file diff --git a/graphql/data/types/ExperimentGroupsType.js b/graphql/data/types/ExperimentGroupsType.js new file mode 100644 index 000000000..ccd38f9c5 --- /dev/null +++ b/graphql/data/types/ExperimentGroupsType.js @@ -0,0 +1,114 @@ +export default new GraphQLInputObjectType({ + name: 'ExperimentGroups', + description: 'The experimental groups to which the user is assigned', + fields: { + anonSignIn: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupAnonSignIn', + description: 'The test of allowing anonymous user authentication', + values: { + NONE: { value: experimentConfig.anonSignIn.NONE }, + AUTHED_USER_ONLY: { + value: experimentConfig.anonSignIn.AUTHED_USER_ONLY, + }, + ANONYMOUS_ALLOWED: { + value: experimentConfig.anonSignIn.ANONYMOUS_ALLOWED, + }, + }, + }), + }, + variousAdSizes: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupVariousAdSizes', + description: 'The test of enabling many different ad sizes', + values: { + NONE: { value: experimentConfig.variousAdSizes.NONE }, + STANDARD: { value: experimentConfig.variousAdSizes.STANDARD }, + VARIOUS: { value: experimentConfig.variousAdSizes.VARIOUS }, + }, + }), + }, + thirdAd: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupThirdAd', + description: 'The test of enabling a third ad', + values: { + NONE: { value: experimentConfig.thirdAd.NONE }, + TWO_ADS: { value: experimentConfig.thirdAd.TWO_ADS }, + THREE_ADS: { value: experimentConfig.thirdAd.THREE_ADS }, + }, + }), + }, + oneAdForNewUsers: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupOneAdForNewUsers', + description: 'The test of showing only one ad to new users', + values: { + NONE: { value: experimentConfig.oneAdForNewUsers.NONE }, + DEFAULT: { value: experimentConfig.oneAdForNewUsers.DEFAULT }, + ONE_AD_AT_FIRST: { + value: experimentConfig.oneAdForNewUsers.ONE_AD_AT_FIRST, + }, + }, + }), + }, + adExplanation: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupAdExplanation', + description: 'The test of showing an explanation of why there are ads', + values: { + NONE: { value: experimentConfig.adExplanation.NONE }, + DEFAULT: { value: experimentConfig.adExplanation.DEFAULT }, + SHOW_EXPLANATION: { + value: experimentConfig.adExplanation.SHOW_EXPLANATION, + }, + }, + }), + }, + searchIntro: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupSearchIntro', + description: + 'The test of showing an introduction message to Search for a Cause', + values: { + NONE: { value: experimentConfig.searchIntro.NONE }, + NO_INTRO: { value: experimentConfig.searchIntro.NO_INTRO }, + INTRO_A: { + value: experimentConfig.searchIntro.INTRO_A, + }, + INTRO_HOMEPAGE: { + value: experimentConfig.searchIntro.INTRO_HOMEPAGE, + }, + }, + }), + }, + referralNotification: { + type: new GraphQLEnumType({ + name: 'ExperimentGroupReferralNotification', + description: + 'The test of showing a notification to ask users to recruit friends', + values: { + NONE: { value: experimentConfig.referralNotification.NONE }, + NO_NOTIFICATION: { + value: experimentConfig.referralNotification.NO_NOTIFICATION, + }, + COPY_A: { + value: experimentConfig.referralNotification.COPY_A, + }, + COPY_B: { + value: experimentConfig.referralNotification.COPY_B, + }, + COPY_C: { + value: experimentConfig.referralNotification.COPY_C, + }, + COPY_D: { + value: experimentConfig.referralNotification.COPY_D, + }, + COPY_E: { + value: experimentConfig.referralNotification.COPY_E, + }, + }, + }), + }, + }, +}); \ No newline at end of file diff --git a/graphql/data/types/MissionType.js b/graphql/data/types/MissionType.js new file mode 100644 index 000000000..65e2fa68f --- /dev/null +++ b/graphql/data/types/MissionType.js @@ -0,0 +1,64 @@ +export default new GraphQLObjectType({ + name: 'Mission', + description: 'A goal that Tabbers complete with a group of friends', + fields: () => ({ + missionId: { + type: new GraphQLNonNull(GraphQLString), + description: 'Mission ID', + }, + status: { + type: new GraphQLNonNull( + new GraphQLEnumType({ + name: 'missionStatus', + description: + 'whether a user has accepted rejected or is pending invitation', + values: { + pending: { value: 'pending' }, + started: { value: 'started' }, + completed: { value: 'completed' }, + }, + }) + ), + description: + 'the current status of the current mission - pending, started, completed', + }, + squadName: { + type: new GraphQLNonNull(GraphQLString), + description: 'the name of the squad', + }, + // sending these both down and calculating on the front end so we can see percent move + tabGoal: { + type: new GraphQLNonNull(GraphQLInt), + description: 'the number of tabs to complete mission', + }, + tabCount: { + type: new GraphQLNonNull(GraphQLInt), + description: "the sum of users' number of tabs towards mission", + }, + acknowledgedMissionComplete: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'if a user has acknowledged mission complete', + }, + acknowledgedMissionStarted: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'if a user has acknowledged mission started', + }, + squadMembers: { + description: 'stats and state of each squad member', + type: new GraphQLNonNull(new GraphQLList(SquadMemberInfo)), + }, + endOfMissionAwards: { + type: new GraphQLNonNull(new GraphQLList(EndOfMissionAward)), + description: + 'the end of mission awards calculated when mission completes', + }, + started: { + type: GraphQLString, + description: 'ISO datetime string of when the mission started', + }, + completed: { + type: GraphQLString, + description: 'ISO datetime string of when the mission completed', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/SearchEnginePersonalizedType.js b/graphql/data/types/SearchEnginePersonalizedType.js new file mode 100644 index 000000000..0780677d8 --- /dev/null +++ b/graphql/data/types/SearchEnginePersonalizedType.js @@ -0,0 +1,15 @@ +export default new GraphQLObjectType({ + name: SEARCH_ENGINE_PERSONALIZED, + description: + 'SearchEngineType extended with fields potentially personalized to the user', + fields: () => ({ + ...searchEngineSharedFields, + searchUrlPersonalized: { + type: new GraphQLNonNull(GraphQLString), + description: + "Use this for the user's search behavior. A search destination URL, with a {searchTerms} placeholder for the client to replace. The URL might be personalized based on the user.", + }, + id: globalIdField(SEARCH_ENGINE_PERSONALIZED), + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/SearchEngineType.js b/graphql/data/types/SearchEngineType.js new file mode 100644 index 000000000..2b29744a0 --- /dev/null +++ b/graphql/data/types/SearchEngineType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: SEARCH_ENGINE, + description: 'all important data for a search engine.', + fields: () => ({ + ...searchEngineSharedFields, + id: globalIdField(SEARCH_ENGINE), + searchUrl: { + type: new GraphQLNonNull(GraphQLString), + description: + 'A search destination URL, with a {searchTerms} placeholder for the client to replace. Use `user.searchEngine` if the user is authenticated.', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/appType.js b/graphql/data/types/appType.js new file mode 100644 index 000000000..2ff52ea6f --- /dev/null +++ b/graphql/data/types/appType.js @@ -0,0 +1,99 @@ +export default new GraphQLObjectType({ + name: 'App', + description: 'Global app fields', + fields: () => ({ + id: globalIdField('App'), + moneyRaised: { + type: GraphQLFloat, + resolve: () => getMoneyRaised(), + }, + dollarsPerDayRate: { + type: GraphQLFloat, + resolve: () => getDollarsPerDayRate(), + }, + referralVcReward: { + type: GraphQLInt, + resolve: () => getReferralVcReward(), + }, + widgets: { + type: widgetConnection, + description: 'All the widgets', + args: connectionArgs, + resolve: (_, args, context) => + connectionFromPromisedArray(getAllBaseWidgets(context.user), args), + }, + charity: { + type: charityType, + description: 'One of the charities', + args: { + charityId: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: (_, { charityId }, context) => + CharityModel.get(context.user, charityId), + }, + charities: { + type: charityConnection, + description: 'All the charities', + args: { + ...connectionArgs, + filters: { + type: new GraphQLInputObjectType({ + name: 'CharitiesFilters', + description: 'Fields on which to filter the list of charities.', + fields: { + isPermanentPartner: { type: GraphQLBoolean }, + }, + }), + }, + }, + resolve: (_, args, context) => { + const { filters } = args + return connectionFromPromisedArray( + getCharities(context.user, filters), + args + ) + }, + }, + causes: { + type: causeConnection, + description: 'All the causes', + args: { + ...connectionArgs, + filters: { + type: new GraphQLInputObjectType({ + name: 'CausesFilters', + description: 'Fields on which to filter the list of causes.', + fields: { + isAvailableToSelect: { type: GraphQLBoolean }, + }, + }), + }, + }, + resolve: (_, args, context) => { + const { filters } = args + return connectionFromPromisedArray( + getCauses(context.user, filters), + args + ) + }, + }, + backgroundImages: { + type: backgroundImageConnection, + description: 'Get all the "legacy" (uncategorized) background Images', + args: connectionArgs, + resolve: (_, args, context) => + connectionFromPromisedArray(getBackgroundImages(context.user), args), + }, + campaign: { + type: campaignType, + description: 'Campaigns (or "charity spotlights") shown to users.', + resolve: (_, args, context) => getCampaign(context.user), + }, + searchEngines: { + type: searchEngineConnection, + description: 'All the search engines', + resolve: (_, args) => connectionFromArray(searchEngines, args), + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/backgroundImageType.js b/graphql/data/types/backgroundImageType.js new file mode 100644 index 000000000..001a637e5 --- /dev/null +++ b/graphql/data/types/backgroundImageType.js @@ -0,0 +1,37 @@ +export default new GraphQLObjectType({ + name: BACKGROUND_IMAGE, + description: 'A background image', + fields: () => ({ + id: globalIdField(BACKGROUND_IMAGE), + name: { + type: GraphQLString, + description: 'the background image name', + }, + image: { + type: GraphQLString, + description: 'The image filename', + }, + imageURL: { + type: GraphQLString, + description: 'The image file URL', + }, + category: { + type: new GraphQLNonNull(GraphQLString), + description: 'the category that the image falls into', + }, + thumbnail: { + type: GraphQLString, + description: 'The image thumbnail filename', + }, + thumbnailURL: { + type: GraphQLString, + description: 'The image thumbnail URL', + }, + timestamp: { + type: GraphQLString, + description: + 'ISO datetime string of when the background image was last set', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/campaignContentType.js b/graphql/data/types/campaignContentType.js new file mode 100644 index 000000000..049c76f1b --- /dev/null +++ b/graphql/data/types/campaignContentType.js @@ -0,0 +1,20 @@ +export default new GraphQLObjectType({ + name: 'CampaignContent', + description: 'Text content for campaigns', + fields: () => ({ + titleMarkdown: { + type: new GraphQLNonNull(GraphQLString), + description: 'The campaign title, using markdown', + }, + descriptionMarkdown: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The primary campaign text content (paragraphs, links, etc.), using markdown', + }, + descriptionMarkdownTwo: { + type: GraphQLString, + description: + 'Additional campaign text content (paragraphs, links, etc.), using markdown', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/campaignGoalType.js b/graphql/data/types/campaignGoalType.js new file mode 100644 index 000000000..6dd9254d2 --- /dev/null +++ b/graphql/data/types/campaignGoalType.js @@ -0,0 +1,52 @@ +export default new GraphQLObjectType({ + name: 'CampaignGoal', + description: + 'Information on progress toward a target impact goal for the campaign', + fields: () => ({ + targetNumber: { + type: new GraphQLNonNull(GraphQLFloat), + description: + 'The goal number of whatever impact units the campaign is hoping to achieve', + }, + currentNumber: { + type: new GraphQLNonNull(GraphQLFloat), + description: + 'The current number of whatever impact units the campaign is hoping to achieve', + }, + impactUnitSingular: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The English word for the impact unit, singular (e.g. Heart, dollar, puppy)', + }, + impactUnitPlural: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The English word for the impact unit, plural (e.g. Hearts, dollars, puppies)', + }, + impactVerbPastParticiple: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The past-tense participle English verb that describes achieving the impact unit (e.g. given, raised, adopted)', + }, + impactVerbPastTense: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The simple past-tense English verb that describes achieving the impact unit (e.g. gave, raised, adopted)', + }, + limitProgressToTargetMax: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'If true, do not display a currentNumber greater than the targetNumber. Instead, limit goal progress to 100% of the target.', + }, + showProgressBarLabel: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'Whether the progress bar should have labels of the current number and goal number', + }, + showProgressBarEndText: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'Whether the progress bar should have an end-of-campaign summary text of the progress', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/campaignSocialSharingType.js b/graphql/data/types/campaignSocialSharingType.js new file mode 100644 index 000000000..a61554670 --- /dev/null +++ b/graphql/data/types/campaignSocialSharingType.js @@ -0,0 +1,88 @@ +export default new GraphQLObjectType({ + name: 'CampaignSocialSharing', + description: + 'Information on progress toward a target impact goal for the campaign', + fields: () => ({ + url: { + type: new GraphQLNonNull(GraphQLString), + description: 'The URL to share', + }, + EmailShareButtonProps: { + type: new GraphQLObjectType({ + name: 'CampaignSocialSharingEmailProps', + description: 'Props for the email social sharing button', + fields: () => ({ + subject: { + type: new GraphQLNonNull(GraphQLString), + description: 'The email subject', + }, + body: { + type: new GraphQLNonNull(GraphQLString), + description: 'The email body', + }, + }), + }), + description: 'Props for the email social sharing button', + }, + FacebookShareButtonProps: { + type: new GraphQLObjectType({ + name: 'CampaignSocialSharingFacebookProps', + description: 'Props for the Facebook social sharing button', + fields: () => ({ + quote: { + type: new GraphQLNonNull(GraphQLString), + description: 'The text to share to Facebook', + }, + }), + }), + description: 'Props for the Facebook social sharing button', + }, + RedditShareButtonProps: { + type: new GraphQLObjectType({ + name: 'CampaignSocialSharingRedditProps', + description: 'Props for the Reddit social sharing button', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + description: 'The text to share to Reddit', + }, + }), + }), + description: 'Props for the Reddit social sharing button', + }, + TumblrShareButtonProps: { + type: new GraphQLObjectType({ + name: 'CampaignSocialSharingTumblrProps', + description: 'Props for the Tumblr social sharing button', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + description: 'The Tumblr title', + }, + caption: { + type: new GraphQLNonNull(GraphQLString), + description: 'The Tumblr caption', + }, + }), + }), + description: 'Props for the Tumblr social sharing button', + }, + TwitterShareButtonProps: { + type: new GraphQLObjectType({ + name: 'CampaignSocialSharingTwitterProps', + description: 'Props for the Twitter social sharing button', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + description: 'The text to share to Twitter', + }, + related: { + type: new GraphQLNonNull(new GraphQLList(GraphQLString)), + description: 'A list of Twitter handles that relate to the post', + }, + }), + }), + description: 'Props for the Twitter social sharing button', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/campaignThemeType.js b/graphql/data/types/campaignThemeType.js new file mode 100644 index 000000000..6b45fff47 --- /dev/null +++ b/graphql/data/types/campaignThemeType.js @@ -0,0 +1,24 @@ +export default new GraphQLObjectType({ + name: 'CampaignTheme', + description: 'Theming/styling for the campaign', + fields: () => ({ + color: { + type: new GraphQLObjectType({ + name: 'CampaignThemeColor', + description: 'Color theming for the campaign', + fields: () => ({ + main: { + type: new GraphQLNonNull(GraphQLString), + description: 'The primary color for the theme', + }, + light: { + type: new GraphQLNonNull(GraphQLString), + description: 'The lighter color for the theme', + }, + }), + }), + description: + 'The goal number of whatever impact units the campaign is hoping to achieve', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/campaignTimeType.js b/graphql/data/types/campaignTimeType.js new file mode 100644 index 000000000..53c7ba7aa --- /dev/null +++ b/graphql/data/types/campaignTimeType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'CampaignTime', + description: 'The start and end times (in ISO timestamps) for the campaign', + fields: () => ({ + start: { + type: new GraphQLNonNull(GraphQLString), + description: 'The start time of the campaign as an ISO timestamp', + }, + end: { + type: new GraphQLNonNull(GraphQLString), + description: 'The end time of the campaign as an ISO timestamp', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/campaignType.js b/graphql/data/types/campaignType.js new file mode 100644 index 000000000..8627d852c --- /dev/null +++ b/graphql/data/types/campaignType.js @@ -0,0 +1,73 @@ +export default new GraphQLObjectType({ + name: 'Campaign', + description: 'Campaigns (or "charity spotlights") shown to users.', + fields: () => ({ + campaignId: { + type: GraphQLString, + description: 'The ID of the campaign', + }, + charity: { + type: charityType, + description: 'The charity that this campaign features', + }, + content: { + type: new GraphQLNonNull(campaignContentType), + description: 'The text content for the campaign', + }, + // Deprecated. + endContent: { + type: campaignContentType, + deprecationReason: + 'The content returned by the server will automatically change when the campaign ends.', + description: + 'The text content for the campaign when it has finished (past the end time)', + }, + isLive: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether or not the campaign should currently show to users', + }, + goal: { + type: campaignGoalType, + description: + 'Information on progress toward a target impact goal for the campaign', + }, + // Deprecated. + numNewUsers: { + type: GraphQLInt, + deprecationReason: 'Replaced by the generalized "goal" data.', + description: 'The number of new users who joined during this campaign.', + }, + showCountdownTimer: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'Whether to show a countdown timer for when the campaign will end', + }, + showHeartsDonationButton: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'Whether to show a button to donate hearts to the charity featured in the campaign -- which requires the "charity " field to be defined', + }, + showProgressBar: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'Whether to show a progress bar -- which requires the "goal" field to be defined', + }, + showSocialSharing: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether to show social sharing buttons', + }, + socialSharing: { + type: campaignSocialSharingType, + description: 'Social sharing buttons', + }, + theme: { + type: campaignThemeType, + description: 'Theming/style for the campaign', + }, + time: { + type: new GraphQLNonNull(campaignTimeType), + description: + 'The start and end times (in ISO timestamps) for the campaign', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/charityType.js b/graphql/data/types/charityType.js new file mode 100644 index 000000000..ae7a0abb2 --- /dev/null +++ b/graphql/data/types/charityType.js @@ -0,0 +1,65 @@ +export default new GraphQLObjectType({ + name: CHARITY, + description: 'A charitable charity', + fields: () => ({ + id: globalIdField(CHARITY), + name: { + type: GraphQLString, + description: 'the charity name', + }, + category: { + type: GraphQLString, + description: 'the charity category', + }, + website: { + type: GraphQLString, + description: 'the charity website', + }, + description: { + type: GraphQLString, + description: 'the charity description', + }, + impact: { + type: GraphQLString, + description: 'the charity impact message', + }, + logo: { + type: GraphQLString, + description: 'the charity logo image URI', + }, + image: { + type: GraphQLString, + description: 'the charity post-donation image URI', + }, + imageCaption: { + type: GraphQLString, + description: 'An optional caption for the post-donation image', + }, + vcReceived: { + type: GraphQLInt, + description: + 'The number of VC the charity has received in a given time period.', + args: { + startTime: { type: GraphQLString }, + endTime: { type: GraphQLString }, + }, + resolve: (charity, args, context) => + getCharityVcReceived( + context.user, + charity.id, + args.startTime, + args.endTime + ), + }, + impactMetrics: { + type: new GraphQLList(impactMetricType), + description: 'Impact Metrics that belong to this Charity', + resolve: (charity) => getImpactMetricsByCharityId(charity.id), + }, + longformDescription: { + type: GraphQLString, + description: 'the longform charity impact message', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/customErrorType.js b/graphql/data/types/customErrorType.js new file mode 100644 index 000000000..54c4e725d --- /dev/null +++ b/graphql/data/types/customErrorType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'CustomError', + description: 'For expected errors, such as during form validation', + fields: () => ({ + code: { + type: GraphQLString, + description: 'The error code', + }, + message: { + type: GraphQLString, + description: 'The error message', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/featureType.js b/graphql/data/types/featureType.js new file mode 100644 index 000000000..cfeeb6979 --- /dev/null +++ b/graphql/data/types/featureType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: FEATURE, + description: 'Feature name and variation value pair applicable to a user.', + fields: () => ({ + featureName: { + type: new GraphQLNonNull(GraphQLString), + description: `Name of the Feature`, + }, + variation: { + type: new GraphQLNonNull(GraphQLString), + description: 'the value of the variation for this specific user', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/groupImpactMetricType.js b/graphql/data/types/groupImpactMetricType.js new file mode 100644 index 000000000..2d660cba4 --- /dev/null +++ b/graphql/data/types/groupImpactMetricType.js @@ -0,0 +1,50 @@ +export default new GraphQLObjectType({ + name: GROUP_IMPACT_METRIC, + description: 'A specific instance of GroupImpactMetric', + fields: () => ({ + id: globalIdField(GROUP_IMPACT_METRIC), + cause: { + type: CauseType, + description: 'The cause ID this GroupImpactMetric belongs to', + resolve: (groupImpactMetric, _args, context) => + getCause(context.user, groupImpactMetric.causeId), + }, + impactMetric: { + type: impactMetricType, + description: 'Information about the ImpactMetric', + resolve: (groupImpactMetric) => + getImpactMetricById(groupImpactMetric.impactMetricId), + }, + dollarProgress: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far', + }, + dollarGoal: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far', + }, + dateStarted: { + type: GraphQLString, + description: + 'ISO datetime string of when this GroupImpactMetric was started', + }, + dateCompleted: { + type: GraphQLString, + description: + 'ISO datetime string of when this GroupImpactMetric was ended', + }, + dollarProgressFromTab: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far from tabs', + }, + dollarProgressFromSearch: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far from search', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/impactMetricType.js b/graphql/data/types/impactMetricType.js new file mode 100644 index 000000000..bbf205dad --- /dev/null +++ b/graphql/data/types/impactMetricType.js @@ -0,0 +1,47 @@ +export default new GraphQLObjectType({ + name: IMPACT_METRIC, + description: 'An instance of ImpactMetric', + fields: () => ({ + id: globalIdField(IMPACT_METRIC), + charity: { + type: charityType, + description: 'Charity ID that this impact metric belongs to', + resolve: (gim, _args, context) => + CharityModel.get(context.user, gim.charityId), + }, + dollarAmount: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'Dollar amount (in micro USDs) required to achieve an instance of this ImpactMetric', + }, + description: { + type: new GraphQLNonNull(GraphQLString), + description: 'Markdown description of this ImpactMetric', + }, + whyValuableDescription: { + type: new GraphQLNonNull(GraphQLString), + description: + 'Markdown. A shorter version of the description that answers "why this impact matters".', + }, + metricTitle: { + type: new GraphQLNonNull(GraphQLString), + description: + 'Metric title. Should be placeable in a sentence. Example: "1 home visit"', + }, + impactTitle: { + type: new GraphQLNonNull(GraphQLString), + description: + 'Impact action title. Should be a longer title with a verb as well as a noun. Example: "Provide 1 visit from a community health worker"', + }, + active: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether or not this GroupImpactMetric is still active', + }, + impactCountPerMetric: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'How many instances of the impact are provided per completion of a GroupImpactMetric run.', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/invitedUsersType.js b/graphql/data/types/invitedUsersType.js new file mode 100644 index 000000000..38c17caa7 --- /dev/null +++ b/graphql/data/types/invitedUsersType.js @@ -0,0 +1,17 @@ +export default new GraphQLObjectType({ + name: INVITED_USERS, + description: `a record of a user email inviting someone`, + fields: () => ({ + id: globalIdField( + INVITED_USERS, + (invitedUsers) => + `${invitedUsers.inviterId}::${invitedUsers.invitedEmail}` + ), + inviterId: { type: new GraphQLNonNull(GraphQLString) }, + invitedEmail: { type: new GraphQLNonNull(GraphQLString) }, + invitedId: { + type: GraphQLString, + description: 'invited users id once user has successfully signed up', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/maxSearchesDayType.js b/graphql/data/types/maxSearchesDayType.js new file mode 100644 index 000000000..9b94a299c --- /dev/null +++ b/graphql/data/types/maxSearchesDayType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'MaxSearchesDay', + description: "Info about the user's day of most searches", + fields: () => ({ + date: { + type: GraphQLString, + description: 'The day (datetime)the most searches occurred', + }, + numSearches: { + type: GraphQLInt, + description: 'The number of searches made on that day', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/maxTabsDayType.js b/graphql/data/types/maxTabsDayType.js new file mode 100644 index 000000000..2239d3831 --- /dev/null +++ b/graphql/data/types/maxTabsDayType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'MaxTabsDay', + description: "Info about the user's day of most opened tabs", + fields: () => ({ + date: { + type: GraphQLString, + description: 'The day the most tabs were opened', + }, + numTabs: { + type: GraphQLInt, + description: 'The number of tabs opened on that day', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/mutationType.js b/graphql/data/types/mutationType.js new file mode 100644 index 000000000..24df16435 --- /dev/null +++ b/graphql/data/types/mutationType.js @@ -0,0 +1,14 @@ +export default new GraphQLObjectType({ + name: 'Mutation', + fields: () => ({ + logTab: logTabMutation, + updateImpact: updateImpactMutation, + createInvitedUsers: createInvitedUsersMutation, + createSquadInvites: createSquadInvitesMutation, + logSearch: logSearchMutation, + logUserRevenue: logUserRevenueMutation, + logUserDataConsent: logUserDataConsentMutation, + donateVc: donateVcMutation, + mergeIntoExistingUser: mergeIntoExistingUserMutation, + logEmailVerified: logEmailVerifiedMutation, + logReferralLinkClick: logReferralLinkClickMutation,; \ No newline at end of file diff --git a/graphql/data/types/queryType.js b/graphql/data/types/queryType.js new file mode 100644 index 000000000..fc003a82b --- /dev/null +++ b/graphql/data/types/queryType.js @@ -0,0 +1,41 @@ +export default new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + node: nodeField, + // Add your own root fields here + app: { + type: appType, + resolve: () => App.getApp(1), + }, + user: { + type: userType, + args: { + userId: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: (_, args, context) => UserModel.get(context.user, args.userId), + }, + wildfire: { + type: wildfireType, + args: { + userId: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: (_, args) => getCauseForWildfire(args.userId), + }, + userImpact: { + type: userImpactType, + args: { + userId: { type: new GraphQLNonNull(GraphQLString) }, + charityId: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: async (_, args, context) => { + const { userId, charityId } = args + return ( + await UserImpactModel.getOrCreate(context.user, { + userId, + charityId, + }) + ).item + }, + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/searchRateLimitType.js b/graphql/data/types/searchRateLimitType.js new file mode 100644 index 000000000..b0562cb98 --- /dev/null +++ b/graphql/data/types/searchRateLimitType.js @@ -0,0 +1,28 @@ +export default new GraphQLObjectType({ + name: 'SearchRateLimit', + description: 'Info about any rate-limiting for VC earned from search queries', + fields: () => ({ + limitReached: { + type: GraphQLBoolean, + description: + "Whether we are currently rate-limiting the user's VC earned from searches", + }, + reason: { + type: new GraphQLEnumType({ + name: 'SearchRateLimitReason', + description: + "Why we are rate-limiting the user's VC earned from searches", + values: { + NONE: { value: 'NONE' }, + ONE_MINUTE_MAX: { value: 'ONE_MINUTE_MAX' }, + FIVE_MINUTE_MAX: { value: 'FIVE_MINUTE_MAX' }, + DAILY_MAX: { value: 'DAILY_MAX' }, + }, + }), + }, + checkIfHuman: { + type: GraphQLBoolean, + description: 'Whether we should present the user with a CAPTCHA', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/userGroupImpactMetricType.js b/graphql/data/types/userGroupImpactMetricType.js new file mode 100644 index 000000000..20efa295f --- /dev/null +++ b/graphql/data/types/userGroupImpactMetricType.js @@ -0,0 +1,42 @@ +export default new GraphQLObjectType({ + name: USER_GROUP_IMPACT_METRIC, + description: 'A specific users contribution to a GroupImpactMetric', + fields: () => ({ + groupImpactMetric: { + type: groupImpactMetricType, + description: 'Information about the GroupImpactMetric', + resolve: (userGroupImpactMetric, _, context) => + GroupImpactMetricModel.get( + context, + userGroupImpactMetric.groupImpactMetricId + ), + }, + id: globalIdField(USER_GROUP_IMPACT_METRIC), + userId: { + type: new GraphQLNonNull(GraphQLString), + description: + 'The ID of the user which the UserGroupImpactMetric belongs to', + }, + dollarContribution: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user', + }, + tabDollarContribution: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from tabs', + }, + searchDollarContribution: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from search', + }, + shopDollarContribution: { + type: new GraphQLNonNull(GraphQLInt), + description: + 'The micro USD amount raised for this instance of GroupImpactMetric so far by this user from shopping', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/userImpactType.js b/graphql/data/types/userImpactType.js new file mode 100644 index 000000000..8c63d5a4e --- /dev/null +++ b/graphql/data/types/userImpactType.js @@ -0,0 +1,36 @@ +export default new GraphQLObjectType({ + name: USER_IMPACT, + description: `a user's charity specific impact`, + fields: () => ({ + id: globalIdField( + USER_IMPACT, + (userImpact) => `${userImpact.userId}::${userImpact.charityId}` + ), + userId: { type: new GraphQLNonNull(GraphQLString) }, + charityId: { type: new GraphQLNonNull(GraphQLString) }, + userImpactMetric: { + type: new GraphQLNonNull(GraphQLFloat), + description: 'a users impact for a specific charity', + }, + pendingUserReferralImpact: { + type: new GraphQLNonNull(GraphQLFloat), + description: 'a users pending impact based on referrals', + }, + pendingUserReferralCount: { + type: new GraphQLNonNull(GraphQLFloat), + description: 'pending user referral count', + }, + visitsUntilNextImpact: { + type: new GraphQLNonNull(GraphQLFloat), + description: 'visits remaining until next recorded impact', + }, + confirmedImpact: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'enables a user to start accruing impact', + }, + hasClaimedLatestReward: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'flag that indicates if user has celebrated latest impact', + }, + }), +}); \ No newline at end of file diff --git a/graphql/data/types/userRecruitType.js b/graphql/data/types/userRecruitType.js new file mode 100644 index 000000000..bfbfa7c55 --- /dev/null +++ b/graphql/data/types/userRecruitType.js @@ -0,0 +1,24 @@ +export default new GraphQLObjectType({ + name: USER_RECRUITS, + description: 'Info about a user recruited by a referring user', + fields: () => ({ + // Ideally, we should build the global ID using the composite value of + // "referringUser" and "userID", which is guaranteed to be unique and can + // resolve back to one object via the nodeInterface. However, for privacy + // concerns, we should then also encrypt the userID because we don't want + // a referrer to know all the IDs of their recruits. For simplicity, we'll + // just use a compound value of "referringUser" and "recruitedAt", which is + // almost certainly unique, and just not implement nodeInterface now. + // https://github.com/graphql/graphql-relay-js/blob/4fdadd3bbf3d5aaf66f1799be3e4eb010c115a4a/src/node/node.js#L138 + id: globalIdField( + USER_RECRUITS, + (recruit) => `${recruit.referringUser}::${recruit.recruitedAt}` + ), + recruitedAt: { + type: GraphQLString, + description: 'ISO datetime string of when the recruited user joined', + }, + }), + // We haven't implemented nodeInterface here because a refetch is unlikely. See above. + // interfaces: [nodeInterface] +}); \ No newline at end of file diff --git a/graphql/data/types/userType.js b/graphql/data/types/userType.js new file mode 100644 index 000000000..b1e077a67 --- /dev/null +++ b/graphql/data/types/userType.js @@ -0,0 +1,339 @@ +export default new GraphQLObjectType({ + name: USER, + description: 'A person who uses our app', + fields: () => ({ + id: globalIdField(USER), + userId: { + type: GraphQLString, + description: + "The users's Firebase ID (not Relay global ID, unlike the `id` field", + resolve: (user) => user.id, + }, + backgroundImage: { + type: backgroundImageType, + description: "Users's background image", + resolve: (user, _args, context) => getBackgroundImage(context.user, user), + }, + userImpact: { + type: userImpactType, + description: + "A user's cause-specific impact for the cause they are currently supporting", + resolve: async (user, _args, context) => + getUserImpact(context.user, user.id), + }, + username: { + type: GraphQLString, + description: "Users's username", + }, + email: { + type: GraphQLString, + description: "User's email", + }, + truexId: { + type: new GraphQLNonNull(GraphQLString), + description: 'a unique user ID sent to video ad partner truex', + resolve: (user, _args, context) => getOrCreateTruexId(context.user, user), + }, + causeId: { + type: GraphQLString, + description: + "The users's cause id. If empty the user does not have a cause.", + resolve: (user) => user.causeId, + }, + cause: { + type: CauseType, + description: 'cause type for the user', + resolve: (user, _args, context) => getCauseByUser(context.user, user.id), + }, + videoAdEligible: { + type: GraphQLBoolean, + description: + 'whether a user has completed 3 video ads in the last 24 hours', + resolve: (user, _args, context) => isVideoAdEligible(context.user, user), + }, + joined: { + type: GraphQLString, + description: 'ISO datetime string of when the user joined', + }, + justCreated: { + type: GraphQLBoolean, + description: + 'Whether or not the user was created during this request. Helpful for a "get or create" mutation', + resolve: (user) => + // The user will only have the 'justCreated' field when it's a + // brand new user item + !!user.justCreated, + }, + vcCurrent: { + type: GraphQLInt, + description: "User's current VC", + }, + vcAllTime: { + type: GraphQLInt, + description: "User's all time VC", + }, + tabs: { + type: GraphQLInt, + description: "User's all time tab count", + }, + tabsToday: { + type: GraphQLInt, + description: "User's tab count for today", + }, + maxTabsDay: { + type: maxTabsDayType, + description: "Info about the user's day of most opened tabs", + resolve: (user) => user.maxTabsDay.maxDay, + }, + level: { + type: GraphQLInt, + description: "User's vc", + }, + v4BetaEnabled: { + type: GraphQLBoolean, + description: 'If true, serve the new Tab V4 app.', + }, + hasViewedIntroFlow: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'if true, user has viewed intro flow in v4', + }, + // TODO: change to heartsForNextLevel to be able to get progress + heartsUntilNextLevel: { + type: GraphQLInt, + description: 'Remaing hearts until next level.', + }, + vcDonatedAllTime: { + type: GraphQLInt, + description: "User's total vc donated", + }, + recruits: { + type: userRecruitsConnection, + description: 'People recruited by this user', + args: { + ...connectionArgs, + startTime: { type: GraphQLString }, + endTime: { type: GraphQLString }, + }, + resolve: (user, args, context) => + connectionFromPromisedArray( + getRecruits(context.user, user.id, args.startTime, args.endTime), + args + ), + }, + numUsersRecruited: { + type: GraphQLInt, + description: 'The number of users this user has recruited', + }, + widgets: { + type: widgetConnection, + description: 'User widgets', + args: { + ...connectionArgs, + enabled: { type: GraphQLBoolean }, + }, + resolve: (user, args, context) => + connectionFromPromisedArray( + getWidgets(context.user, user.id, args.enabled), + args + ), + }, + activeWidget: { + type: GraphQLString, + description: "User's active widget id", + }, + currentMission: { + type: MissionType, + description: 'the current active mission for a user', + resolve: (user) => getCurrentUserMission(user), + }, + pastMissions: { + type: MissionsConnection, + description: 'gets all the past missions for a user', + args: connectionArgs, + resolve: (user, args) => { + return connectionFromPromisedArray(getPastUserMissions(user), args) + }, + }, + backgroundOption: { + type: GraphQLString, + description: "User's background option", + }, + customImage: { + type: GraphQLString, + description: "User's background custom image", + }, + backgroundColor: { + type: GraphQLString, + description: "User's background color", + }, + mergedIntoExistingUser: { + type: GraphQLBoolean, + description: + 'Whether this user was created by an existing user and then merged into the existing user', + }, + searches: { + type: GraphQLInt, + description: "User's all time search count", + }, + notifications: { + type: new GraphQLNonNull( + new GraphQLList( + new GraphQLObjectType({ + name: 'notifications', + description: 'user notifications to show on v4', + fields: () => ({ + code: { + type: GraphQLString, + description: 'the kind of notification it is', + }, + variation: { + type: new GraphQLNonNull(GraphQLString), + description: + 'the variation of the notification given to this user (e.g. for A/B testing)', + }, + }), + }) + ) + ), + description: 'notifications for the v4 user to see', + resolve: (user, args, context) => + getUserNotifications(context.user, user), + }, + searchesToday: { + type: GraphQLInt, + description: "User's search count for today", + }, + searchRateLimit: { + type: searchRateLimitType, + description: 'Info about any search query rate-limiting', + resolve: (user, args, context) => + checkSearchRateLimit(context.user, user.id), + }, + maxSearchesDay: { + type: maxSearchesDayType, + description: "Info about the user's day of most searches", + resolve: (user) => user.maxSearchesDay.maxDay, + }, + experimentActions: { + type: ExperimentActionsOutputType, + description: 'Actions the user has taken during experiments', + resolve: (user) => constructExperimentActionsType(user), + }, + pendingMissionInvites: { + type: new GraphQLNonNull( + new GraphQLList( + new GraphQLObjectType({ + name: 'PendingMissionInvite', + description: 'pending mission invites for user', + fields: () => ({ + missionId: { + type: new GraphQLNonNull(GraphQLString), + description: 'the mission id of the squad invite', + }, + invitingUser: { + type: new GraphQLObjectType({ + name: 'InvitingUser', + description: 'inviting user', + fields: () => ({ + name: { + type: new GraphQLNonNull(GraphQLString), + description: 'the name entered in invite', + }, + }), + }), + }, + }), + }) + ) + ), + }, + hasSeenSquads: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'whether a v4 user has been introduced to squads in the ui', + }, + features: { + type: new GraphQLNonNull(new GraphQLList(featureType)), + description: 'feature values for this specific user', + resolve: (user, args, context) => getUserFeatures(context.user, user), + }, + searchEngine: { + type: SearchEnginePersonalizedType, + description: 'the User’s search engine', + resolve: (user, args, context) => getUserSearchEngine(context.user, user), + }, + showYahooPrompt: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'whether to show the yahoo search prompt', + resolve: (user, _, context) => + getShouldShowYahooPrompt(context.user, user), + }, + showSfacExtensionPrompt: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'whether to show the SFAC extension prompt', + resolve: (user, _, context) => + getShouldShowSfacExtensionPrompt(context.user, user), + }, + showSfacIcon: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'whether to show the SFAC icon (and activity ui element)', + resolve: (user, _, context) => getShouldShowSfacIcon(context.user, user), + }, + sfacActivityState: { + type: new GraphQLNonNull( + new GraphQLEnumType({ + name: 'sfacActivityState', + description: 'what mode in which to show SFAC searches UI', + values: { + new: { value: 'new' }, + active: { value: 'active' }, + inactive: { value: 'inactive' }, + }, + }) + ), + resolve: (user, _, context) => getSfacActivityState(context.user, user), + }, + yahooPaidSearchRewardOptIn: { + type: new GraphQLNonNull(GraphQLBoolean), + description: + 'whether or not the user has opted into searching for extra impact', + }, + userGroupImpactMetric: { + type: userGroupImpactMetricType, + description: 'Current UserGroupImpactMetric', + resolve: (user, _, context) => + UserGroupImpactMetricModel.getOrNull( + context, + user.userGroupImpactMetricId + ), + }, + leaderboard: { + type: new GraphQLList( + new GraphQLObjectType({ + name: 'leaderboardEntry', + description: 'content for each leaderboard', + fields: () => ({ + position: { + type: new GraphQLNonNull(GraphQLInt), + }, + userGroupImpactMetric: { + type: userGroupImpactMetricType, + description: 'UserGroupImpactMetric entity', + }, + user: { + type: userType, + description: 'User associated with this leaderboard entry', + }, + }), + }) + ), + description: 'Current UserGroupImpactMetrics leaderboard', + resolve: (user) => + user.userGroupImpactMetricId && + GroupImpactLeaderboard.getLeaderboardForUser( + user.userGroupImpactMetricId, + user.id + ), + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/videoAdLogType.js b/graphql/data/types/videoAdLogType.js new file mode 100644 index 000000000..7d281d294 --- /dev/null +++ b/graphql/data/types/videoAdLogType.js @@ -0,0 +1,8 @@ +export default new GraphQLObjectType({ + name: VIDEO_AD_LOG, + description: 'Video Ad Log type', + fields: () => ({ + id: globalIdField(VIDEO_AD_LOG), + }), + interfaces: [nodeInterface], +}); \ No newline at end of file diff --git a/graphql/data/types/widgetType.js b/graphql/data/types/widgetType.js new file mode 100644 index 000000000..0e3b5ef94 --- /dev/null +++ b/graphql/data/types/widgetType.js @@ -0,0 +1,40 @@ +export default new GraphQLObjectType({ + name: WIDGET, + description: 'App widget', + fields: () => ({ + id: globalIdField(WIDGET), + name: { + type: GraphQLString, + description: 'Widget display name', + }, + type: { + type: GraphQLString, + description: 'Widget type', + }, + icon: { + type: GraphQLString, + description: 'Widget icon', + }, + enabled: { + type: GraphQLBoolean, + description: 'The Widget enabled state', + }, + visible: { + type: GraphQLBoolean, + description: 'The Widget visible state', + }, + data: { + type: GraphQLString, + description: 'Widget data.', + }, + config: { + type: GraphQLString, + description: 'Widget user specific configuration.', + }, + settings: { + type: GraphQLString, + description: 'Widget general configuration.', + }, + }), + interfaces: [nodeInterface], +}); \ No newline at end of file