Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(db): implement db schema and migration from JSON to SQLite
  • Loading branch information
antonreshetov committed Feb 20, 2025
commit dc5f1fd78e460579d5415e479a386d1716aca1f9
91 changes: 75 additions & 16 deletions src/main/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,85 @@
import path from 'node:path'
/* eslint-disable node/prefer-global/process */
import Database from 'better-sqlite3'
import { app } from 'electron'
import { store } from '../store'

const DB_NAME = 'app.db'
const isDev = process.env.NODE_ENV === 'development'

export function initDB() {
try {
const dbPath = path.join(app.getPath('userData'), 'app-data.db')
const dbPath = `${store.preferences.get('storagePath')}/${DB_NAME}`

// eslint-disable-next-line no-console
const db = new Database(dbPath, { verbose: console.log })
try {
const db = new Database(dbPath, {
// eslint-disable-next-line no-console
verbose: isDev ? console.log : undefined,
})

db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')

// Таблица для папок
db.exec(`
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
defaultLanguage TEXT,
parentId INTEGER,
isOpen INTEGER NOT NULL,
isSystem INTEGER NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
icon TEXT,
FOREIGN KEY(parentId) REFERENCES folders(id)
)
`)

// Таблица для сниппетов
db.exec(`
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
folderId INTEGER,
isDeleted INTEGER NOT NULL,
isFavorites INTEGER NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
FOREIGN KEY(folderId) REFERENCES folders(id)
)
`)

// Таблица для содержимого (фрагментов) сниппетов
db.exec(`
CREATE TABLE IF NOT EXISTS snippet_contents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snippetId INTEGER NOT NULL,
label TEXT,
value TEXT,
language TEXT,
FOREIGN KEY(snippetId) REFERENCES snippets(id)
)
`)

// Таблица для тегов
db.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL
)
`)

db.transaction(() => {
db.prepare(
`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
)
`,
).run()
})()
// Таблица для связи сниппетов с тегами (многие ко многим)
db.exec(`
CREATE TABLE IF NOT EXISTS snippet_tags (
snippetId INTEGER NOT NULL,
tagId INTEGER NOT NULL,
PRIMARY KEY(snippetId, tagId),
FOREIGN KEY(snippetId) REFERENCES snippets(id),
FOREIGN KEY(tagId) REFERENCES tags(id)
)
`)

return db
}
Expand Down
113 changes: 113 additions & 0 deletions src/main/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type Database from 'better-sqlite3'
import type { JSONDB } from './types'

export function migrateJsonToSqlite(jsonData: JSONDB, db: Database.Database) {
// Подготовленные выражения для вставки данных
const insertFolderStmt = db.prepare(`
INSERT INTO folders (name, defaultLanguage, parentId, isOpen, isSystem, createdAt, updatedAt, icon)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)

const updateFolderParentStmt = db.prepare(`
UPDATE folders SET parentId = ? WHERE id = ?
`)

const insertTagStmt = db.prepare(`
INSERT INTO tags (name, createdAt, updatedAt)
VALUES (?, ?, ?)
`)

