Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: achievement database model #867

Merged
merged 16 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/achievement/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Achievement

Achievements are a part of the Gamification.
Documentation follows...
7 changes: 7 additions & 0 deletions common/achievement/aggregator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AggregatorFunction } from './types';

type Aggregator = Record<string, AggregatorFunction>;

// Aggregators are needed to aggregate event values (achievement_event.value) or buckets for evaluation (like sum, count, max, min, avg)

export const aggregators: Aggregator = {};
6 changes: 6 additions & 0 deletions common/achievement/bucket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { BucketFormula } from './types';

type BucketCreatorDefs = Record<string, BucketFormula>;

// Buckets are needed to pre-sort and aggregate certain events by types / a certain time window (e.g. weekly) etc.
export const bucketCreatorDefs: BucketCreatorDefs = {};
4 changes: 4 additions & 0 deletions common/achievement/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Metric } from './types';

// Metric is the connection of the action that happens with the formula for calculating the value for the event
const metrics: Metric[] = [];
64 changes: 64 additions & 0 deletions common/achievement/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type Metric = {
id: number;
metricName: string;
onActions: string[];
formula: FormulaFunction;
};

export type EventValue = number | string | boolean;

export type FormulaContext = {
subcourse?: {
lectures: {
start: Date;
}[];
};
match?: {
lectures: {
start: Date;
}[];
};
appointment?: {
id: number;
duration?: number;
match?: number;
subcourse?: number;
};
};

export type FormulaFunction = (context: FormulaContext) => number | string | boolean;
LomyW marked this conversation as resolved.
Show resolved Hide resolved

// A bucket is seen as a period of time
export interface Bucket {
startTime: Date;
endTime: Date;
}

export interface BucketEvents extends Bucket {
events: TrackEvent[];
}
export interface BucketEventsWithAggr extends BucketEvents {
aggregation: EventValue;
}

type BucketFormulaFunction = (context: FormulaContext) => Bucket[];

export type BucketFormula = {
function: BucketFormulaFunction;
};

export type AggregatorFunction = {
function: (elements: EventValue[]) => number;
};

export type ConditionDataAggregations = {
[key: string]: {
metricId: number;
aggregator: string;
// These two are used to first create all the needed buckets and then aggregate the events that fall into these
// Default: count
bucketAggregator?: string;
// Default: one bucket / event
createBuckets?: string;
};
};
88 changes: 88 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,68 @@ datasource db {
url = env("DATABASE_URL")
}

// Achievement
// A final achievement can consist of several sub-achievements
model achievement_template {
id Int @id @default(autoincrement())
name String @db.VarChar
metrics String[]
templateFor achievement_template_for_enum
// Groups defines the "final-achievement"
group String @db.VarChar
// Group order tells in which place a subordinate step / achievement is
groupOrder Int
// Sequential: Here we store the name of the step (one step => one achievement) for sequential achievements
stepName String @db.VarChar
// what type of achievement is it
type achievement_type_enum
subtitle String @db.VarChar
description String @db.VarChar
image String @db.VarChar
// some achievements show actions that a user can/must perform next.
// therefore we need the action name, type (for the icon) and a redirect link
actionName String? @db.VarChar
actionRedirectLink String? @db.VarChar
actionType achievement_action_type_enum?
// Tiered: If a tiered achievement has been achieved, the text changes
achievedText String? @db.VarChar
// defines what value must be reached to receive the achievement, e.g. "duration == 60", "count == 5", "count == 3 & duration == 60", "series >= 5" (https://github.com/onlabsorg/swan-js)
condition String @db.VarChar
LomyW marked this conversation as resolved.
Show resolved Hide resolved
// defined how values must be aggregated to evaluate the condition
conditionDataAggregations Json @db.Json
isActive Boolean
}

