diff --git a/db/knex_init_db.js b/db/knex_init_db.js index 46bff4bfa6..1a01909f4e 100644 --- a/db/knex_init_db.js +++ b/db/knex_init_db.js @@ -5,10 +5,11 @@ const { log } = require("../src/util"); * ⚠️⚠️⚠️⚠️⚠️⚠️ DO NOT ADD ANYTHING HERE! * IF YOU NEED TO ADD FIELDS, ADD IT TO ./db/knex_migrations * See ./db/knex_migrations/README.md for more information + * @param {"mariadb"|"postgres"} dbType database type, should be either "mariadb" or "postgres" * @returns {Promise} */ -async function createTables() { - log.info("mariadb", "Creating basic tables for MariaDB"); +async function createTables(dbType) { + log.info(dbType, "Creating basic tables"); const knex = R.knex; // TODO: Should check later if it is really the final patch sql file. diff --git a/package-lock.json b/package-lock.json index a6ef4f0362..0f33fbee1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "@popperjs/core": "~2.10.2", "@types/bootstrap": "~5.1.9", "@types/node": "^20.8.6", + "@types/pg": "^8.10.2", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", "@vitejs/plugin-vue": "~5.0.1", @@ -4220,6 +4221,74 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/qs": { "version": "6.9.14", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", @@ -10695,6 +10764,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -11065,6 +11140,15 @@ "node": ">=4.0.0" } }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/pg-pool": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", @@ -11341,6 +11425,12 @@ "node": ">=0.10.0" } }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 567efa1b5a..c825bd8024 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "@popperjs/core": "~2.10.2", "@types/bootstrap": "~5.1.9", "@types/node": "^20.8.6", + "@types/pg": "^8.10.2", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", "@vitejs/plugin-vue": "~5.0.1", diff --git a/server/database.js b/server/database.js index ed4b9e6810..f289218a14 100644 --- a/server/database.js +++ b/server/database.js @@ -6,6 +6,7 @@ const knex = require("knex"); const path = require("path"); const { EmbeddedMariaDB } = require("./embedded-mariadb"); const mysql = require("mysql2/promise"); +const pg = require("pg"); /** * Database & App Data Folder @@ -186,6 +187,18 @@ class Database { fs.writeFileSync(path.join(Database.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4)); } + /** + * Validate a database name + * @param {string} dbName Database name to validate + * @throws {Error} If the database name is invalid + * @returns {void} + */ + static validateDBName(dbName) { + if (!/^\w+$/.test(dbName)) { + throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores"); + } + } + /** * Connect to the database * @param {boolean} testMode Should the connection be started in test mode? @@ -217,7 +230,6 @@ class Database { log.info("db", `Database Type: ${dbConfig.type}`); if (dbConfig.type === "sqlite") { - if (! fs.existsSync(Database.sqlitePath)) { log.info("server", "Copying Database"); fs.copyFileSync(Database.templatePath, Database.sqlitePath); @@ -242,9 +254,7 @@ class Database { } }; } else if (dbConfig.type === "mariadb") { - if (!/^\w+$/.test(dbConfig.dbName)) { - throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores"); - } + this.validateDBName(dbConfig.dbName); const connection = await mysql.createConnection({ host: dbConfig.hostname, @@ -296,6 +306,24 @@ class Database { }, pool: mariadbPoolConfig, }; + } else if (dbConfig.type === "postgres") { + this.validateDBName(dbConfig.dbName); + + const clientConfig = { + host: dbConfig.hostname, + port: dbConfig.port, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.dbName, + }; + const client = new pg.Client(clientConfig); + await client.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName); + await client.end(); + + config = { + client: "pg", + connection: clientConfig, + }; } else { throw new Error("Unknown Database type: " + dbConfig.type); } @@ -328,6 +356,8 @@ class Database { await this.initSQLite(testMode, noLog); } else if (dbConfig.type.endsWith("mariadb")) { await this.initMariaDB(); + } else if (dbConfig.type === "postgres") { + await this.initPostgres(); } } @@ -377,6 +407,22 @@ class Database { } } + /** + * Initialize PostgresDB + * @returns {Promise} + */ + static async initPostgres() { + log.debug("db", "Checking if PostgresDB database exists..."); + + let hasTable = await R.hasTable("docker_host"); + if (!hasTable) { + const { createTables } = require("../db/knex_init_db"); + await createTables(); + } else { + log.debug("db", "PostgresDB database already exists"); + } + } + /** * Patch the database * @returns {Promise} @@ -702,13 +748,17 @@ class Database { /** * @returns {string} Get the SQL for the current time plus a number of hours + * @throws {Error} If the database type is unknown */ static sqlHourOffset() { if (Database.dbConfig.type === "sqlite") { return "DATETIME('now', ? || ' hours')"; - } else { + } else if (Database.dbConfig.type.endsWith("mariadb")) { return "DATE_ADD(NOW(), INTERVAL ? HOUR)"; + } else if (Database.dbConfig.type === "postgres") { + return "NOW() + INTERVAL '? HOUR'"; } + throw new Error("Unknown database type: " + Database.dbConfig.type); } }