From 8eddc4799e134dc568dbf942559df56d4bee8eb4 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 3 Nov 2019 21:59:58 -0500 Subject: [PATCH] fix: stable db migration + beta migration to stable --- server/core/db.js | 22 +- server/db/beta/index.js | 115 +++++++ .../migrations-sqlite/2.0.0-beta.1.js | 0 .../migrations-sqlite/2.0.0-beta.11.js | 0 .../migrations-sqlite/2.0.0-beta.127.js | 0 .../migrations-sqlite/2.0.0-beta.205.js | 0 .../migrations-sqlite/2.0.0-beta.217.js | 0 .../migrations-sqlite/2.0.0-beta.242.js | 0 .../migrations-sqlite/2.0.0-beta.293.js | 0 .../migrations-sqlite/2.0.0-beta.38.js | 0 .../migrations-sqlite/2.0.0-beta.99.js | 0 .../migrations-sqlite/2.0.0-rc.2.js | 0 .../db/{ => beta}/migrations/2.0.0-beta.1.js | 10 +- .../db/{ => beta}/migrations/2.0.0-beta.11.js | 0 .../{ => beta}/migrations/2.0.0-beta.127.js | 0 .../{ => beta}/migrations/2.0.0-beta.148.js | 0 .../{ => beta}/migrations/2.0.0-beta.205.js | 0 .../{ => beta}/migrations/2.0.0-beta.217.js | 0 .../{ => beta}/migrations/2.0.0-beta.242.js | 0 .../{ => beta}/migrations/2.0.0-beta.293.js | 0 .../db/{ => beta}/migrations/2.0.0-beta.38.js | 0 .../db/{ => beta}/migrations/2.0.0-beta.99.js | 0 server/db/{ => beta}/migrations/2.0.0-rc.2.js | 3 +- server/db/beta/migrations/2.0.0-rc.29.js | 20 ++ server/db/migrations-sqlite/2.0.0.js | 268 +++++++++++++++ server/db/migrations/2.0.0.js | 325 ++++++++++++++++++ server/models/pages.js | 2 +- 27 files changed, 750 insertions(+), 15 deletions(-) create mode 100644 server/db/beta/index.js rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.1.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.11.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.127.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.205.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.217.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.242.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.293.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.38.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-beta.99.js (100%) rename server/db/{ => beta}/migrations-sqlite/2.0.0-rc.2.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.1.js (97%) rename server/db/{ => beta}/migrations/2.0.0-beta.11.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.127.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.148.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.205.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.217.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.242.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.293.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.38.js (100%) rename server/db/{ => beta}/migrations/2.0.0-beta.99.js (100%) rename server/db/{ => beta}/migrations/2.0.0-rc.2.js (98%) create mode 100644 server/db/beta/migrations/2.0.0-rc.29.js create mode 100644 server/db/migrations-sqlite/2.0.0.js create mode 100644 server/db/migrations/2.0.0.js diff --git a/server/core/db.js b/server/core/db.js index ede159c8..969810a0 100644 --- a/server/core/db.js +++ b/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() } diff --git a/server/db/beta/index.js b/server/db/beta/index.js new file mode 100644 index 00000000..aa3157c3 --- /dev/null +++ b/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() + }) + } + } +} diff --git a/server/db/migrations-sqlite/2.0.0-beta.1.js b/server/db/beta/migrations-sqlite/2.0.0-beta.1.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.1.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.1.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.11.js b/server/db/beta/migrations-sqlite/2.0.0-beta.11.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.11.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.11.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.127.js b/server/db/beta/migrations-sqlite/2.0.0-beta.127.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.127.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.127.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.205.js b/server/db/beta/migrations-sqlite/2.0.0-beta.205.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.205.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.205.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.217.js b/server/db/beta/migrations-sqlite/2.0.0-beta.217.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.217.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.217.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.242.js b/server/db/beta/migrations-sqlite/2.0.0-beta.242.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.242.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.242.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.293.js b/server/db/beta/migrations-sqlite/2.0.0-beta.293.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.293.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.293.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.38.js b/server/db/beta/migrations-sqlite/2.0.0-beta.38.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.38.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.38.js diff --git a/server/db/migrations-sqlite/2.0.0-beta.99.js b/server/db/beta/migrations-sqlite/2.0.0-beta.99.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-beta.99.js rename to server/db/beta/migrations-sqlite/2.0.0-beta.99.js diff --git a/server/db/migrations-sqlite/2.0.0-rc.2.js b/server/db/beta/migrations-sqlite/2.0.0-rc.2.js similarity index 100% rename from server/db/migrations-sqlite/2.0.0-rc.2.js rename to server/db/beta/migrations-sqlite/2.0.0-rc.2.js diff --git a/server/db/migrations/2.0.0-beta.1.js b/server/db/beta/migrations/2.0.0-beta.1.js similarity index 97% rename from server/db/migrations/2.0.0-beta.1.js rename to server/db/beta/migrations/2.0.0-beta.1.js index 62ced8a3..7b1e9f84 100644 --- a/server/db/migrations/2.0.0-beta.1.js +++ b/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']) diff --git a/server/db/migrations/2.0.0-beta.11.js b/server/db/beta/migrations/2.0.0-beta.11.js similarity index 100% rename from server/db/migrations/2.0.0-beta.11.js rename to server/db/beta/migrations/2.0.0-beta.11.js diff --git a/server/db/migrations/2.0.0-beta.127.js b/server/db/beta/migrations/2.0.0-beta.127.js similarity index 100% rename from server/db/migrations/2.0.0-beta.127.js rename to server/db/beta/migrations/2.0.0-beta.127.js diff --git a/server/db/migrations/2.0.0-beta.148.js b/server/db/beta/migrations/2.0.0-beta.148.js similarity index 100% rename from server/db/migrations/2.0.0-beta.148.js rename to server/db/beta/migrations/2.0.0-beta.148.js diff --git a/server/db/migrations/2.0.0-beta.205.js b/server/db/beta/migrations/2.0.0-beta.205.js similarity index 100% rename from server/db/migrations/2.0.0-beta.205.js rename to server/db/beta/migrations/2.0.0-beta.205.js diff --git a/server/db/migrations/2.0.0-beta.217.js b/server/db/beta/migrations/2.0.0-beta.217.js similarity index 100% rename from server/db/migrations/2.0.0-beta.217.js rename to server/db/beta/migrations/2.0.0-beta.217.js diff --git a/server/db/migrations/2.0.0-beta.242.js b/server/db/beta/migrations/2.0.0-beta.242.js similarity index 100% rename from server/db/migrations/2.0.0-beta.242.js rename to server/db/beta/migrations/2.0.0-beta.242.js diff --git a/server/db/migrations/2.0.0-beta.293.js b/server/db/beta/migrations/2.0.0-beta.293.js similarity index 100% rename from server/db/migrations/2.0.0-beta.293.js rename to server/db/beta/migrations/2.0.0-beta.293.js diff --git a/server/db/migrations/2.0.0-beta.38.js b/server/db/beta/migrations/2.0.0-beta.38.js similarity index 100% rename from server/db/migrations/2.0.0-beta.38.js rename to server/db/beta/migrations/2.0.0-beta.38.js diff --git a/server/db/migrations/2.0.0-beta.99.js b/server/db/beta/migrations/2.0.0-beta.99.js similarity index 100% rename from server/db/migrations/2.0.0-beta.99.js rename to server/db/beta/migrations/2.0.0-beta.99.js diff --git a/server/db/migrations/2.0.0-rc.2.js b/server/db/beta/migrations/2.0.0-rc.2.js similarity index 98% rename from server/db/migrations/2.0.0-rc.2.js rename to server/db/beta/migrations/2.0.0-rc.2.js index e2427ed9..22987d5f 100644 --- a/server/db/migrations/2.0.0-rc.2.js +++ b/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 => { diff --git a/server/db/beta/migrations/2.0.0-rc.29.js b/server/db/beta/migrations/2.0.0-rc.29.js new file mode 100644 index 00000000..fcbe2725 --- /dev/null +++ b/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 => { } diff --git a/server/db/migrations-sqlite/2.0.0.js b/server/db/migrations-sqlite/2.0.0.js new file mode 100644 index 00000000..4428cfc4 --- /dev/null +++ b/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 => { } diff --git a/server/db/migrations/2.0.0.js b/server/db/migrations/2.0.0.js new file mode 100644 index 00000000..91101890 --- /dev/null +++ b/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 => { } diff --git a/server/models/pages.js b/server/models/pages.js index b8bf9b10..29a63e00 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -13,7 +13,7 @@ const he = require('he') const frontmatterRegex = { html: /^()?(?:\n|\r)*([\w\W]*)*/, - legacy: /^(