Skip to content

Commit

Permalink
fix: change requests
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasFalkowsky committed Dec 19, 2023
1 parent 6bfd232 commit 1d384a3
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 134 deletions.
20 changes: 16 additions & 4 deletions common/achievement/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { ActionID, SpecificNotificationContext } from '../notification/actions';
import { prisma } from '../prisma';
import { TemplateSelectEnum, getAchievementTemplates } from './template';
import tracer from '../logger/tracing';
import { AchievementToCheck } from './types';

async function findUserAchievement<ID extends ActionID>(templateId: number, userId: string, context: SpecificNotificationContext<ID>) {
async function findUserAchievement<ID extends ActionID>(
templateId: number,
userId: string,
context: SpecificNotificationContext<ID>
): Promise<AchievementToCheck> {
const keys = context ? Object.keys(context) : [];
const userAchievement = await prisma.user_achievement.findFirst({
where: {
Expand All @@ -25,12 +30,16 @@ async function findUserAchievement<ID extends ActionID>(templateId: number, user
return userAchievement;
}

async function getOrCreateUserAchievement<ID extends ActionID>(template: Achievement_template, userId: string, context?: SpecificNotificationContext<ID>) {
async function getOrCreateUserAchievement<ID extends ActionID>(
template: Achievement_template,
userId: string,
context?: SpecificNotificationContext<ID>
): Promise<AchievementToCheck> {
const isGlobal =
template.templateFor === achievement_template_for_enum.Global ||
template.templateFor === achievement_template_for_enum.Global_Courses ||
template.templateFor === achievement_template_for_enum.Global_Matches;
const existingUserAchievement = await findUserAchievement(template.id, userId, !isGlobal && context);
const existingUserAchievement: AchievementToCheck = await findUserAchievement(template.id, userId, !isGlobal ? context : undefined);
if (!existingUserAchievement) {
return await createAchievement(template, userId, context);
}
Expand Down Expand Up @@ -74,10 +83,13 @@ async function createNextUserAchievement<ID extends ActionID>(
userId: string,
context: SpecificNotificationContext<ID>
) {
if (templatesForGroup.length <= nextStepIndex) {
return;
}
const nextStepTemplate = templatesForGroup[nextStepIndex];
// Here a user template is created for the next template in the group. This is done to always have the data availible for the next step.
// This could mean to, for example, have the name of a match partner that is not yet availible due to a unfinished matching process.
if (nextStepTemplate && nextStepTemplate.isActive) {
if (nextStepTemplate) {
const createdUserAchievement = await prisma.user_achievement.create({
data: {
userId: userId,
Expand Down
12 changes: 6 additions & 6 deletions common/achievement/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ const logger = getLogger('Achievement');
export const evaluateAchievement = tracer.wrap('achievement.evaluateAchievement', _evaluateAchievement);

async function _evaluateAchievement(
userId: string,
condition: string,
dataAggregation: ConditionDataAggregations,
metrics: string[],
recordValue: number
): Promise<EvaluationResult> {
const achievementEvents = await prisma.achievement_event.findMany({ where: { metric: { in: metrics } }, orderBy: { createdAt: 'desc' } });
const achievementEvents = await prisma.achievement_event.findMany({ where: { userId, metric: { in: metrics } }, orderBy: { createdAt: 'desc' } });

const eventsByMetric: Record<string, Achievement_event[]> = {};
for (const event of achievementEvents) {
Expand All @@ -44,9 +45,6 @@ async function _evaluateAchievement(
const aggregator = dataAggregationObject.aggregator;

const eventsForMetric = eventsByMetric[metricName];
if (!eventsForMetric) {
continue;
}
// we take the relation from the first event, that posesses one, in order to create buckets from it, if needed
const relation = eventsForMetric.find((event) => event.relation)?.relation;

Expand Down Expand Up @@ -88,13 +86,15 @@ async function _evaluateAchievement(
export function createBucketEvents(events: Achievement_event[], bucketConfig: BucketConfig): BucketEvents[] {
switch (bucketConfig.bucketKind) {
case 'default':
return createDefaultBuckets(events, bucketConfig);
return createDefaultBuckets(events);
case 'time':
return createTimeBuckets(events, bucketConfig);
default:
return createDefaultBuckets(events);
}
}

const createDefaultBuckets = (events: Achievement_event[], bucketConfig: BucketConfig): BucketEvents[] => {
const createDefaultBuckets = (events: Achievement_event[]): BucketEvents[] => {
return events.map((event) => ({
kind: 'default',
events: [event],
Expand Down
3 changes: 2 additions & 1 deletion common/achievement/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { evaluateAchievement } from './evaluate';
// TODO: resolver for nextSteps -> get active sequential achievements and important information
const getUserAchievements = async (user: User): Promise<Achievement[]> => {
const userAchievements = await prisma.user_achievement.findMany({
where: { userId: user.userID },
where: { userId: user.userID, AND: { template: { isActive: true } } },
include: { template: true },
});
const userAchievementGroups: { [group: string]: User_achievement[] } = {};
Expand Down Expand Up @@ -70,6 +70,7 @@ const assembleAchievementData = async (userAchievements: User_achievement[], use
if (currentAchievementTemplate.type === achievement_type_enum.STREAK || currentAchievementTemplate.type === achievement_type_enum.TIERED) {
const dataAggregationKeys = Object.keys(currentAchievementTemplate.conditionDataAggregations);
const evaluationResult = await evaluateAchievement(
user.userID,
condition,
currentAchievementTemplate.conditionDataAggregations as ConditionDataAggregations,
currentAchievementTemplate.metrics,
Expand Down
81 changes: 52 additions & 29 deletions common/achievement/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { prisma } from '../prisma';
import { User } from '../user';
import { isGamificationFeatureActive, getMetricsByAction, sortActionTemplatesToGroups } from './util';
import { sortActionTemplatesToGroups } from './util';
import { getLogger } from '../logger/logger';
import { ActionID, SpecificNotificationContext } from '../notification/actions';
import { getTemplatesByAction } from './template';
import { getTemplatesByMetrics } from './template';
import { evaluateAchievement } from './evaluate';
import { AchievementToCheck, ActionEvent, ConditionDataAggregations, UserAchievementContext, UserAchievementTemplate } from './types';
import { AchievementToCheck, ActionEvent, ConditionDataAggregations, UserAchievementTemplate } from './types';
import { createAchievement, getOrCreateUserAchievement } from './create';
// eslint-disable-next-line import/no-cycle
import { actionTakenAt } from '../notification';
import tracer from '../logger/tracing';
import { getMetricsByAction } from './metrics';
import { achievement_type_enum } from '../../graphql/generated';
import { isGamificationFeatureActive } from '../../utils/environment';

const logger = getLogger('Achievement');

export const rewardActionTaken = tracer.wrap('achievement.rewardActionTaken', _rewardActionTaken);
async function _rewardActionTaken<ID extends ActionID>(user: User, actionId: ID, context: SpecificNotificationContext<ID>) {
if (!isGamificationFeatureActive()) {
logger.warn(`Gamification feature is not active`);
return;
}
const templatesForAction = await tracer.trace('achievement.getTemplatesByAction', () => getTemplatesByAction(actionId));
const metricsForAction = getMetricsByAction(actionId);
const templatesForAction = await tracer.trace('achievement.getTemplatesByMetrics', () => getTemplatesByMetrics(metricsForAction));

if (templatesForAction.length === 0) {
logger.debug(`No achievement found for action '${actionId}'`);
Expand All @@ -32,36 +38,46 @@ async function _rewardActionTaken<ID extends ActionID>(user: User, actionId: ID,
user: user,
context,
};
await tracer.trace('achievement.trackEvent', () => trackEvent(actionEvent));
const isEventTracked = await tracer.trace('achievement.trackEvent', () => trackEvent(actionEvent));
if (!isEventTracked) {
logger.warn(`Can't track event for action '${actionId}' for user '${user.userID}'`);
return;
}

for (const [groupName, group] of templatesByGroups) {
await tracer.trace('achievement.evaluateAchievementGroups', async (span) => {
span.setTag('achievement.group', groupName);
let achievementToCheck: AchievementToCheck;
for (const template of group) {
const userAchievement = await tracer.trace('achievement.getOrCreateUserAchievement', () =>
getOrCreateUserAchievement(template, user.userID, context)
);
if (userAchievement.achievedAt === null || userAchievement.recordValue) {
achievementToCheck = userAchievement;
break;
try {
await tracer.trace('achievement.evaluateAchievementGroups', async (span) => {
span.setTag('achievement.group', groupName);
let achievementToCheck: AchievementToCheck;
for (const template of group) {
const userAchievement = await tracer.trace('achievement.getOrCreateUserAchievement', () =>
getOrCreateUserAchievement(template, user.userID, context)
);
if (userAchievement.achievedAt === null || userAchievement.template.type === achievement_type_enum.STREAK) {
achievementToCheck = userAchievement;
break;
}
}
}
span.setTag('achievement.foundToCheck', !!achievementToCheck);
if (achievementToCheck) {
span.setTag('achievement.id', achievementToCheck.id);
await tracer.trace('achievement.checkUserAchievement', () => checkUserAchievement(achievementToCheck as UserAchievementTemplate, actionEvent));
}
});
span.setTag('achievement.foundToCheck', !!achievementToCheck);
if (achievementToCheck) {
span.setTag('achievement.id', achievementToCheck.id);
await tracer.trace('achievement.checkUserAchievement', () =>
checkUserAchievement(achievementToCheck as UserAchievementTemplate, actionEvent)
);
}
});
} catch (e) {
logger.error(`Error occurred while checking achievement for user '${user.userID}'`, e);
}
}
}

async function trackEvent<ID extends ActionID>(event: ActionEvent<ID>) {
const metricsForEvent = getMetricsByAction(event.actionId);

if (!metricsForEvent) {
logger.debug(`Can't track event, because no metrics found for action '${event.actionId}'`);
return;
if (metricsForEvent.length === 0) {
logger.warn(`Can't track event, because no metrics found for action '${event.actionId}'`);
return false;
}

for (const metric of metricsForEvent) {
Expand All @@ -75,6 +91,7 @@ async function trackEvent<ID extends ActionID>(event: ActionEvent<ID>) {
action: event.actionId,
userId: event.user.userID,
relation: event.context.relation ?? '',
createdAt: event.at,
},
});
}
Expand All @@ -91,8 +108,7 @@ async function checkUserAchievement<ID extends ActionID>(userAchievement: UserAc
const evaluationResultValue =
typeof evaluationResult.resultObject[dataAggregationKey] === 'number' ? Number(evaluationResult.resultObject[dataAggregationKey]) : null;
const awardedAchievement = await rewardUser(evaluationResultValue, userAchievement, event);
const userAchievementContext: UserAchievementContext = {};
await createAchievement(awardedAchievement.template, userAchievement.userId, userAchievementContext);
await createAchievement(awardedAchievement.template, userAchievement.userId, event.context);
} else {
await prisma.user_achievement.update({
where: { id: userAchievement.id },
Expand All @@ -103,14 +119,21 @@ async function checkUserAchievement<ID extends ActionID>(userAchievement: UserAc

async function isAchievementConditionMet(achievement: UserAchievementTemplate) {
const {
userId,
recordValue,
template: { condition, conditionDataAggregations, metrics },
} = achievement;
if (!condition) {
return;
logger.error(`No condition found for achievement ${achievement.template.name}`);
}

const { conditionIsMet, resultObject } = await evaluateAchievement(condition, conditionDataAggregations as ConditionDataAggregations, metrics, recordValue);
const { conditionIsMet, resultObject } = await evaluateAchievement(
userId,
condition,
conditionDataAggregations as ConditionDataAggregations,
metrics,
recordValue
);
return { conditionIsMet, resultObject };
}

Expand Down
11 changes: 5 additions & 6 deletions common/achievement/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'reflect-metadata';
// ↑ Needed by typegraphql: https://typegraphql.com/docs/installation.html
import { ActionID } from '../notification/actions';
import { Metric } from './types';

export const metricByName: Map<string, Metric> = new Map();
export const metricsByAction: Map<ActionID, Metric[]> = new Map();
const metricsByAction: Map<ActionID, Metric[]> = new Map();

export const metricExists = (metricName: string) => metricByName.has(metricName);
export function getMetricsByAction<ID extends ActionID>(actionId: ID): Metric[] {
return metricsByAction.get(actionId) || [];
}

function registerMetric(metric: Metric) {
const { metricName, onActions } = metric;
Expand All @@ -26,10 +29,6 @@ function registerMetric(metric: Metric) {

export function registerAllMetrics(metrics: Metric[]) {
metrics.forEach((metric) => {
const { metricName } = metric;
if (metricExists(metricName)) {
throw new Error(`Metric '${metricName}' may only be registered once`);
}
registerMetric(metric);
});
}
40 changes: 10 additions & 30 deletions common/achievement/template.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import 'reflect-metadata';
// ↑ Needed by typegraphql: https://typegraphql.com/docs/installation.html
import { Achievement_template } from '../../graphql/generated';
import { getLogger } from '../logger/logger';
import { ActionID } from '../notification/actions';
import { prisma } from '../prisma';
import { metricsByAction } from './metrics';
import { getMetricsByAction } from './util';
import { Metric } from './types';

const logger = getLogger('Achievement Template');

Expand Down Expand Up @@ -41,44 +40,25 @@ async function getAchievementTemplates(select: TemplateSelectEnum): Promise<Map<
achievementTemplates.get(select)?.get(selection)?.push(template);
}
}
logger.debug(`Loaded ${templatesFromDB.length} achievement templates into the cache`);
logger.info(`Loaded ${templatesFromDB.length} achievement templates into the cache`);
}
return achievementTemplates.get(select);
}

async function getTemplatesByAction<ID extends ActionID>(actionId: ID) {
async function getTemplatesByMetrics(metricsForAction: Metric[]) {
const templatesByMetric = await getAchievementTemplates(TemplateSelectEnum.BY_METRIC);
if (!Object.keys(templatesByMetric)) {
logger.warn(`No achievement templates were found in the database for the action with id: ${actionId}`);
if (Object.keys(templatesByMetric).length === 0) {
logger.debug(`No achievement templates were found in the database for the metrics: ${metricsForAction.map((m) => `${m.metricName}, `)}`);
return [];
}
const metricsForAction = metricsByAction.get(actionId);

let templatesForAction: Achievement_template[] = [];
if (!metricsForAction || !templatesByMetric) {
return [];
} else {
for (const metric of metricsForAction) {
templatesForAction = [...templatesForAction, ...templatesByMetric.get(metric.metricName)];
}
return templatesForAction;
}
}

async function doesTemplateExistForAction<ID extends ActionID>(actionId: ID): Promise<boolean> {
const metrics = getMetricsByAction(actionId);
const achievements = await getAchievementTemplates(TemplateSelectEnum.BY_METRIC);
for (const metric of metrics) {
if (achievements.has(metric.metricName)) {
return true;
}
for (const metric of metricsForAction) {
templatesForAction = [...templatesForAction, ...templatesByMetric.get(metric.metricName)];
}

return false;
}

function isMetricExistingForActionId<ID extends ActionID>(actionId: ID): boolean {
return metricsByAction.has(actionId);
return templatesForAction;
}

export { isMetricExistingForActionId, getAchievementTemplates, doesTemplateExistForAction, getTemplatesByAction };
export { getAchievementTemplates, getTemplatesByMetrics };
11 changes: 2 additions & 9 deletions common/achievement/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Prisma } from '@prisma/client';
import { Achievement_event, Achievement_template, Lecture } from '../../graphql/generated';
import { Achievement_event, Achievement_template, Lecture, User_achievement } from '../../graphql/generated';
import { ActionID, SpecificNotificationContext } from '../notification/actions';
import { User } from '../user';

Expand Down Expand Up @@ -89,13 +88,7 @@ export type ActionEvent<ID extends ActionID> = {
context: SpecificNotificationContext<ID>;
};

export type AchievementToCheck = {
userId: string;
id: number;
achievedAt: Date;
context: Prisma.JsonValue;
template: Achievement_template;
};
export type AchievementToCheck = Pick<User_achievement, 'id' | 'userId' | 'achievedAt' | 'recordValue' | 'context' | 'template'>;

export type EvaluationResult = {
conditionIsMet: boolean;
Expand Down
Loading

0 comments on commit 1d384a3

Please sign in to comment.