From 7c4155447af96f31fe0d3ed5dccb5dd4dc3192ee Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Wed, 26 Sep 2018 15:00:14 +0200
Subject: [PATCH] Root mutations are now type safe

---
 frontend/package.json                         |   3 +-
 frontend/src/api.ts                           |   6 +-
 frontend/src/components/DataExport.vue        |   4 +-
 .../subscriptions/SubscriptionForList.vue     |  66 ++++-----
 frontend/src/store/actions.ts                 |  31 ++--
 frontend/src/store/modules/subscriptions.ts   |   1 -
 frontend/src/store/mutations.ts               | 132 +++++++++---------
 .../store/plugins/lastInteractionPlugin.ts    |   6 +-
 frontend/src/store/store.ts                   |   8 +-
 frontend/yarn.lock                            |   8 +-
 10 files changed, 124 insertions(+), 141 deletions(-)

diff --git a/frontend/package.json b/frontend/package.json
index dab5a75e..4447f55e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,7 +22,8 @@
     "vue-router": "^3.0.1",
     "vuetify": "^1.1.9",
     "vuex": "^3.0.1",
-    "vuex-persistedstate": "^2.5.4"
+    "vuex-persistedstate": "^2.5.4",
+    "vuex-typex": "https://github.com/robinhundt/vuex-typex.git"
   },
   "devDependencies": {
     "@types/chai": "^4.1.0",
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 1f2d2c17..efa7c9de 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -118,10 +118,10 @@ export async function fetchFeedback ({ofSubmission}: {ofSubmission: string}): Pr
   return (await ax.get(url)).data
 }
 
