feat: US-002 — SQLite + Drizzle ORM schema and migrations
- Install better-sqlite3 + drizzle-orm as runtime deps; drizzle-kit + @types/better-sqlite3 as devDeps
- Define 5 tables in src/main/db/schema.ts: clients, projects, tasks, checkpoints, notes
- All IDs are TEXT (UUID); types inferred via InferSelectModel/InferInsertModel
- initDb() in src/main/db/index.ts opens adiuva.db at app.getPath('userData'), runs CREATE TABLE IF NOT EXISTS (non-destructive push), enables WAL mode
- Call initDb() in main process app ready handler
- Externalize better-sqlite3 in vite.main.config.mts; add AutoUnpackNativesPlugin to forge.config.ts
- Add drizzle.config.ts for drizzle-kit CLI support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
drizzle.config.ts
Normal file
7
drizzle.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/main/db/schema.ts',
|
||||
out: './src/main/db/migrations',
|
||||
dialect: 'sqlite',
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||
@@ -19,6 +20,7 @@ const config: ForgeConfig = {
|
||||
new MakerDeb({}),
|
||||
],
|
||||
plugins: [
|
||||
new AutoUnpackNativesPlugin({}),
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
// If you are familiar with Vite configuration, it will look really familiar.
|
||||
|
||||
1344
package-lock.json
generated
1344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@
|
||||
"@electron-forge/plugin-vite": "^7.11.1",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@tanstack/router-vite-plugin": "^1.161.1",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -33,6 +34,7 @@
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"electron": "40.6.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
@@ -43,8 +45,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "^1.161.1",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"framer-motion": "^12.34.2",
|
||||
"lucide-react": "^0.575.0",
|
||||
|
||||
83
src/main/db/index.ts
Normal file
83
src/main/db/index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { app } from 'electron';
|
||||
import path from 'node:path';
|
||||
import * as schema from './schema';
|
||||
|
||||
// SQL to create all tables if they don't exist (non-destructive push strategy)
|
||||
const MIGRATION_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
industry TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
client_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
ai_summary TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'todo',
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
assignee TEXT,
|
||||
due_date INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checkpoints (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
is_ai_suggested INTEGER NOT NULL DEFAULT 0,
|
||||
is_approved INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
type DbInstance = ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
||||
let dbInstance: DbInstance | null = null;
|
||||
|
||||
export function initDb(): DbInstance {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const dbPath = path.join(userDataPath, 'adiuva.db');
|
||||
|
||||
const sqlite = new Database(dbPath);
|
||||
|
||||
// Enable WAL mode for better concurrent read performance
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
|
||||
// Run non-destructive migrations on every start
|
||||
sqlite.exec(MIGRATION_SQL);
|
||||
|
||||
dbInstance = drizzle(sqlite, { schema });
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
export function getDb(): DbInstance {
|
||||
if (!dbInstance) {
|
||||
throw new Error('Database not initialized. Call initDb() first.');
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
66
src/main/db/schema.ts
Normal file
66
src/main/db/schema.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||
|
||||
export const clients = sqliteTable('clients', {
|
||||
id: text('id').primaryKey(),
|
||||
parentId: text('parent_id'),
|
||||
name: text('name').notNull(),
|
||||
industry: text('industry'),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
export const projects = sqliteTable('projects', {
|
||||
id: text('id').primaryKey(),
|
||||
clientId: text('client_id'),
|
||||
name: text('name').notNull(),
|
||||
status: text('status', { enum: ['active', 'archived'] }).notNull().default('active'),
|
||||
aiSummary: text('ai_summary'),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
export const tasks = sqliteTable('tasks', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id'),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
status: text('status').notNull().default('todo'),
|
||||
priority: text('priority').notNull().default('medium'),
|
||||
assignee: text('assignee'),
|
||||
dueDate: integer('due_date', { mode: 'number' }),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
export const checkpoints = sqliteTable('checkpoints', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id').notNull(),
|
||||
title: text('title').notNull(),
|
||||
date: integer('date', { mode: 'number' }).notNull(),
|
||||
isAiSuggested: integer('is_ai_suggested', { mode: 'number' }).notNull().default(0),
|
||||
isApproved: integer('is_approved', { mode: 'number' }).notNull().default(0),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
export const notes = sqliteTable('notes', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id'),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull().default(''),
|
||||
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'number' }).notNull(),
|
||||
});
|
||||
|
||||
// Inferred TypeScript types — no manual duplication
|
||||
export type Client = InferSelectModel<typeof clients>;
|
||||
export type NewClient = InferInsertModel<typeof clients>;
|
||||
|
||||
export type Project = InferSelectModel<typeof projects>;
|
||||
export type NewProject = InferInsertModel<typeof projects>;
|
||||
|
||||
export type Task = InferSelectModel<typeof tasks>;
|
||||
export type NewTask = InferInsertModel<typeof tasks>;
|
||||
|
||||
export type Checkpoint = InferSelectModel<typeof checkpoints>;
|
||||
export type NewCheckpoint = InferInsertModel<typeof checkpoints>;
|
||||
|
||||
export type Note = InferSelectModel<typeof notes>;
|
||||
export type NewNote = InferInsertModel<typeof notes>;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import path from 'node:path';
|
||||
import started from 'electron-squirrel-startup';
|
||||
import { initDb } from './db';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) {
|
||||
@@ -40,7 +41,10 @@ const createWindow = () => {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
app.on('ready', () => {
|
||||
initDb();
|
||||
createWindow();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
|
||||
@@ -4,6 +4,8 @@ import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Externalize native Node modules — they're rebuilt by electron-forge
|
||||
external: ['better-sqlite3'],
|
||||
output: {
|
||||
entryFileNames: 'main.js',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user