• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-api / 14430554978

13 Apr 2025 02:49PM UTC coverage: 82.235% (-0.8%) from 83.071%
14430554978

push

github

web-flow
Merge pull request #365 from cofacts/moderation-apis

feat(admin): Implement moderation API endpoints for media and AI reply

798 of 1030 branches covered (77.48%)

Branch coverage included in aggregate %.

1535 of 1807 relevant lines covered (84.95%)

18.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.45
/src/graphql/models/User.js
1
import {
2
  GraphQLObjectType,
3
  GraphQLString,
4
  GraphQLInt,
5
  GraphQLNonNull,
6
  GraphQLList,
7
  GraphQLID,
8
} from 'graphql';
9
import {
10
  AvatarTypes,
11
  getAvailableAvatarTypes,
12
  getUserId,
13
  avatarUrlResolver,
14
} from 'util/user';
15
import AvatarTypeEnum from './AvatarTypeEnum';
16
import Contribution from './Contribution';
17
import Node from '../interfaces/Node';
18
import { timeRangeInput, createConnectionType } from 'graphql/util';
19
import UserAwardedBadge from './UserAwardedBadge';
20

21
/**
22
 * Field config helper for current user only field.
23
 * Resolves to null if root object is not current user. Otherwise, invoke provided resolver.
24
 *
25
 * @param {GraphQLScalarType | GraphQLObjectType} type
26
 * @param {function?} resolver - Use default resolver if not given.
27
 */
28
export const currentUserOnlyField = (type, resolver) => ({
192✔
29
  type,
30
  description: 'Returns only for current user. Returns `null` otherwise.',
31
  resolve(user, arg, context, info) {
32
    if (!context.user || user.id !== context.user.id) return null;
15✔
33

34
    return resolver ? resolver(user, arg, context, info) : user[info.fieldName];
8✔
35
  },
36
});
37