-export async function fetchExamType ({examPk, fields = []}:
-{examPk?: string, fields?: string[]}): Promise<Exam | Array<Exam>> {
+export async function fetchExamTypes ({fields = []}:
+{fields?: string[]}): Promise<Array<Exam>> {
   const url = addFieldsToUrl({
-    url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
+    url: `/api/examtype/`,
     fields})
   return (await ax.get(url)).data
 }
diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue
index 269d2a27..3daa7cc7 100644
--- a/frontend/src/components/DataExport.vue
+++ b/frontend/src/components/DataExport.vue
@@ -44,7 +44,7 @@
 import {mapGetters} from 'vuex'
 import ax from '@/api'
 import FileSelect from '@/components/util/FileSelect'
-import { mut } from '@/store/mutations'
+import { mutations as mut } from '@/store/mutations'
 import { parseCSVMapMixin } from '@/components/mixins/mixins'
 
 export default {
@@ -86,7 +86,7 @@ export default {
     readMapFileAndCommit (callback) {
       this.fileReader.onload = event => {
         const studentMap = this.parseCSVMap(event.target.result)
-        this.$store.commit(mut.SET_STUDENT_MAP, studentMap)
+        mut.SET_STUDENT_MAP(studentMap)
         callback()
       }
       this.fileReader.readAsText(this.mapFile)
diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue
index b8284c04..50619e3b 100644
--- a/frontend/src/components/subscriptions/SubscriptionForList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionForList.vue
@@ -5,6 +5,7 @@
       :to="subscriptionRoute"
       style="width: 100%"
     >
+      <!-- dynamically set css class depending on active -->
       <v-list-tile-content
         :class="{'inactive-subscription': !active}"
         class="ml-3">
@@ -17,48 +18,31 @@
   </v-layout>
 </template>
 
-<script>
-export default {
-  name: 'subscription-for-list',
-  props: {
-    pk: {
-      types: String,
-      required: true
-    },
-    feedbackStage: {
-      type: String,
-      required: true
-    },
-    queryType: {
-      type: String,
-      required: true
-    },
-    available: {
-      type: Number,
-      required: true
-    },
-    assignments: {
-      type: Array,
-      required: true
-    },
-    queryKey: {
-      type: String
-    }
-  },
-  computed: {
-    name () {
-      return this.$store.getters.resolveSubscriptionKeyToName(
-        {queryKey: this.queryKey, queryType: this.queryType})
-    },
-    active () {
-      return !!this.available || this.assignments.length > 0
-    },
-    subscriptionRoute () {
-      if (this.active) {
-        return {name: 'subscription', params: {pk: this.pk}}
-      }
-      return this.$route.fullPath
+<script lang="ts">
+import {Vue, Component, Prop} from 'vue-property-decorator'
+import {Assignment} from '@/models'
+
+@Component
+export default class SubscriptionForList extends Vue {
+  @Prop({type: String, required: true}) pk!: string
+  @Prop({type: String, required: true}) feedbackStage!: string
+  @Prop({type: String, required: true}) queryType!: string
+  @Prop({type: Number, required: true}) available!: number
+  @Prop({type: Array, required: true}) assignments!: Assignment[]
+  @Prop({type: String, default: ''}) queryKey!: string
+
+  get name () {
+    return this.$store.getters.resolveSubscriptionKeyToName(
+      {queryKey: this.queryKey, queryType: this.queryType})
+  }
+  get active () {
+    return !!this.available || this.assignments.length > 0
+  }
+  get subscriptionRoute () {
+    if (this.active) {
+      return {name: 'subscription', params: {pk: this.pk}}
     }
+    return this.$route.fullPath
   }
 }
 </script>
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index 24babbe0..2ad5920a 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -1,4 +1,4 @@
-import { mut } from './mutations'
+import { mutations as mut } from './mutations'
 import { authMut } from '@/store/modules/authentication'
 import { subNotesMut } from '@/store/modules/submission-notes'
 import * as api from '@/api'
@@ -10,8 +10,8 @@ import {RootState} from '@/store/store'
 const actions: ActionTree<RootState, RootState> = {
   async getExamTypes ({commit, dispatch}) {
     try {
-      const examTypes = await api.fetchExamType({})
-      commit(mut.SET_EXAM_TYPES, examTypes)
+      const examTypes = await api.fetchExamTypes({})
+        mut.SET_EXAM_TYPES(examTypes)
     } catch (err) {
       handleError(err, dispatch, 'Unable to fetch exam types')
     }
@@ -20,7 +20,7 @@ const actions: ActionTree<RootState, RootState> = {
     try {
       const submissionTypes = await api.fetchSubmissionTypes(fields)
       submissionTypes.forEach(type => {
-        commit(mut.UPDATE_SUBMISSION_TYPE, type)
+          mut.UPDATE_SUBMISSION_TYPE(type)
       })
     } catch (err) {
       handleError(err, dispatch, 'Unable to get submission types')
@@ -32,7 +32,7 @@ const actions: ActionTree<RootState, RootState> = {
     try {
       if (opt.studentPks.length === 0) {
         const students = await api.fetchAllStudents()
-        commit(mut.SET_STUDENTS, students)
+          mut.SET_STUDENTS(students)
         return students
       } else {
         const students = await Promise.all(
@@ -41,7 +41,7 @@ const actions: ActionTree<RootState, RootState> = {
             fields: opt.fields
           }))
         )
-        students.forEach(student => commit(mut.SET_STUDENT, student))
+        students.forEach(student => mut.SET_STUDENT(student))
         return students
       }
     } catch (err) {
@@ -52,24 +52,15 @@ const actions: ActionTree<RootState, RootState> = {
   async getTutors ({commit, dispatch}) {
     try {
       const tutors = await api.fetchAllTutors()
-      commit(mut.SET_TUTORS, tutors)
+        mut.SET_TUTORS(tutors)
     } catch (err) {
       handleError(err, dispatch, 'Unable to fetch tutor data')
     }
   },
-  async getAllFeedback ({commit, dispatch}, fields: string[] = []) {
-    try {
-      const feedback = await api.fetchAllFeedback(fields)
-      commit(mut.SET_ALL_FEEDBACK, feedback)
-      commit(mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE)
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch feedback history')
-    }
-  },
   async getFeedback ({commit, dispatch}, {ofSubmission}) {
     try {
       const feedback = await api.fetchFeedback({ofSubmission})
-      commit(mut.SET_FEEDBACK, feedback)
+      mut.SET_FEEDBACK(feedback)
       return feedback
     } catch (err) {
       handleError(err, dispatch, `Unable to fetch feedback ${ofSubmission}`)
@@ -78,7 +69,7 @@ const actions: ActionTree<RootState, RootState> = {
   async getSubmissionFeedbackTest ({commit, dispatch}, {pk}) {
     try {
       const submission = await api.fetchSubmissionFeedbackTests({pk})
-      commit(mut.SET_SUBMISSION, submission)
+      mut.SET_SUBMISSION(submission)
     } catch (err) {
       handleError(err, dispatch, 'Unable to fetch submission')
     }
@@ -86,7 +77,7 @@ const actions: ActionTree<RootState, RootState> = {
   async getStatistics ({commit, dispatch}, opt) {
     try {
       const statistics = await api.fetchStatistics(opt)
-      commit(mut.SET_STATISTICS, statistics)
+      mut.SET_STATISTICS(statistics)
     } catch (err) {
       handleError(err, dispatch, 'Unable to fetch statistics')
     }
@@ -111,7 +102,7 @@ const actions: ActionTree<RootState, RootState> = {
       // TODO this should belong in auth module
       api.changeActiveForUser((state as any).authentication.user.pk, false)
     }
-    commit('RESET_STATE')
+    mut.RESET_STATE()
     commit('submissionNotes/' + subNotesMut.RESET_STATE)
     commit(authMut.SET_MESSAGE, message)
     router.push({name: 'login'})
diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts
index 4888efce..63dc6aae 100644
--- a/frontend/src/store/modules/subscriptions.ts
+++ b/frontend/src/store/modules/subscriptions.ts
@@ -34,7 +34,6 @@ function initialState (): SubscriptionsState {
 
 const MAX_NUMBER_OF_ASSIGNMENTS = 2
 
-// noinspection JSCommentMatchesSignature
 const subscriptionsModule: Module<SubscriptionsState, RootState> = {
   state: initialState(),
   getters: {
diff --git a/frontend/src/store/mutations.ts b/frontend/src/store/mutations.ts
index 252ead24..b4e343be 100644
--- a/frontend/src/store/mutations.ts
+++ b/frontend/src/store/mutations.ts
@@ -1,93 +1,95 @@
 import Vue from 'vue'
+import {getStoreBuilder} from 'vuex-typex'
 
 import {initialState, RootState} from '@/store/store'
-import {MutationTree} from 'vuex'
-import {Exam, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType, Tutor} from '@/models'
+import {Exam, Feedback, Statistics, StudentInfoForListView, SubmissionNoType, SubmissionType, Tutor} from '@/models'
 
-export const mut = Object.freeze({
-  SET_LAST_INTERACTION: 'SET_LAST_INTERACTION',
-  SET_EXAM_TYPES: 'SET_EXAM_TYPES',
-  SET_STUDENTS: 'SET_STUDENTS',
-  SET_STUDENT: 'SET_STUDENT',
-  SET_STUDENT_MAP: 'SET_STUDENT_MAP',
-  SET_TUTORS: 'SET_TUTORS',
-  SET_SUBMISSION: 'SET_SUBMISSION',
-  SET_ALL_FEEDBACK: 'SET_ALL_FEEDBACK',
-  SET_STATISTICS: 'SET_STATISTICS',
-  SET_FEEDBACK: 'SET_FEEDBACK',
-  MAP_FEEDBACK_OF_SUBMISSION_TYPE: 'MAP_FEEDBACK_OF_SUBMISSION_TYPE',
-  UPDATE_SUBMISSION_TYPE: 'UPDATE_SUBMISSION_TYPE',
-  UPDATE_SUBMISSION: 'UPDATE_SUBMISSION',
-  RESET_STATE: 'RESET_STATE'
-})
+export const mb = getStoreBuilder<RootState>()
 
-const mutations: MutationTree<RootState> = {
-  [mut.SET_EXAM_TYPES] (state, examTypes: Array<Exam>) {
+function SET_EXAM_TYPES (state: RootState, examTypes: Array<Exam>) {
     state.examTypes = examTypes.reduce((acc: {[pk: string]: Exam}, curr) => {
-      acc[curr.pk] = curr
-      return acc
+        acc[curr.pk] = curr
+        return acc
     }, {})
-  },
-  [mut.SET_STUDENTS] (state, students: Array<StudentInfoForListView>) {
+}
+function SET_STUDENTS (state: RootState, students: Array<StudentInfoForListView>) {
     state.students = students.reduce((acc: {[pk: string]: StudentInfoForListView}, curr) => {
-      acc[curr.pk] = mapStudent(curr, state.studentMap)
-      return acc
+        acc[curr.pk] = mapStudent(curr, state.studentMap)
+        return acc
     }, {})
-  },
-  [mut.SET_STUDENT] (state, student: StudentInfoForListView) {
+}
+function SET_STUDENT (state: RootState, student: StudentInfoForListView) {
     Vue.set(state.students, student.pk, mapStudent({
-      ...state.students[student.pk],
-      ...student
+        ...state.students[student.pk],
+        ...student
     }, state.studentMap))
-  },
-  [mut.SET_STUDENT_MAP] (state, map) {
+}
+// TODO proper types for student map
+function SET_STUDENT_MAP (state: RootState, map: object) {
     state.studentMap = map
-  },
-  [mut.SET_TUTORS] (state, tutors: Array<Tutor>) {
+}
+function SET_TUTORS (state: RootState, tutors: Array<Tutor>) {
     state.tutors = tutors
-  },
-  [mut.SET_SUBMISSION] (state, submission: SubmissionNoType) {
+}
+function SET_SUBMISSION (state: RootState, submission: SubmissionNoType) {
     Vue.set(state.submissions, submission.pk, submission)
-  },
-  [mut.SET_STATISTICS] (state, statistics: Statistics) {
+}
+function SET_STATISTICS (state: RootState, statistics: Statistics) {
     state.statistics = {
-      ...state.statistics,
-      ...statistics
+        ...state.statistics,
+        ...statistics
     }
-  },
-  [mut.SET_FEEDBACK] (state, feedback) {
-    Vue.set(state.feedback, feedback.pk, {
-      ...state.feedback[feedback.pk],
-      ...feedback,
-      ofSubmissionType: state.submissionTypes[feedback['ofSubmissionType']]
+}
+function SET_FEEDBACK (state: RootState, feedback: Feedback) {
+    if (!feedback.ofSubmissionType) {
+        throw new Error("Can only SET_FEEDBACK when ofSubmissionType is set")
+    }
+    // weird cast is necessary because of the type of Vue.set
+    Vue.set(state.feedback, <string><any>feedback.pk, {
+        ...state.feedback[feedback.pk],
+        ...feedback,
+        // TODO fix this fucking memory leak
+        ofSubmissionType: state.submissionTypes[feedback.ofSubmissionType]
     })
-  },
-  [mut.UPDATE_SUBMISSION_TYPE] (state, submissionType: SubmissionType) {
+}
+function UPDATE_SUBMISSION_TYPE (state: RootState, submissionType: SubmissionType) {
     const updatedSubmissionType = {
-      ...state.submissionTypes[submissionType.pk],
-      ...submissionType
+        ...state.submissionTypes[submissionType.pk],
+        ...submissionType
     }
     Vue.set(state.submissionTypes, submissionType.pk, updatedSubmissionType)
-  },
-  [mut.SET_LAST_INTERACTION] (state) {
+}
+// this func is being exported to use it's name in the latInteractionPlugin
+export function SET_LAST_INTERACTION (state: RootState) {
     state.lastAppInteraction = Date.now()
-  },
-  [mut.RESET_STATE] (state) {
+}
+function RESET_STATE (state: RootState) {
     Object.assign(state, initialState())
-  }
 }
 
 function mapStudent (student: StudentInfoForListView, map: any) {
-  if (Object.keys(map).length > 0) {
-    if (!student.matrikelNo) {
-      throw Error('Student objects need matrikelNo key in order to apply mapping')
-    }
-    return {
-      ...student,
-      ...map[student.matrikelNo]
+    if (Object.keys(map).length > 0) {
+        if (!student.matrikelNo) {
+            throw Error('Student objects need matrikelNo key in order to apply mapping')
+        }
+        return {
+            ...student,
+            ...map[student.matrikelNo]
+        }
     }
-  }
-  return student
+    return student
 }
 
-export default mutations
+export const mutations = {
+    SET_LAST_INTERACTION: mb.commit(SET_LAST_INTERACTION),
+    SET_EXAM_TYPES: mb.commit(SET_EXAM_TYPES),
+    SET_STUDENTS: mb.commit(SET_STUDENTS),
+    SET_STUDENT: mb.commit(SET_STUDENT),
+    SET_STUDENT_MAP: mb.commit(SET_STUDENT_MAP),
+    SET_TUTORS: mb.commit(SET_TUTORS),
+    SET_SUBMISSION: mb.commit(SET_SUBMISSION),
+    SET_STATISTICS: mb.commit(SET_STATISTICS),
+    SET_FEEDBACK: mb.commit(SET_FEEDBACK),
+    UPDATE_SUBMISSION_TYPE: mb.commit(UPDATE_SUBMISSION_TYPE),
+    RESET_STATE: mb.commit(RESET_STATE)
+}
diff --git a/frontend/src/store/plugins/lastInteractionPlugin.ts b/frontend/src/store/plugins/lastInteractionPlugin.ts
index 7c64f2ba..3b674ba8 100644
--- a/frontend/src/store/plugins/lastInteractionPlugin.ts
+++ b/frontend/src/store/plugins/lastInteractionPlugin.ts
@@ -1,10 +1,10 @@
-import {mut} from '@/store/mutations'
+import {mutations as mut, SET_LAST_INTERACTION} from '@/store/mutations'
 import {MutationPayload, Store} from 'vuex'
 
 export function lastInteraction (store: Store<void>) {
   store.subscribe((mutation: MutationPayload) => {
-    if (mutation.type !== mut.SET_LAST_INTERACTION) {
-      store.commit(mut.SET_LAST_INTERACTION)
+    if (mutation.type !== SET_LAST_INTERACTION.name) {
+      mut.SET_LAST_INTERACTION()
     }
   })
 }
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 849519fc..1398aa63 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -1,6 +1,7 @@
 import Vuex from 'vuex'
 import Vue from 'vue'
 import createPersistedState from 'vuex-persistedstate'
+import {getStoreBuilder} from 'vuex-typex'
 
 import studentPage from './modules/student-page'
 import submissionNotes from './modules/submission-notes'
@@ -10,9 +11,9 @@ import subscriptions from './modules/subscriptions'
 import feedbackTable from './modules/feedback_list/feedback-table'
 import feedbackSearchOptions from './modules/feedback_list/feedback-search-options'
 
+import './mutations'
 import actions from './actions'
 import getters from './getters'
-import mutations from '@/store/mutations'
 import {lastInteraction} from '@/store/plugins/lastInteractionPlugin'
 import {
   Exam, Feedback,
@@ -27,7 +28,7 @@ Vue.use(Vuex)
 export interface RootState {
     lastAppInteraction: number
     examTypes: {[pk: string]: Exam}
-    feedback: {[pk: string]: Feedback}
+    feedback: {[pk: number]: Feedback}
     submissionTypes: {[pk: string]: SubmissionType}
     submissions: {[pk: string]: SubmissionNoType}
     students: {[pk: string]: StudentInfoForListView}
@@ -57,7 +58,7 @@ export function initialState (): RootState {
 
 export const persistedStateKey = 'grady'
 
-const store = new Vuex.Store({
+const store = getStoreBuilder<RootState>().vuexStore({
   strict: process.env.NODE_ENV === 'development',
   modules: {
     authentication,
@@ -82,7 +83,6 @@ const store = new Vuex.Store({
     lastInteraction],
   actions,
   getters,
-  mutations,
   state: initialState()
 })
 
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 6d751e73..b91413a1 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -6929,7 +6929,13 @@ vuex-persistedstate@^2.5.4:
     deepmerge "^2.1.0"
     shvl "^1.3.0"
 
-vuex@^3.0.1:
+"vuex-typex@https://github.com/robinhundt/vuex-typex.git":
+  version "3.0.1"
+  resolved "https://github.com/robinhundt/vuex-typex.git#72c5eb30ac7fe7c4bf657ebe4e40e115f309fa39"
+  dependencies:
+    vuex "^3.0.0"
+
+vuex@^3.0.0, vuex@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
 
-- 
GitLab