Browse Source

feat: admin groups - list + create, gql refactoring

NGPixel 7 years ago
parent
commit
7793df9bd4

+ 52 - 12
client/components/admin-groups.vue

@@ -2,7 +2,7 @@
   v-card(flat)
     v-card(flat, color='grey lighten-5').pa-3.pt-4
       .headline.blue--text.text--darken-2 Groups
-      .subheading.grey--text Manage groups
+      .subheading.grey--text Manage groups and their permissions
     v-card
       v-card-title
         v-dialog(v-model='newGroupDialog', max-width='500')
@@ -17,7 +17,7 @@
               v-spacer
               v-btn(flat, @click='newGroupDialog = false') Cancel
               v-btn(color='primary', @click='createGroup') Create
-        v-btn(icon)
+        v-btn(icon, @click='refresh')
           v-icon.grey--text refresh
         v-spacer
         v-text-field(append-icon='search', label='Search', single-line, hide-details, v-model='search')
@@ -44,6 +44,11 @@
 </template>
 
 <script>
+import _ from 'lodash'
+
+import groupsQuery from 'gql/admin-groups-query-list.gql'
+import createGroupMutation from 'gql/admin-groups-mutation-create.gql'
+
 export default {
   data() {
     return {
@@ -71,18 +76,53 @@ export default {
     }
   },
   methods: {
+    async refresh() {
+      await this.$apollo.queries.groups.refetch()
+      this.$store.commit('showNotification', {
+        message: 'Groups have been refreshed.',
+        style: 'success',
+        icon: 'cached'
+      })
+    },
     async createGroup() {
-      // try {
-      //   const resp = await this.$apollo.mutate({
-      //     mutation: CONSTANTS.GRAPH.GROUPS.CREATE,
-      //     variables: {
-      //       name: this.newGroupName
-      //     }
-      //   })
+      this.newGroupDialog = false
+      try {
+        await this.$apollo.mutate({
+          mutation: createGroupMutation,
+          variables: {
+            name: this.newGroupName
+          },
+          update (store, resp) {
+            const data = _.get(resp, 'data.groups.create', { responseResult: {} })
+            if (data.responseResult.succeeded === true) {
+              const apolloData = store.readQuery({ query: groupsQuery })
+              apolloData.groups.list.push(data.group)
+              store.writeQuery({ query: groupsQuery, data: apolloData })
+            } else {
+              throw new Error(data.responseResult.message)
+            }
+          },
+          watchLoading (isLoading) {
+            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-create')
+          }
+        })
+        this.$store.commit('showNotification', {
+          style: 'success',
+          message: `Group has been created successfully.`,
+          icon: 'check'
+        })
+      } catch (err) {
 
-      // } catch (err) {
-
-      // }
+      }
+    }
+  },
+  apollo: {
+    groups: {
+      query: groupsQuery,
+      update: (data) => data.groups.list,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
+      }
     }
   }
 }

+ 17 - 42
client/components/admin-system.vue

@@ -93,24 +93,15 @@
                   v-list-tile-content
                     v-list-tile-title {{ info.postgreVersion }}
                     v-list-tile-sub-title {{ info.postgreHost }}
-
-    v-snackbar(
-      color='success'
-      top
-      v-model='refreshCompleted'
-    )
-      v-icon.mr-3(dark) cached
-      | System Info has been refreshed.
-
 </template>
 
 <script>
-import gql from 'graphql-tag'
-
 import IconCube from 'mdi/cube'
 import IconDatabase from 'mdi/database'
 import IconNodeJs from 'mdi/nodejs'
 
+import systemInfoQuery from 'gql/admin-system-query-info.gql'
+
 export default {
   components: {
     IconCube,
@@ -119,42 +110,26 @@ export default {
   },
   data() {
     return {
-      info: {},
-      refreshCompleted: false
-    }
-  },
-  apollo: {
-    info: {
-      query: gql`
-        query {
-          system {
-            info {
-              currentVersion
-              latestVersion
-              latestVersionReleaseDate
-              operatingSystem
-              hostname
-              cpuCores
-              ramTotal
-              workingDirectory
-              nodeVersion
-              redisVersion
-              redisUsedRAM
-              redisTotalRAM
-              redisHost
-              postgreVersion
-              postgreHost
-            }
-          }
-        }
-      `,
-      update: (data) => data.system.info
+      info: {}
     }
   },
   methods: {
     async refresh() {
       await this.$apollo.queries.info.refetch()
-      this.refreshCompleted = true
+      this.$store.commit('showNotification', {
+        message: 'System Info has been refreshed.',
+        style: 'success',
+        icon: 'cached'
+      })
+    }
+  },
+  apollo: {
+    info: {
+      query: systemInfoQuery,
+      update: (data) => data.system.info,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')
+      }
     }
   }
 }

