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 { MakerZIP } from '@electron-forge/maker-zip';
|
||||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||||
|
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||||
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||||
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||||
@@ -19,6 +20,7 @@ const config: ForgeConfig = {
|
|||||||
new MakerDeb({}),
|
new MakerDeb({}),
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new AutoUnpackNativesPlugin({}),
|
||||||
new VitePlugin({
|
new VitePlugin({
|
||||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
// `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.
|
// 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-forge/plugin-vite": "^7.11.1",
|
||||||
"@electron/fuses": "^1.8.0",
|
"@electron/fuses": "^1.8.0",
|
||||||
"@tanstack/router-vite-plugin": "^1.161.1",
|
"@tanstack/router-vite-plugin": "^1.161.1",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/electron-squirrel-startup": "^1.0.2",
|
"@types/electron-squirrel-startup": "^1.0.2",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
|
"drizzle-kit": "^0.31.9",
|
||||||
"electron": "40.6.0",
|
"electron": "40.6.0",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
@@ -43,8 +45,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-router": "^1.161.1",
|
"@tanstack/react-router": "^1.161.1",
|
||||||
|
"better-sqlite3": "^12.6.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"drizzle-orm": "^0.45.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"framer-motion": "^12.34.2",
|
"framer-motion": "^12.34.2",
|
||||||
"lucide-react": "^0.575.0",
|
"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 { app, BrowserWindow } from 'electron';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import started from 'electron-squirrel-startup';
|
import started from 'electron-squirrel-startup';
|
||||||
|
import { initDb } from './db';
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
if (started) {
|
if (started) {
|
||||||
@@ -40,7 +41,10 @@ const createWindow = () => {
|
|||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// 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
|
// 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
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { defineConfig } from 'vite';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
// Externalize native Node modules — they're rebuilt by electron-forge
|
||||||
|
external: ['better-sqlite3'],
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'main.js',
|
entryFileNames: 'main.js',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user