chore: removed sync engine since we go for filesystem based syncing
This commit is contained in:
5
drizzle/0001_narrow_black_bolt.sql
Normal file
5
drizzle/0001_narrow_black_bolt.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
DROP TABLE `sync_log`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `media` DROP COLUMN `sync_status`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `media` DROP COLUMN `synced_at`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `posts` DROP COLUMN `sync_status`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `posts` DROP COLUMN `synced_at`;
|
||||||
752
drizzle/meta/0001_snapshot.json
Normal file
752
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "c9e34b7f-92a5-4549-99c9-e5a680004bfc",
|
||||||
|
"prevId": "af3c1207-a667-495d-833d-26f7d3451829",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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": "''"
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1771081481654,
|
"when": 1771081481654,
|
||||||
"tag": "0000_initial",
|
"tag": "0000_initial",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1771088786493,
|
||||||
|
"tag": "0001_narrow_black_bolt",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,6 @@ export const posts = sqliteTable('posts', {
|
|||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
publishedAt: integer('published_at', { mode: 'timestamp' }),
|
publishedAt: integer('published_at', { mode: 'timestamp' }),
|
||||||
filePath: text('file_path').notNull().default(''), // Empty for never-published drafts
|
filePath: text('file_path').notNull().default(''), // Empty for never-published drafts
|
||||||
syncStatus: text('sync_status', { enum: ['pending', 'synced', 'conflict'] }).notNull().default('pending'),
|
|
||||||
syncedAt: integer('synced_at', { mode: 'timestamp' }),
|
|
||||||
checksum: text('checksum'),
|
checksum: text('checksum'),
|
||||||
tags: text('tags'), // JSON array stored as text
|
tags: text('tags'), // JSON array stored as text
|
||||||
categories: text('categories'), // JSON array stored as text
|
categories: text('categories'), // JSON array stored as text
|
||||||
@@ -62,24 +60,10 @@ export const media = sqliteTable('media', {
|
|||||||
sidecarPath: text('sidecar_path').notNull(),
|
sidecarPath: text('sidecar_path').notNull(),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
syncStatus: text('sync_status', { enum: ['pending', 'synced', 'conflict'] }).notNull().default('pending'),
|
|
||||||
syncedAt: integer('synced_at', { mode: 'timestamp' }),
|
|
||||||
checksum: text('checksum'),
|
checksum: text('checksum'),
|
||||||
tags: text('tags'), // JSON array stored as text
|
tags: text('tags'), // JSON array stored as text
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync log - tracks sync operations
|
|
||||||
export const syncLog = sqliteTable('sync_log', {
|
|
||||||
id: text('id').primaryKey(),
|
|
||||||
entityType: text('entity_type', { enum: ['post', 'media'] }).notNull(),
|
|
||||||
entityId: text('entity_id').notNull(),
|
|
||||||
operation: text('operation', { enum: ['create', 'update', 'delete'] }).notNull(),
|
|
||||||
status: text('status', { enum: ['pending', 'completed', 'failed'] }).notNull().default('pending'),
|
|
||||||
timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(),
|
|
||||||
errorMessage: text('error_message'),
|
|
||||||
retryCount: integer('retry_count').notNull().default(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
// App settings - stores application configuration
|
// App settings - stores application configuration
|
||||||
export const settings = sqliteTable('settings', {
|
export const settings = sqliteTable('settings', {
|
||||||
key: text('key').primaryKey(),
|
key: text('key').primaryKey(),
|
||||||
@@ -162,8 +146,6 @@ export type Post = typeof posts.$inferSelect;
|
|||||||
export type NewPost = typeof posts.$inferInsert;
|
export type NewPost = typeof posts.$inferInsert;
|
||||||
export type Media = typeof media.$inferSelect;
|
export type Media = typeof media.$inferSelect;
|
||||||
export type NewMedia = typeof media.$inferInsert;
|
export type NewMedia = typeof media.$inferInsert;
|
||||||
export type SyncLogEntry = typeof syncLog.$inferSelect;
|
|
||||||
export type NewSyncLogEntry = typeof syncLog.$inferInsert;
|
|
||||||
export type Setting = typeof settings.$inferSelect;
|
export type Setting = typeof settings.$inferSelect;
|
||||||
export type NewSetting = typeof settings.$inferInsert;
|
export type NewSetting = typeof settings.$inferInsert;
|
||||||
export type PostLink = typeof postLinks.$inferSelect;
|
export type PostLink = typeof postLinks.$inferSelect;
|
||||||
|
|||||||
@@ -520,7 +520,6 @@ export class MediaEngine extends EventEmitter {
|
|||||||
sidecarPath,
|
sidecarPath,
|
||||||
createdAt: mediaData.createdAt,
|
createdAt: mediaData.createdAt,
|
||||||
updatedAt: mediaData.updatedAt,
|
updatedAt: mediaData.updatedAt,
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(mediaData.tags),
|
tags: JSON.stringify(mediaData.tags),
|
||||||
};
|
};
|
||||||
@@ -566,7 +565,6 @@ export class MediaEngine extends EventEmitter {
|
|||||||
alt: updated.alt,
|
alt: updated.alt,
|
||||||
caption: updated.caption,
|
caption: updated.caption,
|
||||||
updatedAt: updated.updatedAt,
|
updatedAt: updated.updatedAt,
|
||||||
syncStatus: 'pending',
|
|
||||||
tags: JSON.stringify(updated.tags),
|
tags: JSON.stringify(updated.tags),
|
||||||
})
|
})
|
||||||
.where(eq(media.id, id));
|
.where(eq(media.id, id));
|
||||||
@@ -933,7 +931,6 @@ export class MediaEngine extends EventEmitter {
|
|||||||
sidecarPath,
|
sidecarPath,
|
||||||
createdAt: new Date(metadata.createdAt),
|
createdAt: new Date(metadata.createdAt),
|
||||||
updatedAt: new Date(metadata.updatedAt),
|
updatedAt: new Date(metadata.updatedAt),
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(metadata.tags),
|
tags: JSON.stringify(metadata.tags),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -328,7 +328,6 @@ export class PostEngine extends EventEmitter {
|
|||||||
updatedAt: post.updatedAt,
|
updatedAt: post.updatedAt,
|
||||||
publishedAt: post.publishedAt,
|
publishedAt: post.publishedAt,
|
||||||
filePath: '',
|
filePath: '',
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(post.tags),
|
tags: JSON.stringify(post.tags),
|
||||||
categories: JSON.stringify(post.categories),
|
categories: JSON.stringify(post.categories),
|
||||||
@@ -404,7 +403,6 @@ export class PostEngine extends EventEmitter {
|
|||||||
author: updated.author,
|
author: updated.author,
|
||||||
updatedAt: updated.updatedAt,
|
updatedAt: updated.updatedAt,
|
||||||
publishedAt: updated.publishedAt,
|
publishedAt: updated.publishedAt,
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(updated.tags),
|
tags: JSON.stringify(updated.tags),
|
||||||
categories: JSON.stringify(updated.categories),
|
categories: JSON.stringify(updated.categories),
|
||||||
@@ -851,7 +849,6 @@ export class PostEngine extends EventEmitter {
|
|||||||
updatedAt: published.updatedAt,
|
updatedAt: published.updatedAt,
|
||||||
publishedAt: published.publishedAt,
|
publishedAt: published.publishedAt,
|
||||||
filePath: newFilePath,
|
filePath: newFilePath,
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(published.tags),
|
tags: JSON.stringify(published.tags),
|
||||||
categories: JSON.stringify(published.categories),
|
categories: JSON.stringify(published.categories),
|
||||||
@@ -1105,7 +1102,6 @@ export class PostEngine extends EventEmitter {
|
|||||||
updatedAt: postData.updatedAt,
|
updatedAt: postData.updatedAt,
|
||||||
publishedAt: postData.publishedAt || postData.updatedAt,
|
publishedAt: postData.publishedAt || postData.updatedAt,
|
||||||
filePath,
|
filePath,
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(postData.tags),
|
tags: JSON.stringify(postData.tags),
|
||||||
categories: JSON.stringify(postData.categories),
|
categories: JSON.stringify(postData.categories),
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import { EventEmitter } from 'events';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import { getDatabase } from '../database';
|
|
||||||
import { syncLog, posts, media, NewSyncLogEntry } from '../database/schema';
|
|
||||||
|
|
||||||
export type SyncDirection = 'push' | 'pull' | 'bidirectional';
|
|
||||||
export type SyncStatus = 'idle' | 'syncing' | 'error';
|
|
||||||
|
|
||||||
export interface SyncConfig {
|
|
||||||
autoSync: boolean;
|
|
||||||
syncInterval: number; // in minutes
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncResult {
|
|
||||||
success: boolean;
|
|
||||||
pushed: number;
|
|
||||||
pulled: number;
|
|
||||||
conflicts: number;
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SyncEngine extends EventEmitter {
|
|
||||||
private syncStatus: SyncStatus = 'idle';
|
|
||||||
private syncConfig: SyncConfig | null = null;
|
|
||||||
private syncIntervalId: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSyncStatus(): SyncStatus {
|
|
||||||
return this.syncStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if sync is configured.
|
|
||||||
* Currently returns false as cloud sync is not implemented.
|
|
||||||
*/
|
|
||||||
isConfigured(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async configure(config: SyncConfig): Promise<void> {
|
|
||||||
this.syncConfig = config;
|
|
||||||
|
|
||||||
// Stop existing auto-sync
|
|
||||||
if (this.syncIntervalId) {
|
|
||||||
clearInterval(this.syncIntervalId);
|
|
||||||
this.syncIntervalId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-sync is disabled as cloud sync is not implemented
|
|
||||||
this.emit('configured', config);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async logSyncOperation(
|
|
||||||
entityId: string,
|
|
||||||
entityType: 'post' | 'media',
|
|
||||||
operation: 'create' | 'update' | 'delete',
|
|
||||||
status: 'pending' | 'completed' | 'failed',
|
|
||||||
errorMessage?: string
|
|
||||||
): Promise<void> {
|
|
||||||
const db = getDatabase().getLocal();
|
|
||||||
|
|
||||||
const logEntry: NewSyncLogEntry = {
|
|
||||||
id: uuidv4(),
|
|
||||||
entityType,
|
|
||||||
entityId,
|
|
||||||
operation,
|
|
||||||
status,
|
|
||||||
timestamp: new Date(),
|
|
||||||
errorMessage,
|
|
||||||
retryCount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.insert(syncLog).values(logEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPendingChangesCount(): Promise<{ posts: number; media: number }> {
|
|
||||||
const db = getDatabase().getLocal();
|
|
||||||
|
|
||||||
const pendingPosts = await db
|
|
||||||
.select()
|
|
||||||
.from(posts)
|
|
||||||
.where(eq(posts.syncStatus, 'pending'))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
const pendingMedia = await db
|
|
||||||
.select()
|
|
||||||
.from(media)
|
|
||||||
.where(eq(media.syncStatus, 'pending'))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
return {
|
|
||||||
posts: pendingPosts.length,
|
|
||||||
media: pendingMedia.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSyncLog(limit = 50): Promise<Array<{
|
|
||||||
id: string;
|
|
||||||
entityType: string;
|
|
||||||
entityId: string;
|
|
||||||
operation: string;
|
|
||||||
status: string;
|
|
||||||
timestamp: Date;
|
|
||||||
errorMessage?: string;
|
|
||||||
}>> {
|
|
||||||
const db = getDatabase().getLocal();
|
|
||||||
|
|
||||||
const logs = await db
|
|
||||||
.select()
|
|
||||||
.from(syncLog)
|
|
||||||
.orderBy(syncLog.timestamp)
|
|
||||||
.limit(limit)
|
|
||||||
.all();
|
|
||||||
|
|
||||||
return logs.map(log => ({
|
|
||||||
id: log.id,
|
|
||||||
entityType: log.entityType,
|
|
||||||
entityId: log.entityId,
|
|
||||||
operation: log.operation,
|
|
||||||
status: log.status,
|
|
||||||
timestamp: log.timestamp,
|
|
||||||
errorMessage: log.errorMessage || undefined,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
stopAutoSync(): void {
|
|
||||||
if (this.syncIntervalId) {
|
|
||||||
clearInterval(this.syncIntervalId);
|
|
||||||
this.syncIntervalId = null;
|
|
||||||
}
|
|
||||||
this.emit('autoSyncStopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync alias for fullSync for backward compatibility
|
|
||||||
*/
|
|
||||||
async sync(direction: SyncDirection = 'bidirectional'): Promise<SyncResult> {
|
|
||||||
return this.fullSync(direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full sync is not currently implemented.
|
|
||||||
* Returns a result indicating sync is not configured.
|
|
||||||
*/
|
|
||||||
async fullSync(_direction: SyncDirection = 'bidirectional'): Promise<SyncResult> {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
pushed: 0,
|
|
||||||
pulled: 0,
|
|
||||||
conflicts: 0,
|
|
||||||
errors: ['Cloud sync not configured'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton instance
|
|
||||||
let syncEngine: SyncEngine | null = null;
|
|
||||||
|
|
||||||
export function getSyncEngine(): SyncEngine {
|
|
||||||
if (!syncEngine) {
|
|
||||||
syncEngine = new SyncEngine();
|
|
||||||
}
|
|
||||||
return syncEngine;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ export { TaskManager, taskManager, type Task, type TaskProgress, type TaskStatus
|
|||||||
export { PostEngine, getPostEngine, type PostData, type PostFilter, type SearchResult, type PaginatedResult, type PaginationOptions } from './PostEngine';
|
export { PostEngine, getPostEngine, type PostData, type PostFilter, type SearchResult, type PaginatedResult, type PaginationOptions } from './PostEngine';
|
||||||
export { MediaEngine, getMediaEngine, type MediaData } from './MediaEngine';
|
export { MediaEngine, getMediaEngine, type MediaData } from './MediaEngine';
|
||||||
export { PostMediaEngine, getPostMediaEngine, postMediaEngine, type PostMediaLinkData } from './PostMediaEngine';
|
export { PostMediaEngine, getPostMediaEngine, postMediaEngine, type PostMediaLinkData } from './PostMediaEngine';
|
||||||
export { SyncEngine, getSyncEngine, type SyncConfig, type SyncResult, type SyncDirection, type SyncStatus } from './SyncEngine';
|
|
||||||
export { ProjectEngine, getProjectEngine, type ProjectData } from './ProjectEngine';
|
export { ProjectEngine, getProjectEngine, type ProjectData } from './ProjectEngine';
|
||||||
export { MetaEngine, getMetaEngine, type ProjectMetadata, DEFAULT_CATEGORIES } from './MetaEngine';
|
export { MetaEngine, getMetaEngine, type ProjectMetadata, DEFAULT_CATEGORIES } from './MetaEngine';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as fsPromises from 'fs/promises';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { getPostEngine, PostData, PostFilter, PaginationOptions } from '../engine/PostEngine';
|
import { getPostEngine, PostData, PostFilter, PaginationOptions } from '../engine/PostEngine';
|
||||||
import { getMediaEngine, MediaData } from '../engine/MediaEngine';
|
import { getMediaEngine, MediaData } from '../engine/MediaEngine';
|
||||||
import { getSyncEngine, SyncConfig, SyncDirection } from '../engine/SyncEngine';
|
|
||||||
import { getProjectEngine, ProjectData } from '../engine/ProjectEngine';
|
import { getProjectEngine, ProjectData } from '../engine/ProjectEngine';
|
||||||
import { getMetaEngine } from '../engine/MetaEngine';
|
import { getMetaEngine } from '../engine/MetaEngine';
|
||||||
import { getTagEngine } from '../engine/TagEngine';
|
import { getTagEngine } from '../engine/TagEngine';
|
||||||
@@ -432,43 +431,6 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.regenerateMissingThumbnails();
|
return engine.regenerateMissingThumbnails();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Sync Handlers ============
|
|
||||||
|
|
||||||
safeHandle('sync:configure', async (_, config: SyncConfig) => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.configure(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:start', async (_, direction: SyncDirection = 'bidirectional') => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.fullSync(direction);
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:getStatus', async () => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.getSyncStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:isConfigured', async () => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.isConfigured();
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:getPendingCount', async () => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.getPendingChangesCount();
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:getLog', async (_, limit?: number) => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.getSyncLog(limit);
|
|
||||||
});
|
|
||||||
|
|
||||||
safeHandle('sync:stopAutoSync', async () => {
|
|
||||||
const engine = getSyncEngine();
|
|
||||||
return engine.stopAutoSync();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============ Task Handlers ============
|
// ============ Task Handlers ============
|
||||||
|
|
||||||
safeHandle('tasks:getAll', async () => {
|
safeHandle('tasks:getAll', async () => {
|
||||||
@@ -880,7 +842,6 @@ export function registerIpcHandlers(): void {
|
|||||||
// Forward engine events to renderer
|
// Forward engine events to renderer
|
||||||
const postEngine = getPostEngine();
|
const postEngine = getPostEngine();
|
||||||
const mediaEngine = getMediaEngine();
|
const mediaEngine = getMediaEngine();
|
||||||
const syncEngine = getSyncEngine();
|
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const metaEngine = getMetaEngine();
|
const metaEngine = getMetaEngine();
|
||||||
const tagEngine = getTagEngine();
|
const tagEngine = getTagEngine();
|
||||||
@@ -926,10 +887,6 @@ export function registerIpcHandlers(): void {
|
|||||||
postMediaEngine.on('mediaReordered', forwardEvent('postMedia:reordered'));
|
postMediaEngine.on('mediaReordered', forwardEvent('postMedia:reordered'));
|
||||||
postMediaEngine.on('rebuilt', forwardEvent('postMedia:rebuilt'));
|
postMediaEngine.on('rebuilt', forwardEvent('postMedia:rebuilt'));
|
||||||
|
|
||||||
syncEngine.on('syncStarted', forwardEvent('sync:started'));
|
|
||||||
syncEngine.on('syncCompleted', forwardEvent('sync:completed'));
|
|
||||||
syncEngine.on('syncFailed', forwardEvent('sync:failed'));
|
|
||||||
|
|
||||||
taskManager.on('taskCreated', forwardEvent('task:created'));
|
taskManager.on('taskCreated', forwardEvent('task:created'));
|
||||||
taskManager.on('taskStarted', forwardEvent('task:started'));
|
taskManager.on('taskStarted', forwardEvent('task:started'));
|
||||||
taskManager.on('taskProgress', forwardEvent('task:progress'));
|
taskManager.on('taskProgress', forwardEvent('task:progress'));
|
||||||
|
|||||||
@@ -255,43 +255,6 @@ function createApplicationMenu(): Menu {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Sync',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Sync Now',
|
|
||||||
accelerator: 'CmdOrCtrl+Shift+S',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.send('menu:syncNow');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Push Changes',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.send('menu:pushChanges');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pull Changes',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.send('menu:pullChanges');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Configure Sync...',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.send('menu:configureSync');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View Sync Log',
|
|
||||||
click: () => {
|
|
||||||
mainWindow?.webContents.send('menu:viewSyncLog');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
|
|||||||
@@ -78,17 +78,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
rebuild: () => ipcRenderer.invoke('postMedia:rebuild'),
|
rebuild: () => ipcRenderer.invoke('postMedia:rebuild'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sync
|
|
||||||
sync: {
|
|
||||||
configure: (config: unknown) => ipcRenderer.invoke('sync:configure', config),
|
|
||||||
start: (direction?: string) => ipcRenderer.invoke('sync:start', direction),
|
|
||||||
getStatus: () => ipcRenderer.invoke('sync:getStatus'),
|
|
||||||
isConfigured: () => ipcRenderer.invoke('sync:isConfigured'),
|
|
||||||
getPendingCount: () => ipcRenderer.invoke('sync:getPendingCount'),
|
|
||||||
getLog: (limit?: number) => ipcRenderer.invoke('sync:getLog', limit),
|
|
||||||
stopAutoSync: () => ipcRenderer.invoke('sync:stopAutoSync'),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
tasks: {
|
tasks: {
|
||||||
getAll: () => ipcRenderer.invoke('tasks:getAll'),
|
getAll: () => ipcRenderer.invoke('tasks:getAll'),
|
||||||
@@ -273,15 +262,6 @@ export interface ElectronAPI {
|
|||||||
getAll: () => Promise<unknown[]>;
|
getAll: () => Promise<unknown[]>;
|
||||||
rebuildFromFiles: () => Promise<void>;
|
rebuildFromFiles: () => Promise<void>;
|
||||||
};
|
};
|
||||||
sync: {
|
|
||||||
configure: (config: unknown) => Promise<void>;
|
|
||||||
start: (direction?: string) => Promise<unknown>;
|
|
||||||
getStatus: () => Promise<string>;
|
|
||||||
isConfigured: () => Promise<boolean>;
|
|
||||||
getPendingCount: () => Promise<{ posts: number; media: number }>;
|
|
||||||
getLog: (limit?: number) => Promise<unknown[]>;
|
|
||||||
stopAutoSync: () => Promise<void>;
|
|
||||||
};
|
|
||||||
dropbox: {
|
dropbox: {
|
||||||
configure: (config: unknown) => Promise<void>;
|
configure: (config: unknown) => Promise<void>;
|
||||||
isConfigured: () => Promise<boolean>;
|
isConfigured: () => Promise<boolean>;
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ const App: React.FC = () => {
|
|||||||
removeMedia,
|
removeMedia,
|
||||||
setTasks,
|
setTasks,
|
||||||
updateTask,
|
updateTask,
|
||||||
setSyncStatus,
|
|
||||||
setSyncConfigured,
|
|
||||||
setPendingChanges,
|
|
||||||
setLoading,
|
setLoading,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
togglePanel,
|
togglePanel,
|
||||||
@@ -74,16 +71,6 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check sync status
|
|
||||||
const syncConfigured = await window.electronAPI?.sync.isConfigured();
|
|
||||||
setSyncConfigured(syncConfigured || false);
|
|
||||||
|
|
||||||
// Get pending changes count
|
|
||||||
const pending = await window.electronAPI?.sync.getPendingCount();
|
|
||||||
if (pending) {
|
|
||||||
setPendingChanges(pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tasks
|
// Load tasks
|
||||||
const tasks = await window.electronAPI?.tasks.getAll();
|
const tasks = await window.electronAPI?.tasks.getAll();
|
||||||
if (tasks) {
|
if (tasks) {
|
||||||
@@ -162,36 +149,6 @@ const App: React.FC = () => {
|
|||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync events
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('sync:started', () => {
|
|
||||||
setSyncStatus('syncing');
|
|
||||||
showToast.loading('Syncing...');
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('sync:completed', async () => {
|
|
||||||
setSyncStatus('idle');
|
|
||||||
showToast.dismiss();
|
|
||||||
showToast.success('Sync completed');
|
|
||||||
const pending = await window.electronAPI?.sync.getPendingCount();
|
|
||||||
if (pending) {
|
|
||||||
setPendingChanges(pending);
|
|
||||||
}
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('sync:failed', (errorMsg: unknown) => {
|
|
||||||
setSyncStatus('error');
|
|
||||||
showToast.dismiss();
|
|
||||||
const message = typeof errorMsg === 'string' && errorMsg ? errorMsg : 'Unknown error';
|
|
||||||
showToast.error(`Sync failed: ${message}`);
|
|
||||||
console.error('Sync failed:', message);
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Task events
|
// Task events
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
window.electronAPI?.on('task:started', (task: unknown) => {
|
window.electronAPI?.on('task:started', (task: unknown) => {
|
||||||
@@ -267,30 +224,6 @@ const App: React.FC = () => {
|
|||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('menu:syncNow', () => {
|
|
||||||
window.electronAPI?.sync.start('bidirectional');
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('menu:pushChanges', () => {
|
|
||||||
window.electronAPI?.sync.start('push');
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('menu:pullChanges', () => {
|
|
||||||
window.electronAPI?.sync.start('pull');
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
unsubscribers.push(
|
|
||||||
window.electronAPI?.on('menu:configureSync', () => {
|
|
||||||
openTab({ type: 'settings', id: 'settings', isTransient: false });
|
|
||||||
}) || (() => {})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rebuild events - clear store on start, reload on complete
|
// Rebuild events - clear store on start, reload on complete
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
window.electronAPI?.on('posts:rebuildStarted', () => {
|
window.electronAPI?.on('posts:rebuildStarted', () => {
|
||||||
|
|||||||
@@ -43,16 +43,8 @@ const ImportIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SyncIcon = () => (
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ActivityBar: React.FC = () => {
|
export const ActivityBar: React.FC = () => {
|
||||||
const { activeView, setActiveView, sidebarVisible, toggleSidebar, syncStatus, pendingChanges, openTab, tabs, activeTabId } = useAppStore();
|
const { activeView, setActiveView, sidebarVisible, toggleSidebar, openTab, tabs, activeTabId } = useAppStore();
|
||||||
|
|
||||||
const totalPending = pendingChanges.posts + pendingChanges.media;
|
|
||||||
|
|
||||||
// Check if settings tab is currently active
|
// Check if settings tab is currently active
|
||||||
const isSettingsTabActive = tabs.some(t => t.type === 'settings' && t.id === activeTabId);
|
const isSettingsTabActive = tabs.some(t => t.type === 'settings' && t.id === activeTabId);
|
||||||
@@ -157,16 +149,6 @@ export const ActivityBar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="activity-bar-bottom">
|
<div className="activity-bar-bottom">
|
||||||
<button
|
|
||||||
className={`activity-bar-item ${syncStatus === 'syncing' ? 'syncing' : ''}`}
|
|
||||||
onClick={() => window.electronAPI?.sync.start()}
|
|
||||||
title={`Sync (${totalPending} pending)`}
|
|
||||||
>
|
|
||||||
<SyncIcon />
|
|
||||||
{totalPending > 0 && (
|
|
||||||
<span className="activity-bar-badge">{totalPending}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className={`activity-bar-item ${isSettingsActive ? 'active' : ''}`}
|
className={`activity-bar-item ${isSettingsActive ? 'active' : ''}`}
|
||||||
onClick={handleSettingsClick}
|
onClick={handleSettingsClick}
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import './StatusBar.css';
|
|||||||
|
|
||||||
export const StatusBar: React.FC = () => {
|
export const StatusBar: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
syncStatus,
|
|
||||||
syncConfigured,
|
|
||||||
pendingChanges,
|
|
||||||
media,
|
media,
|
||||||
tasks,
|
tasks,
|
||||||
selectedPostId,
|
selectedPostId,
|
||||||
@@ -28,7 +25,6 @@ export const StatusBar: React.FC = () => {
|
|||||||
}, [selectedPostId]);
|
}, [selectedPostId]);
|
||||||
|
|
||||||
const runningTasks = tasks.filter(t => t.status === 'running');
|
const runningTasks = tasks.filter(t => t.status === 'running');
|
||||||
const totalPending = pendingChanges.posts + pendingChanges.media;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="status-bar">
|
<div className="status-bar">
|
||||||
@@ -36,20 +32,6 @@ export const StatusBar: React.FC = () => {
|
|||||||
{/* Project Selector */}
|
{/* Project Selector */}
|
||||||
<ProjectSelector />
|
<ProjectSelector />
|
||||||
|
|
||||||
{/* Sync Status */}
|
|
||||||
<div className={`status-bar-item ${!syncConfigured ? 'warning' : ''}`}>
|
|
||||||
<span className={`sync-indicator ${syncStatus}`} />
|
|
||||||
{!syncConfigured ? (
|
|
||||||
<span>Sync not configured</span>
|
|
||||||
) : syncStatus === 'syncing' ? (
|
|
||||||
<span>Syncing...</span>
|
|
||||||
) : totalPending > 0 ? (
|
|
||||||
<span>{totalPending} pending</span>
|
|
||||||
) : (
|
|
||||||
<span>Synced</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Running Tasks */}
|
{/* Running Tasks */}
|
||||||
{runningTasks.length > 0 && (
|
{runningTasks.length > 0 && (
|
||||||
<div className="status-bar-item">
|
<div className="status-bar-item">
|
||||||
|
|||||||
@@ -118,11 +118,6 @@ interface AppState {
|
|||||||
// Confirm delete modal
|
// Confirm delete modal
|
||||||
confirmDeleteModal: ConfirmDeleteDetails | null;
|
confirmDeleteModal: ConfirmDeleteDetails | null;
|
||||||
|
|
||||||
// Sync
|
|
||||||
syncStatus: 'idle' | 'syncing' | 'error';
|
|
||||||
syncConfigured: boolean;
|
|
||||||
pendingChanges: { posts: number; media: number };
|
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -178,10 +173,7 @@ interface AppState {
|
|||||||
setTasks: (tasks: TaskProgress[]) => void;
|
setTasks: (tasks: TaskProgress[]) => void;
|
||||||
updateTask: (taskId: string, task: Partial<TaskProgress>) => void;
|
updateTask: (taskId: string, task: Partial<TaskProgress>) => void;
|
||||||
|
|
||||||
setSyncStatus: (status: 'idle' | 'syncing' | 'error') => void;
|
// Loading Actions
|
||||||
setSyncConfigured: (configured: boolean) => void;
|
|
||||||
setPendingChanges: (changes: { posts: number; media: number }) => void;
|
|
||||||
|
|
||||||
setLoading: (loading: boolean) => void;
|
setLoading: (loading: boolean) => void;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
}
|
}
|
||||||
@@ -223,11 +215,6 @@ export const useAppStore = create<AppState>()(
|
|||||||
// Confirm delete modal
|
// Confirm delete modal
|
||||||
confirmDeleteModal: null,
|
confirmDeleteModal: null,
|
||||||
|
|
||||||
// Initial Sync State
|
|
||||||
syncStatus: 'idle',
|
|
||||||
syncConfigured: false,
|
|
||||||
pendingChanges: { posts: 0, media: 0 },
|
|
||||||
|
|
||||||
// Initial Loading State
|
// Initial Loading State
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -397,11 +384,6 @@ export const useAppStore = create<AppState>()(
|
|||||||
return { tasks: [...state.tasks, { taskId, status: 'running', progress: 0, message: '', startTime: new Date().toISOString(), ...task } as TaskProgress] };
|
return { tasks: [...state.tasks, { taskId, status: 'running', progress: 0, message: '', startTime: new Date().toISOString(), ...task } as TaskProgress] };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Sync Actions
|
|
||||||
setSyncStatus: (syncStatus) => set({ syncStatus }),
|
|
||||||
setSyncConfigured: (syncConfigured) => set({ syncConfigured }),
|
|
||||||
setPendingChanges: (pendingChanges) => set({ pendingChanges }),
|
|
||||||
|
|
||||||
// Loading Actions
|
// Loading Actions
|
||||||
setLoading: (isLoading) => set({ isLoading }),
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
setError: (error) => set({ error }),
|
setError: (error) => set({ error }),
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
/**
|
|
||||||
* SyncEngine Unit Tests
|
|
||||||
*
|
|
||||||
* Tests the REAL SyncEngine class with mocked dependencies.
|
|
||||||
* Note: Cloud sync is currently not implemented, so SyncEngine
|
|
||||||
* always returns "not configured" status.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
||||||
import { SyncEngine, SyncConfig } from '../../src/main/engine/SyncEngine';
|
|
||||||
import { resetMockCounters } from '../utils/factories';
|
|
||||||
|
|
||||||
// Create mock data stores
|
|
||||||
const mockPosts = new Map<string, any>();
|
|
||||||
const mockMedia = new Map<string, any>();
|
|
||||||
const mockSyncLog = new Map<string, any>();
|
|
||||||
|
|
||||||
// Create chainable mock for Drizzle ORM
|
|
||||||
function createSelectChain(data: Map<string, any>) {
|
|
||||||
return {
|
|
||||||
from: vi.fn().mockReturnThis(),
|
|
||||||
where: vi.fn().mockReturnThis(),
|
|
||||||
orderBy: vi.fn().mockReturnThis(),
|
|
||||||
limit: vi.fn().mockReturnThis(),
|
|
||||||
offset: vi.fn().mockReturnThis(),
|
|
||||||
all: vi.fn().mockImplementation(() => Promise.resolve(Array.from(data.values()))),
|
|
||||||
get: vi.fn().mockImplementation(() => Promise.resolve(data.size > 0 ? Array.from(data.values())[0] : undefined)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDrizzleMock(data: Map<string, any>) {
|
|
||||||
return {
|
|
||||||
select: vi.fn(() => createSelectChain(data)),
|
|
||||||
insert: vi.fn(() => ({
|
|
||||||
values: vi.fn((record: any) => {
|
|
||||||
if (record && record.id) {
|
|
||||||
data.set(record.id, record);
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}),
|
|
||||||
onConflictDoUpdate: vi.fn(() => Promise.resolve()),
|
|
||||||
})),
|
|
||||||
update: vi.fn(() => ({
|
|
||||||
set: vi.fn(() => ({
|
|
||||||
where: vi.fn(() => Promise.resolve()),
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
delete: vi.fn(() => ({
|
|
||||||
where: vi.fn(() => Promise.resolve()),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockLocalDb = createDrizzleMock(new Map());
|
|
||||||
const mockRemoteDb = createDrizzleMock(new Map());
|
|
||||||
|
|
||||||
// Mock the database module
|
|
||||||
vi.mock('../../src/main/database', () => ({
|
|
||||||
getDatabase: vi.fn(() => ({
|
|
||||||
getLocal: vi.fn(() => mockLocalDb),
|
|
||||||
getLocalClient: vi.fn(() => null),
|
|
||||||
getRemote: vi.fn(() => null),
|
|
||||||
getDataPaths: vi.fn(() => ({
|
|
||||||
database: '/mock/userData/bds.db',
|
|
||||||
posts: '/mock/userData/posts',
|
|
||||||
media: '/mock/userData/media',
|
|
||||||
})),
|
|
||||||
initializeLocal: vi.fn(),
|
|
||||||
initializeRemote: vi.fn(async () => {}),
|
|
||||||
runRemoteMigrations: vi.fn(async () => {}),
|
|
||||||
close: vi.fn(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock uuid
|
|
||||||
vi.mock('uuid', () => ({
|
|
||||||
v4: vi.fn(() => 'mock-sync-uuid-' + Math.random().toString(36).substr(2, 9)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('SyncEngine', () => {
|
|
||||||
let syncEngine: SyncEngine;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
vi.useFakeTimers();
|
|
||||||
mockPosts.clear();
|
|
||||||
mockMedia.clear();
|
|
||||||
mockSyncLog.clear();
|
|
||||||
resetMockCounters();
|
|
||||||
|
|
||||||
syncEngine = new SyncEngine();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
syncEngine.stopAutoSync();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Constructor and Initialization', () => {
|
|
||||||
it('should create a SyncEngine instance', () => {
|
|
||||||
expect(syncEngine).toBeInstanceOf(SyncEngine);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extend EventEmitter', () => {
|
|
||||||
expect(typeof syncEngine.on).toBe('function');
|
|
||||||
expect(typeof syncEngine.emit).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with idle status', () => {
|
|
||||||
expect(syncEngine.getSyncStatus()).toBe('idle');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not be configured (cloud sync is not implemented)', () => {
|
|
||||||
expect(syncEngine.isConfigured()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Configuration', () => {
|
|
||||||
it('should emit configured event when configure is called', async () => {
|
|
||||||
const handler = vi.fn();
|
|
||||||
syncEngine.on('configured', handler);
|
|
||||||
|
|
||||||
const config: SyncConfig = {
|
|
||||||
autoSync: false,
|
|
||||||
syncInterval: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
await syncEngine.configure(config);
|
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalledWith(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should always return not configured (cloud sync not implemented)', async () => {
|
|
||||||
const config: SyncConfig = {
|
|
||||||
autoSync: false,
|
|
||||||
syncInterval: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
await syncEngine.configure(config);
|
|
||||||
|
|
||||||
// Cloud sync is not implemented, so always returns false
|
|
||||||
expect(syncEngine.isConfigured()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Sync Status', () => {
|
|
||||||
it('should return idle when not syncing', () => {
|
|
||||||
expect(syncEngine.getSyncStatus()).toBe('idle');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Sync Operations', () => {
|
|
||||||
it('should return error when syncing (cloud sync not implemented)', async () => {
|
|
||||||
const result = await syncEngine.sync('bidirectional');
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.errors).toContain('Cloud sync not configured');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return zero counts when sync is not available', async () => {
|
|
||||||
const result = await syncEngine.sync('push');
|
|
||||||
|
|
||||||
expect(result.pushed).toBe(0);
|
|
||||||
expect(result.pulled).toBe(0);
|
|
||||||
expect(result.conflicts).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should accept push direction', async () => {
|
|
||||||
const result = await syncEngine.sync('push');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should accept pull direction', async () => {
|
|
||||||
const result = await syncEngine.sync('pull');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should accept bidirectional direction', async () => {
|
|
||||||
const result = await syncEngine.sync('bidirectional');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to bidirectional when no direction specified', async () => {
|
|
||||||
const result = await syncEngine.sync();
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use fullSync as alias for sync', async () => {
|
|
||||||
const result = await syncEngine.fullSync('push');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.errors).toContain('Cloud sync not configured');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Event Emission', () => {
|
|
||||||
it('should be an EventEmitter', () => {
|
|
||||||
expect(syncEngine.on).toBeDefined();
|
|
||||||
expect(syncEngine.emit).toBeDefined();
|
|
||||||
expect(syncEngine.removeListener).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow adding event listeners', () => {
|
|
||||||
const listener = vi.fn();
|
|
||||||
syncEngine.on('testEvent', listener);
|
|
||||||
syncEngine.emit('testEvent', { data: 'test' });
|
|
||||||
|
|
||||||
expect(listener).toHaveBeenCalledWith({ data: 'test' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow removing event listeners', () => {
|
|
||||||
const listener = vi.fn();
|
|
||||||
syncEngine.on('testEvent', listener);
|
|
||||||
syncEngine.removeListener('testEvent', listener);
|
|
||||||
syncEngine.emit('testEvent', { data: 'test' });
|
|
||||||
|
|
||||||
expect(listener).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SyncResult Structure', () => {
|
|
||||||
it('should return complete SyncResult structure', async () => {
|
|
||||||
const result = await syncEngine.sync();
|
|
||||||
|
|
||||||
expect(result).toHaveProperty('success');
|
|
||||||
expect(result).toHaveProperty('pushed');
|
|
||||||
expect(result).toHaveProperty('pulled');
|
|
||||||
expect(result).toHaveProperty('conflicts');
|
|
||||||
expect(result).toHaveProperty('errors');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have errors as an array', async () => {
|
|
||||||
const result = await syncEngine.sync();
|
|
||||||
|
|
||||||
expect(Array.isArray(result.errors)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have numeric counts', async () => {
|
|
||||||
const result = await syncEngine.sync();
|
|
||||||
|
|
||||||
expect(typeof result.pushed).toBe('number');
|
|
||||||
expect(typeof result.pulled).toBe('number');
|
|
||||||
expect(typeof result.conflicts).toBe('number');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Pending Changes Count', () => {
|
|
||||||
it('should return pending changes count structure', async () => {
|
|
||||||
const count = await syncEngine.getPendingChangesCount();
|
|
||||||
|
|
||||||
expect(count).toHaveProperty('posts');
|
|
||||||
expect(count).toHaveProperty('media');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return zero counts when no pending changes', async () => {
|
|
||||||
const count = await syncEngine.getPendingChangesCount();
|
|
||||||
|
|
||||||
expect(count.posts).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(count.media).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Sync Log', () => {
|
|
||||||
it('should return sync log array', async () => {
|
|
||||||
const logs = await syncEngine.getSyncLog();
|
|
||||||
|
|
||||||
expect(Array.isArray(logs)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should accept limit parameter', async () => {
|
|
||||||
const logs = await syncEngine.getSyncLog(10);
|
|
||||||
|
|
||||||
expect(Array.isArray(logs)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default limit of 50', async () => {
|
|
||||||
const logs = await syncEngine.getSyncLog();
|
|
||||||
|
|
||||||
expect(Array.isArray(logs)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Stop Auto Sync', () => {
|
|
||||||
it('should emit autoSyncStopped event', () => {
|
|
||||||
const handler = vi.fn();
|
|
||||||
syncEngine.on('autoSyncStopped', handler);
|
|
||||||
|
|
||||||
syncEngine.stopAutoSync();
|
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be safe to call multiple times', () => {
|
|
||||||
expect(() => {
|
|
||||||
syncEngine.stopAutoSync();
|
|
||||||
syncEngine.stopAutoSync();
|
|
||||||
syncEngine.stopAutoSync();
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -94,17 +94,6 @@ const mockProjectEngine = {
|
|||||||
getProjectPaths: vi.fn(),
|
getProjectPaths: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSyncEngine = {
|
|
||||||
on: vi.fn(),
|
|
||||||
configure: vi.fn(),
|
|
||||||
fullSync: vi.fn(),
|
|
||||||
getSyncStatus: vi.fn(),
|
|
||||||
isConfigured: vi.fn(),
|
|
||||||
getPendingChangesCount: vi.fn(),
|
|
||||||
getSyncLog: vi.fn(),
|
|
||||||
stopAutoSync: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMetaEngine = {
|
const mockMetaEngine = {
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
setProjectContext: vi.fn(),
|
setProjectContext: vi.fn(),
|
||||||
@@ -189,12 +178,6 @@ vi.mock('../../src/main/engine/ProjectEngine', () => ({
|
|||||||
ProjectData: {},
|
ProjectData: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../src/main/engine/SyncEngine', () => ({
|
|
||||||
getSyncEngine: vi.fn(() => mockSyncEngine),
|
|
||||||
SyncConfig: {},
|
|
||||||
SyncDirection: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../../src/main/engine/MetaEngine', () => ({
|
vi.mock('../../src/main/engine/MetaEngine', () => ({
|
||||||
getMetaEngine: vi.fn(() => mockMetaEngine),
|
getMetaEngine: vi.fn(() => mockMetaEngine),
|
||||||
}));
|
}));
|
||||||
@@ -709,105 +692,6 @@ describe('IPC Handlers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Sync Handlers ============
|
|
||||||
describe('Sync Handlers', () => {
|
|
||||||
describe('sync:configure', () => {
|
|
||||||
it('should configure sync with provided config', async () => {
|
|
||||||
const config = { provider: 'dropbox', accessToken: 'token123' };
|
|
||||||
mockSyncEngine.configure.mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await invokeHandler('sync:configure', config);
|
|
||||||
|
|
||||||
expect(mockSyncEngine.configure).toHaveBeenCalledWith(config);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:start', () => {
|
|
||||||
it('should start sync with default direction', async () => {
|
|
||||||
const syncResult = { success: true, uploaded: 5, downloaded: 3 };
|
|
||||||
mockSyncEngine.fullSync.mockResolvedValue(syncResult);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:start');
|
|
||||||
|
|
||||||
expect(mockSyncEngine.fullSync).toHaveBeenCalledWith('bidirectional');
|
|
||||||
expect(result).toEqual(syncResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start sync with specified direction', async () => {
|
|
||||||
mockSyncEngine.fullSync.mockResolvedValue({ success: true });
|
|
||||||
|
|
||||||
await invokeHandler('sync:start', 'upload');
|
|
||||||
|
|
||||||
expect(mockSyncEngine.fullSync).toHaveBeenCalledWith('upload');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:getStatus', () => {
|
|
||||||
it('should return current sync status', async () => {
|
|
||||||
const status = { state: 'idle', lastSync: new Date() };
|
|
||||||
mockSyncEngine.getSyncStatus.mockResolvedValue(status);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:getStatus');
|
|
||||||
|
|
||||||
expect(mockSyncEngine.getSyncStatus).toHaveBeenCalled();
|
|
||||||
expect(result).toEqual(status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:isConfigured', () => {
|
|
||||||
it('should return true when sync is configured', async () => {
|
|
||||||
mockSyncEngine.isConfigured.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:isConfigured');
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when sync is not configured', async () => {
|
|
||||||
mockSyncEngine.isConfigured.mockResolvedValue(false);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:isConfigured');
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:getPendingCount', () => {
|
|
||||||
it('should return count of pending changes', async () => {
|
|
||||||
mockSyncEngine.getPendingChangesCount.mockResolvedValue(42);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:getPendingCount');
|
|
||||||
|
|
||||||
expect(mockSyncEngine.getPendingChangesCount).toHaveBeenCalled();
|
|
||||||
expect(result).toBe(42);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:getLog', () => {
|
|
||||||
it('should return sync log entries', async () => {
|
|
||||||
const logEntries = [
|
|
||||||
{ timestamp: new Date(), action: 'upload', file: 'post.md' },
|
|
||||||
];
|
|
||||||
mockSyncEngine.getSyncLog.mockResolvedValue(logEntries);
|
|
||||||
|
|
||||||
const result = await invokeHandler('sync:getLog', 10);
|
|
||||||
|
|
||||||
expect(mockSyncEngine.getSyncLog).toHaveBeenCalledWith(10);
|
|
||||||
expect(result).toEqual(logEntries);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sync:stopAutoSync', () => {
|
|
||||||
it('should stop automatic sync', async () => {
|
|
||||||
mockSyncEngine.stopAutoSync.mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await invokeHandler('sync:stopAutoSync');
|
|
||||||
|
|
||||||
expect(mockSyncEngine.stopAutoSync).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============ Meta Handlers ============
|
// ============ Meta Handlers ============
|
||||||
describe('Meta Handlers', () => {
|
describe('Meta Handlers', () => {
|
||||||
describe('meta:getTags', () => {
|
describe('meta:getTags', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user