model user_achievement {
Jonasdoubleyou marked this conversation as resolved.
Show resolved Hide resolved
id Int @id @default(autoincrement())
// the id of the relating achievement_template
templateId Int
LomyW marked this conversation as resolved.
Show resolved Hide resolved
userId Int
LomyW marked this conversation as resolved.
Show resolved Hide resolved
group String @unique @db.VarChar
groupOrder Int
LomyW marked this conversation as resolved.
Show resolved Hide resolved
isSeen Boolean @default(false)
Jonasdoubleyou marked this conversation as resolved.
Show resolved Hide resolved
// achievedAt == null => not achieved, achievedAt != null => achieved
achievedAt DateTime @db.Timestamp(6)
LomyW marked this conversation as resolved.
Show resolved Hide resolved
// Streaks: we have to store the highest record for streaks
recordValue Int?
context Json @db.Json
// template achievement_template @relation(fields: [templateId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

// Save (tracked) event as raw data in DB and evaluate at runtime
model achievement_event {
id Int @id @default(autoincrement())
userId String @db.VarChar
LomyW marked this conversation as resolved.
Show resolved Hide resolved
metric String @db.VarChar
// the value is calculated with the formula given in the metric
value Int
createdAt DateTime @default(now()) @db.Timestamp(6)
// what action triggered the event
action String @db.VarChar
// the relation is like a unique id so that we can later assign the event uniquely, e.g. "course/10", "match/12", "user/10"
relation String @db.VarChar
}

// An attachment is a file uploaded to an object storage and attached to a notification
// We also store it in the DB to be able to track the file's lifecycle, i.e. delete it when the notification is archived
model attachment {
Expand Down Expand Up @@ -347,6 +409,7 @@ model mentor {
verifiedAt DateTime? @db.Timestamp(6)
isRedacted Boolean @default(false)
lastTimeCheckedNotifications DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
lastTimeCheckedAchievements DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
notificationPreferences Json? @db.Json
lastLogin DateTime? @default(now()) @db.Timestamp(6)
wix_id String @unique(map: "IDX_5c42dcf75b1abecf9860e54a12") @db.VarChar
Expand Down Expand Up @@ -482,6 +545,7 @@ model pupil {
isRedacted Boolean @default(false)
// The last time the notification popup was opened in the App:
lastTimeCheckedNotifications DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
lastTimeCheckedAchievements DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
notificationPreferences Json? @db.Json
// The last time the user logged in to the Backend through GraphQL (= with the App):
lastLogin DateTime? @default(now()) @db.Timestamp(6)
Expand Down Expand Up @@ -596,6 +660,7 @@ model screener {
verifiedAt DateTime? @db.Timestamp(6)
isRedacted Boolean @default(false)
lastTimeCheckedNotifications DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
lastTimeCheckedAchievements DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
notificationPreferences Json? @db.Json
lastLogin DateTime? @default(now()) @db.Timestamp(6)
// DEPRECATED, Use Secret instead
Expand Down Expand Up @@ -644,6 +709,7 @@ model student {
// (but still keep the entry to keep foreign keys consistent and for statistics):
isRedacted Boolean @default(false)
lastTimeCheckedNotifications DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
lastTimeCheckedAchievements DateTime? @default(dbgenerated("'1970-01-01 00:00:00'::timestamp without time zone")) @db.Timestamp(6)
LomyW marked this conversation as resolved.
Show resolved Hide resolved
notificationPreferences Json? @db.Json
lastLogin DateTime? @default(now()) @db.Timestamp(6)
// DEPRECATED: We once had a registration form through Wix, the wix_id is still sometimes used as a foreign key
Expand Down Expand Up @@ -873,6 +939,7 @@ model waiting_list_enrollment {
pupil pupil? @relation(fields: [pupilId], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_c019519c21578e119799586d7ed")
}


enum course_category_enum {
language
focus
Expand Down Expand Up @@ -1143,6 +1210,27 @@ enum notification_sender_enum {
CERTIFICATE_OF_CONDUCT
}

enum achievement_template_for_enum {
Course
Global_Courses
Match
Global_Matches
Global
}

enum achievement_type_enum {
LomyW marked this conversation as resolved.
Show resolved Hide resolved
SEQUENTIAL
TIERED
STREAK
}

enum achievement_action_type_enum {
Appointment
Action
Wait
Info
}

enum secret_type_enum {
PASSWORD
TOKEN
Expand Down