38
const User = new GraphQLObjectType({
38✔
39
  name: 'User',
40
  interfaces: [Node],
41
  fields: () => ({
31✔
42
    id: { type: new GraphQLNonNull(GraphQLID) },
43
    slug: { type: GraphQLString },
44
    email: currentUserOnlyField(GraphQLString),
45
    name: { type: GraphQLString },
46
    bio: { type: GraphQLString },
47

48
    avatarUrl: {
49
      type: GraphQLString,
50
      description: 'returns avatar url from facebook, github or gravatar',
51
      resolve: avatarUrlResolver(),
52
    },
53
    avatarData: {
54
      type: GraphQLString,
55
      description:
56
        'return avatar data as JSON string, currently only used when avatarType is OpenPeeps',
57
    },
58
    avatarType: {
59
      type: AvatarTypeEnum,
60
      resolver(user) {
61
        return user?.avatarType ?? AvatarTypes.Gravatar;
×
62
      },
63
    },
64

65
    availableAvatarTypes: currentUserOnlyField(
66
      new GraphQLList(GraphQLString),
67
      (user) => getAvailableAvatarTypes(user)
2✔
68
    ),
69

70
    appId: { type: GraphQLString },
71
    appUserId: currentUserOnlyField(GraphQLString),
72

73
    facebookId: currentUserOnlyField(GraphQLString),
74
    githubId: currentUserOnlyField(GraphQLString),
75
    twitterId: currentUserOnlyField(GraphQLString),
76
    repliedArticleCount: {
77
      type: GraphQLNonNull(GraphQLInt),
78
      description: 'Number of articles this user has replied to',
79
      resolve: (user, args, context) =>
80
        context.loaders.repliedArticleCountLoader
2✔
81
          .load(user.id)
82
          .then((num) => num || 0),
2✔
83
    },
84
    votedArticleReplyCount: {
85
      type: GraphQLNonNull(GraphQLInt),
86
      description: 'Number of article replies this user has given feedbacks',
87
      resolve: (user, args, context) =>
88
        context.loaders.votedArticleReplyCountLoader
2✔
89
          .load(user.id)
90
          .then((num) => num || 0),
2✔
91
    },
92

93
    level: {
94
      type: new GraphQLNonNull(GraphQLInt),
95
      async resolve(user, arg, context) {
96
        const { level } =
97
          (await context.loaders.userLevelLoader.load(user.id)) || {};
2!
98
        return level || 0;
2✔
99
      },
100
    },
101
    points: {
102
      type: new GraphQLNonNull(
103
        new GraphQLObjectType({
104
          name: 'PointInfo',
105
          description:
106
            "Information of a user's point. Only available for current user.",
107
          fields: {
108
            total: {
109
              type: new GraphQLNonNull(GraphQLInt),
110
              description: 'Points earned by the current user',
111
            },
112
            currentLevel: {
113
              type: new GraphQLNonNull(GraphQLInt),
114
              description: 'Points required for current level',
115
            },
116
            nextLevel: {
117
              type: new GraphQLNonNull(GraphQLInt),
118
              description:
119
                'Points required for next level. null when there is no next level.',
120
            },
121
          },
122
        })
123
      ),
124
      resolve: async (user, arg, context) => {
125
        const { totalPoints, currentLevelPoints, nextLevelPoints } =
126
          (await context.loaders.userLevelLoader.load(user.id)) || {};
2!
127

128
        return {
2✔
129
          total: totalPoints || 0,
3✔
130
          currentLevel: currentLevelPoints || 0,
3✔
131
          nextLevel: nextLevelPoints || 1,
2!
132
        };
133
      },
134
    },
135
    createdAt: { type: GraphQLString },
136
    updatedAt: { type: GraphQLString },
137
    lastActiveAt: { type: GraphQLString },
138

139
    contributions: {
140
      type: new GraphQLList(Contribution),
141
      description: 'List of contributions made by the user',
142
      args: {
143
        dateRange: {
144
          type: timeRangeInput,
145
          description:
146
            'List only the contributions between the specific time range.',
147
        },
148
      },
149
      resolve: async ({ id }, { dateRange }, { loaders }) => {
150
        return await loaders.contributionsLoader.load({
1✔
151
          userId: id,
152
          dateRange,
153
        });
154
      },
155
    },
156

157
    blockedReason: {
158
      description:
159
        'If not null, the user is blocked with the announcement in this string.',
160
      type: GraphQLString,
161
    },
162

163
    badges: {
164
      type: new GraphQLNonNull(
165
        new GraphQLList(new GraphQLNonNull(UserAwardedBadge))
166
      ),
167
      description: 'Badges awarded to the user.',
168
      resolve: (user) => user.badges || [],
2!
169
    },
170

171
    majorBadgeBorderUrl: {
172
      type: GraphQLString,
173
      description: 'returns badge background image url',
174
      resolve: async (user, args, { loaders }) => {
175
        if (!user.badges || !Array.isArray(user.badges)) {
2!
176
          return null;
×
177
        }
178

179
        const displayItem = user.badges.find(
2✔
180
          (badge) => badge.isDisplayed === true
1✔
181
        );
182
        if (!displayItem) {
2✔
183
          return null;
1✔
184
        }
185

186
        const badgeInfo = await loaders.docLoader.load({
1✔
187
          index: 'badges',
188
          id: displayItem.badgeId,
189
        });
190

191
        return badgeInfo?.borderImage || null;
1!
192
      },
193
    },
194

195
    majorBadgeId: {
196
      type: GraphQLString,
197
      description: 'returns badge icon url',
198
      resolve: async (user) => {
199
        if (!user.badges || !Array.isArray(user.badges)) return null;
×
200

201
        const displayItem = user.badges.find(
×
202
          (badge) => badge.isDisplayed === true
×
203
        );
204
        if (!displayItem) return null;
×
205

206
        return displayItem.badgeId;
×
207
      },
208
    },
209

210
    majorBadgeImageUrl: {
211
      type: GraphQLString,
212
      description: 'returns badge icon url',
213
      resolve: async (user, args, { loaders }) => {
214
        if (!user.badges || !Array.isArray(user.badges)) return null;
2!
215

216
        const displayItem = user.badges.find(
2✔
217
          (badge) => badge.isDisplayed === true
1✔
218
        );
219
        if (!displayItem) return null;
2✔
220

221
        const badgeInfo = await loaders.docLoader.load({
1✔
222
          index: 'badges',
223
          id: displayItem.badgeId,
224
        });
225

226
        return badgeInfo?.icon || null;
1!
227
      },
228
    },
229

230
    majorBadgeName: {
231
      type: GraphQLString,
232
      description: 'returns badge name',
233
      resolve: async (user, args, { loaders }) => {
234
        if (!user.badges || !Array.isArray(user.badges)) return null;
2!
235

236
        const displayItem = user.badges.find(
2✔
237
          (badge) => badge.isDisplayed === true
1✔
238
        );
239
        if (!displayItem) return null;
2✔
240

241
        const badgeInfo = await loaders.docLoader.load({
1✔
242
          index: 'badges',
243
          id: displayItem.badgeId,
244
        });
245

246
        return badgeInfo?.name || null;
1!
247
      },
248
    },
249
  }),
250
});
251

252
export default User;
253

254
export const userFieldResolver = async (
38✔
255
  { userId, appId },
256
  args,
257
  { loaders, ...context }
258
) => {
259
  // If the root document is created by website users or if the userId is already converted to db userId,
260
  // we can resolve user from userId.
261
  //
262
  if (userId && appId) {
28✔
263
    const id = getUserId({ appId, userId });
19✔
264
    const user = await loaders.docLoader.load({ index: 'users', id });
19✔
265
    if (user) return user;
19✔
266
  }
267

268
  /* TODO: some unit tests are depending on this code block, need to clean up those tests and then
269
     remove the following lines, and the corresponding unit test. */
270

271
  // If the user comes from the same client as the root document, return the user id.
272
  //
273
  if (context.appId && context.appId === appId) return { id: userId };
17✔
274

275
  // If not, this client is not allowed to see user.
276
  //
277
  return null;
10✔
278
};
279

280
export const UserConnection = createConnectionType('UserConnection', User);
38✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc