Skip to content

Commit

Permalink
feat(1165): implements scheduled update task states jobs
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Oct 18, 2024
1 parent 29bbae6 commit a9220d7
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 5 deletions.
33 changes: 32 additions & 1 deletion src/utils/worker/create.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { prisma } from '../prisma';

// import { dayDuration } from './externalTasksJob';
import * as templates from './mail/templates';
import { log } from './utils';

export const defaultJobDelay = process.env.WORKER_JOBS_DELAY ? parseInt(process.env.WORKER_JOBS_DELAY, 10) : 1000;

Expand All @@ -14,6 +16,7 @@ export enum jobKind {
email = 'email',
cron = 'cron',
comment = 'comment',
criteriaToUpdate = 'criteriaToUpdate',
}

type Templates = typeof templates;
Expand All @@ -25,15 +28,22 @@ export interface JobDataMap {
data: any;
};
cron: {
template: 'goalPing';
template: 'goalPing' | 'externalTaskCheck';
};
comment: {
goalId: string;
activityId: string;
description: string;
};
criteriaToUpdate: {
id: string;
};
}

export const castJobData = <Kind extends jobKind>(kind: Kind, data: unknown): data is JobDataMap[Kind] => {
return data != null;
};

export type JobKind = keyof JobDataMap;

interface CreateJobProps<K extends keyof JobDataMap> {
Expand All @@ -43,10 +53,27 @@ interface CreateJobProps<K extends keyof JobDataMap> {
cron?: string;
}

export const pickScheduledLastJob = async (kind: JobKind) => {
const res = await prisma.job.findMany({
where: { kind, state: jobState.scheduled },
orderBy: [{ createdAt: 'desc' }, { updatedAt: 'desc' }],
take: 1,
skip: 0,
});

if (res.length) {
return res[0];
}

return null;
};

export function createJob<K extends keyof JobDataMap>(
kind: K,
{ data, priority, delay = defaultJobDelay, cron }: CreateJobProps<K>,
) {
log(`create new ${kind} job`, JSON.stringify(data));

return prisma.job.create({
data: {
state: jobState.scheduled,
Expand Down Expand Up @@ -86,3 +113,7 @@ export function createCommentJob(data: JobDataMap['comment'], delay?: number) {
delay,
});
}

export const createCriteriaToUpdate = (data: JobDataMap['criteriaToUpdate'], delay?: number) => {
return createJob('criteriaToUpdate', { data, delay });
};
242 changes: 242 additions & 0 deletions src/utils/worker/externalTasksJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { ExternalTask, Prisma } from '@prisma/client';
import assert from 'assert';

import type { JiraIssue } from '../integration/jira';
import { jiraService } from '../integration/jira';
import { prisma } from '../prisma';
import { recalculateCriteriaScore } from '../recalculateCriteriaScore';

import { getSheep } from './sheep';
import { castJobData, createCriteriaToUpdate, jobKind, jobState } from './create';

export const dayDuration = 24 * 60 * 60 * 1000;

const createSQLValues = (sources: JiraIssue[]) =>
Prisma.join(
sources.map(({ key, id, status, summary, issuetype, reporter, project, resolution }) =>
Prisma.join(
[
id,
key,
summary,
status.name,
status.statusCategory.id,
status?.statusCategory.name,
status.statusCategory?.id ? status.statusCategory?.id : Number(status.id),
status.statusCategory?.colorName ?? null,
status.iconUrl,
project.name,
project.key,
issuetype.name,
issuetype.iconUrl,
issuetype.id,
reporter.emailAddress,
reporter.key,
reporter.displayName || reporter.name || null,
resolution?.name || null,
resolution?.id || null,
],
',',
'(',
')',
),
),
);

const getResolvedJiraTasks = async (ids: string[]) => {
if (ids.length) {
const results = await jiraService.instance.searchJira(
`key in (${ids.map((id) => `"${id}"`).join(',')}) and resolution is not EMPTY`,
);

return results.issues.map((issue: { fields: { [key: string]: unknown } }) => ({
...issue,
...issue.fields,
})) as Array<JiraIssue>;
}

return null;
};

export const updateAssociatedGoalsByCriteriaIds = (_criteriaIds: string[]) => {};

const getSheepOrThrow = async () => {
const { activityId } = (await getSheep()) || {};

assert(activityId, 'No avaliable sheeps');

return activityId;
};

// get all incompleted criteria with associated jira tasks
const getCriteriaWithTasks = async (from: Date) =>
prisma.goalAchieveCriteria.findMany({
where: {
externalTaskId: { not: null },
isDone: false,
updatedAt: { lte: from },
AND: [
{
OR: [{ deleted: false }, { deleted: null }],
},
],
},
select: {
id: true,
goalId: true,
externalTask: true,
},
});

const updateExternalTasks = (tasks: JiraIssue[]) => {
// update external tasks
const values = createSQLValues(tasks); // (val1-1, val1-2, ...), (val2-1, val2-2, ...), ...

const valuesToUpdate = Prisma.sql`(
VALUES${values}
) AS task(
"externalId", "key", "title",
"state", "stateId", "stateCategoryName", "stateCategoryId", "stateColor", "stateIcon",
"project", "projectKey",
"type", "typeIcon", "typeId",
"ownerEmail", "ownerKey", "ownerName",
"resolution", "resolutionId"
)`;

const rawSql = Prisma.sql`
UPDATE "ExternalTask"
SET
"title" = task."title",
"externalId" = task."externalId",
"externalKey" = task."key",
"type" = task."type",
"typeIconUrl" = task."typeIcon",
"typeId" = task."typeId",
"state" = task."state",
"stateId" = task."stateId",
"stateColor" = task."stateColor",
"stateIconUrl" = task."stateIcon",
"stateCategoryId" = cast(task."stateCategoryId" as int),
"stateCategoryName" = task."stateCategoryName",
"project" = task."project",
"projectId" = task."projectKey",
"ownerName" = task."ownerName",
"ownerEmail" = task."ownerEmail",
"ownerId" = task."ownerKey",
"resolution" = task."resolution",
"resolutionId" = task."resolutionId"
FROM ${valuesToUpdate}
WHERE "ExternalTask"."externalKey" = task."key"
RETURNING *
`;

return prisma.$queryRaw`${rawSql}` as unknown as Promise<ExternalTask[]>;
};

export const externalTasksJob = async (criteriaId: string) => {
const sheepActivityId = await getSheepOrThrow();

const actualCriteria = await prisma.goalAchieveCriteria.findUniqueOrThrow({
where: { id: criteriaId },
select: {
id: true,
goalId: true,
},
});

const recalcScore = recalculateCriteriaScore(actualCriteria.goalId).makeChain(
'recalcCurrentGoalScore',
'recalcLinkedGoalsScores',
'recalcAverageProjectScore',
);

await prisma
.$transaction(async (ctx) => {
await Promise.all([
ctx.goalAchieveCriteria.update({
where: { id: criteriaId },
data: {
isDone: true,
},
}),
ctx.goalHistory.create({
data: {
goalId: actualCriteria.goalId,
subject: 'criteria',
action: 'complete',
previousValue: null,
nextValue: criteriaId,
activityId: sheepActivityId,
},
}),
]);
})
.then(() => recalcScore.run());
};

const updateMinInterval = 300;
const tasksPeriod = 1000 * 60; // every minute

export const externalTaskCheckJob = async () => {
const atYesterday = new Date();
atYesterday.setDate(atYesterday.getDate() - 1);

// getting all criteria which updated at last week or earlier
const criteriaListPromise = getCriteriaWithTasks(atYesterday);
const notCompetedJobsPromise = prisma.job.findMany({
where: {
kind: jobKind.criteriaToUpdate,
state: jobState.scheduled,
},
});

const [criteriaList, jobs] = await Promise.all([criteriaListPromise, notCompetedJobsPromise]);

if (!criteriaList.length) {
return;
}

const externalTaskKeys = criteriaList.reduce((acc, c) => {
if (c.externalTask != null) {
acc.add(c.externalTask.externalKey);
}

return acc;
}, new Set<string>());

const externalTasks = await getResolvedJiraTasks(Array.from(externalTaskKeys));

if (!externalTasks?.length) {
return;
}

const updatedTasks = await updateExternalTasks(externalTasks);

const updatedExternalTaskKeys = updatedTasks.reduce<Record<string, true>>((acc, { id }) => {
acc[id] = true;
return acc;
}, {});

const criteriaIdsToUpdate = criteriaList.filter(({ externalTask }) => {
if (externalTask) {
return externalTask.id in updatedExternalTaskKeys;
}

return false;
});

const interval = Math.max(Math.floor(tasksPeriod / criteriaList.length), updateMinInterval);

const plannedCriteriaIds = jobs.reduce((acc, { data }) => {
if (castJobData(jobKind.criteriaToUpdate, data)) {
acc.add(data.id);
}
return acc;
}, new Set<string>());

criteriaIdsToUpdate.forEach(async ({ id }, index) => {
if (!plannedCriteriaIds.has(id)) {
await createCriteriaToUpdate({ id }, index * interval);
}
});
};
18 changes: 14 additions & 4 deletions src/utils/worker/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import * as emailTemplates from './mail/templates';
import { sendMail } from './mail';
import { JobDataMap } from './create';
import { goalPingJob } from './goalPingJob';
import { externalTaskCheckJob, externalTasksJob } from './externalTasksJob';

export const email = async ({ template, data }: JobDataMap['email']) => {
const renderedTemplate = await emailTemplates[template](data);
return sendMail(renderedTemplate);
};

export const cron = async ({ template }: JobDataMap['cron']) => {
if (template === 'goalPing') {
goalPingJob();
} else {
throw new Error('No supported cron jobs');
switch (template) {
case 'externalTaskCheck':
externalTaskCheckJob();
break;
case 'goalPing':
goalPingJob();
break;
default:
throw new Error('No supported cron jobs');
}
};

Expand All @@ -27,3 +33,7 @@ export const comment = async ({ activityId, description, goalId }: JobDataMap['c
shouldUpdateGoal: false,
});
};

export const criteriaToUpdate = async ({ id }: JobDataMap['criteriaToUpdate']) => {
await externalTasksJob(id);
};

0 comments on commit a9220d7

Please sign in to comment.