mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
|
import Database from "better-sqlite3";
|
||
|
import {existsSync} from "fs";
|
||
|
import {join} from "path";
|
||
|
|
||
|
// This section will serve as the documentation for the database, because in order to guarantee
|
||
|
// that a database created now will have the same structure as a database that has been migrated
|
||
|
// through different versions, a new database starts at version one and goes through the same
|
||
|
// migration process. Creating separate statements for migrations and creating a new database will
|
||
|
// allow for some dangerous out of sync definitions. For example, version 9 via migration might
|
||
|
// have a column that forgot to be dropped while version 9 via creation won't include that column,
|
||
|
// so when someone tries to use an INSERT statement, it'll throw an error because of discrepancies.
|
||
|
|
||
|
// -=[ Current Schema ]=-
|
||
|
// System: Version (INT UNIQUE)
|
||
|
// Users: ID, Money (INT), LastReceived (TIME), LastMonday (TIME), TimezoneOffset (INT NULLABLE), DaylightSavingsRegion (INT), EcoBetInsurance (INT)
|
||
|
// Guilds: ID, Prefix (TEXT NULLABLE), WelcomeType (INT), WelcomeChannel (TEXT NULLABLE), WelcomeMessage (TEXT NULLABLE), StreamingChannel (TEXT NULLABLE), HasMessageEmbeds (BOOL)
|
||
|
// Members: UserID, GuildID, StreamCategory (TEXT NULLABLE)
|
||
|
// Webhooks: ID, Token (TEXT)
|
||
|
// TodoLists: UserID, Timestamp (TIME), Entry (TEXT)
|
||
|
// StreamingRoles: GuildID, RoleID, Category (TEXT)
|
||
|
// DefaultChannelNames: GuildID, ChannelID, Name (TEXT)
|
||
|
// AutoRoles: GuildID, RoleID
|
||
|
|
||
|
// -=[ Notes ]=-
|
||
|
// - Unless otherwise directed above (NULLABLE), assume the "NOT NULL" constraint.
|
||
|
// - IDs use the "UNIQUE ON CONFLICT REPLACE" constraint to enable implicit UPSERT statements.
|
||
|
// - This way, you don't need to do INSERT INTO ... ON CONFLICT(...) DO UPDATE SET ...
|
||
|
// - For the sake of simplicity, any Discord ID will be stored and retrieved as a string.
|
||
|
// - Any datetime stuff (marked as TIME) will be stored as a UNIX timestamp in milliseconds (INT).
|
||
|
// - Booleans (marked as BOOL) will be stored as an integer, either 0 or 1 (though it just checks for 0).
|
||
|
|
||
|
// Calling migrations[2]() migrates the database from version 2 to version 3.
|
||
|
// NOTE: Once a migration is written, DO NOT change that migration or it'll break all future migrations.
|
||
|
const migrations: (() => void)[] = [
|
||
|
() => {
|
||
|
const hasLegacyData = existsSync(join("data", "config.json")) && existsSync(join("data", "storage.json"));
|
||
|
|
||
|
// Generate initial state
|
||
|
// Stuff like CREATE TABLE IF NOT EXISTS should be handled by the migration system.
|
||
|
generateSQLMigration([
|
||
|
`CREATE TABLE System (
|
||
|
Version INT NOT NULL UNIQUE
|
||
|
)`,
|
||
|
"INSERT INTO System VALUES (1)",
|
||
|
`CREATE TABLE Users (
|
||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||
|
Money INT NOT NULL DEFAULT 0,
|
||
|
LastReceived INT NOT NULL DEFAULT -1,
|
||
|
LastMonday INT NOT NULL DEFAULT -1,
|
||
|
TimezoneOffset INT,
|
||
|
DaylightSavingsRegion INT NOT NULL DEFAULT 0,
|
||
|
EcoBetInsurance INT NOT NULL DEFAULT 0
|
||
|
)`,
|
||
|
`CREATE TABLE Guilds (
|
||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||
|
Prefix TEXT,
|
||
|
WelcomeType INT NOT NULL DEFAULT 0,
|
||
|
WelcomeChannel TEXT,
|
||
|
WelcomeMessage TEXT,
|
||
|
StreamingChannel TEXT,
|
||
|
HasMessageEmbeds INT NOT NULL CHECK(HasMessageEmbeds BETWEEN 0 AND 1) DEFAULT 1
|
||
|
)`,
|
||
|
`CREATE TABLE Members (
|
||
|
UserID TEXT NOT NULL,
|
||
|
GuildID TEXT NOT NULL,
|
||
|
StreamCategory TEXT,
|
||
|
UNIQUE (UserID, GuildID) ON CONFLICT REPLACE
|
||
|
)`,
|
||
|
`CREATE TABLE Webhooks (
|
||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||
|
Token TEXT NOT NULL
|
||
|
)`,
|
||
|
`CREATE TABLE TodoLists (
|
||
|
UserID TEXT NOT NULL,
|
||
|
Timestamp INT NOT NULL,
|
||
|
Entry TEXT NOT NULL
|
||
|
)`,
|
||
|
`CREATE TABLE StreamingRoles (
|
||
|
GuildID TEXT NOT NULL,
|
||
|
RoleID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||
|
Category TEXT NOT NULL
|
||
|
)`,
|
||
|
`CREATE TABLE DefaultChannelNames (
|
||
|
GuildID TEXT NOT NULL,
|
||
|
ChannelID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||
|
Name TEXT NOT NULL
|
||
|
)`,
|
||
|
`CREATE TABLE AutoRoles (
|
||
|
GuildID TEXT NOT NULL,
|
||
|
RoleID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE
|
||
|
)`
|
||
|
])();
|
||
|
|
||
|
// Load initial data if present
|
||
|
if (hasLegacyData) {
|
||
|
generateSQLMigration([])();
|
||
|
}
|
||
|
}
|
||
|
// "UPDATE System SET Version=2" when the time comes
|
||
|
];
|
||
|
|
||
|
const isExistingDatabase = existsSync(join("data", "main.db"));
|
||
|
export const db = new Database(join("data", "main.db"));
|
||
|
let version = -1;
|
||
|
|
||
|
// Get existing version if applicable and throw error if corrupt data.
|
||
|
// The data is considered corrupt if it exists and:
|
||
|
// - The System table doesn't exist (throws an error)
|
||
|
// - There isn't exactly one entry in System for Version
|
||
|
if (isExistingDatabase) {
|
||
|
try {
|
||
|
const {Version, Amount} = db.prepare("SELECT Version, Count(Version) AS Amount FROM System").get() as {
|
||
|
Version: number | null;
|
||
|
Amount: number;
|
||
|
};
|
||
|
|
||
|
if (!Version) {
|
||
|
console.error("No version entry in the System table.");
|
||
|
} else if (Amount === 1) {
|
||
|
version = Version;
|
||
|
} else {
|
||
|
console.error("More than one version entry in the System table.");
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.error(error);
|
||
|
console.error("Invalid database, take a look at it manually.");
|
||
|
}
|
||
|
} else {
|
||
|
version = 0;
|
||
|
}
|
||
|
|
||
|
// Then loop through all the versions
|
||
|
if (version !== -1) {
|
||
|
for (let v = version; v < migrations.length; v++) {
|
||
|
migrations[v]();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function generateSQLMigration(statements: string[]): () => void {
|
||
|
return () => {
|
||
|
for (const statement of statements) {
|
||
|
db.prepare(statement).run();
|
||
|
}
|
||
|
};
|
||
|
}
|