db 마이그레이션 기능 #17
10 changed files with 551 additions and 544 deletions
|
@ -1,50 +0,0 @@
|
|||
// import { promises } from "fs";
|
||||
// const { readdir, writeFile } = promises;
|
||||
// import { dirname, join } from "path";
|
||||
// import { createGenerator } from "ts-json-schema-generator";
|
||||
|
||||
// async function genSchema(path: string, typename: string) {
|
||||
// const gen = createGenerator({
|
||||
// path: path,
|
||||
// type: typename,
|
||||
// tsconfig: "tsconfig.json",
|
||||
// });
|
||||
// const schema = gen.createSchema(typename);
|
||||
// if (schema.definitions != undefined) {
|
||||
// const definitions = schema.definitions;
|
||||
// const definition = definitions[typename];
|
||||
// if (typeof definition == "object") {
|
||||
// let property = definition.properties;
|
||||
// if (property) {
|
||||
// property["$schema"] = {
|
||||
// type: "string",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const text = JSON.stringify(schema);
|
||||
// await writeFile(join(dirname(path), `${typename}.schema.json`), text);
|
||||
// }
|
||||
// function capitalize(s: string) {
|
||||
// return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
// }
|
||||
// async function setToALL(path: string) {
|
||||
// console.log(`scan ${path}`);
|
||||
// const direntry = await readdir(path, { withFileTypes: true });
|
||||
// const works = direntry
|
||||
// .filter((x) => x.isFile() && x.name.endsWith("Config.ts"))
|
||||
// .map((x) => {
|
||||
// const name = x.name;
|
||||
// const m = /(.+)\.ts/.exec(name);
|
||||
// if (m !== null) {
|
||||
// const typename = m[1];
|
||||
// return genSchema(join(path, typename), capitalize(typename));
|
||||
// }
|
||||
// });
|
||||
// await Promise.all(works);
|
||||
// const subdir = direntry.filter((x) => x.isDirectory()).map((x) => x.name);
|
||||
// for (const x of subdir) {
|
||||
// await setToALL(join(path, x));
|
||||
// }
|
||||
// }
|
||||
// setToALL("src");
|
|
@ -65,8 +65,8 @@ export async function up(db: Kysely<any>) {
|
|||
await db
|
||||
.insertInto('schema_migration')
|
||||
.values({
|
||||
version: '0.0.1',
|
||||
dirty: false,
|
||||
version: '2024-12-27',
|
||||
dirty: 0,
|
||||
})
|
||||
.execute();
|
||||
|
||||
|
|
18
packages/server/migrations/2025-06-26.ts
Normal file
18
packages/server/migrations/2025-06-26.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>) {
|
||||
await db.schema
|
||||
.createTable("user_settings")
|
||||
.addColumn("username", "varchar(256)", col => col.notNull().primaryKey())
|
||||
.addColumn("settings", "jsonb", col => col.notNull())
|
||||
.addForeignKeyConstraint("user_settings_username_fk", ["username"], "users", ["username"])
|
||||
.execute();
|
||||
|
||||
await db.updateTable("schema_migration")
|
||||
.set({ version: "2025-06-26", dirty: 0 })
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>) {
|
||||
throw new Error('Downward migrations are not supported. Restore from backup.');
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>) {
|
||||
await db.schema
|
||||
.createTable('schema_migration')
|
||||
.addColumn('version', 'char(16)')
|
||||
.addColumn('dirty', 'boolean')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('users')
|
||||
.addColumn('username', 'varchar(256)', col => col.primaryKey())
|
||||
.addColumn('password_hash', 'varchar(64)', col => col.notNull())
|
||||
.addColumn('password_salt', 'varchar(64)', col => col.notNull())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('document')
|
||||
.addColumn('id', 'serial', col => col.primaryKey())
|
||||
.addColumn('title', 'varchar(512)', col => col.notNull())
|
||||
.addColumn('content_type', 'varchar(16)', col => col.notNull())
|
||||
.addColumn('basepath', 'varchar(256)', col => col.notNull())
|
||||
.addColumn('filename', 'varchar(512)', col => col.notNull())
|
||||
.addColumn('content_hash', 'varchar')
|
||||
.addColumn('additional', 'json')
|
||||
.addColumn("pagenum", "integer", col => col.notNull())
|
||||
.addColumn('created_at', 'integer', col => col.notNull())
|
||||
.addColumn('modified_at', 'integer', col => col.notNull())
|
||||
.addColumn('deleted_at', 'integer')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('tags')
|
||||
.addColumn('name', 'varchar', col => col.primaryKey())
|
||||
.addColumn('description', 'text')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('doc_tag_relation')
|
||||
.addColumn('doc_id', 'integer', col => col.notNull())
|
||||
.addColumn('tag_name', 'varchar', col => col.notNull())
|
||||
.addForeignKeyConstraint('doc_id_fk', ['doc_id'], 'document', ['id'])
|
||||
.addForeignKeyConstraint('tag_name_fk', ['tag_name'], 'tags', ['name'])
|
||||
.addPrimaryKeyConstraint('doc_tag_relation_pk', ['doc_id', 'tag_name'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable('permissions')
|
||||
.addColumn('username', 'varchar', col => col.notNull())
|
||||
.addColumn('name', 'varchar', col => col.notNull())
|
||||
.addPrimaryKeyConstraint('permissions_pk', ['username', 'name'])
|
||||
.addForeignKeyConstraint('username_fk', ['username'], 'users', ['username'])
|
||||
.execute();
|
||||
|
||||
// create admin account.
|
||||
await db
|
||||
.insertInto('users')
|
||||
.values({
|
||||
username: 'admin',
|
||||
password_hash: 'unchecked',
|
||||
password_salt: 'unchecked',
|
||||
})
|
||||
.execute();
|
||||
|
||||
await db
|
||||
.insertInto('schema_migration')
|
||||
.values({
|
||||
version: '0.0.1',
|
||||
dirty: false,
|
||||
})
|
||||
.execute();
|
||||
|
||||
// create indexes
|
||||
await db.schema.createIndex("index_document_basepath_filename")
|
||||
.on("document")
|
||||
.columns(["basepath", "filename"])
|
||||
.execute();
|
||||
await db.schema.createIndex("index_document_content_hash")
|
||||
.on("document")
|
||||
.columns(["content_hash"])
|
||||
.execute();
|
||||
await db.schema.createIndex("index_document_created_at")
|
||||
.on("document")
|
||||
.columns(["created_at"])
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>) {
|
||||
throw new Error('Downward migrations are not supported. Restore from backup.');
|
||||
}
|
|
@ -6,37 +6,37 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/app.ts",
|
||||
"start": "tsx src/app.ts"
|
||||
"start": "tsx src/app.ts",
|
||||
"migrate": "tsx tools/migration.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@std/async": "npm:@jsr/std__async@^1.0.12",
|
||||
"@zip.js/zip.js": "^2.7.60",
|
||||
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
||||
"@zip.js/zip.js": "^2.7.62",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"dbtype": "workspace:dbtype",
|
||||
"dotenv": "^16.4.5",
|
||||
"jose": "^5.9.3",
|
||||
"koa": "^2.15.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"jose": "^5.10.0",
|
||||
"koa": "^2.16.1",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-compose": "^4.1.0",
|
||||
"koa-router": "^12.0.1",
|
||||
"kysely": "^0.27.4",
|
||||
"kysely": "^0.27.6",
|
||||
"natural-orderby": "^2.0.3",
|
||||
"tiny-async-pool": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa-compose": "^3.2.8",
|
||||
"@types/koa-router": "^7.4.8",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/node": "^22.15.33",
|
||||
"@types/tiny-async-pool": "^1.0.5",
|
||||
"nodemon": "^3.1.7",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2"
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
// contextBridge.exposeInMainWorld("electron", {
|
||||
// passwordReset: async (username: string, toPw: string) => {
|
||||
// return await ipcRenderer.invoke("reset_password", username, toPw);
|
||||
// },
|
||||
// });
|
|
@ -1,14 +1,16 @@
|
|||
import { Kysely } from "kysely";
|
||||
import { getKysely } from "./db/kysely.ts";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export async function connectDB() {
|
||||
const kysely = getKysely();
|
||||
|
||||
let tries = 0;
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
try {
|
||||
console.log("try to connect db");
|
||||
await kysely.selectNoFrom(eb=> eb.val(1).as("dummy")).execute();
|
||||
await kysely.selectNoFrom(eb => eb.val(1).as("dummy")).execute();
|
||||
console.log("connect success");
|
||||
} catch (err) {
|
||||
if (tries < 3) {
|
||||
|
@ -25,20 +27,72 @@ export async function connectDB() {
|
|||
}
|
||||
|
||||
async function checkTableExists(kysely: Kysely<any>, table: string) {
|
||||
const result = await kysely.selectFrom("sqlite_master").where("type", "=", "table").where("name", "=", table).executeTakeFirst();
|
||||
const result = await kysely.selectFrom("sqlite_master")
|
||||
.selectAll()
|
||||
.where("type", "=", "table")
|
||||
.where("name", "=", table)
|
||||
.executeTakeFirst();
|
||||
return result !== undefined;
|
||||
}
|
||||
|
||||
export async function migrateDB() {
|
||||
const kysely = getKysely();
|
||||
let version_number = 0;
|
||||
// is schema_migration exists?
|
||||
const hasTable = await checkTableExists(kysely, "schema_migration");
|
||||
if (!hasTable) {
|
||||
// migrate from 0
|
||||
// create schema_migration
|
||||
// 2. 마이그레이션 실행 (최초 마이그레이션)
|
||||
const migration = await import("../migrations/2024-12-27.ts");
|
||||
await migration.up(kysely);
|
||||
console.log("최초 마이그레이션 완료");
|
||||
return;
|
||||
}
|
||||
|
||||
const version = await kysely.selectFrom("schema_migration").executeTakeFirst();
|
||||
// 현재 버전 확인
|
||||
const row = await kysely.selectFrom("schema_migration").selectAll().executeTakeFirst();
|
||||
const currentVersion = row?.version ?? "0001-01-01"; // 기본값 설정
|
||||
|
||||
// 마이그레이션 목록 정의 (버전순 정렬 필수)
|
||||
const migrations = await readMigrations();
|
||||
|
||||
// 현재 버전보다 높은 migration만 실행
|
||||
let lastestVersion = currentVersion;
|
||||
console.log(`현재 DB 버전: ${currentVersion}`);
|
||||
for (const m of migrations) {
|
||||
if (compareVersion(m.version, currentVersion) > 0) {
|
||||
console.log(`마이그레이션 실행: ${m.version}`);
|
||||
const migration = await import(m.file);
|
||||
await migration.up(kysely);
|
||||
await kysely.updateTable("schema_migration")
|
||||
.set({ version: m.version, dirty: 0 })
|
||||
.execute();
|
||||
lastestVersion = m.version;
|
||||
console.log(`마이그레이션 완료: ${m.version}`);
|
||||
}
|
||||
}
|
||||
if (lastestVersion !== currentVersion) {
|
||||
console.log(`마이그레이션 완료. ${currentVersion} -> ${lastestVersion}`);
|
||||
} else {
|
||||
console.log("마이그레이션 필요 없음");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function readMigrations(): Promise<{ version: string; file: string }[]> {
|
||||
const migrationsDir = path.join(import.meta.dirname, "../migrations");
|
||||
const files = (await fs.readdir(migrationsDir))
|
||||
.filter(file => file.endsWith(".ts"))
|
||||
.map(file => {
|
||||
const version = file.match(/(\d{4}-\d{2}-\d{2})/)?.[0] || "0001-01-01";
|
||||
return { version, file: `../migrations/${file}` };
|
||||
});
|
||||
return files.sort((a, b) => compareVersion(a.version, b.version));
|
||||
}
|
||||
|
||||
// Date 기반 버전 비교 함수.
|
||||
function compareVersion(a: string, b: string): number {
|
||||
const dateA = new Date(a);
|
||||
const dateB = new Date(b);
|
||||
if (dateA < dateB) return -1;
|
||||
if (dateA > dateB) return 1;
|
||||
return 0; // 같을 경우
|
||||
}
|
|
@ -238,8 +238,6 @@ class ServerApplication {
|
|||
static async createServer() {
|
||||
const db = await connectDB();
|
||||
|
||||
// todo : db migration
|
||||
|
||||
const app = new ServerApplication({
|
||||
userController: createSqliteUserController(db),
|
||||
documentController: createSqliteDocumentAccessor(db),
|
||||
|
|
16
packages/server/tools/migration.ts
Normal file
16
packages/server/tools/migration.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { migrateDB } from "../src/database.ts";
|
||||
import { config } from "dotenv";
|
||||
|
||||
config(); // Load environment variables from .env file
|
||||
|
||||
export async function runMigration() {
|
||||
try {
|
||||
await migrateDB();
|
||||
console.log("Database migration completed successfully.");
|
||||
} catch (error) {
|
||||
console.error("Database migration failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await runMigration();
|
814
pnpm-lock.yaml
generated
814
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue