Преглед на файлове

Merge pull request #98 from pawelmalak/db-migrations

Added database migrations
pawelmalak преди 3 години
родител
ревизия
afc0f16470
променени са 10 файла, в които са добавени 304 реда и са изтрити 54 реда
  1. 2 1
      .env
  2. 3 0
      CHANGELOG.md
  3. 1 1
      client/.env
  4. 0 32
      db.js
  5. 50 0
      db/index.js
  6. 189 0
      db/migrations/00_initial.js
  7. 21 0
      db/utils/backupDb.js
  8. 24 20
      models/Config.js
  9. 13 0
      package-lock.json
  10. 1 0
      package.json

+ 2 - 1
.env

@@ -1,2 +1,3 @@
 PORT=5005
-NODE_ENV=development
+NODE_ENV=development
+VERSION=1.6.8

+ 3 - 0
CHANGELOG.md

@@ -1,3 +1,6 @@
+### v1.6.8 (2021-10-05)
+- Implemented migration system for database
+
 ### v1.6.7 (2021-10-04)
 - Add multiple labels to Docker Compose ([#90](https://github.com/pawelmalak/flame/issues/90))
 - Custom icons via Docker Compose labels ([#91](https://github.com/pawelmalak/flame/issues/91))

+ 1 - 1
client/.env

@@ -1 +1 @@
-REACT_APP_VERSION=1.6.7
+REACT_APP_VERSION=1.6.8

+ 0 - 32
db.js

@@ -1,32 +0,0 @@
-const { Sequelize } = require('sequelize');
-const Logger = require('./utils/Logger');
-const logger = new Logger();
-
-const sequelize = new Sequelize({
-  dialect: 'sqlite',
-  storage: './data/db.sqlite',
-  logging: false,
-});
-
-const connectDB = async () => {
-  try {
-    await sequelize.authenticate();
-    logger.log('Connected to database');
-
-    const syncModels = true;
-
-    if (syncModels) {
-      logger.log('Starting model synchronization');
-      await sequelize.sync({ alter: true });
-      logger.log('All models were synchronized');
-    }
-  } catch (error) {
-    logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR');
-    process.exit(1);
-  }
-};
-
-module.exports = {
-  connectDB,
-  sequelize,
-};

+ 50 - 0
db/index.js

@@ -0,0 +1,50 @@
+const { Sequelize } = require('sequelize');
+const { join } = require('path');
+const fs = require('fs');
+const Umzug = require('umzug');
+const backupDB = require('./utils/backupDb');
+
+const Logger = require('../utils/Logger');
+const logger = new Logger();
+
+const sequelize = new Sequelize({
+  dialect: 'sqlite',
+  storage: './data/db.sqlite',
+  logging: false,
+});
+
+const umzug = new Umzug({
+  migrations: {
+    path: join(__dirname, './migrations'),
+    params: [sequelize.getQueryInterface()],
+  },
+  storage: 'sequelize',
+  storageOptions: {
+    sequelize,
+  },
+});
+
+const connectDB = async () => {
+  try {
+    backupDB();
+
+    await sequelize.authenticate();
+    logger.log('Connected to database');
+
+    // migrations
+    const pendingMigrations = await umzug.pending();
+
+    if (pendingMigrations.length > 0) {
+      logger.log('Executing pending migrations');
+      await umzug.up();
+    }
+  } catch (error) {
+    logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR');
+    process.exit(1);
+  }
+};
+
+module.exports = {
+  connectDB,
+  sequelize,
+};

+ 189 - 0
db/migrations/00_initial.js

@@ -0,0 +1,189 @@
+const { DataTypes } = require('sequelize');
+const { INTEGER, DATE, STRING, TINYINT, FLOAT, TEXT } = DataTypes;
+
+const up = async (query) => {
+  // CONFIG TABLE
+  await query.createTable('config', {
+    id: {
+      type: INTEGER,
+      autoIncrement: true,
+      primaryKey: true,
+    },
+    key: {
+      type: STRING,
+      allowNull: false,
+      unique: true,
+    },
+    value: {
+      type: STRING,
+      allowNull: false,
+    },
+    valueType: {
+      type: STRING,
+      allowNull: false,
+    },
+    isLocked: {
+      type: TINYINT,
+      defaultValue: 0,
+    },
+    createdAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    updatedAt: {
+      type: DATE,
+      allowNull: false,
+    },
+  });
+
+  // WEATHER TABLE
+  await query.createTable('weather', {
+    id: {
+      type: INTEGER,
+      autoIncrement: true,
+      primaryKey: true,
+    },
+    externalLastUpdate: {
+      type: STRING,
+    },
+    tempC: {
+      type: FLOAT,
+    },
+    tempF: {
+      type: FLOAT,
+    },
+    isDay: {
+      type: INTEGER,
+    },
+    cloud: {
+      type: INTEGER,
+    },
+    conditionText: {
+      type: TEXT,
+    },
+    conditionCode: {
+      type: INTEGER,
+    },
+    createdAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    updatedAt: {
+      type: DATE,
+      allowNull: false,
+    },
+  });
+
+  // CATEGORIES TABLE
+  await query.createTable('categories', {
+    id: {
+      type: INTEGER,
+      autoIncrement: true,
+      primaryKey: true,
+    },
+    name: {
+      type: STRING,
+      allowNull: false,
+    },
+    isPinned: {
+      type: TINYINT,
+      defaultValue: 0,
+    },
+    createdAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    updatedAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    orderId: {
+      type: INTEGER,
+      defaultValue: null,
+    },
+  });
+
+  // BOOKMARKS TABLE
+  await query.createTable('bookmarks', {
+    id: {
+      type: INTEGER,
+      autoIncrement: true,
+      primaryKey: true,
+    },
+    name: {
+      type: STRING,
+      allowNull: false,
+    },
+    url: {
+      type: STRING,
+      allowNull: false,
+    },
+    categoryId: {
+      type: INTEGER,
+      allowNull: false,
+    },
+    icon: {
+      type: STRING,
+      defaultValue: '',
+    },
+    createdAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    updatedAt: {
+      type: DATE,
+      allowNull: false,
+    },
+  });
+
+  // APPS TABLE
+  await query.createTable('apps', {
+    id: {
+      type: INTEGER,
+      autoIncrement: true,
+      primaryKey: true,
+    },
+    name: {
+      type: STRING,
+      allowNull: false,
+    },
+    url: {
+      type: STRING,
+      allowNull: false,
+    },
+    icon: {
+      type: STRING,
+      allowNull: false,
+      defaultValue: 'cancel',
+    },
+    isPinned: {
+      type: TINYINT,
+      defaultValue: 0,
+    },
+    createdAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    updatedAt: {
+      type: DATE,
+      allowNull: false,
+    },
+    orderId: {
+      type: INTEGER,
+      defaultValue: null,
+    },
+  });
+};
+
+const down = async (query) => {
+  await query.dropTable('config');
+  await query.dropTable('weather');
+  await query.dropTable('categories');
+  await query.dropTable('bookmarks');
+  await query.dropTable('apps');
+};
+
+module.exports = {
+  up,
+  down,
+};

+ 21 - 0
db/utils/backupDb.js

@@ -0,0 +1,21 @@
+const fs = require('fs');
+
+const backupDB = () => {
+  if (!fs.existsSync('data/db_backups')) {
+    fs.mkdirSync('data/db_backups');
+  }
+
+  const version = process.env.VERSION;
+  const slug = `db-${version.replace(/\./g, '')}-backup.sqlite`;
+
+  const srcPath = 'data/db.sqlite';
+  const destPath = `data/db_backups/${slug}`;
+
+  if (fs.existsSync(srcPath)) {
+    if (!fs.existsSync(destPath)) {
+      fs.copyFileSync(srcPath, destPath);
+    }
+  }
+};
+
+module.exports = backupDB;

+ 24 - 20
models/Config.js

@@ -1,26 +1,30 @@
 const { DataTypes } = require('sequelize');
 const { sequelize } = require('../db');
 
-const Config = sequelize.define('Config', {
-  key: {
-    type: DataTypes.STRING,
-    allowNull: false,
-    unique: true
+const Config = sequelize.define(
+  'Config',
+  {
+    key: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      unique: true,
+    },
+    value: {
+      type: DataTypes.STRING,
+      allowNull: false,
+    },
+    valueType: {
+      type: DataTypes.STRING,
+      allowNull: false,
+    },
+    isLocked: {
+      type: DataTypes.TINYINT,
+      defaultValue: 0,
+    },
   },
-  value: {
-    type: DataTypes.STRING,
-    allowNull: false
-  },
-  valueType: {
-    type: DataTypes.STRING,
-    allowNull: false
-  },
-  isLocked: {
-    type: DataTypes.BOOLEAN,
-    defaultValue: false
+  {
+    tableName: 'config',
   }
-}, {
-  tableName: 'config'
-});
+);
 
-module.exports = Config;
+module.exports = Config;

+ 13 - 0
package-lock.json

@@ -525,6 +525,11 @@
         "inherits": "~2.0.0"
       }
     },
+    "bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
     "body-parser": {
       "version": "1.19.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -3240,6 +3245,14 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "umzug": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
+      "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==",
+      "requires": {
+        "bluebird": "^3.7.2"
+      }
+    },
     "undefsafe": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "node-schedule": "^2.0.0",
     "sequelize": "^6.6.2",
     "sqlite3": "^5.0.2",
+    "umzug": "^2.3.0",
     "ws": "^7.4.6"
   },
   "devDependencies": {