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

hicommonwealth / commonwealth / 13763618099

10 Mar 2025 11:32AM UTC coverage: 45.048% (-0.2%) from 45.252%
13763618099

Pull #11335

github

web-flow
Merge 0b2fc4d4d into a53d0f274
Pull Request #11335: Graphile Worker + Quest job scheduling

1329 of 3294 branches covered (40.35%)

Branch coverage included in aggregate %.

33 of 63 new or added lines in 7 files covered. (52.38%)

1 existing line in 1 file now uncovered.

2542 of 5299 relevant lines covered (47.97%)

37.98 hits per line

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

63.27
/libs/model/src/quest/UpdateQuest.command.ts
1
import { Command, InvalidInput } from '@hicommonwealth/core';
2
import * as schemas from '@hicommonwealth/schemas';
3
import { AllChannelQuestActionNames } from '@hicommonwealth/schemas';
4
import z from 'zod';
5
import { models } from '../database';
6
import { isSuperAdmin } from '../middleware';
7
import {
8
  mustBeValidDateRange,
9
  mustExist,
10
  mustNotBeStarted,
11
  mustNotExist,
12
} from '../middleware/guards';
13
import {
14
  GraphileTaskNames,
15
  removeJob,
16
  rescheduleJobs,
17
  scheduleTask,
18
} from '../services/graphileWorker';
19
import { getDelta } from '../utils';
20