const insertSnippetStmt = db.prepare(`
INSERT INTO snippets (name, description, folderId, isDeleted, isFavorites, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)

const insertSnippetContentStmt = db.prepare(`
INSERT INTO snippet_contents (snippetId, label, value, language)
VALUES (?, ?, ?, ?)
`)

const insertSnippetTagStmt = db.prepare(`
INSERT INTO snippet_tags (snippetId, tagId)
VALUES (?, ?)
`)

// Словари для сопоставления оригинальных string id с новыми числовыми id
const folderIdMap: Record<string, number> = {}
const tagIdMap: Record<string, number> = {}
const snippetIdMap: Record<string, number> = {}

// Транзакция для миграции данных
const transaction = db.transaction(() => {
// Миграция папок
jsonData.folders.forEach((folder) => {
const result = insertFolderStmt.run(
folder.name,
folder.defaultLanguage || null,
null, // parentId обновим позже
folder.isOpen ? 1 : 0,
folder.isSystem ? 1 : 0,
folder.createdAt,
folder.updatedAt,
folder.icon || null,
)
folderIdMap[folder.id] = Number(result.lastInsertRowid)
})

// Обновляем поле parentId для папок, у которых оно задано
jsonData.folders.forEach((folder) => {
if (folder.parentId) {
const newId = folderIdMap[folder.id]
const parentNewId = folderIdMap[folder.parentId]
if (parentNewId) {
updateFolderParentStmt.run(parentNewId, newId)
}
}
})

// Миграция тегов
jsonData.tags.forEach((tag) => {
const result = insertTagStmt.run(tag.name, tag.createdAt, tag.updatedAt)
tagIdMap[tag.id] = Number(result.lastInsertRowid)
})

// Миграция сниппетов, их содержимого и связей с тегами
jsonData.snippets.forEach((snippet) => {
// Определяем новый id папки для сниппета
const mappedFolderId = folderIdMap[snippet.folderId] || null
const result = insertSnippetStmt.run(
snippet.name,
snippet.description || null,
mappedFolderId,
snippet.isDeleted ? 1 : 0,
snippet.isFavorites ? 1 : 0,
snippet.createdAt,
snippet.updatedAt,
)
const newSnippetId = Number(result.lastInsertRowid)
snippetIdMap[snippet.id] = newSnippetId

// Устанавливаем содержимое сниппета
snippet.content.forEach((content) => {
insertSnippetContentStmt.run(
newSnippetId,
content.label || null,
content.value || null,
content.language || null,
)
})

// Устанавливаем связи сниппета с тегами
if (snippet.tagsIds && snippet.tagsIds.length > 0) {
snippet.tagsIds.forEach((tagOrigId) => {
const mappedTagId = tagIdMap[tagOrigId]
if (mappedTagId) {
insertSnippetTagStmt.run(newSnippetId, mappedTagId)
}
})
}
})
})

transaction()
}
43 changes: 43 additions & 0 deletions src/main/db/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface Folder {
id: string
name: string
defaultLanguage: string | null
parentId: string | null
isOpen: boolean
isSystem: boolean
createdAt: number
updatedAt: number
icon: string | null
}

interface SnippetContent {
label: string
value: string
language: string
}

interface Snippet {
id: string
name: string
content: SnippetContent[]
description: string | null
folderId: string
tagsIds: string[]
isDeleted: boolean
isFavorites: boolean
createdAt: number
updatedAt: number
}

interface Tag {
id: string
name: string
createdAt: number
updatedAt: number
}

export interface JSONDB {
folders: Folder[]
snippets: Snippet[]
tags: Tag[]
}
22 changes: 18 additions & 4 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
/* eslint-disable node/prefer-global/process */
import type Database from 'better-sqlite3'
import type { DBQueryArgs } from './types'
import { readFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { app, BrowserWindow, ipcMain } from 'electron'
import { initDB } from './db'
import { migrateJsonToSqlite } from './db/migrate'
import { store } from './store'

let db: Database.Database

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true' // Отключаем security warnings

const isDev = process.env.NODE_ENV === 'development'

let db: Database.Database
let mainWindow: BrowserWindow
let isQuitting = false

Expand Down Expand Up @@ -55,8 +56,21 @@ app.whenReady().then(() => {
createWindow()

db = initDB()
const stmt = db.prepare('INSERT INTO settings (key, value) VALUES (?, ?)')
stmt.run('theme', 'light')

if (store.app.get('isAutoMigratedFromJson')) {
return
}

const jsonDbPath = `${store.preferences.get('storagePath')}/db.json`
const jsonData = readFileSync(jsonDbPath, 'utf8')

try {
migrateJsonToSqlite(JSON.parse(jsonData), db)
store.app.set('isAutoMigratedFromJson', true)
}
catch (err) {
console.error('Error migrating JSON to SQLite:', err)
}
})

app.on('activate', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/main/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import app from './module/app'
import preferences from './module/preferences'

export const store = {
app,
preferences,
}
11 changes: 6 additions & 5 deletions src/main/store/module/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import Store from 'electron-store'

interface StoreSchema {
bounds: object
isAutoMigratedFromJson: boolean
}

export default new Store<StoreSchema>({
name: 'app',
schema: {
bounds: {
default: {},
type: 'object',
},
cwd: 'v2',

defaults: {
bounds: {},
isAutoMigratedFromJson: false,
},
})
22 changes: 22 additions & 0 deletions src/main/store/module/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { homedir, platform } from 'node:os'
import Store from 'electron-store'

interface StoreSchema {
storagePath: string
backupPath: string
}

const isWin = platform() === 'win32'

const storagePath = isWin ? `${homedir()}\\massCode` : `${homedir()}/massCode`
const backupPath = isWin ? `${storagePath}\\backups` : `${storagePath}/backups`

export default new Store<StoreSchema>({
name: 'preferences',
cwd: 'v2',

defaults: {
storagePath,
backupPath,
},
})
Loading