chore: moved to proper drizzle orm and migrations
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,10 +29,6 @@ build/
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
# Drizzle ORM
|
|
||||||
drizzle/
|
|
||||||
migrations/
|
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# Environment & Secrets
|
# Environment & Secrets
|
||||||
# ===================
|
# ===================
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import type { Config } from 'drizzle-kit';
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
export default {
|
export default defineConfig({
|
||||||
schema: './src/main/database/schema.ts',
|
schema: './src/main/database/schema.ts',
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
driver: 'libsql',
|
dialect: 'sqlite',
|
||||||
dbCredentials: {
|
});
|
||||||
url: 'file:./data/bds.db',
|
|
||||||
},
|
|
||||||
} satisfies Config;
|
|
||||||
|
|||||||
134
drizzle/0000_initial.sql
Normal file
134
drizzle/0000_initial.sql
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
CREATE TABLE `chat_conversations` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`model` text,
|
||||||
|
`copilot_session_id` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `chat_messages` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`conversation_id` text NOT NULL,
|
||||||
|
`role` text NOT NULL,
|
||||||
|
`content` text,
|
||||||
|
`tool_call_id` text,
|
||||||
|
`tool_calls` text,
|
||||||
|
`created_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `import_definitions` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`wxr_file_path` text,
|
||||||
|
`uploads_folder_path` text,
|
||||||
|
`last_analysis_result` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `media` (
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`filename` text NOT NULL,
|
||||||
|
`original_name` text NOT NULL,
|
||||||
|
`mime_type` text NOT NULL,
|
||||||
|
`size` integer NOT NULL,
|
||||||
|
`width` integer,
|
||||||
|
`height` integer,
|
||||||
|
`alt` text,
|
||||||
|
`caption` text,
|
||||||
|
`file_path` text NOT NULL,
|
||||||
|
`sidecar_path` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
`sync_status` text DEFAULT 'pending' NOT NULL,
|
||||||
|
`synced_at` integer,
|
||||||
|
`checksum` text,
|
||||||
|
`tags` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `post_links` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`source_post_id` text NOT NULL,
|
||||||
|
`target_post_id` text NOT NULL,
|
||||||
|
`link_text` text,
|
||||||
|
`created_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `post_media` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`post_id` text NOT NULL,
|
||||||
|
`media_id` text NOT NULL,
|
||||||
|
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `post_media_post_media_idx` ON `post_media` (`post_id`,`media_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `posts` (
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`slug` text NOT NULL,
|
||||||
|
`excerpt` text,
|
||||||
|
`content` text,
|
||||||
|
`status` text DEFAULT 'draft' NOT NULL,
|
||||||
|
`author` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
`published_at` integer,
|
||||||
|
`file_path` text DEFAULT '' NOT NULL,
|
||||||
|
`sync_status` text DEFAULT 'pending' NOT NULL,
|
||||||
|
`synced_at` integer,
|
||||||
|
`checksum` text,
|
||||||
|
`tags` text,
|
||||||
|
`categories` text,
|
||||||
|
`published_title` text,
|
||||||
|
`published_content` text,
|
||||||
|
`published_tags` text,
|
||||||
|
`published_categories` text,
|
||||||
|
`published_excerpt` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `posts_project_slug_idx` ON `posts` (`project_id`,`slug`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `projects` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`slug` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`data_path` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
`is_active` integer DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `projects_slug_unique` ON `projects` (`slug`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `settings` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `sync_log` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`entity_type` text NOT NULL,
|
||||||
|
`entity_id` text NOT NULL,
|
||||||
|
`operation` text NOT NULL,
|
||||||
|
`status` text DEFAULT 'pending' NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL,
|
||||||
|
`error_message` text,
|
||||||
|
`retry_count` integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `tags` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`color` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `tags_project_name_idx` ON `tags` (`project_id`,`name`);
|
||||||
850
drizzle/meta/0000_snapshot.json
Normal file
850
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,850 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "af3c1207-a667-495d-833d-26f7d3451829",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"chat_conversations": {
|
||||||
|
"name": "chat_conversations",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"copilot_session_id": {
|
||||||
|
"name": "copilot_session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"chat_messages": {
|
||||||
|
"name": "chat_messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"conversation_id": {
|
||||||
|
"name": "conversation_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_call_id": {
|
||||||
|
"name": "tool_call_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_calls": {
|
||||||
|
"name": "tool_calls",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"import_definitions": {
|
||||||
|
"name": "import_definitions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"wxr_file_path": {
|
||||||
|
"name": "wxr_file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"uploads_folder_path": {
|
||||||
|
"name": "uploads_folder_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_analysis_result": {
|
||||||
|
"name": "last_analysis_result",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"name": "media",
|
||||||
|
"columns": {
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"name": "filename",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"original_name": {
|
||||||
|
"name": "original_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"name": "mime_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"name": "size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"name": "width",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"alt": {
|
||||||
|
"name": "alt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"name": "caption",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sidecar_path": {
|
||||||
|
"name": "sidecar_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sync_status": {
|
||||||
|
"name": "sync_status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"synced_at": {
|
||||||
|
"name": "synced_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"name": "checksum",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"post_links": {
|
||||||
|
"name": "post_links",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source_post_id": {
|
||||||
|
"name": "source_post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"target_post_id": {
|
||||||
|
"name": "target_post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"link_text": {
|
||||||
|
"name": "link_text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"post_media": {
|
||||||
|
"name": "post_media",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"post_id": {
|
||||||
|
"name": "post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"media_id": {
|
||||||
|
"name": "media_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "sort_order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"post_media_post_media_idx": {
|
||||||
|
"name": "post_media_post_media_idx",
|
||||||
|
"columns": [
|
||||||
|
"post_id",
|
||||||
|
"media_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"posts": {
|
||||||
|
"name": "posts",
|
||||||
|
"columns": {
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"excerpt": {
|
||||||
|
"name": "excerpt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'draft'"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_at": {
|
||||||
|
"name": "published_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"sync_status": {
|
||||||
|
"name": "sync_status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"synced_at": {
|
||||||
|
"name": "synced_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"name": "checksum",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_title": {
|
||||||
|
"name": "published_title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_content": {
|
||||||
|
"name": "published_content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_tags": {
|
||||||
|
"name": "published_tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_categories": {
|
||||||
|
"name": "published_categories",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_excerpt": {
|
||||||
|
"name": "published_excerpt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"posts_project_slug_idx": {
|
||||||
|
"name": "posts_project_slug_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"data_path": {
|
||||||
|
"name": "data_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"projects_slug_unique": {
|
||||||
|
"name": "projects_slug_unique",
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"name": "settings",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sync_log": {
|
||||||
|
"name": "sync_log",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entity_type": {
|
||||||
|
"name": "entity_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"name": "entity_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"name": "operation",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"error_message": {
|
||||||
|
"name": "error_message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"retry_count": {
|
||||||
|
"name": "retry_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"tags_project_name_idx": {
|
||||||
|
"name": "tags_project_name_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1771081481654,
|
||||||
|
"tag": "0000_initial",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
492
package-lock.json
generated
492
package-lock.json
generated
@@ -53,7 +53,7 @@
|
|||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"drizzle-kit": "^1.0.0-beta.9-e89174b",
|
"drizzle-kit": "^0.31.9",
|
||||||
"electron": "^40.4.0",
|
"electron": "^40.4.0",
|
||||||
"electron-builder": "^26.7.0",
|
"electron-builder": "^26.7.0",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
@@ -1001,9 +1001,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@drizzle-team/brocli": {
|
"node_modules/@drizzle-team/brocli": {
|
||||||
"version": "0.11.0",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||||
"integrity": "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==",
|
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
@@ -1444,6 +1444,442 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
|
||||||
|
"deprecated": "Merged into tsx: https://tsx.is",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.18.20",
|
||||||
|
"source-map-support": "^0.5.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/android-arm": "0.18.20",
|
||||||
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
|
"@esbuild/android-x64": "0.18.20",
|
||||||
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild-kit/esm-loader": {
|
||||||
|
"version": "2.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
|
||||||
|
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
|
||||||
|
"deprecated": "Merged into tsx: https://tsx.is",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@esbuild-kit/core-utils": "^3.3.2",
|
||||||
|
"get-tsconfig": "^4.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -2636,19 +3072,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@js-temporal/polyfill": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz",
|
|
||||||
"integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"jsbi": "^4.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jsonjoy.com/base64": {
|
"node_modules/@jsonjoy.com/base64": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
|
||||||
@@ -6834,16 +7257,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/drizzle-kit": {
|
"node_modules/drizzle-kit": {
|
||||||
"version": "1.0.0-beta.9-e89174b",
|
"version": "0.31.9",
|
||||||
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-1.0.0-beta.9-e89174b.tgz",
|
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.9.tgz",
|
||||||
"integrity": "sha512-Xrw3k8E2CbSZr+kqe3k5W4oxd2fbEyczjKtyGIkAq0x9Wqpa/VtAT6Mkh83sIzqG4OSN7lOoUafsDxSE/AR7RA==",
|
"integrity": "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@drizzle-team/brocli": "^0.11.0",
|
"@drizzle-team/brocli": "^0.10.2",
|
||||||
"@js-temporal/polyfill": "^0.5.1",
|
"@esbuild-kit/esm-loader": "^2.5.5",
|
||||||
"esbuild": "^0.25.10",
|
"esbuild": "^0.25.4",
|
||||||
"tsx": "^4.20.6"
|
"esbuild-register": "^3.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"drizzle-kit": "bin.cjs"
|
"drizzle-kit": "bin.cjs"
|
||||||
@@ -7403,6 +7826,7 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -7438,6 +7862,19 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild-register": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"esbuild": ">=0.12 <1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -8603,13 +9040,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsbi": {
|
|
||||||
"version": "4.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz",
|
|
||||||
"integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "28.0.0",
|
"version": "28.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.0.0.tgz",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -11,7 +11,7 @@
|
|||||||
"dev:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .",
|
"dev:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .",
|
||||||
"start": "concurrently --kill-others \"npm run dev:renderer\" \"npm run start:electron\"",
|
"start": "concurrently --kill-others \"npm run dev:renderer\" \"npm run start:electron\"",
|
||||||
"start:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .",
|
"start:electron": "wait-on http://localhost:5173 && cross-env NODE_ENV=development node ./node_modules/electron/cli.js .",
|
||||||
"build": "npm run build:main && npm run build:renderer",
|
"build": "npm run db:generate && npm run build:main && npm run build:renderer",
|
||||||
"build:main": "node ./node_modules/typescript/bin/tsc -p tsconfig.main.json",
|
"build:main": "node ./node_modules/typescript/bin/tsc -p tsconfig.main.json",
|
||||||
"build:renderer": "node ./node_modules/vite/bin/vite.js build",
|
"build:renderer": "node ./node_modules/vite/bin/vite.js build",
|
||||||
"start:prod": "node ./node_modules/electron/cli.js .",
|
"start:prod": "node ./node_modules/electron/cli.js .",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"drizzle-kit": "^1.0.0-beta.9-e89174b",
|
"drizzle-kit": "^0.31.9",
|
||||||
"electron": "^40.4.0",
|
"electron": "^40.4.0",
|
||||||
"electron-builder": "^26.7.0",
|
"electron-builder": "^26.7.0",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
@@ -92,8 +92,15 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
|
"drizzle/**/*",
|
||||||
"node_modules/**/*"
|
"node_modules/**/*"
|
||||||
],
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "drizzle",
|
||||||
|
"to": "drizzle"
|
||||||
|
}
|
||||||
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis"
|
"target": "nsis"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { createClient, Client } from '@libsql/client';
|
import { createClient, Client } from '@libsql/client';
|
||||||
import { drizzle } from 'drizzle-orm/libsql';
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { migrate } from 'drizzle-orm/libsql/migrator';
|
||||||
|
import { eq, sql } from 'drizzle-orm';
|
||||||
import * as schema from './schema';
|
import * as schema from './schema';
|
||||||
|
import { projects } from './schema';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -77,421 +80,78 @@ export class DatabaseConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getActiveProject(): Promise<{ id: string; name: string; slug: string } | null> {
|
async getActiveProject(): Promise<{ id: string; name: string; slug: string } | null> {
|
||||||
if (!this.localClient) return null;
|
if (!this.localDb) return null;
|
||||||
const result = await this.localClient.execute('SELECT id, name, slug FROM projects WHERE is_active = 1 LIMIT 1');
|
const rows = await this.localDb
|
||||||
if (result.rows.length === 0) return null;
|
.select({ id: projects.id, name: projects.name, slug: projects.slug })
|
||||||
const row = result.rows[0];
|
.from(projects)
|
||||||
return {
|
.where(eq(projects.isActive, true))
|
||||||
id: row.id as string,
|
.limit(1);
|
||||||
name: row.name as string,
|
if (rows.length === 0) return null;
|
||||||
slug: row.slug as string,
|
return rows[0];
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setActiveProject(projectId: string): Promise<void> {
|
async setActiveProject(projectId: string): Promise<void> {
|
||||||
if (!this.localClient) return;
|
if (!this.localDb) return;
|
||||||
await this.localClient.execute('UPDATE projects SET is_active = 0');
|
// Deactivate all projects
|
||||||
await this.localClient.execute({
|
await this.localDb
|
||||||
sql: 'UPDATE projects SET is_active = 1 WHERE id = ?',
|
.update(projects)
|
||||||
args: [projectId],
|
.set({ isActive: false });
|
||||||
});
|
// Activate the selected project
|
||||||
|
await this.localDb
|
||||||
|
.update(projects)
|
||||||
|
.set({ isActive: true })
|
||||||
|
.where(eq(projects.id, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runMigrations(): Promise<void> {
|
private async runMigrations(): Promise<void> {
|
||||||
if (!this.localClient) return;
|
if (!this.localClient || !this.localDb) return;
|
||||||
|
|
||||||
// Create tables if they don't exist using batch execution
|
// Determine migrations folder path (works in both dev and production)
|
||||||
await this.localClient.executeMultiple(`
|
// In production, migrations are bundled in the app resources
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
const isDev = !app.isPackaged;
|
||||||
id TEXT PRIMARY KEY,
|
const migrationsFolder = isDev
|
||||||
name TEXT NOT NULL,
|
? path.join(app.getAppPath(), 'drizzle')
|
||||||
slug TEXT NOT NULL UNIQUE,
|
: path.join(process.resourcesPath, 'drizzle');
|
||||||
description TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
is_active INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS posts (
|
// Run Drizzle migrations (creates __drizzle_migrations table automatically)
|
||||||
id TEXT PRIMARY KEY,
|
await migrate(this.localDb, { migrationsFolder });
|
||||||
project_id TEXT NOT NULL DEFAULT 'default',
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
excerpt TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'draft',
|
|
||||||
author TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
published_at INTEGER,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
synced_at INTEGER,
|
|
||||||
checksum TEXT,
|
|
||||||
tags TEXT,
|
|
||||||
categories TEXT,
|
|
||||||
published_title TEXT,
|
|
||||||
published_content TEXT,
|
|
||||||
published_tags TEXT,
|
|
||||||
published_categories TEXT,
|
|
||||||
published_excerpt TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS media (
|
// Create FTS5 virtual tables (not supported by Drizzle schema)
|
||||||
id TEXT PRIMARY KEY,
|
// These use IF NOT EXISTS so they're safe to run every time
|
||||||
project_id TEXT NOT NULL DEFAULT 'default',
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
original_name TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL,
|
|
||||||
size INTEGER NOT NULL,
|
|
||||||
width INTEGER,
|
|
||||||
height INTEGER,
|
|
||||||
alt TEXT,
|
|
||||||
caption TEXT,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
sidecar_path TEXT NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
synced_at INTEGER,
|
|
||||||
checksum TEXT,
|
|
||||||
tags TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sync_log (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
entity_type TEXT NOT NULL,
|
|
||||||
entity_id TEXT NOT NULL,
|
|
||||||
operation TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
timestamp INTEGER NOT NULL,
|
|
||||||
error_message TEXT,
|
|
||||||
retry_count INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS post_links (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
source_post_id TEXT NOT NULL,
|
|
||||||
target_post_id TEXT NOT NULL,
|
|
||||||
link_text TEXT,
|
|
||||||
created_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS post_media (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
post_id TEXT NOT NULL,
|
|
||||||
media_id TEXT NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_posts_sync_status ON posts(sync_status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_media_sync_status ON media(sync_status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_media_post ON post_media(post_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_media_media ON post_media(media_id);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS post_media_post_media_idx ON post_media(post_id, media_id);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tags (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
color TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tags_project_id ON tags(project_id);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS tags_project_name_idx ON tags(project_id, name);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Check if project_id column exists in posts table, add if missing (migration)
|
|
||||||
const postsColumns = await this.localClient.execute(
|
|
||||||
"SELECT name FROM pragma_table_info('posts') WHERE name = 'project_id'"
|
|
||||||
);
|
|
||||||
if (postsColumns.rows.length === 0) {
|
|
||||||
await this.localClient.execute(
|
|
||||||
"ALTER TABLE posts ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'"
|
|
||||||
);
|
|
||||||
await this.localClient.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_posts_project_id ON posts(project_id)"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await this.localClient.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_posts_project_id ON posts(project_id)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if project_id column exists in media table, add if missing (migration)
|
|
||||||
const mediaColumns = await this.localClient.execute(
|
|
||||||
"SELECT name FROM pragma_table_info('media') WHERE name = 'project_id'"
|
|
||||||
);
|
|
||||||
if (mediaColumns.rows.length === 0) {
|
|
||||||
await this.localClient.execute(
|
|
||||||
"ALTER TABLE media ADD COLUMN project_id TEXT NOT NULL DEFAULT 'default'"
|
|
||||||
);
|
|
||||||
await this.localClient.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_media_project_id ON media(project_id)"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await this.localClient.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_media_project_id ON media(project_id)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: Add published snapshot columns for discard functionality
|
|
||||||
const publishedContentCol = await this.localClient.execute(
|
|
||||||
"SELECT name FROM pragma_table_info('posts') WHERE name = 'published_content'"
|
|
||||||
);
|
|
||||||
if (publishedContentCol.rows.length === 0) {
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN published_title TEXT");
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN published_content TEXT");
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN published_tags TEXT");
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN published_categories TEXT");
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN published_excerpt TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: Add content column for draft body text stored in DB
|
|
||||||
const contentCol = await this.localClient.execute(
|
|
||||||
"SELECT name FROM pragma_table_info('posts') WHERE name = 'content'"
|
|
||||||
);
|
|
||||||
if (contentCol.rows.length === 0) {
|
|
||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN content TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: Update slug unique constraint to be project-scoped
|
|
||||||
// SQLite doesn't allow dropping column-level UNIQUE constraints, so we must recreate the table
|
|
||||||
// Check if the posts table has a column-level UNIQUE on slug (from the table definition)
|
|
||||||
const tableInfo = await this.localClient.execute(
|
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='posts'"
|
|
||||||
);
|
|
||||||
const tableSql = tableInfo.rows[0]?.sql as string || '';
|
|
||||||
const hasColumnLevelUnique = tableSql.includes('slug TEXT NOT NULL UNIQUE') ||
|
|
||||||
tableSql.includes('slug TEXT UNIQUE') ||
|
|
||||||
/slug\s+TEXT[^,]*UNIQUE/i.test(tableSql);
|
|
||||||
|
|
||||||
if (hasColumnLevelUnique) {
|
|
||||||
console.log('Migrating posts table to remove column-level UNIQUE constraint on slug...');
|
|
||||||
|
|
||||||
// Create new table without the UNIQUE constraint
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS posts_new (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL DEFAULT 'default',
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
excerpt TEXT,
|
|
||||||
content TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'draft',
|
|
||||||
author TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
published_at INTEGER,
|
|
||||||
file_path TEXT NOT NULL DEFAULT '',
|
|
||||||
sync_status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
synced_at INTEGER,
|
|
||||||
checksum TEXT,
|
|
||||||
tags TEXT,
|
|
||||||
categories TEXT,
|
|
||||||
published_title TEXT,
|
|
||||||
published_content TEXT,
|
|
||||||
published_tags TEXT,
|
|
||||||
published_categories TEXT,
|
|
||||||
published_excerpt TEXT
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Copy data
|
|
||||||
await this.localClient.execute(`
|
|
||||||
INSERT INTO posts_new
|
|
||||||
SELECT id, project_id, title, slug, excerpt, content, status, author,
|
|
||||||
created_at, updated_at, published_at, file_path, sync_status,
|
|
||||||
synced_at, checksum, tags, categories, published_title,
|
|
||||||
published_content, published_tags, published_categories, published_excerpt
|
|
||||||
FROM posts
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Drop old table and rename new one
|
|
||||||
await this.localClient.execute('DROP TABLE posts');
|
|
||||||
await this.localClient.execute('ALTER TABLE posts_new RENAME TO posts');
|
|
||||||
|
|
||||||
// Recreate indexes
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug)');
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status)');
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_sync_status ON posts(sync_status)');
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at)');
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_project_id ON posts(project_id)');
|
|
||||||
await this.localClient.execute('CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug)');
|
|
||||||
|
|
||||||
console.log('Posts table migration complete');
|
|
||||||
} else {
|
|
||||||
// Just ensure the composite unique index exists
|
|
||||||
const compositeSlugIndex = await this.localClient.execute(
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='index' AND name='posts_project_slug_idx' AND tbl_name='posts'"
|
|
||||||
);
|
|
||||||
if (compositeSlugIndex.rows.length === 0) {
|
|
||||||
await this.localClient.execute(
|
|
||||||
"CREATE UNIQUE INDEX posts_project_slug_idx ON posts(project_id, slug)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create FTS5 virtual table for full-text search
|
|
||||||
// Stores: id (unindexed, for lookups), project_id (unindexed, for filtering), content (stemmed text for matching)
|
|
||||||
// Post data for display comes from the posts table or filesystem files
|
|
||||||
await this.localClient.execute(`
|
await this.localClient.execute(`
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
||||||
id UNINDEXED,
|
id UNINDEXED,
|
||||||
project_id UNINDEXED,
|
project_id UNINDEXED,
|
||||||
content,
|
content,
|
||||||
content_rowid=rowid
|
content_rowid=rowid
|
||||||
);
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Migration: Check if old FTS schema exists and recreate with project_id
|
|
||||||
// Old schema had: id, content (or even older: id, title, content, excerpt, tags, categories)
|
|
||||||
// New schema has: id, project_id, content (for project-scoped search)
|
|
||||||
try {
|
|
||||||
// Try to query project_id - if it doesn't exist, we need to migrate
|
|
||||||
await this.localClient.execute("SELECT project_id FROM posts_fts LIMIT 0");
|
|
||||||
// project_id exists, check for old multi-column schema
|
|
||||||
try {
|
|
||||||
await this.localClient.execute("SELECT title FROM posts_fts LIMIT 0");
|
|
||||||
// Old multi-column schema exists - recreate
|
|
||||||
console.log('Migrating posts_fts table to new schema with project_id...');
|
|
||||||
await this.localClient.execute('DROP TABLE IF EXISTS posts_fts');
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE VIRTUAL TABLE posts_fts USING fts5(
|
|
||||||
id UNINDEXED,
|
|
||||||
project_id UNINDEXED,
|
|
||||||
content,
|
|
||||||
content_rowid=rowid
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
console.log('FTS table migrated - rebuild index required');
|
|
||||||
} catch {
|
|
||||||
// No title column - we have the correct new schema
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// project_id doesn't exist - migrate from old schema
|
|
||||||
console.log('Migrating posts_fts table to add project_id...');
|
|
||||||
await this.localClient.execute('DROP TABLE IF EXISTS posts_fts');
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE VIRTUAL TABLE posts_fts USING fts5(
|
|
||||||
id UNINDEXED,
|
|
||||||
project_id UNINDEXED,
|
|
||||||
content,
|
|
||||||
content_rowid=rowid
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
console.log('FTS table migrated - rebuild index required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create FTS5 virtual table for media full-text search
|
|
||||||
// Stores: id (unindexed, for lookups), project_id (unindexed, for filtering),
|
|
||||||
// content (stemmed text from original_name, alt, caption, tags)
|
|
||||||
await this.localClient.execute(`
|
await this.localClient.execute(`
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5(
|
||||||
id UNINDEXED,
|
id UNINDEXED,
|
||||||
project_id UNINDEXED,
|
project_id UNINDEXED,
|
||||||
content,
|
content,
|
||||||
content_rowid=rowid
|
content_rowid=rowid
|
||||||
);
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Migration: Ensure tags table exists (for databases created before tags feature)
|
|
||||||
const tagsTableExists = await this.localClient.execute(
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='tags'"
|
|
||||||
);
|
|
||||||
if (tagsTableExists.rows.length === 0) {
|
|
||||||
console.log('Creating tags table...');
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE TABLE tags (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
color TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await this.localClient.execute('CREATE INDEX idx_tags_project_id ON tags(project_id)');
|
|
||||||
await this.localClient.execute('CREATE UNIQUE INDEX tags_project_name_idx ON tags(project_id, name)');
|
|
||||||
console.log('Tags table created successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: Add data_path column to projects table
|
|
||||||
const dataPathCol = await this.localClient.execute(
|
|
||||||
"SELECT name FROM pragma_table_info('projects') WHERE name = 'data_path'"
|
|
||||||
);
|
|
||||||
if (dataPathCol.rows.length === 0) {
|
|
||||||
await this.localClient.execute("ALTER TABLE projects ADD COLUMN data_path TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create default project if none exists
|
// Create default project if none exists
|
||||||
const existingProjects = await this.localClient.execute('SELECT COUNT(*) as count FROM projects');
|
const existingProjects = await this.localDb
|
||||||
if (existingProjects.rows[0] && (existingProjects.rows[0].count as number) === 0) {
|
.select({ count: sql<number>`COUNT(*)` })
|
||||||
const now = Date.now();
|
.from(projects);
|
||||||
await this.localClient.execute({
|
if (existingProjects[0] && existingProjects[0].count === 0) {
|
||||||
sql: 'INSERT INTO projects (id, name, slug, description, created_at, updated_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
const now = new Date();
|
||||||
args: ['default', 'Default Project', 'default', 'Your first blog project', now, now, 1],
|
await this.localDb.insert(projects).values({
|
||||||
|
id: 'default',
|
||||||
|
name: 'Default Project',
|
||||||
|
slug: 'default',
|
||||||
|
description: 'Your first blog project',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
isActive: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create chat_conversations table for AI chat persistence
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_conversations (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
model TEXT,
|
|
||||||
copilot_session_id TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_chat_conversations_updated_at ON chat_conversations(updated_at)');
|
|
||||||
|
|
||||||
// Create chat_messages table for storing conversation messages
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
conversation_id TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
content TEXT,
|
|
||||||
tool_call_id TEXT,
|
|
||||||
tool_calls TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (conversation_id) REFERENCES chat_conversations(id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_chat_messages_conversation_id ON chat_messages(conversation_id)');
|
|
||||||
|
|
||||||
// Create import_definitions table for WXR import configurations
|
|
||||||
await this.localClient.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS import_definitions (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
wxr_file_path TEXT,
|
|
||||||
uploads_folder_path TEXT,
|
|
||||||
last_analysis_result TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_import_definitions_project_id ON import_definitions(project_id)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { eq, desc, asc } from 'drizzle-orm';
|
||||||
import { DatabaseConnection } from '../database/connection';
|
import { DatabaseConnection } from '../database/connection';
|
||||||
|
import { chatConversations, chatMessages, settings } from '../database/schema';
|
||||||
|
|
||||||
export interface ChatConversationData {
|
export interface ChatConversationData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -45,19 +47,18 @@ export class ChatEngine {
|
|||||||
* Create a new chat conversation
|
* Create a new chat conversation
|
||||||
*/
|
*/
|
||||||
async createConversation(input: CreateConversationInput = {}): Promise<ChatConversationData> {
|
async createConversation(input: CreateConversationInput = {}): Promise<ChatConversationData> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = `chat_${uuidv4()}`;
|
const id = `chat_${uuidv4()}`;
|
||||||
const title = input.title || 'New Chat';
|
const title = input.title || 'New Chat';
|
||||||
const model = input.model || 'claude-sonnet-4';
|
const model = input.model || 'claude-sonnet-4';
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
|
|
||||||
await client.execute({
|
await drizzle.insert(chatConversations).values({
|
||||||
sql: `INSERT INTO chat_conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`,
|
id,
|
||||||
args: [id, title, model, now, now],
|
title,
|
||||||
|
model,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add system prompt as first message if provided
|
// Add system prompt as first message if provided
|
||||||
@@ -66,7 +67,7 @@ export class ChatEngine {
|
|||||||
conversationId: id,
|
conversationId: id,
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: input.systemPrompt,
|
content: input.systemPrompt,
|
||||||
createdAt: new Date(now),
|
createdAt: now,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +75,8 @@ export class ChatEngine {
|
|||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
model,
|
model,
|
||||||
createdAt: new Date(now),
|
createdAt: now,
|
||||||
updatedAt: new Date(now),
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,42 +84,40 @@ export class ChatEngine {
|
|||||||
* Get a conversation by ID with all messages
|
* Get a conversation by ID with all messages
|
||||||
*/
|
*/
|
||||||
async getConversation(id: string): Promise<(ChatConversationData & { messages: ChatMessageData[] }) | null> {
|
async getConversation(id: string): Promise<(ChatConversationData & { messages: ChatMessageData[] }) | null> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const convResult = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT * FROM chat_conversations WHERE id = ?`,
|
.select()
|
||||||
args: [id],
|
.from(chatConversations)
|
||||||
});
|
.where(eq(chatConversations.id, id));
|
||||||
|
|
||||||
if (convResult.rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = convResult.rows[0];
|
const row = rows[0];
|
||||||
const conversation: ChatConversationData = {
|
const conversation: ChatConversationData = {
|
||||||
id: row.id as string,
|
id: row.id,
|
||||||
title: row.title as string,
|
title: row.title,
|
||||||
model: row.model as string | undefined,
|
model: row.model || undefined,
|
||||||
createdAt: new Date(row.created_at as number),
|
createdAt: row.createdAt,
|
||||||
updatedAt: new Date(row.updated_at as number),
|
updatedAt: row.updatedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messagesResult = await client.execute({
|
const messageRows = await drizzle
|
||||||
sql: `SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY created_at ASC`,
|
.select()
|
||||||
args: [id],
|
.from(chatMessages)
|
||||||
});
|
.where(eq(chatMessages.conversationId, id))
|
||||||
|
.orderBy(asc(chatMessages.createdAt));
|
||||||
|
|
||||||
const messages: ChatMessageData[] = messagesResult.rows.map(r => ({
|
const messages: ChatMessageData[] = messageRows.map(r => ({
|
||||||
id: r.id as number,
|
id: r.id,
|
||||||
conversationId: r.conversation_id as string,
|
conversationId: r.conversationId,
|
||||||
role: r.role as 'system' | 'user' | 'assistant' | 'tool',
|
role: r.role,
|
||||||
content: r.content as string | undefined,
|
content: r.content || undefined,
|
||||||
toolCallId: r.tool_call_id as string | undefined,
|
toolCallId: r.toolCallId || undefined,
|
||||||
toolCalls: r.tool_calls as string | undefined,
|
toolCalls: r.toolCalls || undefined,
|
||||||
createdAt: new Date(r.created_at as number),
|
createdAt: r.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { ...conversation, messages };
|
return { ...conversation, messages };
|
||||||
@@ -128,22 +127,20 @@ export class ChatEngine {
|
|||||||
* Get all conversations, sorted by most recently updated
|
* Get all conversations, sorted by most recently updated
|
||||||
*/
|
*/
|
||||||
async getRecentConversations(limit: number = 50): Promise<ChatConversationData[]> {
|
async getRecentConversations(limit: number = 50): Promise<ChatConversationData[]> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT * FROM chat_conversations ORDER BY updated_at DESC LIMIT ?`,
|
.select()
|
||||||
args: [limit],
|
.from(chatConversations)
|
||||||
});
|
.orderBy(desc(chatConversations.updatedAt))
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
return result.rows.map(row => ({
|
return rows.map(row => ({
|
||||||
id: row.id as string,
|
id: row.id,
|
||||||
title: row.title as string,
|
title: row.title,
|
||||||
model: row.model as string | undefined,
|
model: row.model || undefined,
|
||||||
createdAt: new Date(row.created_at as number),
|
createdAt: row.createdAt,
|
||||||
updatedAt: new Date(row.updated_at as number),
|
updatedAt: row.updatedAt,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,90 +148,66 @@ export class ChatEngine {
|
|||||||
* Update a conversation's metadata
|
* Update a conversation's metadata
|
||||||
*/
|
*/
|
||||||
async updateConversation(id: string, updates: Partial<Pick<ChatConversationData, 'title' | 'model'>>): Promise<void> {
|
async updateConversation(id: string, updates: Partial<Pick<ChatConversationData, 'title' | 'model'>>): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const setClauses: string[] = ['updated_at = ?'];
|
await drizzle
|
||||||
const args: (string | number | null)[] = [Date.now()];
|
.update(chatConversations)
|
||||||
|
.set({
|
||||||
if (updates.title !== undefined) {
|
...updates,
|
||||||
setClauses.push('title = ?');
|
updatedAt: new Date(),
|
||||||
args.push(updates.title);
|
})
|
||||||
}
|
.where(eq(chatConversations.id, id));
|
||||||
if (updates.model !== undefined) {
|
|
||||||
setClauses.push('model = ?');
|
|
||||||
args.push(updates.model);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push(id);
|
|
||||||
|
|
||||||
await client.execute({
|
|
||||||
sql: `UPDATE chat_conversations SET ${setClauses.join(', ')} WHERE id = ?`,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a conversation and all its messages
|
* Delete a conversation and all its messages
|
||||||
*/
|
*/
|
||||||
async deleteConversation(id: string): Promise<void> {
|
async deleteConversation(id: string): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Messages are deleted via CASCADE, but let's be explicit
|
// Messages are deleted via CASCADE, but let's be explicit
|
||||||
await client.execute({
|
await drizzle
|
||||||
sql: `DELETE FROM chat_messages WHERE conversation_id = ?`,
|
.delete(chatMessages)
|
||||||
args: [id],
|
.where(eq(chatMessages.conversationId, id));
|
||||||
});
|
|
||||||
|
|
||||||
await client.execute({
|
await drizzle
|
||||||
sql: `DELETE FROM chat_conversations WHERE id = ?`,
|
.delete(chatConversations)
|
||||||
args: [id],
|
.where(eq(chatConversations.id, id));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a message to a conversation
|
* Add a message to a conversation
|
||||||
*/
|
*/
|
||||||
async addMessage(message: Omit<ChatMessageData, 'id'>): Promise<ChatMessageData> {
|
async addMessage(message: Omit<ChatMessageData, 'id'>): Promise<ChatMessageData> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
const createdAt = message.createdAt || new Date();
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdAt = message.createdAt?.getTime() || Date.now();
|
const result = await drizzle
|
||||||
|
.insert(chatMessages)
|
||||||
const result = await client.execute({
|
.values({
|
||||||
sql: `INSERT INTO chat_messages (conversation_id, role, content, tool_call_id, tool_calls, created_at)
|
conversationId: message.conversationId,
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
role: message.role,
|
||||||
args: [
|
content: message.content || null,
|
||||||
message.conversationId,
|
toolCallId: message.toolCallId || null,
|
||||||
message.role,
|
toolCalls: message.toolCalls || null,
|
||||||
message.content || null,
|
|
||||||
message.toolCallId || null,
|
|
||||||
message.toolCalls || null,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
],
|
})
|
||||||
});
|
.returning({ id: chatMessages.id });
|
||||||
|
|
||||||
// Update conversation's updated_at timestamp
|
// Update conversation's updated_at timestamp
|
||||||
await client.execute({
|
await drizzle
|
||||||
sql: `UPDATE chat_conversations SET updated_at = ? WHERE id = ?`,
|
.update(chatConversations)
|
||||||
args: [createdAt, message.conversationId],
|
.set({ updatedAt: createdAt })
|
||||||
});
|
.where(eq(chatConversations.id, message.conversationId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(result.lastInsertRowid),
|
id: result[0].id,
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
toolCallId: message.toolCallId,
|
toolCallId: message.toolCallId,
|
||||||
toolCalls: message.toolCalls,
|
toolCalls: message.toolCalls,
|
||||||
createdAt: new Date(createdAt),
|
createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,24 +215,22 @@ export class ChatEngine {
|
|||||||
* Get messages for a conversation
|
* Get messages for a conversation
|
||||||
*/
|
*/
|
||||||
async getMessages(conversationId: string): Promise<ChatMessageData[]> {
|
async getMessages(conversationId: string): Promise<ChatMessageData[]> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY created_at ASC`,
|
.select()
|
||||||
args: [conversationId],
|
.from(chatMessages)
|
||||||
});
|
.where(eq(chatMessages.conversationId, conversationId))
|
||||||
|
.orderBy(asc(chatMessages.createdAt));
|
||||||
|
|
||||||
return result.rows.map(r => ({
|
return rows.map(r => ({
|
||||||
id: r.id as number,
|
id: r.id,
|
||||||
conversationId: r.conversation_id as string,
|
conversationId: r.conversationId,
|
||||||
role: r.role as 'system' | 'user' | 'assistant' | 'tool',
|
role: r.role,
|
||||||
content: r.content as string | undefined,
|
content: r.content || undefined,
|
||||||
toolCallId: r.tool_call_id as string | undefined,
|
toolCallId: r.toolCallId || undefined,
|
||||||
toolCalls: r.tool_calls as string | undefined,
|
toolCalls: r.toolCalls || undefined,
|
||||||
createdAt: new Date(r.created_at as number),
|
createdAt: r.createdAt,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,34 +238,27 @@ export class ChatEngine {
|
|||||||
* Clear all messages from a conversation (but keep the conversation)
|
* Clear all messages from a conversation (but keep the conversation)
|
||||||
*/
|
*/
|
||||||
async clearMessages(conversationId: string): Promise<void> {
|
async clearMessages(conversationId: string): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.execute({
|
await drizzle
|
||||||
sql: `DELETE FROM chat_messages WHERE conversation_id = ?`,
|
.delete(chatMessages)
|
||||||
args: [conversationId],
|
.where(eq(chatMessages.conversationId, conversationId));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get default system prompt for new conversations
|
* Get default system prompt for new conversations
|
||||||
*/
|
*/
|
||||||
async getDefaultSystemPrompt(): Promise<string> {
|
async getDefaultSystemPrompt(): Promise<string> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
return this.getBuiltInSystemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT value FROM settings WHERE key = 'chat_system_prompt'`,
|
.select()
|
||||||
args: [],
|
.from(settings)
|
||||||
});
|
.where(eq(settings.key, 'chat_system_prompt'));
|
||||||
|
|
||||||
// Return saved prompt if it exists and is non-empty
|
// Return saved prompt if it exists and is non-empty
|
||||||
if (result.rows.length > 0 && result.rows[0].value) {
|
if (rows.length > 0 && rows[0].value) {
|
||||||
return result.rows[0].value as string;
|
return rows[0].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getBuiltInSystemPrompt();
|
return this.getBuiltInSystemPrompt();
|
||||||
@@ -305,25 +269,30 @@ export class ChatEngine {
|
|||||||
* Pass empty string to reset to built-in default.
|
* Pass empty string to reset to built-in default.
|
||||||
*/
|
*/
|
||||||
async setDefaultSystemPrompt(prompt: string): Promise<void> {
|
async setDefaultSystemPrompt(prompt: string): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If empty string, delete the setting to use built-in default
|
// If empty string, delete the setting to use built-in default
|
||||||
if (!prompt || prompt.trim() === '') {
|
if (!prompt || prompt.trim() === '') {
|
||||||
await client.execute({
|
await drizzle
|
||||||
sql: `DELETE FROM settings WHERE key = ?`,
|
.delete(settings)
|
||||||
args: ['chat_system_prompt'],
|
.where(eq(settings.key, 'chat_system_prompt'));
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
await drizzle
|
||||||
await client.execute({
|
.insert(settings)
|
||||||
sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`,
|
.values({
|
||||||
args: ['chat_system_prompt', prompt, now],
|
key: 'chat_system_prompt',
|
||||||
});
|
value: prompt,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: settings.key,
|
||||||
|
set: {
|
||||||
|
value: prompt,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -360,16 +329,15 @@ When answering questions:
|
|||||||
* Get a setting by key
|
* Get a setting by key
|
||||||
*/
|
*/
|
||||||
async getSetting(key: string): Promise<string | null> {
|
async getSetting(key: string): Promise<string | null> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) return null;
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT value FROM settings WHERE key = ?`,
|
.select()
|
||||||
args: [key],
|
.from(settings)
|
||||||
});
|
.where(eq(settings.key, key));
|
||||||
|
|
||||||
if (result.rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
return result.rows[0].value as string;
|
return rows[0].value;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -378,34 +346,37 @@ When answering questions:
|
|||||||
* Set a setting by key
|
* Set a setting by key
|
||||||
*/
|
*/
|
||||||
async setSetting(key: string, value: string): Promise<void> {
|
async setSetting(key: string, value: string): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
await drizzle
|
||||||
await client.execute({
|
.insert(settings)
|
||||||
sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`,
|
.values({
|
||||||
args: [key, value, now],
|
key,
|
||||||
});
|
value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: settings.key,
|
||||||
|
set: {
|
||||||
|
value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get selected model for new conversations
|
* Get selected model for new conversations
|
||||||
*/
|
*/
|
||||||
async getSelectedModel(): Promise<string> {
|
async getSelectedModel(): Promise<string> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
return 'claude-sonnet-4';
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await drizzle
|
||||||
sql: `SELECT value FROM settings WHERE key = 'chat_model'`,
|
.select()
|
||||||
args: [],
|
.from(settings)
|
||||||
});
|
.where(eq(settings.key, 'chat_model'));
|
||||||
|
|
||||||
if (result.rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
return result.rows[0].value as string;
|
return rows[0].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'claude-sonnet-4';
|
return 'claude-sonnet-4';
|
||||||
@@ -415,15 +386,21 @@ When answering questions:
|
|||||||
* Set selected model for new conversations
|
* Set selected model for new conversations
|
||||||
*/
|
*/
|
||||||
async setSelectedModel(model: string): Promise<void> {
|
async setSelectedModel(model: string): Promise<void> {
|
||||||
const client = this.db.getLocalClient();
|
const drizzle = this.db.getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
await drizzle
|
||||||
await client.execute({
|
.insert(settings)
|
||||||
sql: `INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)`,
|
.values({
|
||||||
args: ['chat_model', model, now],
|
key: 'chat_model',
|
||||||
});
|
value: model,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: settings.key,
|
||||||
|
set: {
|
||||||
|
value: model,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
|
import { importDefinitions } from '../database/schema';
|
||||||
|
|
||||||
export interface ImportDefinitionData {
|
export interface ImportDefinitionData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,12 +24,8 @@ export interface ImportDefinitionData {
|
|||||||
export class ImportDefinitionEngine {
|
export class ImportDefinitionEngine {
|
||||||
private currentProjectId: string = 'default';
|
private currentProjectId: string = 'default';
|
||||||
|
|
||||||
private getClient() {
|
private getDb() {
|
||||||
const client = getDatabase().getLocalClient();
|
return getDatabase().getLocal();
|
||||||
if (!client) {
|
|
||||||
throw new Error('Database not initialized');
|
|
||||||
}
|
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setProjectContext(projectId: string): void {
|
setProjectContext(projectId: string): void {
|
||||||
@@ -39,15 +37,20 @@ export class ImportDefinitionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createDefinition(name?: string): Promise<ImportDefinitionData> {
|
async createDefinition(name?: string): Promise<ImportDefinitionData> {
|
||||||
const client = this.getClient();
|
const db = this.getDb();
|
||||||
const id = `import_${uuidv4()}`;
|
const id = `import_${uuidv4()}`;
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
const defName = name || 'Untitled Import';
|
const defName = name || 'Untitled Import';
|
||||||
|
|
||||||
await client.execute({
|
await db.insert(importDefinitions).values({
|
||||||
sql: `INSERT INTO import_definitions (id, project_id, name, wxr_file_path, uploads_folder_path, last_analysis_result, created_at, updated_at)
|
id,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
projectId: this.currentProjectId,
|
||||||
args: [id, this.currentProjectId, defName, null, null, null, now, now],
|
name: defName,
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -57,31 +60,37 @@ export class ImportDefinitionEngine {
|
|||||||
wxrFilePath: null,
|
wxrFilePath: null,
|
||||||
uploadsFolderPath: null,
|
uploadsFolderPath: null,
|
||||||
lastAnalysisResult: null,
|
lastAnalysisResult: null,
|
||||||
createdAt: new Date(now).toISOString(),
|
createdAt: now.toISOString(),
|
||||||
updatedAt: new Date(now).toISOString(),
|
updatedAt: now.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefinition(id: string): Promise<ImportDefinitionData | null> {
|
async getDefinition(id: string): Promise<ImportDefinitionData | null> {
|
||||||
const client = this.getClient();
|
const db = this.getDb();
|
||||||
const result = await client.execute({
|
|
||||||
sql: `SELECT * FROM import_definitions WHERE id = ? AND project_id = ?`,
|
|
||||||
args: [id, this.currentProjectId],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.rows.length === 0) return null;
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(importDefinitions)
|
||||||
|
.where(and(
|
||||||
|
eq(importDefinitions.id, id),
|
||||||
|
eq(importDefinitions.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
return this.rowToData(result.rows[0] as any);
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
|
return this.rowToData(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllForProject(): Promise<ImportDefinitionData[]> {
|
async getAllForProject(): Promise<ImportDefinitionData[]> {
|
||||||
const client = this.getClient();
|
const db = this.getDb();
|
||||||
const result = await client.execute({
|
|
||||||
sql: `SELECT * FROM import_definitions WHERE project_id = ? ORDER BY updated_at DESC`,
|
|
||||||
args: [this.currentProjectId],
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.rows.map((row: any) => this.rowToData(row));
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(importDefinitions)
|
||||||
|
.where(eq(importDefinitions.projectId, this.currentProjectId))
|
||||||
|
.orderBy(desc(importDefinitions.updatedAt));
|
||||||
|
|
||||||
|
return rows.map(row => this.rowToData(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDefinition(
|
async updateDefinition(
|
||||||
@@ -92,42 +101,35 @@ export class ImportDefinitionEngine {
|
|||||||
const existing = await this.getDefinition(id);
|
const existing = await this.getDefinition(id);
|
||||||
if (!existing) return null;
|
if (!existing) return null;
|
||||||
|
|
||||||
const setClauses: string[] = [];
|
const db = this.getDb();
|
||||||
const args: any[] = [];
|
|
||||||
|
// Build update object
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
if (updates.name !== undefined) {
|
if (updates.name !== undefined) {
|
||||||
setClauses.push('name = ?');
|
updateData.name = updates.name;
|
||||||
args.push(updates.name);
|
|
||||||
}
|
}
|
||||||
if (updates.wxrFilePath !== undefined) {
|
if (updates.wxrFilePath !== undefined) {
|
||||||
setClauses.push('wxr_file_path = ?');
|
updateData.wxrFilePath = updates.wxrFilePath;
|
||||||
args.push(updates.wxrFilePath);
|
|
||||||
}
|
}
|
||||||
if (updates.uploadsFolderPath !== undefined) {
|
if (updates.uploadsFolderPath !== undefined) {
|
||||||
setClauses.push('uploads_folder_path = ?');
|
updateData.uploadsFolderPath = updates.uploadsFolderPath;
|
||||||
args.push(updates.uploadsFolderPath);
|
|
||||||
}
|
}
|
||||||
if (updates.lastAnalysisResult !== undefined) {
|
if (updates.lastAnalysisResult !== undefined) {
|
||||||
setClauses.push('last_analysis_result = ?');
|
updateData.lastAnalysisResult = typeof updates.lastAnalysisResult === 'string'
|
||||||
args.push(typeof updates.lastAnalysisResult === 'string'
|
|
||||||
? updates.lastAnalysisResult
|
? updates.lastAnalysisResult
|
||||||
: JSON.stringify(updates.lastAnalysisResult));
|
: JSON.stringify(updates.lastAnalysisResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setClauses.length === 0) return existing;
|
await db
|
||||||
|
.update(importDefinitions)
|
||||||
const now = Date.now();
|
.set(updateData)
|
||||||
setClauses.push('updated_at = ?');
|
.where(and(
|
||||||
args.push(now);
|
eq(importDefinitions.id, id),
|
||||||
|
eq(importDefinitions.projectId, this.currentProjectId)
|
||||||
// WHERE clause args
|
));
|
||||||
args.push(id, this.currentProjectId);
|
|
||||||
|
|
||||||
const client = this.getClient();
|
|
||||||
await client.execute({
|
|
||||||
sql: `UPDATE import_definitions SET ${setClauses.join(', ')} WHERE id = ? AND project_id = ?`,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.getDefinition(id);
|
return this.getDefinition(id);
|
||||||
}
|
}
|
||||||
@@ -137,38 +139,41 @@ export class ImportDefinitionEngine {
|
|||||||
const existing = await this.getDefinition(id);
|
const existing = await this.getDefinition(id);
|
||||||
if (!existing) return false;
|
if (!existing) return false;
|
||||||
|
|
||||||
const client = this.getClient();
|
const db = this.getDb();
|
||||||
await client.execute({
|
|
||||||
sql: `DELETE FROM import_definitions WHERE id = ? AND project_id = ?`,
|
await db
|
||||||
args: [id, this.currentProjectId],
|
.delete(importDefinitions)
|
||||||
});
|
.where(and(
|
||||||
|
eq(importDefinitions.id, id),
|
||||||
|
eq(importDefinitions.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private rowToData(row: any): ImportDefinitionData {
|
private rowToData(row: typeof importDefinitions.$inferSelect): ImportDefinitionData {
|
||||||
let parsedResult: unknown | null = null;
|
let parsedResult: unknown | null = null;
|
||||||
if (row.last_analysis_result) {
|
if (row.lastAnalysisResult) {
|
||||||
try {
|
try {
|
||||||
parsedResult = JSON.parse(row.last_analysis_result);
|
parsedResult = JSON.parse(row.lastAnalysisResult);
|
||||||
} catch {
|
} catch {
|
||||||
parsedResult = row.last_analysis_result;
|
parsedResult = row.lastAnalysisResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
projectId: row.project_id,
|
projectId: row.projectId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
wxrFilePath: row.wxr_file_path ?? null,
|
wxrFilePath: row.wxrFilePath ?? null,
|
||||||
uploadsFolderPath: row.uploads_folder_path ?? null,
|
uploadsFolderPath: row.uploadsFolderPath ?? null,
|
||||||
lastAnalysisResult: parsedResult,
|
lastAnalysisResult: parsedResult,
|
||||||
createdAt: typeof row.created_at === 'number'
|
createdAt: row.createdAt instanceof Date
|
||||||
? new Date(row.created_at).toISOString()
|
? row.createdAt.toISOString()
|
||||||
: row.created_at,
|
: new Date(row.createdAt as unknown as number).toISOString(),
|
||||||
updatedAt: typeof row.updated_at === 'number'
|
updatedAt: row.updatedAt instanceof Date
|
||||||
? new Date(row.updated_at).toISOString()
|
? row.updatedAt.toISOString()
|
||||||
: row.updated_at,
|
: new Date(row.updatedAt as unknown as number).toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
import { eq, and, asc, sql, like } from 'drizzle-orm';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
|
import { tags, posts } from '../database/schema';
|
||||||
import { taskManager } from './TaskManager';
|
import { taskManager } from './TaskManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +127,15 @@ export class TagEngine extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDb() {
|
||||||
|
return getDatabase().getLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For JSON operations that Drizzle doesn't support natively
|
||||||
|
private getClient() {
|
||||||
|
return getDatabase().getLocalClient();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default internal project directory (in userData).
|
* Returns the default internal project directory (in userData).
|
||||||
*/
|
*/
|
||||||
@@ -167,11 +178,10 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Get all tags with their post counts for the tag cloud
|
* Get all tags with their post counts for the tag cloud
|
||||||
*/
|
*/
|
||||||
async getTagsWithCounts(): Promise<TagWithCount[]> {
|
async getTagsWithCounts(): Promise<TagWithCount[]> {
|
||||||
const client = getDatabase().getLocalClient();
|
const client = this.getClient();
|
||||||
if (!client) return [];
|
if (!client) return [];
|
||||||
|
|
||||||
// Query tags with counts from posts
|
// Query tags with counts from posts - requires raw SQL for JSON operations
|
||||||
// Use a subquery to count posts per tag name
|
|
||||||
const result = await client.execute({
|
const result = await client.execute({
|
||||||
sql: `
|
sql: `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -202,8 +212,7 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Create a new tag
|
* Create a new tag
|
||||||
*/
|
*/
|
||||||
async createTag(input: CreateTagInput): Promise<TagData> {
|
async createTag(input: CreateTagInput): Promise<TagData> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) throw new Error('Database not initialized');
|
|
||||||
|
|
||||||
const name = input.name.trim().toLowerCase();
|
const name = input.name.trim().toLowerCase();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -215,29 +224,36 @@ export class TagEngine extends EventEmitter {
|
|||||||
throw new Error('Invalid color format. Use hex format like #ff0000 or #f00');
|
throw new Error('Invalid color format. Use hex format like #ff0000 or #f00');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate
|
// Check for duplicate using Drizzle
|
||||||
const existing = await client.execute({
|
const existing = await db
|
||||||
sql: 'SELECT id, name FROM tags WHERE project_id = ? AND LOWER(name) = LOWER(?)',
|
.select({ id: tags.id })
|
||||||
args: [this.currentProjectId, name],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.projectId, this.currentProjectId),
|
||||||
|
sql`LOWER(${tags.name}) = LOWER(${name})`
|
||||||
|
));
|
||||||
|
|
||||||
if (existing.rows.length > 0) {
|
if (existing.length > 0) {
|
||||||
throw new Error(`Tag "${name}" already exists`);
|
throw new Error(`Tag "${name}" already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
const tag: TagData = {
|
const tag: TagData = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
projectId: this.currentProjectId,
|
projectId: this.currentProjectId,
|
||||||
name,
|
name,
|
||||||
color: input.color,
|
color: input.color,
|
||||||
createdAt: new Date(now),
|
createdAt: now,
|
||||||
updatedAt: new Date(now),
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
await client.execute({
|
await db.insert(tags).values({
|
||||||
sql: 'INSERT INTO tags (id, project_id, name, color, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
id: tag.id,
|
||||||
args: [tag.id, tag.projectId, tag.name, tag.color || null, now, now],
|
projectId: tag.projectId,
|
||||||
|
name: tag.name,
|
||||||
|
color: tag.color || null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit('tagCreated', tag);
|
this.emit('tagCreated', tag);
|
||||||
@@ -250,57 +266,53 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Update a tag
|
* Update a tag
|
||||||
*/
|
*/
|
||||||
async updateTag(id: string, input: UpdateTagInput): Promise<TagData | null> {
|
async updateTag(id: string, input: UpdateTagInput): Promise<TagData | null> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) return null;
|
|
||||||
|
|
||||||
// Get existing tag
|
// Get existing tag
|
||||||
const existing = await client.execute({
|
const existing = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [id, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (existing.rows.length === 0) {
|
if (existing.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = existing.rows[0] as any;
|
const row = existing[0];
|
||||||
|
|
||||||
// Validate color if provided
|
// Validate color if provided
|
||||||
if (input.color !== undefined && input.color !== null && !isValidHexColor(input.color)) {
|
if (input.color !== undefined && input.color !== null && !isValidHexColor(input.color)) {
|
||||||
throw new Error('Invalid color format. Use hex format like #ff0000 or #f00');
|
throw new Error('Invalid color format. Use hex format like #ff0000 or #f00');
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
if (input.color === undefined) {
|
||||||
const updates: string[] = [];
|
|
||||||
const args: any[] = [];
|
|
||||||
|
|
||||||
if (input.color !== undefined) {
|
|
||||||
updates.push('color = ?');
|
|
||||||
args.push(input.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) {
|
|
||||||
// No updates
|
// No updates
|
||||||
return this.rowToTagData(row);
|
return this.rowToTagData(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
updates.push('updated_at = ?');
|
const now = new Date();
|
||||||
args.push(now);
|
|
||||||
args.push(id);
|
|
||||||
args.push(this.currentProjectId);
|
|
||||||
|
|
||||||
await client.execute({
|
await db
|
||||||
sql: `UPDATE tags SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`,
|
.update(tags)
|
||||||
args,
|
.set({
|
||||||
});
|
color: input.color,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
const updatedTag: TagData = {
|
const updatedTag: TagData = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
projectId: row.project_id,
|
projectId: row.projectId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
color: input.color !== undefined ? input.color || undefined : row.color,
|
color: input.color !== undefined ? input.color || undefined : row.color || undefined,
|
||||||
createdAt: new Date(row.created_at),
|
createdAt: row.createdAt,
|
||||||
updatedAt: new Date(now),
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emit('tagUpdated', updatedTag);
|
this.emit('tagUpdated', updatedTag);
|
||||||
@@ -313,21 +325,25 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Delete a tag and remove it from all posts (runs as background task)
|
* Delete a tag and remove it from all posts (runs as background task)
|
||||||
*/
|
*/
|
||||||
async deleteTag(id: string): Promise<DeleteTagResult> {
|
async deleteTag(id: string): Promise<DeleteTagResult> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
|
const client = this.getClient();
|
||||||
if (!client) throw new Error('Database not initialized');
|
if (!client) throw new Error('Database not initialized');
|
||||||
|
|
||||||
// Get tag
|
// Get tag
|
||||||
const tagResult = await client.execute({
|
const tagRows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [id, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (tagResult.rows.length === 0) {
|
if (tagRows.length === 0) {
|
||||||
throw new Error('Tag not found');
|
throw new Error('Tag not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = tagResult.rows[0] as any;
|
const tag = tagRows[0];
|
||||||
const tagName = tag.name as string;
|
const tagName = tag.name;
|
||||||
|
|
||||||
// Run the deletion as a background task
|
// Run the deletion as a background task
|
||||||
return taskManager.runTask({
|
return taskManager.runTask({
|
||||||
@@ -336,15 +352,15 @@ export class TagEngine extends EventEmitter {
|
|||||||
execute: async (onProgress) => {
|
execute: async (onProgress) => {
|
||||||
onProgress(0, `Finding posts with tag "${tagName}"...`);
|
onProgress(0, `Finding posts with tag "${tagName}"...`);
|
||||||
|
|
||||||
// Find all posts with this tag
|
// Find all posts with this tag - requires raw SQL for JSON
|
||||||
const postsResult = await client.execute({
|
const postsResult = await client.execute({
|
||||||
sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
|
sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
|
||||||
args: [this.currentProjectId, `%"${tagName}"%`],
|
args: [this.currentProjectId, `%"${tagName}"%`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const postsToUpdate = postsResult.rows.filter((row: any) => {
|
const postsToUpdate = postsResult.rows.filter((row: any) => {
|
||||||
const tags: string[] = JSON.parse(row.tags || '[]');
|
const postTags: string[] = JSON.parse(row.tags || '[]');
|
||||||
return tags.includes(tagName);
|
return postTags.includes(tagName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = postsToUpdate.length;
|
const total = postsToUpdate.length;
|
||||||
@@ -352,13 +368,16 @@ export class TagEngine extends EventEmitter {
|
|||||||
|
|
||||||
for (const row of postsToUpdate) {
|
for (const row of postsToUpdate) {
|
||||||
const postId = row.id as string;
|
const postId = row.id as string;
|
||||||
const tags: string[] = JSON.parse((row as any).tags || '[]');
|
const postTags: string[] = JSON.parse((row as any).tags || '[]');
|
||||||
const newTags = tags.filter(t => t !== tagName);
|
const newTags = postTags.filter(t => t !== tagName);
|
||||||
|
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'UPDATE posts SET tags = ?, updated_at = ? WHERE id = ?',
|
.update(posts)
|
||||||
args: [JSON.stringify(newTags), Date.now(), postId],
|
.set({
|
||||||
});
|
tags: JSON.stringify(newTags),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(posts.id, postId));
|
||||||
|
|
||||||
updated++;
|
updated++;
|
||||||
onProgress((updated / total) * 80, `Updated ${updated}/${total} posts...`);
|
onProgress((updated / total) * 80, `Updated ${updated}/${total} posts...`);
|
||||||
@@ -367,10 +386,12 @@ export class TagEngine extends EventEmitter {
|
|||||||
onProgress(90, 'Deleting tag...');
|
onProgress(90, 'Deleting tag...');
|
||||||
|
|
||||||
// Delete the tag
|
// Delete the tag
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'DELETE FROM tags WHERE id = ? AND project_id = ?',
|
.delete(tags)
|
||||||
args: [id, this.currentProjectId],
|
.where(and(
|
||||||
});
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
onProgress(100, 'Complete');
|
onProgress(100, 'Complete');
|
||||||
|
|
||||||
@@ -386,7 +407,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Merge multiple source tags into a target tag (runs as background task)
|
* Merge multiple source tags into a target tag (runs as background task)
|
||||||
*/
|
*/
|
||||||
async mergeTags(sourceTagIds: string[], targetTagId: string): Promise<MergeTagsResult> {
|
async mergeTags(sourceTagIds: string[], targetTagId: string): Promise<MergeTagsResult> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
|
const client = this.getClient();
|
||||||
if (!client) throw new Error('Database not initialized');
|
if (!client) throw new Error('Database not initialized');
|
||||||
|
|
||||||
if (sourceTagIds.length === 0) {
|
if (sourceTagIds.length === 0) {
|
||||||
@@ -394,30 +416,36 @@ export class TagEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify all source tags exist
|
// Verify all source tags exist
|
||||||
const sourceTags: any[] = [];
|
const sourceTags: (typeof tags.$inferSelect)[] = [];
|
||||||
for (const id of sourceTagIds) {
|
for (const id of sourceTagIds) {
|
||||||
const result = await client.execute({
|
const rows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [id, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
if (result.rows.length > 0) {
|
eq(tags.id, id),
|
||||||
sourceTags.push(result.rows[0]);
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
if (rows.length > 0) {
|
||||||
|
sourceTags.push(rows[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify target tag exists
|
// Verify target tag exists
|
||||||
const targetResult = await client.execute({
|
const targetRows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [targetTagId, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, targetTagId),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (targetResult.rows.length === 0) {
|
if (targetRows.length === 0) {
|
||||||
throw new Error('Target tag not found');
|
throw new Error('Target tag not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetTag = targetResult.rows[0] as any;
|
const targetTag = targetRows[0];
|
||||||
const targetName = targetTag.name as string;
|
const targetName = targetTag.name;
|
||||||
const sourceNames = sourceTags.map((t: any) => t.name as string);
|
const sourceNames = sourceTags.map(t => t.name);
|
||||||
|
|
||||||
// Run as background task
|
// Run as background task
|
||||||
return taskManager.runTask({
|
return taskManager.runTask({
|
||||||
@@ -441,19 +469,22 @@ export class TagEngine extends EventEmitter {
|
|||||||
|
|
||||||
for (const row of postsResult.rows) {
|
for (const row of postsResult.rows) {
|
||||||
const postId = row.id as string;
|
const postId = row.id as string;
|
||||||
const tags: string[] = JSON.parse((row as any).tags || '[]');
|
const postTags: string[] = JSON.parse((row as any).tags || '[]');
|
||||||
|
|
||||||
if (tags.includes(sourceName)) {
|
if (postTags.includes(sourceName)) {
|
||||||
// Remove source tag and add target if not already present
|
// Remove source tag and add target if not already present
|
||||||
const newTags = tags.filter(t => t !== sourceName);
|
const newTags = postTags.filter(t => t !== sourceName);
|
||||||
if (!newTags.includes(targetName)) {
|
if (!newTags.includes(targetName)) {
|
||||||
newTags.push(targetName);
|
newTags.push(targetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'UPDATE posts SET tags = ?, updated_at = ? WHERE id = ?',
|
.update(posts)
|
||||||
args: [JSON.stringify(newTags), Date.now(), postId],
|
.set({
|
||||||
});
|
tags: JSON.stringify(newTags),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(posts.id, postId));
|
||||||
|
|
||||||
totalPostsUpdated++;
|
totalPostsUpdated++;
|
||||||
}
|
}
|
||||||
@@ -464,10 +495,12 @@ export class TagEngine extends EventEmitter {
|
|||||||
|
|
||||||
// Delete source tags
|
// Delete source tags
|
||||||
for (const id of sourceTagIds) {
|
for (const id of sourceTagIds) {
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'DELETE FROM tags WHERE id = ? AND project_id = ?',
|
.delete(tags)
|
||||||
args: [id, this.currentProjectId],
|
.where(and(
|
||||||
});
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(100, 'Complete');
|
onProgress(100, 'Complete');
|
||||||
@@ -491,7 +524,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Rename a tag (runs as background task to update posts)
|
* Rename a tag (runs as background task to update posts)
|
||||||
*/
|
*/
|
||||||
async renameTag(id: string, newName: string): Promise<RenameTagResult> {
|
async renameTag(id: string, newName: string): Promise<RenameTagResult> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
|
const client = this.getClient();
|
||||||
if (!client) throw new Error('Database not initialized');
|
if (!client) throw new Error('Database not initialized');
|
||||||
|
|
||||||
newName = newName.trim().toLowerCase();
|
newName = newName.trim().toLowerCase();
|
||||||
@@ -500,29 +534,36 @@ export class TagEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get existing tag
|
// Get existing tag
|
||||||
const tagResult = await client.execute({
|
const tagRows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [id, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (tagResult.rows.length === 0) {
|
if (tagRows.length === 0) {
|
||||||
throw new Error('Tag not found');
|
throw new Error('Tag not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = tagResult.rows[0] as any;
|
const tag = tagRows[0];
|
||||||
const oldName = tag.name as string;
|
const oldName = tag.name;
|
||||||
|
|
||||||
if (oldName === newName) {
|
if (oldName === newName) {
|
||||||
return { success: true, postsUpdated: 0, oldName, newName };
|
return { success: true, postsUpdated: 0, oldName, newName };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate
|
// Check for duplicate
|
||||||
const duplicateResult = await client.execute({
|
const duplicateRows = await db
|
||||||
sql: 'SELECT id FROM tags WHERE project_id = ? AND LOWER(name) = LOWER(?) AND id != ?',
|
.select({ id: tags.id })
|
||||||
args: [this.currentProjectId, newName, id],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.projectId, this.currentProjectId),
|
||||||
|
sql`LOWER(${tags.name}) = LOWER(${newName})`,
|
||||||
|
sql`${tags.id} != ${id}`
|
||||||
|
));
|
||||||
|
|
||||||
if (duplicateResult.rows.length > 0) {
|
if (duplicateRows.length > 0) {
|
||||||
throw new Error(`Tag "${newName}" already exists`);
|
throw new Error(`Tag "${newName}" already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,8 +581,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const postsToUpdate = postsResult.rows.filter((row: any) => {
|
const postsToUpdate = postsResult.rows.filter((row: any) => {
|
||||||
const tags: string[] = JSON.parse(row.tags || '[]');
|
const postTags: string[] = JSON.parse(row.tags || '[]');
|
||||||
return tags.includes(oldName);
|
return postTags.includes(oldName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = postsToUpdate.length;
|
const total = postsToUpdate.length;
|
||||||
@@ -549,13 +590,16 @@ export class TagEngine extends EventEmitter {
|
|||||||
|
|
||||||
for (const row of postsToUpdate) {
|
for (const row of postsToUpdate) {
|
||||||
const postId = row.id as string;
|
const postId = row.id as string;
|
||||||
const tags: string[] = JSON.parse((row as any).tags || '[]');
|
const postTags: string[] = JSON.parse((row as any).tags || '[]');
|
||||||
const newTags = tags.map(t => t === oldName ? newName : t);
|
const updatedTags = postTags.map(t => t === oldName ? newName : t);
|
||||||
|
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'UPDATE posts SET tags = ?, updated_at = ? WHERE id = ?',
|
.update(posts)
|
||||||
args: [JSON.stringify(newTags), Date.now(), postId],
|
.set({
|
||||||
});
|
tags: JSON.stringify(updatedTags),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(posts.id, postId));
|
||||||
|
|
||||||
updated++;
|
updated++;
|
||||||
onProgress((updated / total) * 80, `Updated ${updated}/${total} posts...`);
|
onProgress((updated / total) * 80, `Updated ${updated}/${total} posts...`);
|
||||||
@@ -564,10 +608,16 @@ export class TagEngine extends EventEmitter {
|
|||||||
onProgress(90, 'Updating tag record...');
|
onProgress(90, 'Updating tag record...');
|
||||||
|
|
||||||
// Update the tag name
|
// Update the tag name
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'UPDATE tags SET name = ?, updated_at = ? WHERE id = ? AND project_id = ?',
|
.update(tags)
|
||||||
args: [newName, Date.now(), id, this.currentProjectId],
|
.set({
|
||||||
});
|
name: newName,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
onProgress(100, 'Complete');
|
onProgress(100, 'Complete');
|
||||||
|
|
||||||
@@ -590,75 +640,84 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Get a tag by ID
|
* Get a tag by ID
|
||||||
*/
|
*/
|
||||||
async getTag(id: string): Promise<TagData | null> {
|
async getTag(id: string): Promise<TagData | null> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) return null;
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE id = ? AND project_id = ?',
|
.select()
|
||||||
args: [id, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, id),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.rowToTagData(result.rows[0] as any);
|
return this.rowToTagData(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a tag by name (case-insensitive)
|
* Get a tag by name (case-insensitive)
|
||||||
*/
|
*/
|
||||||
async getTagByName(name: string): Promise<TagData | null> {
|
async getTagByName(name: string): Promise<TagData | null> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) return null;
|
const normalizedName = name.trim().toLowerCase();
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE project_id = ? AND LOWER(name) = LOWER(?)',
|
.select()
|
||||||
args: [this.currentProjectId, name.trim().toLowerCase()],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.projectId, this.currentProjectId),
|
||||||
|
sql`LOWER(${tags.name}) = LOWER(${normalizedName})`
|
||||||
|
));
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.rowToTagData(result.rows[0] as any);
|
return this.rowToTagData(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all tags for the current project
|
* Get all tags for the current project
|
||||||
*/
|
*/
|
||||||
async getAllTags(): Promise<TagData[]> {
|
async getAllTags(): Promise<TagData[]> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) return [];
|
|
||||||
|
|
||||||
const result = await client.execute({
|
const rows = await db
|
||||||
sql: 'SELECT * FROM tags WHERE project_id = ? ORDER BY name ASC',
|
.select()
|
||||||
args: [this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(eq(tags.projectId, this.currentProjectId))
|
||||||
|
.orderBy(asc(tags.name));
|
||||||
|
|
||||||
return result.rows.map((row: any) => this.rowToTagData(row));
|
return rows.map(row => this.rowToTagData(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get post IDs that have a specific tag
|
* Get post IDs that have a specific tag
|
||||||
*/
|
*/
|
||||||
async getPostsWithTag(tagId: string): Promise<string[]> {
|
async getPostsWithTag(tagId: string): Promise<string[]> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
|
const client = this.getClient();
|
||||||
if (!client) return [];
|
if (!client) return [];
|
||||||
|
|
||||||
// First get the tag name
|
// First get the tag name
|
||||||
const tagResult = await client.execute({
|
const tagRows = await db
|
||||||
sql: 'SELECT name FROM tags WHERE id = ? AND project_id = ?',
|
.select({ name: tags.name })
|
||||||
args: [tagId, this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.id, tagId),
|
||||||
|
eq(tags.projectId, this.currentProjectId)
|
||||||
|
));
|
||||||
|
|
||||||
if (tagResult.rows.length === 0) {
|
if (tagRows.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagName = (tagResult.rows[0] as any).name as string;
|
const tagName = tagRows[0].name;
|
||||||
|
|
||||||
// Find posts with this tag
|
// Find posts with this tag - requires raw SQL for JSON
|
||||||
const postsResult = await client.execute({
|
const postsResult = await client.execute({
|
||||||
sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
|
sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
|
||||||
args: [this.currentProjectId, `%"${tagName}"%`],
|
args: [this.currentProjectId, `%"${tagName}"%`],
|
||||||
@@ -666,8 +725,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
|
|
||||||
return postsResult.rows
|
return postsResult.rows
|
||||||
.filter((row: any) => {
|
.filter((row: any) => {
|
||||||
const tags: string[] = JSON.parse(row.tags || '[]');
|
const postTags: string[] = JSON.parse(row.tags || '[]');
|
||||||
return tags.includes(tagName);
|
return postTags.includes(tagName);
|
||||||
})
|
})
|
||||||
.map((row: any) => row.id as string);
|
.map((row: any) => row.id as string);
|
||||||
}
|
}
|
||||||
@@ -676,19 +735,18 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Sync tags from existing posts - discover tags that exist in posts but not in tags table
|
* Sync tags from existing posts - discover tags that exist in posts but not in tags table
|
||||||
*/
|
*/
|
||||||
async syncTagsFromPosts(): Promise<SyncTagsResult> {
|
async syncTagsFromPosts(): Promise<SyncTagsResult> {
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) throw new Error('Database not initialized');
|
|
||||||
|
|
||||||
// Get all tags from posts
|
// Get all tags from posts
|
||||||
const postsResult = await client.execute({
|
const postRows = await db
|
||||||
sql: 'SELECT tags FROM posts WHERE project_id = ?',
|
.select({ tags: posts.tags })
|
||||||
args: [this.currentProjectId],
|
.from(posts)
|
||||||
});
|
.where(eq(posts.projectId, this.currentProjectId));
|
||||||
|
|
||||||
const discoveredTags = new Set<string>();
|
const discoveredTags = new Set<string>();
|
||||||
for (const row of postsResult.rows) {
|
for (const row of postRows) {
|
||||||
const tags: string[] = JSON.parse((row as any).tags || '[]');
|
const postTags: string[] = JSON.parse(row.tags || '[]');
|
||||||
for (const tag of tags) {
|
for (const tag of postTags) {
|
||||||
if (tag.trim()) {
|
if (tag.trim()) {
|
||||||
discoveredTags.add(tag.trim().toLowerCase());
|
discoveredTags.add(tag.trim().toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -696,23 +754,27 @@ export class TagEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get existing tags
|
// Get existing tags
|
||||||
const existingResult = await client.execute({
|
const existingRows = await db
|
||||||
sql: 'SELECT name FROM tags WHERE project_id = ?',
|
.select({ name: tags.name })
|
||||||
args: [this.currentProjectId],
|
.from(tags)
|
||||||
});
|
.where(eq(tags.projectId, this.currentProjectId));
|
||||||
|
|
||||||
const existingNames = new Set(existingResult.rows.map((row: any) => (row.name as string).toLowerCase()));
|
const existingNames = new Set(existingRows.map(row => row.name.toLowerCase()));
|
||||||
|
|
||||||
// Find missing tags
|
// Find missing tags
|
||||||
const missingTags = Array.from(discoveredTags).filter(t => !existingNames.has(t));
|
const missingTags = Array.from(discoveredTags).filter(t => !existingNames.has(t));
|
||||||
const added: string[] = [];
|
const added: string[] = [];
|
||||||
|
|
||||||
// Add missing tags
|
// Add missing tags
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
for (const tagName of missingTags) {
|
for (const tagName of missingTags) {
|
||||||
await client.execute({
|
await db.insert(tags).values({
|
||||||
sql: 'INSERT INTO tags (id, project_id, name, color, created_at, updated_at) VALUES (?, ?, ?, NULL, ?, ?)',
|
id: uuidv4(),
|
||||||
args: [uuidv4(), this.currentProjectId, tagName, now, now],
|
projectId: this.currentProjectId,
|
||||||
|
name: tagName,
|
||||||
|
color: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
added.push(tagName);
|
added.push(tagName);
|
||||||
}
|
}
|
||||||
@@ -731,14 +793,14 @@ export class TagEngine extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Convert database row to TagData
|
* Convert database row to TagData
|
||||||
*/
|
*/
|
||||||
private rowToTagData(row: any): TagData {
|
private rowToTagData(row: typeof tags.$inferSelect): TagData {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
projectId: row.project_id,
|
projectId: row.projectId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
color: row.color || undefined,
|
color: row.color || undefined,
|
||||||
createdAt: new Date(row.created_at),
|
createdAt: row.createdAt,
|
||||||
updatedAt: new Date(row.updated_at),
|
updatedAt: row.updatedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,12 +810,12 @@ export class TagEngine extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private async saveTagsToFile(): Promise<void> {
|
private async saveTagsToFile(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const tags = await this.getAllTags();
|
const allTags = await this.getAllTags();
|
||||||
const filePath = this.getTagsFilePath();
|
const filePath = this.getTagsFilePath();
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
|
|
||||||
// Serialize to portable format - only name and optional color
|
// Serialize to portable format - only name and optional color
|
||||||
const serialized: SerializedTag[] = tags.map(tag => {
|
const serialized: SerializedTag[] = allTags.map(tag => {
|
||||||
const entry: SerializedTag = { name: tag.name };
|
const entry: SerializedTag = { name: tag.name };
|
||||||
if (tag.color) {
|
if (tag.color) {
|
||||||
entry.color = tag.color;
|
entry.color = tag.color;
|
||||||
@@ -778,10 +840,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
const content = await fs.readFile(filePath, 'utf-8');
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
const rawTags: any[] = JSON.parse(content);
|
const rawTags: any[] = JSON.parse(content);
|
||||||
|
|
||||||
const client = getDatabase().getLocalClient();
|
const db = this.getDb();
|
||||||
if (!client) return;
|
const now = new Date();
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
for (const tag of rawTags) {
|
for (const tag of rawTags) {
|
||||||
// Support both portable format { name, color? } and legacy format with id
|
// Support both portable format { name, color? } and legacy format with id
|
||||||
@@ -791,23 +851,36 @@ export class TagEngine extends EventEmitter {
|
|||||||
const color = tag.color || null;
|
const color = tag.color || null;
|
||||||
|
|
||||||
// Check if tag with this name already exists
|
// Check if tag with this name already exists
|
||||||
const existing = await client.execute({
|
const existing = await db
|
||||||
sql: 'SELECT id FROM tags WHERE project_id = ? AND LOWER(name) = LOWER(?)',
|
.select({ id: tags.id })
|
||||||
args: [this.currentProjectId, name],
|
.from(tags)
|
||||||
});
|
.where(and(
|
||||||
|
eq(tags.projectId, this.currentProjectId),
|
||||||
|
sql`LOWER(${tags.name}) = LOWER(${name})`
|
||||||
|
));
|
||||||
|
|
||||||
if (existing.rows.length === 0) {
|
if (existing.length === 0) {
|
||||||
// Create new tag with fresh ID
|
// Create new tag with fresh ID
|
||||||
await client.execute({
|
await db.insert(tags).values({
|
||||||
sql: 'INSERT INTO tags (id, project_id, name, color, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
id: uuidv4(),
|
||||||
args: [uuidv4(), this.currentProjectId, name, color, now, now],
|
projectId: this.currentProjectId,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
} else if (color) {
|
} else if (color) {
|
||||||
// Update color if provided and tag exists
|
// Update color if provided and tag exists
|
||||||
await client.execute({
|
await db
|
||||||
sql: 'UPDATE tags SET color = ?, updated_at = ? WHERE project_id = ? AND LOWER(name) = LOWER(?)',
|
.update(tags)
|
||||||
args: [color, now, this.currentProjectId, name],
|
.set({
|
||||||
});
|
color,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(tags.projectId, this.currentProjectId),
|
||||||
|
sql`LOWER(${tags.name}) = LOWER(${name})`
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -10,91 +10,58 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|||||||
// Store for mock data
|
// Store for mock data
|
||||||
const mockDefinitions = new Map<string, any>();
|
const mockDefinitions = new Map<string, any>();
|
||||||
|
|
||||||
const mockLocalClient = {
|
// Create chainable mock for Drizzle ORM that is thenable (can be awaited)
|
||||||
execute: vi.fn(async (query: { sql: string; args: any[] }) => {
|
function createSelectChain(getData: () => any[]) {
|
||||||
const sql = query.sql.trim();
|
const chain: any = {
|
||||||
|
from: vi.fn().mockImplementation(() => chain),
|
||||||
|
where: vi.fn().mockImplementation(() => chain),
|
||||||
|
orderBy: vi.fn().mockImplementation(() => chain),
|
||||||
|
limit: vi.fn().mockImplementation(() => chain),
|
||||||
|
// Make the chain thenable so it can be awaited directly
|
||||||
|
then: (resolve: (value: any[]) => void, reject?: (reason: any) => void) => {
|
||||||
|
return Promise.resolve(getData()).then(resolve, reject);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
// INSERT
|
// Track what data Drizzle queries should return
|
||||||
if (sql.startsWith('INSERT')) {
|
let mockDrizzleSelectResults: any[][] = [];
|
||||||
const row = {
|
|
||||||
id: query.args[0],
|
|
||||||
project_id: query.args[1],
|
|
||||||
name: query.args[2],
|
|
||||||
wxr_file_path: query.args[3] ?? null,
|
|
||||||
uploads_folder_path: query.args[4] ?? null,
|
|
||||||
last_analysis_result: query.args[5] ?? null,
|
|
||||||
created_at: query.args[6],
|
|
||||||
updated_at: query.args[7],
|
|
||||||
};
|
|
||||||
mockDefinitions.set(row.id, row);
|
|
||||||
return { rows: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELECT by id
|
const mockLocalDb = {
|
||||||
if (sql.startsWith('SELECT') && sql.includes('WHERE id = ?') && sql.includes('project_id = ?')) {
|
select: vi.fn(() => createSelectChain(() => mockDrizzleSelectResults.shift() || [])),
|
||||||
const id = query.args[0];
|
insert: vi.fn(() => ({
|
||||||
const projectId = query.args[1];
|
values: vi.fn((data: any) => {
|
||||||
const def = mockDefinitions.get(id);
|
if (data && data.id) {
|
||||||
if (def && def.project_id === projectId) {
|
mockDefinitions.set(data.id, {
|
||||||
return { rows: [def] };
|
id: data.id,
|
||||||
|
projectId: data.projectId,
|
||||||
|
name: data.name,
|
||||||
|
wxrFilePath: data.wxrFilePath,
|
||||||
|
uploadsFolderPath: data.uploadsFolderPath,
|
||||||
|
lastAnalysisResult: data.lastAnalysisResult,
|
||||||
|
createdAt: data.createdAt,
|
||||||
|
updatedAt: data.updatedAt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { rows: [] };
|
return Promise.resolve();
|
||||||
}
|
}),
|
||||||
|
})),
|
||||||
// SELECT all for project
|
update: vi.fn(() => ({
|
||||||
if (sql.startsWith('SELECT') && sql.includes('WHERE project_id = ?') && sql.includes('ORDER BY')) {
|
set: vi.fn(() => ({
|
||||||
const projectId = query.args[0];
|
where: vi.fn(() => Promise.resolve()),
|
||||||
const rows = Array.from(mockDefinitions.values())
|
})),
|
||||||
.filter(d => d.project_id === projectId)
|
})),
|
||||||
.sort((a, b) => b.updated_at - a.updated_at);
|
delete: vi.fn(() => ({
|
||||||
return { rows };
|
where: vi.fn(() => Promise.resolve()),
|
||||||
}
|
})),
|
||||||
|
|
||||||
// UPDATE
|
|
||||||
if (sql.startsWith('UPDATE')) {
|
|
||||||
// Find the id in args (last two args are id and project_id in WHERE)
|
|
||||||
const id = query.args[query.args.length - 2];
|
|
||||||
const projectId = query.args[query.args.length - 1];
|
|
||||||
const def = mockDefinitions.get(id);
|
|
||||||
if (def && def.project_id === projectId) {
|
|
||||||
// Apply updates based on the SET clause
|
|
||||||
// Parse set fields from the sql
|
|
||||||
const setMatch = sql.match(/SET (.+?) WHERE/);
|
|
||||||
if (setMatch) {
|
|
||||||
const setParts = setMatch[1].split(', ');
|
|
||||||
let argIdx = 0;
|
|
||||||
for (const part of setParts) {
|
|
||||||
const field = part.split(' = ')[0].trim();
|
|
||||||
def[field] = query.args[argIdx];
|
|
||||||
argIdx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { rowsAffected: 1, rows: [] };
|
|
||||||
}
|
|
||||||
return { rowsAffected: 0, rows: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE
|
|
||||||
if (sql.startsWith('DELETE')) {
|
|
||||||
const id = query.args[0];
|
|
||||||
const projectId = query.args[1];
|
|
||||||
const def = mockDefinitions.get(id);
|
|
||||||
if (def && def.project_id === projectId) {
|
|
||||||
mockDefinitions.delete(id);
|
|
||||||
return { rowsAffected: 1, rows: [] };
|
|
||||||
}
|
|
||||||
return { rowsAffected: 0, rows: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { rows: [] };
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the database module
|
// Mock the database module
|
||||||
vi.mock('../../src/main/database', () => ({
|
vi.mock('../../src/main/database', () => ({
|
||||||
getDatabase: vi.fn(() => ({
|
getDatabase: vi.fn(() => ({
|
||||||
getLocal: vi.fn(() => null),
|
getLocal: vi.fn(() => mockLocalDb),
|
||||||
getLocalClient: vi.fn(() => mockLocalClient),
|
getLocalClient: vi.fn(() => null),
|
||||||
getRemote: vi.fn(() => null),
|
getRemote: vi.fn(() => null),
|
||||||
getDataPaths: vi.fn(() => ({
|
getDataPaths: vi.fn(() => ({
|
||||||
database: '/mock/userData/bds.db',
|
database: '/mock/userData/bds.db',
|
||||||
@@ -122,6 +89,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockDefinitions.clear();
|
mockDefinitions.clear();
|
||||||
|
mockDrizzleSelectResults = [];
|
||||||
engine = new ImportDefinitionEngine();
|
engine = new ImportDefinitionEngine();
|
||||||
engine.setProjectContext('test-project');
|
engine.setProjectContext('test-project');
|
||||||
});
|
});
|
||||||
@@ -168,17 +136,24 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should insert into the database', async () => {
|
it('should insert into the database', async () => {
|
||||||
await engine.createDefinition('Test Import');
|
await engine.createDefinition('Test Import');
|
||||||
|
|
||||||
expect(mockLocalClient.execute).toHaveBeenCalledTimes(1);
|
expect(mockLocalDb.insert).toHaveBeenCalledTimes(1);
|
||||||
const call = mockLocalClient.execute.mock.calls[0][0];
|
|
||||||
expect(call.sql).toContain('INSERT INTO import_definitions');
|
|
||||||
expect(call.args[2]).toBe('Test Import');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getDefinition', () => {
|
describe('getDefinition', () => {
|
||||||
it('should return a definition by ID', async () => {
|
it('should return a definition by ID', async () => {
|
||||||
const created = await engine.createDefinition('My Import');
|
const created = await engine.createDefinition('My Import');
|
||||||
mockLocalClient.execute.mockClear();
|
// Set up mock to return the created definition
|
||||||
|
mockDrizzleSelectResults = [[{
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'My Import',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}]];
|
||||||
|
|
||||||
const def = await engine.getDefinition(created.id);
|
const def = await engine.getDefinition(created.id);
|
||||||
|
|
||||||
@@ -188,6 +163,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent ID', async () => {
|
it('should return null for non-existent ID', async () => {
|
||||||
|
mockDrizzleSelectResults = [[]];
|
||||||
const def = await engine.getDefinition('non-existent-id');
|
const def = await engine.getDefinition('non-existent-id');
|
||||||
|
|
||||||
expect(def).toBeNull();
|
expect(def).toBeNull();
|
||||||
@@ -196,6 +172,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should not return definitions from other projects', async () => {
|
it('should not return definitions from other projects', async () => {
|
||||||
const created = await engine.createDefinition('My Import');
|
const created = await engine.createDefinition('My Import');
|
||||||
engine.setProjectContext('other-project');
|
engine.setProjectContext('other-project');
|
||||||
|
mockDrizzleSelectResults = [[]]; // would be filtered by project
|
||||||
|
|
||||||
const def = await engine.getDefinition(created.id);
|
const def = await engine.getDefinition(created.id);
|
||||||
|
|
||||||
@@ -204,9 +181,17 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
|
|
||||||
it('should parse lastAnalysisResult JSON', async () => {
|
it('should parse lastAnalysisResult JSON', async () => {
|
||||||
const created = await engine.createDefinition('My Import');
|
const created = await engine.createDefinition('My Import');
|
||||||
// Manually set analysis result in mock store
|
// Set up mock to return the definition with analysis result
|
||||||
const storedDef = mockDefinitions.get(created.id);
|
mockDrizzleSelectResults = [[{
|
||||||
storedDef.last_analysis_result = JSON.stringify({ posts: { total: 5 } });
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'My Import',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: JSON.stringify({ posts: { total: 5 } }),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}]];
|
||||||
|
|
||||||
const def = await engine.getDefinition(created.id);
|
const def = await engine.getDefinition(created.id);
|
||||||
|
|
||||||
@@ -216,6 +201,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
|
|
||||||
describe('getAllForProject', () => {
|
describe('getAllForProject', () => {
|
||||||
it('should return empty array when no definitions exist', async () => {
|
it('should return empty array when no definitions exist', async () => {
|
||||||
|
mockDrizzleSelectResults = [[]];
|
||||||
const defs = await engine.getAllForProject();
|
const defs = await engine.getAllForProject();
|
||||||
|
|
||||||
expect(defs).toEqual([]);
|
expect(defs).toEqual([]);
|
||||||
@@ -224,6 +210,11 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should return all definitions for the current project', async () => {
|
it('should return all definitions for the current project', async () => {
|
||||||
await engine.createDefinition('Import 1');
|
await engine.createDefinition('Import 1');
|
||||||
await engine.createDefinition('Import 2');
|
await engine.createDefinition('Import 2');
|
||||||
|
// Mock returning both definitions
|
||||||
|
mockDrizzleSelectResults = [[
|
||||||
|
{ id: 'id1', projectId: 'test-project', name: 'Import 1', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() },
|
||||||
|
{ id: 'id2', projectId: 'test-project', name: 'Import 2', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() },
|
||||||
|
]];
|
||||||
|
|
||||||
const defs = await engine.getAllForProject();
|
const defs = await engine.getAllForProject();
|
||||||
|
|
||||||
@@ -235,6 +226,10 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
engine.setProjectContext('other-project');
|
engine.setProjectContext('other-project');
|
||||||
await engine.createDefinition('Import B');
|
await engine.createDefinition('Import B');
|
||||||
engine.setProjectContext('test-project');
|
engine.setProjectContext('test-project');
|
||||||
|
// Mock returning only the test-project definition
|
||||||
|
mockDrizzleSelectResults = [[
|
||||||
|
{ id: 'id1', projectId: 'test-project', name: 'Import A', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() },
|
||||||
|
]];
|
||||||
|
|
||||||
const defs = await engine.getAllForProject();
|
const defs = await engine.getAllForProject();
|
||||||
|
|
||||||
@@ -243,10 +238,12 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return definitions ordered by updatedAt DESC', async () => {
|
it('should return definitions ordered by updatedAt DESC', async () => {
|
||||||
await engine.createDefinition('Older');
|
const olderDate = new Date('2024-01-01');
|
||||||
// Small delay to ensure different timestamps
|
const newerDate = new Date('2024-02-01');
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
mockDrizzleSelectResults = [[
|
||||||
await engine.createDefinition('Newer');
|
{ id: 'id2', projectId: 'test-project', name: 'Newer', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: newerDate, updatedAt: newerDate },
|
||||||
|
{ id: 'id1', projectId: 'test-project', name: 'Older', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: olderDate, updatedAt: olderDate },
|
||||||
|
]];
|
||||||
|
|
||||||
const defs = await engine.getAllForProject();
|
const defs = await engine.getAllForProject();
|
||||||
|
|
||||||
@@ -258,6 +255,29 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
describe('updateDefinition', () => {
|
describe('updateDefinition', () => {
|
||||||
it('should update the name', async () => {
|
it('should update the name', async () => {
|
||||||
const created = await engine.createDefinition('Old Name');
|
const created = await engine.createDefinition('Old Name');
|
||||||
|
// First call for getDefinition check, second for returning updated data
|
||||||
|
mockDrizzleSelectResults = [
|
||||||
|
[{ // getDefinition call inside updateDefinition
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Old Name',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
[{ // return after update
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'New Name',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
|
||||||
const updated = await engine.updateDefinition(created.id, { name: 'New Name' });
|
const updated = await engine.updateDefinition(created.id, { name: 'New Name' });
|
||||||
|
|
||||||
@@ -267,6 +287,29 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
|
|
||||||
it('should update wxrFilePath', async () => {
|
it('should update wxrFilePath', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
|
// First call for check, second for returning updated data
|
||||||
|
mockDrizzleSelectResults = [
|
||||||
|
[{ // getDefinition check
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
[{ // return after update
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: '/path/to/export.xml',
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
|
||||||
const updated = await engine.updateDefinition(created.id, { wxrFilePath: '/path/to/export.xml' });
|
const updated = await engine.updateDefinition(created.id, { wxrFilePath: '/path/to/export.xml' });
|
||||||
|
|
||||||
@@ -275,6 +318,28 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
|
|
||||||
it('should update uploadsFolderPath', async () => {
|
it('should update uploadsFolderPath', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
|
mockDrizzleSelectResults = [
|
||||||
|
[{ // getDefinition check
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
[{ // return after update
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: '/path/to/uploads',
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
|
||||||
const updated = await engine.updateDefinition(created.id, { uploadsFolderPath: '/path/to/uploads' });
|
const updated = await engine.updateDefinition(created.id, { uploadsFolderPath: '/path/to/uploads' });
|
||||||
|
|
||||||
@@ -284,6 +349,28 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should update lastAnalysisResult as JSON', async () => {
|
it('should update lastAnalysisResult as JSON', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
const report = { posts: { total: 10, new: 5 } };
|
const report = { posts: { total: 10, new: 5 } };
|
||||||
|
mockDrizzleSelectResults = [
|
||||||
|
[{ // getDefinition check
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
[{ // return after update
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: JSON.stringify(report),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
|
||||||
const updated = await engine.updateDefinition(created.id, { lastAnalysisResult: JSON.stringify(report) });
|
const updated = await engine.updateDefinition(created.id, { lastAnalysisResult: JSON.stringify(report) });
|
||||||
|
|
||||||
@@ -291,6 +378,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent definition', async () => {
|
it('should return null for non-existent definition', async () => {
|
||||||
|
mockDrizzleSelectResults = [[]];
|
||||||
const updated = await engine.updateDefinition('non-existent', { name: 'Test' });
|
const updated = await engine.updateDefinition('non-existent', { name: 'Test' });
|
||||||
|
|
||||||
expect(updated).toBeNull();
|
expect(updated).toBeNull();
|
||||||
@@ -299,6 +387,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should not update definitions from other projects', async () => {
|
it('should not update definitions from other projects', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
engine.setProjectContext('other-project');
|
engine.setProjectContext('other-project');
|
||||||
|
mockDrizzleSelectResults = [[]]; // Would be filtered by project
|
||||||
|
|
||||||
const updated = await engine.updateDefinition(created.id, { name: 'Hacked' });
|
const updated = await engine.updateDefinition(created.id, { name: 'Hacked' });
|
||||||
|
|
||||||
@@ -309,6 +398,16 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
describe('deleteDefinition', () => {
|
describe('deleteDefinition', () => {
|
||||||
it('should delete an existing definition', async () => {
|
it('should delete an existing definition', async () => {
|
||||||
const created = await engine.createDefinition('To Delete');
|
const created = await engine.createDefinition('To Delete');
|
||||||
|
mockDrizzleSelectResults = [[{
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'To Delete',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}]];
|
||||||
|
|
||||||
const result = await engine.deleteDefinition(created.id);
|
const result = await engine.deleteDefinition(created.id);
|
||||||
|
|
||||||
@@ -316,6 +415,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for non-existent definition', async () => {
|
it('should return false for non-existent definition', async () => {
|
||||||
|
mockDrizzleSelectResults = [[]];
|
||||||
const result = await engine.deleteDefinition('non-existent');
|
const result = await engine.deleteDefinition('non-existent');
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -324,6 +424,7 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
it('should not delete definitions from other projects', async () => {
|
it('should not delete definitions from other projects', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
engine.setProjectContext('other-project');
|
engine.setProjectContext('other-project');
|
||||||
|
mockDrizzleSelectResults = [[]]; // Would be filtered by project
|
||||||
|
|
||||||
const result = await engine.deleteDefinition(created.id);
|
const result = await engine.deleteDefinition(created.id);
|
||||||
|
|
||||||
@@ -332,8 +433,21 @@ describe('ImportDefinitionEngine', () => {
|
|||||||
|
|
||||||
it('should remove the definition from the database', async () => {
|
it('should remove the definition from the database', async () => {
|
||||||
const created = await engine.createDefinition('Test');
|
const created = await engine.createDefinition('Test');
|
||||||
|
// First call returns the definition for delete
|
||||||
|
mockDrizzleSelectResults = [[{
|
||||||
|
id: created.id,
|
||||||
|
projectId: 'test-project',
|
||||||
|
name: 'Test',
|
||||||
|
wxrFilePath: null,
|
||||||
|
uploadsFolderPath: null,
|
||||||
|
lastAnalysisResult: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}]];
|
||||||
await engine.deleteDefinition(created.id);
|
await engine.deleteDefinition(created.id);
|
||||||
|
|
||||||
|
// Second call returns empty for get
|
||||||
|
mockDrizzleSelectResults = [[]];
|
||||||
const def = await engine.getDefinition(created.id);
|
const def = await engine.getDefinition(created.id);
|
||||||
expect(def).toBeNull();
|
expect(def).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,19 +14,34 @@ const mockTags = new Map<string, any>();
|
|||||||
const mockPosts = new Map<string, any>();
|
const mockPosts = new Map<string, any>();
|
||||||
let mockExecuteArgs: any[] = [];
|
let mockExecuteArgs: any[] = [];
|
||||||
|
|
||||||
// Create chainable mock for Drizzle ORM
|
// Configure what data the Drizzle select chain returns - supports queue for multiple calls
|
||||||
|
let mockSelectDataQueue: any[][] = [];
|
||||||
|
let mockSelectDataDefault: any[] = [];
|
||||||
|
|
||||||
|
function getNextMockSelectData(): any[] {
|
||||||
|
if (mockSelectDataQueue.length > 0) {
|
||||||
|
return mockSelectDataQueue.shift()!;
|
||||||
|
}
|
||||||
|
if (mockSelectDataDefault.length > 0) {
|
||||||
|
return mockSelectDataDefault;
|
||||||
|
}
|
||||||
|
return Array.from(mockTags.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chainable mock for Drizzle ORM that is thenable (can be awaited)
|
||||||
function createSelectChain() {
|
function createSelectChain() {
|
||||||
return {
|
const chain: any = {
|
||||||
from: vi.fn().mockReturnThis(),
|
from: vi.fn().mockImplementation(() => chain),
|
||||||
where: vi.fn().mockImplementation(function(this: any) {
|
where: vi.fn().mockImplementation(() => chain),
|
||||||
return this;
|
orderBy: vi.fn().mockImplementation(() => chain),
|
||||||
}),
|
limit: vi.fn().mockImplementation(() => chain),
|
||||||
orderBy: vi.fn().mockReturnThis(),
|
offset: vi.fn().mockImplementation(() => chain),
|
||||||
limit: vi.fn().mockReturnThis(),
|
// Make the chain thenable so it can be awaited directly
|
||||||
offset: vi.fn().mockReturnThis(),
|
then: (resolve: (value: any[]) => void, reject?: (reason: any) => void) => {
|
||||||
all: vi.fn().mockImplementation(() => Promise.resolve(Array.from(mockTags.values()))),
|
return Promise.resolve(getNextMockSelectData()).then(resolve, reject);
|
||||||
get: vi.fn().mockImplementation(() => Promise.resolve(undefined)),
|
},
|
||||||
};
|
};
|
||||||
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDrizzleMock() {
|
function createDrizzleMock() {
|
||||||
@@ -115,6 +130,8 @@ describe('TagEngine', () => {
|
|||||||
mockTags.clear();
|
mockTags.clear();
|
||||||
mockPosts.clear();
|
mockPosts.clear();
|
||||||
mockExecuteArgs = [];
|
mockExecuteArgs = [];
|
||||||
|
mockSelectDataQueue = [];
|
||||||
|
mockSelectDataDefault = [];
|
||||||
resetMockCounters();
|
resetMockCounters();
|
||||||
tagEngine = new TagEngine();
|
tagEngine = new TagEngine();
|
||||||
});
|
});
|
||||||
@@ -198,9 +215,8 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error for duplicate tag name', async () => {
|
it('should throw error for duplicate tag name', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
// Drizzle ORM: check for existing tag with same name
|
||||||
rows: [{ id: 'existing', name: 'react' }],
|
mockSelectDataQueue = [[{ id: 'existing', name: 'react' }]];
|
||||||
});
|
|
||||||
|
|
||||||
await expect(tagEngine.createTag({ name: 'react' })).rejects.toThrow('Tag "react" already exists');
|
await expect(tagEngine.createTag({ name: 'react' })).rejects.toThrow('Tag "react" already exists');
|
||||||
});
|
});
|
||||||
@@ -208,9 +224,7 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('updateTag', () => {
|
describe('updateTag', () => {
|
||||||
it('should update tag color', async () => {
|
it('should update tag color', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataDefault = [{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }];
|
||||||
rows: [{ id: 'tag-1', name: 'react', color: null }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tagEngine.updateTag('tag-1', { color: '#61dafb' });
|
const result = await tagEngine.updateTag('tag-1', { color: '#61dafb' });
|
||||||
|
|
||||||
@@ -219,9 +233,7 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should emit tagUpdated event', async () => {
|
it('should emit tagUpdated event', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataDefault = [{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }];
|
||||||
rows: [{ id: 'tag-1', name: 'react', color: null }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
tagEngine.on('tagUpdated', handler);
|
tagEngine.on('tagUpdated', handler);
|
||||||
@@ -232,7 +244,7 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent tag', async () => {
|
it('should return null for non-existent tag', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] });
|
mockSelectDataDefault = [];
|
||||||
|
|
||||||
const result = await tagEngine.updateTag('non-existent', { color: '#fff' });
|
const result = await tagEngine.updateTag('non-existent', { color: '#fff' });
|
||||||
|
|
||||||
@@ -242,15 +254,17 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('deleteTag', () => {
|
describe('deleteTag', () => {
|
||||||
it('should delete tag and remove from posts as a background task', async () => {
|
it('should delete tag and remove from posts as a background task', async () => {
|
||||||
mockLocalClient.execute
|
// Drizzle ORM: get tag first
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'react', color: null }] }) // Find tag
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [
|
[{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
|
];
|
||||||
|
// Raw SQL: find posts with tag
|
||||||
|
mockLocalClient.execute.mockImplementationOnce(async () => ({
|
||||||
|
rows: [
|
||||||
{ id: 'post-1', tags: '["react", "typescript"]' },
|
{ id: 'post-1', tags: '["react", "typescript"]' },
|
||||||
{ id: 'post-2', tags: '["react"]' },
|
{ id: 'post-2', tags: '["react"]' },
|
||||||
] }) // Posts with tag
|
],
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Update post-1
|
}));
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Update post-2
|
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Delete tag
|
|
||||||
|
|
||||||
const result = await tagEngine.deleteTag('tag-1');
|
const result = await tagEngine.deleteTag('tag-1');
|
||||||
|
|
||||||
@@ -259,10 +273,10 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should emit tagDeleted event', async () => {
|
it('should emit tagDeleted event', async () => {
|
||||||
mockLocalClient.execute
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'react', color: null }] })
|
[{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [] })
|
];
|
||||||
.mockResolvedValueOnce({ rows: [] });
|
mockLocalClient.execute.mockImplementationOnce(async () => ({ rows: [] }));
|
||||||
|
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
tagEngine.on('tagDeleted', handler);
|
tagEngine.on('tagDeleted', handler);
|
||||||
@@ -273,7 +287,7 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error for non-existent tag', async () => {
|
it('should throw error for non-existent tag', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] });
|
mockSelectDataDefault = [];
|
||||||
|
|
||||||
await expect(tagEngine.deleteTag('non-existent')).rejects.toThrow('Tag not found');
|
await expect(tagEngine.deleteTag('non-existent')).rejects.toThrow('Tag not found');
|
||||||
});
|
});
|
||||||
@@ -281,14 +295,16 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('mergeTags', () => {
|
describe('mergeTags', () => {
|
||||||
it('should merge multiple tags into one', async () => {
|
it('should merge multiple tags into one', async () => {
|
||||||
|
// Drizzle ORM selects: source tag 1, source tag 2, target tag
|
||||||
|
mockSelectDataQueue = [
|
||||||
|
[{ id: 'tag-1', name: 'js', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
|
[{ id: 'tag-2', name: 'javascript', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
|
[{ id: 'tag-3', name: 'ecmascript', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
|
];
|
||||||
|
// Raw SQL for finding posts with tags
|
||||||
mockLocalClient.execute
|
mockLocalClient.execute
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'js' }] }) // Source tag 1
|
.mockResolvedValueOnce({ rows: [{ id: 'post-1', tags: '["js"]' }] }) // Posts with source tag 1
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-2', name: 'javascript' }] }) // Source tag 2
|
.mockResolvedValueOnce({ rows: [{ id: 'post-2', tags: '["javascript"]' }] }); // Posts with source tag 2
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-3', name: 'ecmascript' }] }) // Target tag
|
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'post-1' }, { id: 'post-2' }] }) // Posts with source tags
|
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Update posts
|
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Delete source tag 1
|
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Delete source tag 2
|
|
||||||
|
|
||||||
const result = await tagEngine.mergeTags(['tag-1', 'tag-2'], 'tag-3');
|
const result = await tagEngine.mergeTags(['tag-1', 'tag-2'], 'tag-3');
|
||||||
|
|
||||||
@@ -298,11 +314,11 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should emit tagsMerged event', async () => {
|
it('should emit tagsMerged event', async () => {
|
||||||
mockLocalClient.execute
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'js' }] })
|
[{ id: 'tag-1', name: 'js', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-2', name: 'javascript' }] })
|
[{ id: 'tag-2', name: 'javascript', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [] })
|
];
|
||||||
.mockResolvedValueOnce({ rows: [] });
|
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] }); // No posts with source tag
|
||||||
|
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
tagEngine.on('tagsMerged', handler);
|
tagEngine.on('tagsMerged', handler);
|
||||||
@@ -317,9 +333,10 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when target tag does not exist', async () => {
|
it('should throw error when target tag does not exist', async () => {
|
||||||
mockLocalClient.execute
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'js' }] })
|
[{ id: 'tag-1', name: 'js', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Target not found
|
[], // Target not found
|
||||||
|
];
|
||||||
|
|
||||||
await expect(tagEngine.mergeTags(['tag-1'], 'non-existent')).rejects.toThrow('Target tag not found');
|
await expect(tagEngine.mergeTags(['tag-1'], 'non-existent')).rejects.toThrow('Target tag not found');
|
||||||
});
|
});
|
||||||
@@ -327,12 +344,13 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('renameTags (batch rename)', () => {
|
describe('renameTags (batch rename)', () => {
|
||||||
it('should rename multiple tags and update posts', async () => {
|
it('should rename multiple tags and update posts', async () => {
|
||||||
mockLocalClient.execute
|
// First call: get existing tag, Second call: check for duplicate
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'old-name' }] })
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Check no duplicate
|
[{ id: 'tag-1', name: 'old-name', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'post-1' }] }) // Posts with tag
|
[], // no duplicate
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Update posts
|
];
|
||||||
.mockResolvedValueOnce({ rows: [] }); // Update tag name
|
// Raw SQL for finding posts with the tag
|
||||||
|
mockLocalClient.execute.mockResolvedValueOnce({ rows: [{ id: 'post-1', tags: '["old-name"]' }] });
|
||||||
|
|
||||||
const result = await tagEngine.renameTag('tag-1', 'new-name');
|
const result = await tagEngine.renameTag('tag-1', 'new-name');
|
||||||
|
|
||||||
@@ -341,11 +359,11 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should emit tagRenamed event', async () => {
|
it('should emit tagRenamed event', async () => {
|
||||||
mockLocalClient.execute
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [{ id: 'tag-1', name: 'old-name' }] })
|
[{ id: 'tag-1', name: 'old-name', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }],
|
||||||
.mockResolvedValueOnce({ rows: [] })
|
[], // no duplicate
|
||||||
.mockResolvedValueOnce({ rows: [] })
|
];
|
||||||
.mockResolvedValueOnce({ rows: [] });
|
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] }); // no posts to update
|
||||||
|
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
tagEngine.on('tagRenamed', handler);
|
tagEngine.on('tagRenamed', handler);
|
||||||
@@ -361,9 +379,8 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('getTag', () => {
|
describe('getTag', () => {
|
||||||
it('should return tag by ID', async () => {
|
it('should return tag by ID', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
// Set up mock data for Drizzle select (camelCase properties)
|
||||||
rows: [{ id: 'tag-1', name: 'react', color: '#61dafb', project_id: 'default', created_at: Date.now(), updated_at: Date.now() }],
|
mockSelectDataDefault = [{ id: 'tag-1', name: 'react', color: '#61dafb', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }];
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tagEngine.getTag('tag-1');
|
const result = await tagEngine.getTag('tag-1');
|
||||||
|
|
||||||
@@ -373,7 +390,7 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existent tag', async () => {
|
it('should return null for non-existent tag', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({ rows: [] });
|
mockSelectDataDefault = [];
|
||||||
|
|
||||||
const result = await tagEngine.getTag('non-existent');
|
const result = await tagEngine.getTag('non-existent');
|
||||||
|
|
||||||
@@ -383,9 +400,7 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('getTagByName', () => {
|
describe('getTagByName', () => {
|
||||||
it('should return tag by name', async () => {
|
it('should return tag by name', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataDefault = [{ id: 'tag-1', name: 'react', color: '#61dafb', projectId: 'default', createdAt: new Date(), updatedAt: new Date() }];
|
||||||
rows: [{ id: 'tag-1', name: 'react', color: '#61dafb', project_id: 'default', created_at: Date.now(), updated_at: Date.now() }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tagEngine.getTagByName('react');
|
const result = await tagEngine.getTagByName('react');
|
||||||
|
|
||||||
@@ -394,9 +409,7 @@ describe('TagEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be case-insensitive', async () => {
|
it('should be case-insensitive', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataDefault = [{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() }];
|
||||||
rows: [{ id: 'tag-1', name: 'react', color: null }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tagEngine.getTagByName('REACT');
|
const result = await tagEngine.getTagByName('REACT');
|
||||||
|
|
||||||
@@ -406,12 +419,10 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('getAllTags', () => {
|
describe('getAllTags', () => {
|
||||||
it('should return all tags for the current project', async () => {
|
it('should return all tags for the current project', async () => {
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataDefault = [
|
||||||
rows: [
|
{ id: 'tag-1', name: 'react', color: null, projectId: 'default', createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: 'tag-1', name: 'react', color: null, project_id: 'default', created_at: Date.now(), updated_at: Date.now() },
|
{ id: 'tag-2', name: 'vue', color: '#42b883', projectId: 'default', createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: 'tag-2', name: 'vue', color: '#42b883', project_id: 'default', created_at: Date.now(), updated_at: Date.now() },
|
];
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tagEngine.getAllTags();
|
const result = await tagEngine.getAllTags();
|
||||||
|
|
||||||
@@ -423,17 +434,15 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('getPostsWithTag', () => {
|
describe('getPostsWithTag', () => {
|
||||||
it('should return post IDs that have the specified tag', async () => {
|
it('should return post IDs that have the specified tag', async () => {
|
||||||
// First call: get tag name from id
|
// First call: Drizzle ORM to get tag name from id
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
mockSelectDataQueue = [[{ name: 'react' }]];
|
||||||
rows: [{ name: 'react' }],
|
// Second call: raw SQL to find posts with this tag
|
||||||
});
|
mockLocalClient.execute.mockImplementationOnce(async () => ({
|
||||||
// Second call: find posts with this tag
|
|
||||||
mockLocalClient.execute.mockResolvedValueOnce({
|
|
||||||
rows: [
|
rows: [
|
||||||
{ id: 'post-1', tags: '["react", "typescript"]' },
|
{ id: 'post-1', tags: '["react", "typescript"]' },
|
||||||
{ id: 'post-2', tags: '["react"]' },
|
{ id: 'post-2', tags: '["react"]' },
|
||||||
],
|
],
|
||||||
});
|
}));
|
||||||
|
|
||||||
const result = await tagEngine.getPostsWithTag('tag-1');
|
const result = await tagEngine.getPostsWithTag('tag-1');
|
||||||
|
|
||||||
@@ -467,12 +476,11 @@ describe('TagEngine', () => {
|
|||||||
|
|
||||||
describe('syncTagsFromPosts', () => {
|
describe('syncTagsFromPosts', () => {
|
||||||
it('should discover tags from existing posts and add missing ones', async () => {
|
it('should discover tags from existing posts and add missing ones', async () => {
|
||||||
mockLocalClient.execute
|
// First call: get posts' tags, Second call: get existing tags
|
||||||
.mockResolvedValueOnce({ rows: [{ tags: '["react", "javascript"]' }, { tags: '["vue", "typescript"]' }] }) // Get posts
|
mockSelectDataQueue = [
|
||||||
.mockResolvedValueOnce({ rows: [{ name: 'react' }] }) // Existing tags
|
[{ tags: '["react", "javascript"]' }, { tags: '["vue", "typescript"]' }],
|
||||||
.mockResolvedValueOnce({ rows: [] }) // Insert missing tags
|
[{ name: 'react' }], // existing tags
|
||||||
.mockResolvedValueOnce({ rows: [] })
|
];
|
||||||
.mockResolvedValueOnce({ rows: [] });
|
|
||||||
|
|
||||||
const result = await tagEngine.syncTagsFromPosts();
|
const result = await tagEngine.syncTagsFromPosts();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user