Browse Source

fix: stable db migration + beta migration to stable

NGPixel 5 năm trước cách đây
mục cha
commit
8eddc4799e

+ 14 - 8
server/core/db.js

@@ -6,6 +6,7 @@ const Knex = require('knex')
 const Objection = require('objection')
 
 const migrationSource = require('../db/migrator-source')
+const migrateFromBeta = require('../db/beta')
 
 /* global WIKI */
 
@@ -110,15 +111,8 @@ module.exports = {
     // Set init tasks
     let conAttempts = 0
     let initTasks = {
-      // -> Migrate DB Schemas
-      async syncSchemas() {
-        return self.knex.migrate.latest({
-          tableName: 'migrations',
-          migrationSource
-        })
-      },
       // -> Attempt initial connection
-      async connect() {
+      async connect () {
         try {
           WIKI.logger.info('Connecting to database...')
           await self.knex.raw('SELECT 1 + 1;')
@@ -133,11 +127,23 @@ module.exports = {
             throw err
           }
         }
+      },
+      // -> Migrate DB Schemas
+      async syncSchemas () {
+        return self.knex.migrate.latest({
+          tableName: 'migrations',
+          migrationSource
+        })
+      },
+      // -> Migrate DB Schemas from beta
+      async migrateFromBeta () {
+        return migrateFromBeta.migrate(self.knex)
       }
     }
 
     let initTasksQueue = (WIKI.IS_MASTER) ? [
       initTasks.connect,
+      initTasks.migrateFromBeta,
       initTasks.syncSchemas
     ] : [
       () => { return Promise.resolve() }

+ 115 - 0
server/db/beta/index.js

@@ -0,0 +1,115 @@
+const _ = require('lodash')
+const path = require('path')
+const fs = require('fs-extra')
+const semver = require('semver')
+
+/* global WIKI */
+
+module.exports = {
+  async migrate (knex) {
+    const migrationsTableExists = await knex.schema.hasTable('migrations')
+    if (!migrationsTableExists) {
+      return
+    }
+
+    const dbCompat = {
+      charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
+    }
+
+    const migrations = await knex('migrations')
+    if (_.some(migrations, m => m.name.indexOf('2.0.0-beta') >= 0)) {
+      // -> Pre-beta.241 locale field length fix
+      const localeColnInfo = await knex('pages').columnInfo('localeCode')
+      if (WIKI.config.db.type !== 'sqlite' && localeColnInfo.maxLength === 2) {
+        // -> Load locales
+        const locales = await knex('locales')
+        await knex.schema
+          // -> Remove constraints
+          .table('users', table => {
+            table.dropForeign('localeCode')
+          })
+          .table('pages', table => {
+            table.dropForeign('localeCode')
+          })
+          .table('pageHistory', table => {
+            table.dropForeign('localeCode')
+          })
+          .table('pageTree', table => {
+            table.dropForeign('localeCode')
+          })
+          // -> Recreate locales table
+          .dropTable('locales')
+          .createTable('locales', table => {
+            if (dbCompat.charset) { table.charset('utf8mb4') }
+            table.string('code', 5).notNullable().primary()
+            table.json('strings')
+            table.boolean('isRTL').notNullable().defaultTo(false)
+            table.string('name').notNullable()
+            table.string('nativeName').notNullable()
+            table.integer('availability').notNullable().defaultTo(0)
+            table.string('createdAt').notNullable()
+            table.string('updatedAt').notNullable()
+          })
+        await knex('locales').insert(locales)
+        // -> Alter columns length
+        await knex.schema
+          .table('users', table => {
+            table.string('localeCode', 5).notNullable().defaultTo('en').alter()
+          })
+          .table('pages', table => {
+            table.string('localeCode', 5).alter()
+          })
+          .table('pageHistory', table => {
+            table.string('localeCode', 5).alter()
+          })
+          .table('pageTree', table => {
+            table.string('localeCode', 5).alter()
+          })
+          // -> Restore restraints
+          .table('users', table => {
+            table.foreign('localeCode').references('code').inTable('locales')
+          })
+          .table('pages', table => {
+            table.foreign('localeCode').references('code').inTable('locales')
+          })
+          .table('pageHistory', table => {
+            table.foreign('localeCode').references('code').inTable('locales')
+          })
+          .table('pageTree', table => {
+            table.foreign('localeCode').references('code').inTable('locales')
+          })
+      }
+
+      // -> Advance to latest beta/rc migration state
+      const baseMigrationPath = path.join(WIKI.SERVERPATH, (WIKI.config.db.type !== 'sqlite') ? 'db/beta/migrations' : 'db/beta/migrations-sqlite')
+      await knex.migrate.latest({
+        tableName: 'migrations',
+        migrationSource: {
+          async getMigrations() {
+            const migrationFiles = await fs.readdir(baseMigrationPath)
+            return migrationFiles.sort(semver.compare).map(m => ({
+              file: m,
+              directory: baseMigrationPath
+            }))
+          },
+          getMigrationName(migration) {
+            return migration.file
+          },
+          getMigration(migration) {
+            return require(path.join(baseMigrationPath, migration.file))
+          }
+        }
+      })
+
+      // -> Cleanup migration table
+      await knex('migrations').truncate()
+
+      // -> Advance to stable 2.0 migration state
+      await knex('migrations').insert({
+        name: '2.0.0.js',
+        batch: 1,
+        migration_time: knex.fn.now()
+      })
+    }
+  }
+}

+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.1.js → server/db/beta/migrations-sqlite/2.0.0-beta.1.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.11.js → server/db/beta/migrations-sqlite/2.0.0-beta.11.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.127.js → server/db/beta/migrations-sqlite/2.0.0-beta.127.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.205.js → server/db/beta/migrations-sqlite/2.0.0-beta.205.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.217.js → server/db/beta/migrations-sqlite/2.0.0-beta.217.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.242.js → server/db/beta/migrations-sqlite/2.0.0-beta.242.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.293.js → server/db/beta/migrations-sqlite/2.0.0-beta.293.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.38.js → server/db/beta/migrations-sqlite/2.0.0-beta.38.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-beta.99.js → server/db/beta/migrations-sqlite/2.0.0-beta.99.js


+ 0 - 0
server/db/migrations-sqlite/2.0.0-rc.2.js → server/db/beta/migrations-sqlite/2.0.0-rc.2.js


+ 5 - 5
server/db/migrations/2.0.0-beta.1.js → server/db/beta/migrations/2.0.0-beta.1.js

@@ -69,7 +69,7 @@ exports.up = knex => {
     // LOCALES -----------------------------
     .createTable('locales', table => {
       if (dbCompat.charset) { table.charset('utf8mb4') }
-      table.string('code', 5).notNullable().primary()
+      table.string('code', 2).notNullable().primary()
       table.json('strings')
       table.boolean('isRTL').notNullable().defaultTo(false)
       table.string('name').notNullable()
@@ -243,26 +243,26 @@ exports.up = knex => {
     .table('pageHistory', table => {
       table.integer('pageId').unsigned().references('id').inTable('pages')
       table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
+      table.string('localeCode', 2).references('code').inTable('locales')
       table.integer('authorId').unsigned().references('id').inTable('users')
     })
     .table('pages', table => {
       table.string('editorKey').references('key').inTable('editors')
-      table.string('localeCode', 5).references('code').inTable('locales')
+      table.string('localeCode', 2).references('code').inTable('locales')
       table.integer('authorId').unsigned().references('id').inTable('users')
       table.integer('creatorId').unsigned().references('id').inTable('users')
     })
     .table('pageTree', table => {
       table.integer('parent').unsigned().references('id').inTable('pageTree')
       table.integer('pageId').unsigned().references('id').inTable('pages')
-      table.string('localeCode', 5).references('code').inTable('locales')
+      table.string('localeCode', 2).references('code').inTable('locales')
     })
     .table('userKeys', table => {
       table.integer('userId').unsigned().references('id').inTable('users')
     })
     .table('users', table => {
       table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
-      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
+      table.string('localeCode', 2).references('code').inTable('locales').notNullable().defaultTo('en')
       table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
 
       table.unique(['providerKey', 'email'])

+ 0 - 0
server/db/migrations/2.0.0-beta.11.js → server/db/beta/migrations/2.0.0-beta.11.js


+ 0 - 0
server/db/migrations/2.0.0-beta.127.js → server/db/beta/migrations/2.0.0-beta.127.js


+ 0 - 0
server/db/migrations/2.0.0-beta.148.js → server/db/beta/migrations/2.0.0-beta.148.js


+ 0 - 0
server/db/migrations/2.0.0-beta.205.js → server/db/beta/migrations/2.0.0-beta.205.js


+ 0 - 0
server/db/migrations/2.0.0-beta.217.js → server/db/beta/migrations/2.0.0-beta.217.js


+ 0 - 0
server/db/migrations/2.0.0-beta.242.js → server/db/beta/migrations/2.0.0-beta.242.js


+ 0 - 0
server/db/migrations/2.0.0-beta.293.js → server/db/beta/migrations/2.0.0-beta.293.js


+ 0 - 0
server/db/migrations/2.0.0-beta.38.js → server/db/beta/migrations/2.0.0-beta.38.js


+ 0 - 0
server/db/migrations/2.0.0-beta.99.js → server/db/beta/migrations/2.0.0-beta.99.js


+ 2 - 1
server/db/migrations/2.0.0-rc.2.js → server/db/beta/migrations/2.0.0-rc.2.js

@@ -1,10 +1,11 @@
 /* global WIKI */
 
-exports.up = knex => {
+exports.up = async knex => {
   const dbCompat = {
     charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
     selfCascadeDelete: WIKI.config.db.type !== 'mssql'
   }
+
   return knex.schema
     .dropTable('pageTree')
     .createTable('pageTree', table => {

+ 20 - 0
server/db/beta/migrations/2.0.0-rc.29.js

@@ -0,0 +1,20 @@
+/* global WIKI */
+
+exports.up = knex => {
+  return knex.schema
+    .table('pages', table => {
+      switch (WIKI.config.db.type) {
+        case 'mariadb':
+        case 'mysql':
+          table.specificType('content', 'LONGTEXT').alter()
+          table.specificType('render', 'LONGTEXT').alter()
+          break
+        case 'mssql':
+          table.specificType('content', 'VARCHAR(max)').alter()
+          table.specificType('render', 'VARCHAR(max)').alter()
+          break
+      }
+    })
+}
+
+exports.down = knex => { }

+ 268 - 0
server/db/migrations-sqlite/2.0.0.js

@@ -0,0 +1,268 @@
+exports.up = knex => {
+  return knex.schema
+    // =====================================
+    // MODEL TABLES
+    // =====================================
+    // ANALYTICS ---------------------------
+    .createTable('analytics', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+    // ASSETS ------------------------------
+    .createTable('assets', table => {
+      table.increments('id').primary()
+      table.string('filename').notNullable()
+      table.string('hash').notNullable().defaultTo('')
+      table.string('ext').notNullable()
+      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
+      table.string('mime').notNullable().defaultTo('application/octet-stream')
+      table.integer('fileSize').unsigned().comment('In kilobytes')
+      table.json('metadata')
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+
+      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    // ASSET DATA --------------------------
+    .createTable('assetData', table => {
+      table.integer('id').primary()
+      table.binary('data').notNullable()
+    })
+    // ASSET FOLDERS -----------------------
+    .createTable('assetFolders', table => {
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.string('slug').notNullable()
+      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
+    })
+    // AUTHENTICATION ----------------------
+    .createTable('authentication', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+      table.boolean('selfRegistration').notNullable().defaultTo(false)
+      table.json('domainWhitelist').notNullable()
+      table.json('autoEnrollGroups').notNullable()
+    })
+    // COMMENTS ----------------------------
+    .createTable('comments', table => {
+      table.increments('id').primary()
+      table.text('content').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+
+      table.integer('pageId').unsigned().references('id').inTable('pages')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    // EDITORS -----------------------------
+    .createTable('editors', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+    // GROUPS ------------------------------
+    .createTable('groups', table => {
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.json('permissions').notNullable()
+      table.json('pageRules').notNullable()
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // LOCALES -----------------------------
+    .createTable('locales', table => {
+      table.string('code', 5).notNullable().primary()
+      table.json('strings')
+      table.boolean('isRTL').notNullable().defaultTo(false)
+      table.string('name').notNullable()
+      table.string('nativeName').notNullable()
+      table.integer('availability').notNullable().defaultTo(0)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // LOGGING ----------------------------
+    .createTable('loggers', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.string('level').notNullable().defaultTo('warn')
+      table.json('config')
+    })
+    // NAVIGATION ----------------------------
+    .createTable('navigation', table => {
+      table.string('key').notNullable().primary()
+      table.json('config')
+    })
+    // PAGE HISTORY ------------------------
+    .createTable('pageHistory', table => {
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isPublished').notNullable().defaultTo(false)
+      table.string('publishStartDate')
+      table.string('publishEndDate')
+      table.text('content')
+      table.string('contentType').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('action').defaultTo('updated')
+
+      table.integer('pageId').unsigned()
+      table.string('editorKey').references('key').inTable('editors')
+      table.string('localeCode', 5).references('code').inTable('locales')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    // PAGE LINKS --------------------------
+    .createTable('pageLinks', table => {
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.string('path').notNullable()
+      table.string('localeCode', 5).notNullable()
+    })
+    // PAGES -------------------------------
+    .createTable('pages', table => {
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isPublished').notNullable().defaultTo(false)
+      table.string('privateNS')
+      table.string('publishStartDate')
+      table.string('publishEndDate')
+      table.text('content')
+      table.text('render')
+      table.json('toc')
+      table.string('contentType').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+
+      table.string('editorKey').references('key').inTable('editors')
+      table.string('localeCode', 5).references('code').inTable('locales')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+      table.integer('creatorId').unsigned().references('id').inTable('users')
+    })
+    // PAGE TREE ---------------------------
+    .createTable('pageTree', table => {
+      table.integer('id').primary()
+      table.string('path').notNullable()
+      table.integer('depth').unsigned().notNullable()
+      table.string('title').notNullable()
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isFolder').notNullable().defaultTo(false)
+      table.string('privateNS')
+
+      table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.string('localeCode', 5).references('code').inTable('locales')
+    })
+    // RENDERERS ---------------------------
+    .createTable('renderers', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config')
+    })
+    // SEARCH ------------------------------
+    .createTable('searchEngines', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config')
+    })
+    // SETTINGS ----------------------------
+    .createTable('settings', table => {
+      table.string('key').notNullable().primary()
+      table.json('value')
+      table.string('updatedAt').notNullable()
+    })
+    // STORAGE -----------------------------
+    .createTable('storage', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
+      table.json('config')
+      table.string('syncInterval')
+      table.json('state')
+    })
+    // TAGS --------------------------------
+    .createTable('tags', table => {
+      table.increments('id').primary()
+      table.string('tag').notNullable().unique()
+      table.string('title')
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // USER KEYS ---------------------------
+    .createTable('userKeys', table => {
+      table.increments('id').primary()
+      table.string('kind').notNullable()
+      table.string('token').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('validUntil').notNullable()
+
+      table.integer('userId').unsigned().references('id').inTable('users')
+    })
+    // USERS -------------------------------
+    .createTable('users', table => {
+      table.increments('id').primary()
+      table.string('email').notNullable()
+      table.string('name').notNullable()
+      table.string('providerId')
+      table.string('password')
+      table.boolean('tfaIsActive').notNullable().defaultTo(false)
+      table.string('tfaSecret')
+      table.string('jobTitle').defaultTo('')
+      table.string('location').defaultTo('')
+      table.string('pictureUrl')
+      table.string('timezone').notNullable().defaultTo('America/New_York')
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.boolean('isActive').notNullable().defaultTo(false)
+      table.boolean('isVerified').notNullable().defaultTo(false)
+      table.boolean('mustChangePwd').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+
+      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
+      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
+      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
+    })
+    // =====================================
+    // RELATION TABLES
+    // =====================================
+    // PAGE HISTORY TAGS ---------------------------
+    .createTable('pageHistoryTags', table => {
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
+      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
+    })
+    // PAGE TAGS ---------------------------
+    .createTable('pageTags', table => {
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
+    })
+    // USER GROUPS -------------------------
+    .createTable('userGroups', table => {
+      table.increments('id').primary()
+      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
+      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
+    })
+    // =====================================
+    // REFERENCES
+    // =====================================
+    .table('users', table => {
+      table.unique(['providerKey', 'email'])
+    })
+    // =====================================
+    // INDEXES
+    // =====================================
+    .table('pageLinks', table => {
+      table.index(['path', 'localeCode'])
+    })
+}
+
+exports.down = knex => { }

+ 325 - 0
server/db/migrations/2.0.0.js

@@ -0,0 +1,325 @@
+/* global WIKI */
+
+exports.up = knex => {
+  const dbCompat = {
+    blobLength: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
+    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`),
+    selfCascadeDelete: WIKI.config.db.type !== 'mssql'
+  }
+  return knex.schema
+    // =====================================
+    // MODEL TABLES
+    // =====================================
+    // ANALYTICS ---------------------------
+    .createTable('analytics', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+    // ASSETS ------------------------------
+    .createTable('assets', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('filename').notNullable()
+      table.string('hash').notNullable()
+      table.string('ext').notNullable()
+      table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
+      table.string('mime').notNullable().defaultTo('application/octet-stream')
+      table.integer('fileSize').unsigned().comment('In kilobytes')
+      table.json('metadata')
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // ASSET DATA --------------------------
+    .createTable('assetData', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.integer('id').primary()
+      if (dbCompat.blobLength) {
+        table.specificType('data', 'LONGBLOB').notNullable()
+      } else {
+        table.binary('data').notNullable()
+      }
+    })
+    // ASSET FOLDERS -----------------------
+    .createTable('assetFolders', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.string('slug').notNullable()
+      table.integer('parentId').unsigned().references('id').inTable('assetFolders')
+    })
+    // AUTHENTICATION ----------------------
+    .createTable('authentication', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+      table.boolean('selfRegistration').notNullable().defaultTo(false)
+      table.json('domainWhitelist').notNullable()
+      table.json('autoEnrollGroups').notNullable()
+    })
+    // COMMENTS ----------------------------
+    .createTable('comments', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.text('content').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // EDITORS -----------------------------
+    .createTable('editors', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+    // GROUPS ------------------------------
+    .createTable('groups', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('name').notNullable()
+      table.json('permissions').notNullable()
+      table.json('pageRules').notNullable()
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // LOCALES -----------------------------
+    .createTable('locales', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('code', 5).notNullable().primary()
+      table.json('strings')
+      table.boolean('isRTL').notNullable().defaultTo(false)
+      table.string('name').notNullable()
+      table.string('nativeName').notNullable()
+      table.integer('availability').notNullable().defaultTo(0)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // LOGGING ----------------------------
+    .createTable('loggers', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.string('level').notNullable().defaultTo('warn')
+      table.json('config')
+    })
+    // NAVIGATION ----------------------------
+    .createTable('navigation', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.json('config')
+    })
+    // PAGE HISTORY ------------------------
+    .createTable('pageHistory', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isPublished').notNullable().defaultTo(false)
+      table.string('publishStartDate')
+      table.string('publishEndDate')
+      table.string('action').defaultTo('updated')
+      table.integer('pageId').unsigned()
+      table.text('content')
+      table.string('contentType').notNullable()
+      table.string('createdAt').notNullable()
+    })
+    // PAGE LINKS --------------------------
+    .createTable('pageLinks', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('localeCode', 5).notNullable()
+    })
+    // PAGES -------------------------------
+    .createTable('pages', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('hash').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isPublished').notNullable().defaultTo(false)
+      table.string('privateNS')
+      table.string('publishStartDate')
+      table.string('publishEndDate')
+      switch (WIKI.config.db.type) {
+        case 'postgres':
+        case 'sqlite':
+          table.text('content')
+          table.text('render')
+          break
+        case 'mariadb':
+        case 'mysql':
+          table.specificType('content', 'LONGTEXT')
+          table.specificType('render', 'LONGTEXT')
+          break
+        case 'mssql':
+          table.specificType('content', 'VARCHAR(max)')
+          table.specificType('render', 'VARCHAR(max)')
+          break
+      }
+      table.json('toc')
+      table.string('contentType').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // PAGE TREE ---------------------------
+    .createTable('pageTree', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.integer('id').unsigned().primary()
+      table.string('path').notNullable()
+      table.integer('depth').unsigned().notNullable()
+      table.string('title').notNullable()
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isFolder').notNullable().defaultTo(false)
+      table.string('privateNS')
+    })
+    // RENDERERS ---------------------------
+    .createTable('renderers', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config')
+    })
+    // SEARCH ------------------------------
+    .createTable('searchEngines', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config')
+    })
+    // SETTINGS ----------------------------
+    .createTable('settings', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.json('value')
+      table.string('updatedAt').notNullable()
+    })
+    // STORAGE -----------------------------
+    .createTable('storage', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
+      table.json('config')
+      table.string('syncInterval')
+      table.json('state')
+    })
+    // TAGS --------------------------------
+    .createTable('tags', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('tag').notNullable().unique()
+      table.string('title')
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // USER KEYS ---------------------------
+    .createTable('userKeys', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('kind').notNullable()
+      table.string('token').notNullable()
+      table.string('createdAt').notNullable()
+      table.string('validUntil').notNullable()
+    })
+    // USERS -------------------------------
+    .createTable('users', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.string('email').notNullable()
+      table.string('name').notNullable()
+      table.string('providerId')
+      table.string('password')
+      table.boolean('tfaIsActive').notNullable().defaultTo(false)
+      table.string('tfaSecret')
+      table.string('jobTitle').defaultTo('')
+      table.string('location').defaultTo('')
+      table.string('pictureUrl')
+      table.string('timezone').notNullable().defaultTo('America/New_York')
+      table.boolean('isSystem').notNullable().defaultTo(false)
+      table.boolean('isActive').notNullable().defaultTo(false)
+      table.boolean('isVerified').notNullable().defaultTo(false)
+      table.boolean('mustChangePwd').notNullable().defaultTo(false)
+      table.string('createdAt').notNullable()
+      table.string('updatedAt').notNullable()
+    })
+    // =====================================
+    // RELATION TABLES
+    // =====================================
+    // PAGE HISTORY TAGS ---------------------------
+    .createTable('pageHistoryTags', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
+      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
+    })
+    // PAGE TAGS ---------------------------
+    .createTable('pageTags', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
+    })
+    // USER GROUPS -------------------------
+    .createTable('userGroups', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.increments('id').primary()
+      table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
+      table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')
+    })
+    // =====================================
+    // REFERENCES
+    // =====================================
+    .table('assets', table => {
+      table.integer('folderId').unsigned().references('id').inTable('assetFolders')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    .table('comments', table => {
+      table.integer('pageId').unsigned().references('id').inTable('pages')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    .table('pageHistory', table => {
+      table.string('editorKey').references('key').inTable('editors')
+      table.string('localeCode', 5).references('code').inTable('locales')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
+    .table('pageLinks', table => {
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.index(['path', 'localeCode'])
+    })
+    .table('pages', table => {
+      table.string('editorKey').references('key').inTable('editors')
+      table.string('localeCode', 5).references('code').inTable('locales')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+      table.integer('creatorId').unsigned().references('id').inTable('users')
+    })
+    .table('pageTree', table => {
+      if (dbCompat.selfCascadeDelete) {
+        table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
+      } else {
+        table.integer('parent').unsigned()
+      }
+      table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
+      table.string('localeCode', 5).references('code').inTable('locales')
+    })
+    .table('userKeys', table => {
+      table.integer('userId').unsigned().references('id').inTable('users')
+    })
+    .table('users', table => {
+      table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')
+      table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
+      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
+
+      table.unique(['providerKey', 'email'])
+    })
+}
+
+exports.down = knex => { }

+ 1 - 1
server/models/pages.js

@@ -13,7 +13,7 @@ const he = require('he')
 
 const frontmatterRegex = {
   html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/,
-  legacy: /^(<!-- TITLE: ?([\w\W]+?) -{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) -{2}>)?(?:\n|\r)*([\w\W]*)*/i,
+  legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
   markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/
 }