+ 19 - 0
client/components/admin.vue

@@ -69,10 +69,22 @@
 
     v-footer.py-2.justify-center(app, absolute, color='grey lighten-3', inset, height='auto')
       .caption.grey--text.text--darken-1 Powered by Wiki.js
+
+    v-snackbar(
+      :color='notification.style'
+      bottom,
+      right,
+      multi-line,
+      v-model='notificationState'
+    )
+      .text-xs-left
+        v-icon.mr-3(dark) {{ notification.icon }}
+        span {{ notification.message }}
 </template>
 
 <script>
 import VueRouter from 'vue-router'
+import { mapState } from 'vuex'
 
 const router = new VueRouter({
   mode: 'history',
@@ -105,6 +117,13 @@ export default {
       adminDrawerShown: true
     }
   },
+  computed: {
+    ...mapState(['notification']),
+    notificationState: {
+      get() { return this.notification.isActive },
+      set(newState) { this.$store.commit('updateNotificationState', newState) }
+    }
+  },
   router
 }
 </script>

+ 11 - 50
client/components/login.vue

@@ -38,10 +38,13 @@
 </template>
 
 <script>
-/* global graphQL, siteConfig */
+/* global siteConfig */
 
 import _ from 'lodash'
-import gql from 'graphql-tag'
+
+import strategiesQuery from 'gql/login-query-strategies.gql'
+import loginMutation from 'gql/login-mutation-login.gql'
+import tfaMutation from 'gql/login-mutation-tfa.gql'
 
 export default {
   data () {
@@ -81,22 +84,8 @@ export default {
     },
     refreshStrategies () {
       this.isLoading = true
-      graphQL.query({
-        query: gql`
-          query {
-            authentication {
-              providers(
-                filter: "isEnabled eq true",
-                orderBy: "title ASC"
-              ) {
-                key
-                title
-                useForm
-                icon
-              }
-            }
-          }
-        `
+      this.$apollo.query({
+        query: strategiesQuery
       }).then(resp => {
         if (_.has(resp, 'data.authentication.providers')) {
           this.strategies = _.get(resp, 'data.authentication.providers', [])
@@ -131,23 +120,8 @@ export default {
         this.$refs.iptPassword.focus()
       } else {
         this.isLoading = true
-        graphQL.mutate({
-          mutation: gql`
-            mutation($username: String!, $password: String!, $provider: String!) {
-              authentication {
-                login(username: $username, password: $password, provider: $provider) {
-                  operation {
-                    succeeded
-                    code
-                    slug
-                    message
-                  }
-                  tfaRequired
-                  tfaLoginToken
-                }
-              }
-            }
-          `,
+        this.$apollo.mutate({
+          mutation: loginMutation,
           variables: {
             username: this.username,
             password: this.password,
@@ -199,21 +173,8 @@ export default {
         this.$refs.iptTFA.focus()
       } else {
         this.isLoading = true
-        graphQL.mutate({
-          mutation: gql`
-            mutation($loginToken: String!, $securityCode: String!) {
-              authentication {
-                loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
-                  operation {
-                    succeeded
-                    code
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
+        this.$apollo.mutate({
+          mutation: tfaMutation,
           variables: {
             loginToken: this.loginToken,
             securityCode: this.securityCode

+ 6 - 1
client/components/nav-header.vue

@@ -61,7 +61,7 @@
           color='blue'
         )
     v-spacer
-    v-progress-circular.mr-3(indeterminate, color='blue', v-show='$apollo.loading')
+    v-progress-circular.mr-3(indeterminate, color='blue', v-show='isLoading')
     slot(name='actions')
     transition(name='navHeaderSearch')
       v-btn(icon, @click='searchToggle', v-if='!searchIsShown')
@@ -88,6 +88,8 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+
 export default {
   data() {
     return {
@@ -97,6 +99,9 @@ export default {
       search: ''
     }
   },
+  computed: {
+    ...mapGetters(['isLoading'])
+  },
   methods: {
     searchToggle() {
       this.searchIsLoading = false

+ 18 - 0
client/graph/admin-groups-mutation-create.gql

@@ -0,0 +1,18 @@
+mutation ($name: String!) {
+  groups {
+    create(name: $name) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+      group {
+        id
+        name
+        createdAt
+        updatedAt
+      }
+    }
+  }
+}

+ 11 - 0
client/graph/admin-groups-query-list.gql

@@ -0,0 +1,11 @@
+query {
+  groups {
+    list {
+      id
+      name
+      userCount
+      createdAt
+      updatedAt
+    }
+  }
+}

+ 21 - 0
client/graph/admin-system-query-info.gql

@@ -0,0 +1,21 @@
+query {
+  system {
+    info {
+      currentVersion
+      latestVersion
+      latestVersionReleaseDate
+      operatingSystem
+      hostname
+      cpuCores
+      ramTotal
+      workingDirectory
+      nodeVersion
+      redisVersion
+      redisUsedRAM
+      redisTotalRAM
+      redisHost
+      postgreVersion
+      postgreHost
+    }
+  }
+}

+ 14 - 0
client/graph/login-mutation-login.gql

@@ -0,0 +1,14 @@
+mutation($username: String!, $password: String!, $provider: String!) {
+  authentication {
+    login(username: $username, password: $password, provider: $provider) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+      tfaRequired
+      tfaLoginToken
+    }
+  }
+}

+ 12 - 0
client/graph/login-mutation-tfa.gql

@@ -0,0 +1,12 @@
+mutation($loginToken: String!, $securityCode: String!) {
+  authentication {
+    loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 13 - 0
client/graph/login-query-strategies.gql

@@ -0,0 +1,13 @@
+query {
+  authentication {
+    providers(
+      filter: "isEnabled eq true",
+      orderBy: "title ASC"
+    ) {
+      key
+      title
+      useForm
+      icon
+    }
+  }
+}

+ 30 - 13
client/store/index.js

@@ -1,24 +1,41 @@
+import _ from 'lodash'
 import Vue from 'vue'
 import Vuex from 'vuex'
 
-import navigator from './modules/navigator'
-
 Vue.use(Vuex)
 
 export default new Vuex.Store({
   state: {
-    loading: false
+    loadingStack: [],
+    notification: {
+      message: '',
+      style: 'primary',
+      icon: 'cached',
+      isActive: false
+    }
   },
-  mutations: {
-    loadingChange: (state, loadingState) => { state.loading = loadingState }
+  getters: {
+    isLoading: state => { return state.loadingStack.length > 0 }
   },
-  actions: {
-    alert({ dispatch }, opts) { dispatch('navigator/alert', opts) },
-    startLoading({ commit }) { commit('loadingChange', true) },
-    stopLoading({ commit }) { commit('loadingChange', false) }
+  mutations: {
+    loadingStart (state, stackName) {
+      state.loadingStack = _.union(state.loadingStack, [stackName])
+    },
+    loadingStop (state, stackName) {
+      state.loadingStack = _.without(state.loadingStack, stackName)
+    },
+    showNotification (state, opts) {
+      state.notification = _.defaults(opts, {
+        message: '',
+        style: 'primary',
+        icon: 'cached',
+        isActive: true
+      })
+    },
+    updateNotificationState (state, newState) {
+      state.notification.isActive = newState
+    }
   },
-  getters: {},
-  modules: {
-    navigator
-  }
+  actions: { },
+  modules: { }
 })

+ 0 - 40
client/store/modules/navigator.js

@@ -1,40 +0,0 @@
-import debounce from 'lodash/debounce'
-
-export default {
-  namespaced: true,
-  state: {
-    subtitleShown: false,
-    subtitleStyle: '',
-    subtitleIcon: false,
-    subtitleText: '',
-    subtitleStatic: 'Welcome'
-  },
-  getters: {},
-  mutations: {
-    subtitleChange (state, opts) {
-      state.subtitleShown = (opts.shown === true)
-      state.subtitleStyle = opts.style || ''
-      state.subtitleIcon = opts.icon || false
-      state.subtitleText = opts.msg || ''
-    },
-    subtitleStatic (state, text) {
-      state.subtitleText = text
-      state.subtitleStatic = text
-    }
-  },
-  actions: {
-    alert ({ commit, dispatch }, opts) {
-      opts.shown = true
-      commit('subtitleChange', opts)
-      dispatch('alertDismiss')
-    },
-    alertDismiss: debounce(({ commit, state }) => {
-      let opts = {
-        shown: false,
-        style: state.subtitleStyle,
-        msg: state.subtitleStatic
-      }
-      commit('subtitleChange', opts)
-    }, 5000)
-  }
-}

+ 8 - 1
config.sample.yml

@@ -21,14 +21,21 @@ paths:
 # ---------------------------------------------------------------------
 # Database
 # ---------------------------------------------------------------------
-# PostgreSQL 9.5 or later required
+# Supported Database Engines:
+# - postgres = PostgreSQL 9.5 or later
+# - mysql = MySQL 5.7.8 or later
+# - sqlite = SQLite 3.9 or later
 
 db:
+  type: postgres
+  # PostgreSQL and MySQL only:
   host: localhost
   port: 5432
   user: wikijs
   pass: wikijsrocks
   db: wiki
+  # SQLite only:
+  storage: path/to/database.sqlite
 
 # ---------------------------------------------------------------------
 # Redis

+ 7 - 1
dev/webpack/webpack.dev.js

@@ -162,6 +162,11 @@ module.exports = {
           }
         ]
       },
+      {
+        test: /\.(graphql|gql)$/,
+        exclude: /node_modules/,
+        loader: 'graphql-tag/loader'
+      },
       {
         test: /.jsx$/,
         loader: 'babel-loader',
@@ -226,7 +231,8 @@ module.exports = {
     alias: {
       '@': path.join(process.cwd(), 'client'),
       'vue$': 'vue/dist/vue.esm.js',
-      'mdi': path.resolve(process.cwd(), 'node_modules/vue-material-design-icons'),
+      'gql': path.join(process.cwd(), 'client/graph'),
+      'mdi': path.join(process.cwd(), 'node_modules/vue-material-design-icons'),
       // Duplicates fixes:
       'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'),
       'apollo-utilities': path.join(process.cwd(), 'node_modules/apollo-utilities'),

+ 6 - 0
dev/webpack/webpack.prod.js

@@ -169,6 +169,11 @@ module.exports = {
           }
         ]
       },
+      {
+        test: /\.(graphql|gql)$/,
+        exclude: /node_modules/,
+        loader: 'graphql-tag/loader'
+      },
       {
         test: /.jsx$/,
         loader: 'babel-loader',
@@ -255,6 +260,7 @@ module.exports = {
     alias: {
       '@': path.join(process.cwd(), 'client'),
       'vue$': 'vue/dist/vue.esm.js',
+      'gql': path.join(process.cwd(), 'client/graph'),
       'mdi': path.resolve(process.cwd(), 'node_modules/vue-material-design-icons'),
       // Duplicates fixes:
       'apollo-link': path.join(process.cwd(), 'node_modules/apollo-link'),

+ 2 - 0
package.json

@@ -98,6 +98,7 @@
     "moment-timezone": "0.5.14",
     "mongodb": "3.0.4",
     "multer": "1.3.0",
+    "mysql2": "1.5.3",
     "node-2fa": "1.1.2",
     "oauth2orize": "1.11.0",
     "ora": "2.0.0",
@@ -130,6 +131,7 @@
     "semver": "5.5.0",
     "sequelize": "4.37.1",
     "serve-favicon": "2.4.5",
+    "sqlite3": "4.0.0",
     "uuid": "3.2.1",
     "validator": "9.4.1",
     "validator-as-promised": "1.0.2",

+ 2 - 0
server/app/data.yml

@@ -11,11 +11,13 @@ defaults:
       repo: ./repo
       data: ./data
     db:
+      type: postgres
       host: localhost
       port: 5432
       user: wikijs
       pass: wikijsrocks
       db: wiki
+      storage: ./db.sqlite
     redis:
       host: localhost
       port: 6379

+ 8 - 4
server/core/db.js

@@ -64,7 +64,8 @@ module.exports = {
     this.inst = new this.Sequelize(WIKI.config.db.db, WIKI.config.db.user, WIKI.config.db.pass, {
       host: WIKI.config.db.host,
       port: WIKI.config.db.port,
-      dialect: 'postgres',
+      dialect: WIKI.config.db.type,
+      storage: WIKI.config.db.storage,
       pool: {
         max: 10,
         min: 0,
@@ -77,9 +78,9 @@ module.exports = {
     // Attempt to connect and authenticate to DB
 
     this.inst.authenticate().then(() => {
-      WIKI.logger.info('Database (PostgreSQL) connection: [ OK ]')
+      WIKI.logger.info(`Database (${WIKI.config.db.type}) connection: [ OK ]`)
     }).catch(err => {
-      WIKI.logger.error('Failed to connect to PostgreSQL instance.')
+      WIKI.logger.error(`Failed to connect to ${WIKI.config.db.type} instance.`)
       WIKI.logger.error(err)
       process.exit(1)
     })
@@ -112,7 +113,10 @@ module.exports = {
       },
       // -> Set Connection App Name
       setAppName() {
-        return self.inst.query(`set application_name = 'WIKI.js'`, { raw: true })
+        switch (WIKI.config.db.type) {
+          case 'postgres':
+            return self.inst.query(`set application_name = 'WIKI.js'`, { raw: true })
+        }
       }
     }
 

+ 2 - 2
server/graph/resolvers/authentication.js

@@ -35,7 +35,7 @@ module.exports = {
         let authResult = await WIKI.db.User.login(args, context)
         return {
           ...authResult,
-          operation: graphHelper.generateSuccess('Login success')
+          responseResult: graphHelper.generateSuccess('Login success')
         }
       } catch (err) {
         return graphHelper.generateError(err)
@@ -46,7 +46,7 @@ module.exports = {
         let authResult = await WIKI.db.User.loginTFA(args, context)
         return {
           ...authResult,
-          operation: graphHelper.generateSuccess('TFA success')
+          responseResult: graphHelper.generateSuccess('TFA success')
         }
       } catch (err) {
         return graphHelper.generateError(err)

+ 26 - 4
server/graph/resolvers/group.js

@@ -1,3 +1,4 @@
+const graphHelper = require('../../helpers/graph')
 
 /* global WIKI */
 
@@ -11,8 +12,22 @@ module.exports = {
     async groups() { return {} }
   },
   GroupQuery: {
-    list(obj, args, context, info) {
-      return WIKI.db.Group.findAll({ where: args })
+    async list(obj, args, context, info) {
+      return WIKI.db.Group.findAll({
+        attributes: {
+          include: [[WIKI.db.inst.fn('COUNT', WIKI.db.inst.col('users.id')), 'userCount']]
+        },
+        include: [{
+          model: WIKI.db.User,
+          attributes: [],
+          through: {
+            attributes: []
+          }
+        }],
+        raw: true,
+        // TODO: Figure out how to exclude these extra fields...
+        group: ['group.id', 'users->userGroups.createdAt', 'users->userGroups.updatedAt', 'users->userGroups.version', 'users->userGroups.userId', 'users->userGroups.groupId']
+      })
     }
   },
   GroupMutation: {
@@ -29,8 +44,15 @@ module.exports = {
         })
       })
     },
-    create(obj, args) {
-      return WIKI.db.Group.create(args)
+    async create(obj, args) {
+      const group = await WIKI.db.Group.create({
+        name: args.name
+      })
+      console.info(group)
+      return {
+        responseResult: graphHelper.generateSuccess('Group created successfully.'),
+        group
+      }
     },
     delete(obj, args) {
       return WIKI.db.Group.destroy({

+ 1 - 1
server/graph/schemas/authentication.graphql

@@ -59,7 +59,7 @@ type AuthenticationProvider {
 }
 
 type AuthenticationLoginResponse {
-  operation: ResponseStatus
+  responseResult: ResponseStatus
   tfaRequired: Boolean
   tfaLoginToken: String
 }

+ 2 - 2
server/graph/schemas/common.graphql

@@ -39,12 +39,12 @@ input KeyValuePairInput {
 }
 
 type DefaultResponse {
-  operation: ResponseStatus
+  responseResult: ResponseStatus
 }
 
 type ResponseStatus {
   succeeded: Boolean!
-  code: Int!
+  errorCode: Int!
   slug: String!
   message: String
 }

+ 2 - 1
server/graph/schemas/group.graphql

@@ -55,7 +55,7 @@ type GroupMutation {
 # -----------------------------------------------
 
 type GroupResponse {
-  operation: ResponseStatus!
+  responseResult: ResponseStatus!
   group: Group
 }
 
@@ -64,6 +64,7 @@ type Group {
   name: String!
   rights: [String]
   users: [User]
+  userCount: Int
   createdAt: Date!
   updatedAt: Date!
 }

+ 3 - 3
server/helpers/graph.js

@@ -5,7 +5,7 @@ module.exports = {
   generateSuccess (msg) {
     return {
       succeeded: true,
-      code: 0,
+      errorCode: 0,
       slug: 'ok',
       message: _.defaultTo(msg, 'Operation succeeded.')
     }
@@ -13,11 +13,11 @@ module.exports = {
   generateError (err, complete = true) {
     const error = {
       succeeded: false,
-      code: err.code || 1,
+      errorCode: err.code || 1,
       slug: err.name,
       message: err.message || 'An unexpected error occured.'
     }
-    return (complete) ? { operation: error } : error
+    return (complete) ? { responseResult: error } : error
   },
   filter (arr, filterString) {
     const prvFilter = new Filter(_.toString(filterString).replace(/'/g, `"`))

+ 1 - 1
server/models/file.js

@@ -14,7 +14,7 @@ module.exports = (sequelize, DataTypes) => {
       defaultValue: 'application/octet-stream'
     },
     extra: {
-      type: DataTypes.JSONB,
+      type: DataTypes.JSON,
       allowNull: true
     },
     filename: {

+ 1 - 1
server/models/setting.js

@@ -8,7 +8,7 @@ module.exports = (sequelize, DataTypes) => {
       allowNull: false
     },
     config: {
-      type: DataTypes.JSONB,
+      type: DataTypes.JSON,
       allowNull: false
     }
   }, {

+ 150 - 4
yarn.lock

@@ -444,6 +444,10 @@ ansi-styles@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
 
+ansicolors@~0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
+
 any-observable@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
@@ -2277,6 +2281,13 @@ capture-stack-trace@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
 
+cardinal@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9"
+  dependencies:
+    ansicolors "~0.2.1"
+    redeyed "~1.0.0"
+
 caseless@~0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
@@ -3546,6 +3557,10 @@ delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
 
+denque@1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/denque/-/denque-1.2.3.tgz#98c50c8dd8cdfae318cc5859cc8ee3da0f9b0cc2"
+
 denque@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/denque/-/denque-1.1.1.tgz#10229c2b88eec1bd15ff82c5fde356e7beb6db9e"
@@ -3591,6 +3606,10 @@ detect-indent@^4.0.0:
   dependencies:
     repeating "^2.0.0"
 
+detect-libc@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
 detect-newline@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
@@ -4124,6 +4143,10 @@ esprima@^4.0.0, esprima@~4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
 
+esprima@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9"
+
 esquery@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
@@ -4770,6 +4793,12 @@ fs-extra@5.0.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
+fs-minipass@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+  dependencies:
+    minipass "^2.2.1"
+
 fs-readdir-recursive@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
@@ -5646,7 +5675,7 @@ iconv-lite@0.4.13:
   version "0.4.13"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
 
-iconv-lite@0.4.19, iconv-lite@~0.4.13:
+iconv-lite@0.4.19, iconv-lite@^0.4.18, iconv-lite@~0.4.13:
   version "0.4.19"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
 
@@ -5676,6 +5705,12 @@ ignore-loader@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.2.tgz#d81f240376d0ba4f0d778972c3ad25874117a463"
 
+ignore-walk@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+  dependencies:
+    minimatch "^3.0.4"
+
 ignore@^3.3.3:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
@@ -7474,6 +7509,10 @@ long-timeout@~0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
 
+long@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+
 longest@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -7499,7 +7538,11 @@ lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
 
-lru-cache@^4.0.1, lru-cache@^4.0.2, lru-cache@^4.1.1:
+lru-cache@2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.5.0.tgz#d82388ae9c960becbea0c73bb9eb79b6c6ce9aeb"
+
+lru-cache@4.1.1, lru-cache@^4.0.1, lru-cache@^4.0.2, lru-cache@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
   dependencies:
@@ -7885,6 +7928,19 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
+minipass@^2.2.1, minipass@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40"
+  dependencies:
+    safe-buffer "^5.1.1"
+    yallist "^3.0.0"
+
+minizlib@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+  dependencies:
+    minipass "^2.2.1"
+
 mississippi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -8018,10 +8074,37 @@ mv@~2:
     ncp "~2.0.0"
     rimraf "~2.4.0"
 
+mysql2@1.5.3:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-1.5.3.tgz#d905a1a06da0469828287608ce68647b8130748f"
+  dependencies:
+    cardinal "1.0.0"
+    denque "1.2.3"
+    generate-function "^2.0.0"
+    iconv-lite "^0.4.18"
+    long "^4.0.0"
+    lru-cache "4.1.1"
+    named-placeholders "1.1.1"
+    object-assign "^4.1.1"
+    readable-stream "2.3.5"
+    safe-buffer "^5.0.1"
+    seq-queue "0.0.5"
+    sqlstring "2.3.1"
+
+named-placeholders@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.1.tgz#3b7a0d26203dd74b3a9df4c9cfb827b2fb907e64"
+  dependencies:
+    lru-cache "2.5.0"
+
 nan@^2.3.0, nan@^2.3.2, nan@^2.3.3:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
 
+nan@~2.9.2:
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866"
+
 nanomatch@^1.2.5:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
@@ -8069,7 +8152,7 @@ ncp@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
 
-needle@^2.1.0:
+needle@^2.1.0, needle@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.0.tgz#f14efc69cee1024b72c8b21c7bdf94a731dc12fa"
   dependencies:
@@ -8194,6 +8277,21 @@ node-pre-gyp@^0.6.36:
     tar "^2.2.1"
     tar-pack "^3.4.0"
 
+node-pre-gyp@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.0.tgz#bdd4c3afac9b1b1ebff0a9ff3362859eb6781bb8"
+  dependencies:
+    detect-libc "^1.0.2"
+    mkdirp "^0.5.1"
+    needle "^2.2.0"
+    nopt "^4.0.1"
+    npm-packlist "^1.1.6"
+    npmlog "^4.0.2"
+    rc "^1.1.7"
+    rimraf "^2.6.1"
+    semver "^5.3.0"
+    tar "^4"
+
 node-sass@4.7.2:
   version "4.7.2"
   resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
@@ -8288,6 +8386,10 @@ notp@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/notp/-/notp-2.0.3.tgz#a9fd11e25cfe1ccb39fc6689544ee4c10ef9a577"
 
+npm-bundled@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
 npm-conf@^1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9"
@@ -8295,6 +8397,13 @@ npm-conf@^1.1.0:
     config-chain "^1.1.11"
     pify "^3.0.0"
 
+npm-packlist@^1.1.6:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+  dependencies:
+    ignore-walk "^3.0.1"
+    npm-bundled "^1.0.1"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -10506,7 +10615,7 @@ readable-stream@1.1.x:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.0.5, readable-stream@^2.3.5:
+readable-stream@2.3.5, readable-stream@^2.0.5, readable-stream@^2.3.5:
   version "2.3.5"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d"
   dependencies:
@@ -10574,6 +10683,12 @@ redent@^1.0.0:
     indent-string "^2.1.0"
     strip-indent "^1.0.1"
 
+redeyed@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a"
+  dependencies:
+    esprima "~3.0.0"
+
 redis-commands@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b"
@@ -11216,6 +11331,10 @@ send@0.16.2:
     range-parser "~1.2.0"
     statuses "~1.4.0"
 
+seq-queue@0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
+
 sequelize@4.37.1:
   version "4.37.1"
   resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-4.37.1.tgz#17aa97f269b7621015c73e77aa6b2134f45da7d7"
@@ -11564,6 +11683,17 @@ sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
 
+sqlite3@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.0.tgz#cc0e093ab51873f50d9dfc4126fcbef15d486570"
+  dependencies:
+    nan "~2.9.2"
+    node-pre-gyp "~0.9.0"
+
+sqlstring@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
+
 sshpk@^1.7.0:
   version "1.13.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
@@ -11927,6 +12057,18 @@ tar@^2.0.0, tar@^2.2.1:
     fstream "^1.0.2"
     inherits "2"
 
+tar@^4:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749"
+  dependencies:
+    chownr "^1.0.1"
+    fs-minipass "^1.2.5"
+    minipass "^2.2.4"
+    minizlib "^1.1.0"
+    mkdirp "^0.5.0"
+    safe-buffer "^5.1.1"
+    yallist "^3.0.2"
+
 temp@^0.8.1:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
@@ -13053,6 +13195,10 @@ yallist@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
 
+yallist@^3.0.0, yallist@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
 yaml-front-matter@^3.4.1:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/yaml-front-matter/-/yaml-front-matter-3.4.1.tgz#e52e84fea6983b93755e9b1564dba989b006b5a5"