21
export function UpdateQuest(): Command<typeof schemas.UpdateQuest> {
22
  return {
11✔
23
    ...schemas.UpdateQuest,
24
    auth: [isSuperAdmin],
25
    secure: true,
26
    body: async ({ payload }) => {
27
      const {
28
        quest_id,
29
        name,
30
        description,
31
        community_id,
32
        image_url,
33
        start_date,
34
        end_date,
35
        max_xp_to_end,
36
        action_metas,
37
      } = payload;
11✔
38

39
      const quest = await models.Quest.scope('withPrivateData').findOne({
11✔
40
        where: { id: quest_id },
41
      });
42
      mustExist(`Quest with id "${quest_id}`, quest);
11✔
43

44
      if (name) {
11✔
45
        const existingName = await models.Quest.findOne({
1✔
46
          where: { community_id: community_id ?? null, name },
1!
47
          attributes: ['id'],
48
        });
49
        mustNotExist(
1✔
50
          `Quest named "${name}" in community "${community_id}"`,
51
          existingName,
52
        );
53
      }
54

55
      mustNotBeStarted(start_date ?? quest.start_date);
10✔
56
      mustBeValidDateRange(
9✔
57
        start_date ?? quest.start_date,
18✔
58
        end_date ?? quest.end_date,
17✔
59
      );
60

61
      let channelActionMeta:
62
        | Omit<z.infer<typeof schemas.QuestActionMeta>, 'quest_id'>
63
        | undefined;
64
      if (action_metas) {
9!
65
        if (quest.quest_type === 'channel') {
9!
NEW
UNCOV
66
          if (action_metas.length > 1) {
×
NEW
67
            throw new InvalidInput(
×
68
              'Cannot have more than one action per channel quest',
69
            );
70
          }
NEW
71
          channelActionMeta = action_metas[0];
×
72

NEW
73
          if (
×
74
            channelActionMeta &&
×
75
            !AllChannelQuestActionNames.some(
NEW
76
              (e) => e === channelActionMeta!.event_name,
×
77
            )
78
          ) {
NEW
79
            throw new InvalidInput(
×
80
              `Invalid action "${channelActionMeta.event_name}" for channel quest`,
81
            );
82
          }
83
        }
84

85
        const c_id = community_id || quest.community_id;
9✔
86
        await Promise.all(
9✔
87
          action_metas.map(async (action_meta) => {
88
            if (action_meta.content_id) {
17✔
89
              // make sure content_id exists
90
              const [content, id] = action_meta.content_id.split(':'); // this has been validated by the schema
1✔
91
              if (content === 'thread') {
1!
92
                const thread = await models.Thread.findOne({
×
93
                  where: c_id ? { id: +id, community_id: c_id } : { id: +id },
×
94
                });
95
                mustExist(`Thread with id "${id}"`, thread);
×
96
              } else if (content === 'comment') {
1!
97
                const comment = await models.Comment.findOne({
1✔
98
                  where: { id: +id },
99
                  include: c_id
1!
100
                    ? [
101
                        {
102
                          model: models.Thread,
103
                          attributes: ['community_id'],
104
                          required: true,
105
                          where: { community_id: c_id },
106
                        },
107
                      ]
108
                    : [],
109
                });
110
                mustExist(`Comment with id "${id}"`, comment);
1✔
111
              }
112
            }
113
          }),
114
        );
115
      }
116

117
      await models.sequelize.transaction(async (transaction) => {
8✔
118
        // Add scheduled job for new TwitterMetrics action
119
        if (
8!
120
          quest.quest_type === 'channel' &&
8!
121
          channelActionMeta?.event_name === 'TwitterMetrics'
122
        ) {
NEW
123
          const job = await scheduleTask(
×
124
            GraphileTaskNames.AwardTwitterQuestXp,
125
            {
126
              quest_id: quest.id!,
127
              quest_end_date: quest.end_date,
128
            },
129
            {
130
              transaction,
131
            },
132
          );
133

NEW
134
          quest.scheduled_job_id = job.id;
×
NEW
135
          await quest.save({ transaction });
×
136
        }
137

138
        if (action_metas?.length) {
8!
139
          const existingTwitterMetricsAction =
140
            await models.QuestActionMeta.findOne({
8✔
141
              where: {
142
                quest_id,
143
                event_name: 'TwitterMetrics',
144
              },
145
              transaction,
146
            });
147
          if (
8!
148
            existingTwitterMetricsAction &&
8!
149
            !channelActionMeta &&
150
            quest.scheduled_job_id
151
          ) {
NEW
152
            await removeJob({
×
153
              jobId: quest.scheduled_job_id,
154
              transaction,
155
            });
156
          }
157

158
          // clean existing action_metas
159
          await models.QuestActionMeta.destroy({
8✔
160
            where: { quest_id },
161
            transaction,
162
          });
163
          // create new action_metas
164
          await models.QuestActionMeta.bulkCreate(
8✔
165
            action_metas.map((action_meta) => ({
16✔
166
              ...action_meta,
167
              quest_id,
168
            })),
169
          );
170
        }
171

172
        const delta = getDelta(quest, {
8✔
173
          name,
174
          description,
175
          community_id,
176
          image_url,
177
          start_date,
178
          end_date,
179
          max_xp_to_end,
180
        });
181
        if (Object.keys(delta).length) {
8✔
182
          await models.Quest.update(delta, {
1✔
183
            where: { id: quest_id },
184
            transaction,
185
          });
186

187
          // reschedule the quest job if end date is updated on a TwitterMetrics quest
188
          if (
1!
189
            delta.end_date &&
3!
190
            delta.end_date > quest.end_date &&
191
            quest.quest_type === 'channel' &&
192
            channelActionMeta?.event_name === 'TwitterMetrics' &&
193
            quest.scheduled_job_id
194
          ) {
NEW
195
            await rescheduleJobs({
×
196
              jobIds: [quest.scheduled_job_id],
197
              options: {
198
                runAt: delta.end_date,
199
              },
200
              transaction,
201
            });
202
          }
203
        }
204
      });
205

206
      const updated = await models.Quest.findOne({
8✔
207
        where: { id: quest_id },
208
        include: { model: models.QuestActionMeta, as: 'action_metas' },
209
      });
210
      return updated!.toJSON();
8✔
211
    },
212
  };
213
}
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