diff --git a/frontend/bindings/voidraft/internal/models/ent/models.ts b/frontend/bindings/voidraft/internal/models/ent/models.ts index 87dc9ae..cd43c8f 100644 --- a/frontend/bindings/voidraft/internal/models/ent/models.ts +++ b/frontend/bindings/voidraft/internal/models/ent/models.ts @@ -19,37 +19,45 @@ export class Document { "id"?: number; /** - * 创建时间 + * UUID for cross-device sync (UUIDv7) + */ + "uuid": string; + + /** + * creation time */ "created_at": string; /** - * 最后更新时间 + * update time */ "updated_at": string; /** - * 删除时间,NULL表示未删除 + * deleted at */ "deleted_at"?: string | null; /** - * 文档标题 + * document title */ "title": string; /** - * 文档内容 + * document content */ "content": string; /** - * 是否锁定 + * document locked status */ "locked": boolean; /** Creates a new Document instance. */ constructor($$source: Partial = {}) { + if (!("uuid" in $$source)) { + this["uuid"] = ""; + } if (!("created_at" in $$source)) { this["created_at"] = ""; } @@ -88,37 +96,45 @@ export class Extension { "id"?: number; /** - * 创建时间 + * UUID for cross-device sync (UUIDv7) + */ + "uuid": string; + + /** + * creation time */ "created_at": string; /** - * 最后更新时间 + * update time */ "updated_at": string; /** - * 删除时间,NULL表示未删除 + * deleted at */ "deleted_at"?: string | null; /** - * 扩展标识符 + * extension key */ "key": string; /** - * 是否启用 + * extension enabled or not */ "enabled": boolean; /** - * 扩展配置 + * extension config */ "config": { [_: string]: any }; /** Creates a new Extension instance. */ constructor($$source: Partial = {}) { + if (!("uuid" in $$source)) { + this["uuid"] = ""; + } if (!("created_at" in $$source)) { this["created_at"] = ""; } @@ -142,10 +158,10 @@ export class Extension { * Creates a new Extension instance from a string or object. */ static createFrom($$source: any = {}): Extension { - const $$createField6_0 = $$createType0; + const $$createField7_0 = $$createType0; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("config" in $$parsedSource) { - $$parsedSource["config"] = $$createField6_0($$parsedSource["config"]); + $$parsedSource["config"] = $$createField7_0($$parsedSource["config"]); } return new Extension($$parsedSource as Partial); } @@ -161,42 +177,50 @@ export class KeyBinding { "id"?: number; /** - * 创建时间 + * UUID for cross-device sync (UUIDv7) + */ + "uuid": string; + + /** + * creation time */ "created_at": string; /** - * 最后更新时间 + * update time */ "updated_at": string; /** - * 删除时间,NULL表示未删除 + * deleted at */ "deleted_at"?: string | null; /** - * 快捷键标识符 + * key binding key */ "key": string; /** - * 快捷键命令 + * key binding command */ "command": string; /** - * 所属扩展标识符 + * key binding extension */ "extension"?: string; /** - * 是否启用 + * key binding enabled */ "enabled": boolean; /** Creates a new KeyBinding instance. */ constructor($$source: Partial = {}) { + if (!("uuid" in $$source)) { + this["uuid"] = ""; + } if (!("created_at" in $$source)) { this["created_at"] = ""; } @@ -235,37 +259,45 @@ export class Theme { "id"?: number; /** - * 创建时间 + * UUID for cross-device sync (UUIDv7) + */ + "uuid": string; + + /** + * creation time */ "created_at": string; /** - * 最后更新时间 + * update time */ "updated_at": string; /** - * 删除时间,NULL表示未删除 + * deleted at */ "deleted_at"?: string | null; /** - * 主题标识符 + * theme key */ "key": string; /** - * 主题类型 + * theme type */ "type": theme$0.Type; /** - * 主题颜色配置 + * theme colors */ "colors": { [_: string]: any }; /** Creates a new Theme instance. */ constructor($$source: Partial = {}) { + if (!("uuid" in $$source)) { + this["uuid"] = ""; + } if (!("created_at" in $$source)) { this["created_at"] = ""; } @@ -289,10 +321,10 @@ export class Theme { * Creates a new Theme instance from a string or object. */ static createFrom($$source: any = {}): Theme { - const $$createField6_0 = $$createType0; + const $$createField7_0 = $$createType0; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("colors" in $$parsedSource) { - $$parsedSource["colors"] = $$createField6_0($$parsedSource["colors"]); + $$parsedSource["colors"] = $$createField7_0($$parsedSource["colors"]); } return new Theme($$parsedSource as Partial); } diff --git a/frontend/bindings/voidraft/internal/services/backupservice.ts b/frontend/bindings/voidraft/internal/services/backupservice.ts index 960822e..5ff03b8 100644 --- a/frontend/bindings/voidraft/internal/services/backupservice.ts +++ b/frontend/bindings/voidraft/internal/services/backupservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * BackupService 提供基于Git的备份功能 + * BackupService 提供基于Git的备份同步功能 * @module */ @@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic import * as models$0 from "../models/models.js"; /** - * HandleConfigChange 处理备份配置变更 + * HandleConfigChange 处理配置变更 */ export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(395287784, config) as any; @@ -34,15 +34,7 @@ export function Initialize(): Promise & { cancel(): void } { } /** - * PushToRemote 推送本地更改到远程仓库 - */ -export function PushToRemote(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(262644139) as any; - return $resultPromise; -} - -/** - * Reinitialize 重新初始化备份服务,用于响应配置变更 + * Reinitialize 重新初始化 */ export function Reinitialize(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(301562543) as any; @@ -50,7 +42,7 @@ export function Reinitialize(): Promise & { cancel(): void } { } /** - * ServiceShutdown 服务关闭时的清理工作 + * ServiceShutdown 服务关闭 */ export function ServiceShutdown(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(422131801) as any; @@ -63,7 +55,7 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(3035755449) as any; @@ -77,3 +69,11 @@ export function StopAutoBackup(): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(2641894021) as any; return $resultPromise; } + +/** + * Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入 + */ +export function Sync(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3420383211) as any; + return $resultPromise; +} diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 2868820..99b020a 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -46,7 +46,7 @@ export default { keybindings: { headers: { shortcut: 'Shortcut', - category: 'Category', + extension: 'Extension', description: 'Description' }, commands: { @@ -227,14 +227,10 @@ export default { sshKeyPassphrase: 'SSH Key Passphrase', sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase', backupOperations: 'Backup Operations', - pushToRemote: 'Push to Remote', - pushing: 'Pushing...', + syncToRemote: 'Sync to Remote', + syncing: 'Syncing...', actions: { - push: 'Push', - }, - status: { - success: 'Success', - failed: 'Failed' + sync: 'Sync', } }, }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 7192be7..87bbd0b 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -46,7 +46,7 @@ export default { keybindings: { headers: { shortcut: '快捷键', - category: '分类', + extension: '扩展', description: '描述' }, commands: { @@ -229,14 +229,10 @@ export default { sshKeyPassphrase: 'SSH密钥密码', sshKeyPassphrasePlaceholder: '请输入SSH密钥密码', backupOperations: '备份操作', - pushToRemote: '推送到远程', - pushing: '推送中...', + syncToRemote: '同步到远程', + syncing: '同步中...', actions: { - push: '推送', - }, - status: { - success: '成功', - failed: '失败' + sync: '同步', } }, }, diff --git a/frontend/src/stores/backupStore.ts b/frontend/src/stores/backupStore.ts index 530a877..8d13e36 100644 --- a/frontend/src/stores/backupStore.ts +++ b/frontend/src/stores/backupStore.ts @@ -1,49 +1,31 @@ import { defineStore } from 'pinia'; -import { ref, onScopeDispose } from 'vue'; +import { ref } from 'vue'; import { BackupService } from '@/../bindings/voidraft/internal/services'; -import { useConfigStore } from '@/stores/configStore'; -import { createTimerManager } from '@/common/utils/timerUtils'; export const useBackupStore = defineStore('backup', () => { - const isPushing = ref(false); - const message = ref(null); - const isError = ref(false); - - const timer = createTimerManager(); - const configStore = useConfigStore(); + const isSyncing = ref(false); + const error = ref(null); - onScopeDispose(() => timer.clear()); - - const pushToRemote = async () => { - const isConfigured = Boolean(configStore.config.backup.repo_url?.trim()); - - if (isPushing.value || !isConfigured) { + const sync = async (): Promise => { + if (isSyncing.value) { return; } + isSyncing.value = true; + error.value = null; + try { - isPushing.value = true; - message.value = null; - timer.clear(); - - await BackupService.PushToRemote(); - - isError.value = false; - message.value = 'push successful'; - timer.set(() => { message.value = null; }, 3000); - } catch (error) { - isError.value = true; - message.value = error instanceof Error ? error.message : 'backup operation failed'; - timer.set(() => { message.value = null; }, 5000); + await BackupService.Sync(); + } catch (e) { + error.value = e instanceof Error ? e.message : String(e); } finally { - isPushing.value = false; + isSyncing.value = false; } }; return { - isPushing, - message, - isError, - pushToRemote + isSyncing, + error, + sync }; }); \ No newline at end of file diff --git a/frontend/src/views/editor/keymap/index.ts b/frontend/src/views/editor/keymap/index.ts index d8e0466..7ea01aa 100644 --- a/frontend/src/views/editor/keymap/index.ts +++ b/frontend/src/views/editor/keymap/index.ts @@ -1,6 +1,5 @@ import { Extension } from '@codemirror/state'; import { useKeybindingStore } from '@/stores/keybindingStore'; -import { useExtensionStore } from '@/stores/extensionStore'; import { Manager } from './manager'; /** @@ -8,24 +7,13 @@ import { Manager } from './manager'; */ export const createDynamicKeymapExtension = async (): Promise => { const keybindingStore = useKeybindingStore(); - const extensionStore = useExtensionStore(); // 确保快捷键配置已加载 if (keybindingStore.keyBindings.length === 0) { await keybindingStore.loadKeyBindings(); } - // 确保扩展配置已加载 - if (extensionStore.extensions.length === 0) { - await extensionStore.loadExtensions(); - } - - // 获取启用的扩展key列表 - const enabledExtensionKeys = extensionStore.enabledExtensions - .map(ext => ext.key) - .filter((key): key is string => key !== undefined); - - return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionKeys); + return Manager.createKeymapExtension(keybindingStore.keyBindings); }; /** @@ -34,14 +22,7 @@ export const createDynamicKeymapExtension = async (): Promise => { */ export const updateKeymapExtension = (view: any): void => { const keybindingStore = useKeybindingStore(); - const extensionStore = useExtensionStore(); - - // 获取启用的扩展key列表 - const enabledExtensionKeys = extensionStore.enabledExtensions - .map(ext => ext.key) - .filter((key): key is string => key !== undefined); - - Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionKeys); + Manager.updateKeymap(view, keybindingStore.keyBindings); }; // 导出相关模块 diff --git a/frontend/src/views/editor/keymap/manager.ts b/frontend/src/views/editor/keymap/manager.ts index 7d1daf8..c92fd0e 100644 --- a/frontend/src/views/editor/keymap/manager.ts +++ b/frontend/src/views/editor/keymap/manager.ts @@ -14,10 +14,9 @@ export class Manager { /** * 将后端快捷键配置转换为CodeMirror快捷键绑定 * @param keyBindings 后端快捷键配置列表 - * @param enabledExtensions 启用的扩展key列表,如果不提供则使用所有启用的快捷键 * @returns 转换结果 */ - static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): KeymapResult { + static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeymapResult { const result: KeyBinding[] = []; for (const binding of keyBindings) { @@ -26,11 +25,6 @@ export class Manager { continue; } - // 如果提供了扩展列表,则只处理启用扩展的快捷键 - if (enabledExtensions && binding.extension && !enabledExtensions.includes(binding.extension)) { - continue; - } - // 检查命令是否已注册(使用 key 字段作为命令标识符) if (!binding.key || !isCommandRegistered(binding.key)) { continue; @@ -59,13 +53,10 @@ export class Manager { /** * 创建CodeMirror快捷键扩展 * @param keyBindings 后端快捷键配置列表 - * @param enabledExtensions 启用的扩展key列表 * @returns CodeMirror扩展 */ - static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: string[]): Extension { - const {keyBindings: cmKeyBindings} = - this.convertToKeyBindings(keyBindings, enabledExtensions); - + static createKeymapExtension(keyBindings: KeyBindingConfig[]): Extension { + const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings); return this.compartment.of(keymap.of(cmKeyBindings)); } @@ -73,12 +64,9 @@ export class Manager { * 动态更新快捷键扩展 * @param view 编辑器视图 * @param keyBindings 后端快捷键配置列表 - * @param enabledExtensions 启用的扩展key列表 */ - static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: string[]): void { - const {keyBindings: cmKeyBindings} = - this.convertToKeyBindings(keyBindings, enabledExtensions); - + static updateKeymap(view: any, keyBindings: KeyBindingConfig[]): void { + const {keyBindings: cmKeyBindings} = this.convertToKeyBindings(keyBindings); view.dispatch({ effects: this.compartment.reconfigure(keymap.of(cmKeyBindings)) }); diff --git a/frontend/src/views/settings/pages/BackupPage.vue b/frontend/src/views/settings/pages/BackupPage.vue index dbfc466..b7f4a17 100644 --- a/frontend/src/views/settings/pages/BackupPage.vue +++ b/frontend/src/views/settings/pages/BackupPage.vue @@ -2,7 +2,7 @@ import {useConfigStore} from '@/stores/configStore'; import {useBackupStore} from '@/stores/backupStore'; import {useI18n} from 'vue-i18n'; -import {computed} from 'vue'; +import {computed, ref, watch, onUnmounted} from 'vue'; import SettingSection from '../components/SettingSection.vue'; import SettingItem from '../components/SettingItem.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue'; @@ -13,6 +13,37 @@ const {t} = useI18n(); const configStore = useConfigStore(); const backupStore = useBackupStore(); +// 消息显示状态 +const message = ref(null); +const isError = ref(false); +let messageTimer: ReturnType | null = null; + +const clearMessage = () => { + if (messageTimer) { + clearTimeout(messageTimer); + messageTimer = null; + } + message.value = null; +}; + +// 监听同步完成,显示消息并自动消失 +watch(() => backupStore.isSyncing, (syncing, wasSyncing) => { + if (wasSyncing && !syncing) { + clearMessage(); + if (backupStore.error) { + message.value = backupStore.error; + isError.value = true; + messageTimer = setTimeout(clearMessage, 5000); + } else { + message.value = 'Sync successful'; + isError.value = false; + messageTimer = setTimeout(clearMessage, 3000); + } + } +}); + +onUnmounted(clearMessage); + const authMethodOptions = computed(() => [ {value: AuthMethod.Token, label: t('settings.backup.authMethods.token')}, {value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')}, @@ -172,18 +203,18 @@ const selectSshKeyFile = async () => { @@ -257,7 +288,7 @@ const selectSshKeyFile = async () => { } // 按钮样式 -.push-button { +.sync-button { padding: 8px 16px; background-color: var(--settings-input-bg); border: 1px solid var(--settings-input-border); @@ -294,7 +325,7 @@ const selectSshKeyFile = async () => { animation: spin 1s linear infinite; } - &.backing-up { + &.syncing { background-color: #2196f3; border-color: #2196f3; color: white; diff --git a/frontend/src/views/settings/pages/KeyBindingsPage.vue b/frontend/src/views/settings/pages/KeyBindingsPage.vue index 6c7c12f..516950c 100644 --- a/frontend/src/views/settings/pages/KeyBindingsPage.vue +++ b/frontend/src/views/settings/pages/KeyBindingsPage.vue @@ -3,33 +3,27 @@ import { useI18n } from 'vue-i18n'; import { onMounted, computed } from 'vue'; import SettingSection from '../components/SettingSection.vue'; import { useKeybindingStore } from '@/stores/keybindingStore'; -import { useExtensionStore } from '@/stores/extensionStore'; import { useSystemStore } from '@/stores/systemStore'; import { getCommandDescription } from '@/views/editor/keymap/commands'; import { KeyBindingKey } from '@/../bindings/voidraft/internal/models/models'; const { t } = useI18n(); const keybindingStore = useKeybindingStore(); -const extensionStore = useExtensionStore(); const systemStore = useSystemStore(); // 加载数据 onMounted(async () => { await keybindingStore.loadKeyBindings(); - await extensionStore.loadExtensions(); }); // 从store中获取快捷键数据并转换为显示格式 const keyBindings = computed(() => { - // 只显示启用扩展的快捷键 - const enabledExtensionIds = new Set(extensionStore.enabledExtensionIds); - return keybindingStore.keyBindings - .filter(kb => kb.enabled && (!kb.extension || enabledExtensionIds.has(kb.extension))) + .filter(kb => kb.enabled) .map(kb => ({ - id: kb.key, - keys: parseKeyBinding(kb.command || '', kb.key), - category: kb.extension || '', + key: kb.key, + command: parseKeyBinding(kb.command || '', kb.key), + extension: kb.extension || '', description: kb.key ? (getCommandDescription(kb.key) || kb.key) : '' })); }); @@ -204,25 +198,25 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => {
{{ t('keybindings.headers.shortcut') }}
-
{{ t('keybindings.headers.category') }}
+
{{ t('keybindings.headers.extension') }}
{{ t('keybindings.headers.description') }}
{{ key }}
-
{{ binding.category }}
+
{{ binding.extension }}
{{ binding.description }}
@@ -276,7 +270,7 @@ const parseKeyBinding = (keyStr: string, keyBindingKey?: string): string[] => { } } - .category-col { + .extension-col { width: 80px; padding: 0 10px 0 0; font-size: 13px; diff --git a/go.mod b/go.mod index e685af0..6318606 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( entgo.io/ent v0.14.5 github.com/creativeprojects/go-selfupdate v1.5.1 github.com/go-git/go-git/v5 v5.16.3 + github.com/google/uuid v1.6.0 github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/structs v1.0.0 @@ -52,7 +53,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect diff --git a/internal/models/config.go b/internal/models/config.go index eafdc73..87e3acc 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -208,7 +208,7 @@ func NewDefaultAppConfig() *AppConfig { }, Appearance: AppearanceConfig{ Language: LangEnUS, - SystemTheme: SystemThemeAuto, + SystemTheme: SystemThemeDark, CurrentTheme: "default-dark", // 默认使用 default-dark 主题 }, Updates: UpdatesConfig{ diff --git a/internal/models/ent/document.go b/internal/models/ent/document.go index 5d94438..536cfbe 100644 --- a/internal/models/ent/document.go +++ b/internal/models/ent/document.go @@ -16,17 +16,19 @@ type Document struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // 创建时间 + // UUID for cross-device sync (UUIDv7) + UUID string `json:"uuid"` + // creation time CreatedAt string `json:"created_at"` - // 最后更新时间 + // update time UpdatedAt string `json:"updated_at"` - // 删除时间,NULL表示未删除 + // deleted at DeletedAt *string `json:"deleted_at,omitempty"` - // 文档标题 + // document title Title string `json:"title"` - // 文档内容 + // document content Content string `json:"content"` - // 是否锁定 + // document locked status Locked bool `json:"locked"` selectValues sql.SelectValues } @@ -40,7 +42,7 @@ func (*Document) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case document.FieldID: values[i] = new(sql.NullInt64) - case document.FieldCreatedAt, document.FieldUpdatedAt, document.FieldDeletedAt, document.FieldTitle, document.FieldContent: + case document.FieldUUID, document.FieldCreatedAt, document.FieldUpdatedAt, document.FieldDeletedAt, document.FieldTitle, document.FieldContent: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -63,6 +65,12 @@ func (_m *Document) assignValues(columns []string, values []any) error { return fmt.Errorf("unexpected type %T for field id", value) } _m.ID = int(value.Int64) + case document.FieldUUID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field uuid", values[i]) + } else if value.Valid { + _m.UUID = value.String + } case document.FieldCreatedAt: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -136,6 +144,9 @@ func (_m *Document) String() string { var builder strings.Builder builder.WriteString("Document(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("uuid=") + builder.WriteString(_m.UUID) + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(_m.CreatedAt) builder.WriteString(", ") diff --git a/internal/models/ent/document/document.go b/internal/models/ent/document/document.go index 96c36da..c313bcc 100644 --- a/internal/models/ent/document/document.go +++ b/internal/models/ent/document/document.go @@ -12,6 +12,8 @@ const ( Label = "document" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldUUID holds the string denoting the uuid field in the database. + FieldUUID = "uuid" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. @@ -31,6 +33,7 @@ const ( // Columns holds all SQL columns for document fields. var Columns = []string{ FieldID, + FieldUUID, FieldCreatedAt, FieldUpdatedAt, FieldDeletedAt, @@ -57,6 +60,8 @@ func ValidColumn(column string) bool { var ( Hooks [2]ent.Hook Interceptors [1]ent.Interceptor + // DefaultUUID holds the default value on creation for the "uuid" field. + DefaultUUID func() string // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() string // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -77,6 +82,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// ByUUID orders the results by the uuid field. +func ByUUID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUUID, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/internal/models/ent/document/where.go b/internal/models/ent/document/where.go index f6dd854..8e5460a 100644 --- a/internal/models/ent/document/where.go +++ b/internal/models/ent/document/where.go @@ -53,6 +53,11 @@ func IDLTE(id int) predicate.Document { return predicate.Document(sql.FieldLTE(FieldID, id)) } +// UUID applies equality check predicate on the "uuid" field. It's identical to UUIDEQ. +func UUID(v string) predicate.Document { + return predicate.Document(sql.FieldEQ(FieldUUID, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v string) predicate.Document { return predicate.Document(sql.FieldEQ(FieldCreatedAt, v)) @@ -83,6 +88,81 @@ func Locked(v bool) predicate.Document { return predicate.Document(sql.FieldEQ(FieldLocked, v)) } +// UUIDEQ applies the EQ predicate on the "uuid" field. +func UUIDEQ(v string) predicate.Document { + return predicate.Document(sql.FieldEQ(FieldUUID, v)) +} + +// UUIDNEQ applies the NEQ predicate on the "uuid" field. +func UUIDNEQ(v string) predicate.Document { + return predicate.Document(sql.FieldNEQ(FieldUUID, v)) +} + +// UUIDIn applies the In predicate on the "uuid" field. +func UUIDIn(vs ...string) predicate.Document { + return predicate.Document(sql.FieldIn(FieldUUID, vs...)) +} + +// UUIDNotIn applies the NotIn predicate on the "uuid" field. +func UUIDNotIn(vs ...string) predicate.Document { + return predicate.Document(sql.FieldNotIn(FieldUUID, vs...)) +} + +// UUIDGT applies the GT predicate on the "uuid" field. +func UUIDGT(v string) predicate.Document { + return predicate.Document(sql.FieldGT(FieldUUID, v)) +} + +// UUIDGTE applies the GTE predicate on the "uuid" field. +func UUIDGTE(v string) predicate.Document { + return predicate.Document(sql.FieldGTE(FieldUUID, v)) +} + +// UUIDLT applies the LT predicate on the "uuid" field. +func UUIDLT(v string) predicate.Document { + return predicate.Document(sql.FieldLT(FieldUUID, v)) +} + +// UUIDLTE applies the LTE predicate on the "uuid" field. +func UUIDLTE(v string) predicate.Document { + return predicate.Document(sql.FieldLTE(FieldUUID, v)) +} + +// UUIDContains applies the Contains predicate on the "uuid" field. +func UUIDContains(v string) predicate.Document { + return predicate.Document(sql.FieldContains(FieldUUID, v)) +} + +// UUIDHasPrefix applies the HasPrefix predicate on the "uuid" field. +func UUIDHasPrefix(v string) predicate.Document { + return predicate.Document(sql.FieldHasPrefix(FieldUUID, v)) +} + +// UUIDHasSuffix applies the HasSuffix predicate on the "uuid" field. +func UUIDHasSuffix(v string) predicate.Document { + return predicate.Document(sql.FieldHasSuffix(FieldUUID, v)) +} + +// UUIDIsNil applies the IsNil predicate on the "uuid" field. +func UUIDIsNil() predicate.Document { + return predicate.Document(sql.FieldIsNull(FieldUUID)) +} + +// UUIDNotNil applies the NotNil predicate on the "uuid" field. +func UUIDNotNil() predicate.Document { + return predicate.Document(sql.FieldNotNull(FieldUUID)) +} + +// UUIDEqualFold applies the EqualFold predicate on the "uuid" field. +func UUIDEqualFold(v string) predicate.Document { + return predicate.Document(sql.FieldEqualFold(FieldUUID, v)) +} + +// UUIDContainsFold applies the ContainsFold predicate on the "uuid" field. +func UUIDContainsFold(v string) predicate.Document { + return predicate.Document(sql.FieldContainsFold(FieldUUID, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v string) predicate.Document { return predicate.Document(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/internal/models/ent/document_create.go b/internal/models/ent/document_create.go index 08e1d76..4554dd3 100644 --- a/internal/models/ent/document_create.go +++ b/internal/models/ent/document_create.go @@ -19,6 +19,20 @@ type DocumentCreate struct { hooks []Hook } +// SetUUID sets the "uuid" field. +func (_c *DocumentCreate) SetUUID(v string) *DocumentCreate { + _c.mutation.SetUUID(v) + return _c +} + +// SetNillableUUID sets the "uuid" field if the given value is not nil. +func (_c *DocumentCreate) SetNillableUUID(v *string) *DocumentCreate { + if v != nil { + _c.SetUUID(*v) + } + return _c +} + // SetCreatedAt sets the "created_at" field. func (_c *DocumentCreate) SetCreatedAt(v string) *DocumentCreate { _c.mutation.SetCreatedAt(v) @@ -132,6 +146,13 @@ func (_c *DocumentCreate) ExecX(ctx context.Context) { // defaults sets the default values of the builder before save. func (_c *DocumentCreate) defaults() error { + if _, ok := _c.mutation.UUID(); !ok { + if document.DefaultUUID == nil { + return fmt.Errorf("ent: uninitialized document.DefaultUUID (forgotten import ent/runtime?)") + } + v := document.DefaultUUID() + _c.mutation.SetUUID(v) + } if _, ok := _c.mutation.CreatedAt(); !ok { if document.DefaultCreatedAt == nil { return fmt.Errorf("ent: uninitialized document.DefaultCreatedAt (forgotten import ent/runtime?)") @@ -202,6 +223,10 @@ func (_c *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) { _node = &Document{config: _c.config} _spec = sqlgraph.NewCreateSpec(document.Table, sqlgraph.NewFieldSpec(document.FieldID, field.TypeInt)) ) + if value, ok := _c.mutation.UUID(); ok { + _spec.SetField(document.FieldUUID, field.TypeString, value) + _node.UUID = value + } if value, ok := _c.mutation.CreatedAt(); ok { _spec.SetField(document.FieldCreatedAt, field.TypeString, value) _node.CreatedAt = value diff --git a/internal/models/ent/document_query.go b/internal/models/ent/document_query.go index 90513f4..0ab92b9 100644 --- a/internal/models/ent/document_query.go +++ b/internal/models/ent/document_query.go @@ -265,12 +265,12 @@ func (_q *DocumentQuery) Clone() *DocumentQuery { // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // Count int `json:"count,omitempty"` // } // // client.Document.Query(). -// GroupBy(document.FieldCreatedAt). +// GroupBy(document.FieldUUID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (_q *DocumentQuery) GroupBy(field string, fields ...string) *DocumentGroupBy { @@ -288,11 +288,11 @@ func (_q *DocumentQuery) GroupBy(field string, fields ...string) *DocumentGroupB // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // } // // client.Document.Query(). -// Select(document.FieldCreatedAt). +// Select(document.FieldUUID). // Scan(ctx, &v) func (_q *DocumentQuery) Select(fields ...string) *DocumentSelect { _q.ctx.Fields = append(_q.ctx.Fields, fields...) diff --git a/internal/models/ent/document_update.go b/internal/models/ent/document_update.go index 8f35cba..5b2a443 100644 --- a/internal/models/ent/document_update.go +++ b/internal/models/ent/document_update.go @@ -170,6 +170,9 @@ func (_u *DocumentUpdate) sqlSave(ctx context.Context) (_node int, err error) { } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(document.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(document.FieldUpdatedAt, field.TypeString, value) } @@ -385,6 +388,9 @@ func (_u *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(document.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(document.FieldUpdatedAt, field.TypeString, value) } diff --git a/internal/models/ent/entql.go b/internal/models/ent/entql.go index f6f465d..2682906 100644 --- a/internal/models/ent/entql.go +++ b/internal/models/ent/entql.go @@ -28,6 +28,7 @@ var schemaGraph = func() *sqlgraph.Schema { }, Type: "Document", Fields: map[string]*sqlgraph.FieldSpec{ + document.FieldUUID: {Type: field.TypeString, Column: document.FieldUUID}, document.FieldCreatedAt: {Type: field.TypeString, Column: document.FieldCreatedAt}, document.FieldUpdatedAt: {Type: field.TypeString, Column: document.FieldUpdatedAt}, document.FieldDeletedAt: {Type: field.TypeString, Column: document.FieldDeletedAt}, @@ -47,6 +48,7 @@ var schemaGraph = func() *sqlgraph.Schema { }, Type: "Extension", Fields: map[string]*sqlgraph.FieldSpec{ + extension.FieldUUID: {Type: field.TypeString, Column: extension.FieldUUID}, extension.FieldCreatedAt: {Type: field.TypeString, Column: extension.FieldCreatedAt}, extension.FieldUpdatedAt: {Type: field.TypeString, Column: extension.FieldUpdatedAt}, extension.FieldDeletedAt: {Type: field.TypeString, Column: extension.FieldDeletedAt}, @@ -66,6 +68,7 @@ var schemaGraph = func() *sqlgraph.Schema { }, Type: "KeyBinding", Fields: map[string]*sqlgraph.FieldSpec{ + keybinding.FieldUUID: {Type: field.TypeString, Column: keybinding.FieldUUID}, keybinding.FieldCreatedAt: {Type: field.TypeString, Column: keybinding.FieldCreatedAt}, keybinding.FieldUpdatedAt: {Type: field.TypeString, Column: keybinding.FieldUpdatedAt}, keybinding.FieldDeletedAt: {Type: field.TypeString, Column: keybinding.FieldDeletedAt}, @@ -86,6 +89,7 @@ var schemaGraph = func() *sqlgraph.Schema { }, Type: "Theme", Fields: map[string]*sqlgraph.FieldSpec{ + theme.FieldUUID: {Type: field.TypeString, Column: theme.FieldUUID}, theme.FieldCreatedAt: {Type: field.TypeString, Column: theme.FieldCreatedAt}, theme.FieldUpdatedAt: {Type: field.TypeString, Column: theme.FieldUpdatedAt}, theme.FieldDeletedAt: {Type: field.TypeString, Column: theme.FieldDeletedAt}, @@ -143,6 +147,11 @@ func (f *DocumentFilter) WhereID(p entql.IntP) { f.Where(p.Field(document.FieldID)) } +// WhereUUID applies the entql string predicate on the uuid field. +func (f *DocumentFilter) WhereUUID(p entql.StringP) { + f.Where(p.Field(document.FieldUUID)) +} + // WhereCreatedAt applies the entql string predicate on the created_at field. func (f *DocumentFilter) WhereCreatedAt(p entql.StringP) { f.Where(p.Field(document.FieldCreatedAt)) @@ -213,6 +222,11 @@ func (f *ExtensionFilter) WhereID(p entql.IntP) { f.Where(p.Field(extension.FieldID)) } +// WhereUUID applies the entql string predicate on the uuid field. +func (f *ExtensionFilter) WhereUUID(p entql.StringP) { + f.Where(p.Field(extension.FieldUUID)) +} + // WhereCreatedAt applies the entql string predicate on the created_at field. func (f *ExtensionFilter) WhereCreatedAt(p entql.StringP) { f.Where(p.Field(extension.FieldCreatedAt)) @@ -283,6 +297,11 @@ func (f *KeyBindingFilter) WhereID(p entql.IntP) { f.Where(p.Field(keybinding.FieldID)) } +// WhereUUID applies the entql string predicate on the uuid field. +func (f *KeyBindingFilter) WhereUUID(p entql.StringP) { + f.Where(p.Field(keybinding.FieldUUID)) +} + // WhereCreatedAt applies the entql string predicate on the created_at field. func (f *KeyBindingFilter) WhereCreatedAt(p entql.StringP) { f.Where(p.Field(keybinding.FieldCreatedAt)) @@ -358,6 +377,11 @@ func (f *ThemeFilter) WhereID(p entql.IntP) { f.Where(p.Field(theme.FieldID)) } +// WhereUUID applies the entql string predicate on the uuid field. +func (f *ThemeFilter) WhereUUID(p entql.StringP) { + f.Where(p.Field(theme.FieldUUID)) +} + // WhereCreatedAt applies the entql string predicate on the created_at field. func (f *ThemeFilter) WhereCreatedAt(p entql.StringP) { f.Where(p.Field(theme.FieldCreatedAt)) diff --git a/internal/models/ent/extension.go b/internal/models/ent/extension.go index 7e9fc7f..acb9495 100644 --- a/internal/models/ent/extension.go +++ b/internal/models/ent/extension.go @@ -17,17 +17,19 @@ type Extension struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // 创建时间 + // UUID for cross-device sync (UUIDv7) + UUID string `json:"uuid"` + // creation time CreatedAt string `json:"created_at"` - // 最后更新时间 + // update time UpdatedAt string `json:"updated_at"` - // 删除时间,NULL表示未删除 + // deleted at DeletedAt *string `json:"deleted_at,omitempty"` - // 扩展标识符 + // extension key Key string `json:"key"` - // 是否启用 + // extension enabled or not Enabled bool `json:"enabled"` - // 扩展配置 + // extension config Config map[string]interface{} `json:"config"` selectValues sql.SelectValues } @@ -43,7 +45,7 @@ func (*Extension) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case extension.FieldID: values[i] = new(sql.NullInt64) - case extension.FieldCreatedAt, extension.FieldUpdatedAt, extension.FieldDeletedAt, extension.FieldKey: + case extension.FieldUUID, extension.FieldCreatedAt, extension.FieldUpdatedAt, extension.FieldDeletedAt, extension.FieldKey: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -66,6 +68,12 @@ func (_m *Extension) assignValues(columns []string, values []any) error { return fmt.Errorf("unexpected type %T for field id", value) } _m.ID = int(value.Int64) + case extension.FieldUUID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field uuid", values[i]) + } else if value.Valid { + _m.UUID = value.String + } case extension.FieldCreatedAt: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -141,6 +149,9 @@ func (_m *Extension) String() string { var builder strings.Builder builder.WriteString("Extension(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("uuid=") + builder.WriteString(_m.UUID) + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(_m.CreatedAt) builder.WriteString(", ") diff --git a/internal/models/ent/extension/extension.go b/internal/models/ent/extension/extension.go index a14bfbf..c0df3ee 100644 --- a/internal/models/ent/extension/extension.go +++ b/internal/models/ent/extension/extension.go @@ -12,6 +12,8 @@ const ( Label = "extension" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldUUID holds the string denoting the uuid field in the database. + FieldUUID = "uuid" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. @@ -31,6 +33,7 @@ const ( // Columns holds all SQL columns for extension fields. var Columns = []string{ FieldID, + FieldUUID, FieldCreatedAt, FieldUpdatedAt, FieldDeletedAt, @@ -57,6 +60,8 @@ func ValidColumn(column string) bool { var ( Hooks [2]ent.Hook Interceptors [1]ent.Interceptor + // DefaultUUID holds the default value on creation for the "uuid" field. + DefaultUUID func() string // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() string // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -75,6 +80,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// ByUUID orders the results by the uuid field. +func ByUUID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUUID, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/internal/models/ent/extension/where.go b/internal/models/ent/extension/where.go index 97ad797..4906bf1 100644 --- a/internal/models/ent/extension/where.go +++ b/internal/models/ent/extension/where.go @@ -53,6 +53,11 @@ func IDLTE(id int) predicate.Extension { return predicate.Extension(sql.FieldLTE(FieldID, id)) } +// UUID applies equality check predicate on the "uuid" field. It's identical to UUIDEQ. +func UUID(v string) predicate.Extension { + return predicate.Extension(sql.FieldEQ(FieldUUID, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v string) predicate.Extension { return predicate.Extension(sql.FieldEQ(FieldCreatedAt, v)) @@ -78,6 +83,81 @@ func Enabled(v bool) predicate.Extension { return predicate.Extension(sql.FieldEQ(FieldEnabled, v)) } +// UUIDEQ applies the EQ predicate on the "uuid" field. +func UUIDEQ(v string) predicate.Extension { + return predicate.Extension(sql.FieldEQ(FieldUUID, v)) +} + +// UUIDNEQ applies the NEQ predicate on the "uuid" field. +func UUIDNEQ(v string) predicate.Extension { + return predicate.Extension(sql.FieldNEQ(FieldUUID, v)) +} + +// UUIDIn applies the In predicate on the "uuid" field. +func UUIDIn(vs ...string) predicate.Extension { + return predicate.Extension(sql.FieldIn(FieldUUID, vs...)) +} + +// UUIDNotIn applies the NotIn predicate on the "uuid" field. +func UUIDNotIn(vs ...string) predicate.Extension { + return predicate.Extension(sql.FieldNotIn(FieldUUID, vs...)) +} + +// UUIDGT applies the GT predicate on the "uuid" field. +func UUIDGT(v string) predicate.Extension { + return predicate.Extension(sql.FieldGT(FieldUUID, v)) +} + +// UUIDGTE applies the GTE predicate on the "uuid" field. +func UUIDGTE(v string) predicate.Extension { + return predicate.Extension(sql.FieldGTE(FieldUUID, v)) +} + +// UUIDLT applies the LT predicate on the "uuid" field. +func UUIDLT(v string) predicate.Extension { + return predicate.Extension(sql.FieldLT(FieldUUID, v)) +} + +// UUIDLTE applies the LTE predicate on the "uuid" field. +func UUIDLTE(v string) predicate.Extension { + return predicate.Extension(sql.FieldLTE(FieldUUID, v)) +} + +// UUIDContains applies the Contains predicate on the "uuid" field. +func UUIDContains(v string) predicate.Extension { + return predicate.Extension(sql.FieldContains(FieldUUID, v)) +} + +// UUIDHasPrefix applies the HasPrefix predicate on the "uuid" field. +func UUIDHasPrefix(v string) predicate.Extension { + return predicate.Extension(sql.FieldHasPrefix(FieldUUID, v)) +} + +// UUIDHasSuffix applies the HasSuffix predicate on the "uuid" field. +func UUIDHasSuffix(v string) predicate.Extension { + return predicate.Extension(sql.FieldHasSuffix(FieldUUID, v)) +} + +// UUIDIsNil applies the IsNil predicate on the "uuid" field. +func UUIDIsNil() predicate.Extension { + return predicate.Extension(sql.FieldIsNull(FieldUUID)) +} + +// UUIDNotNil applies the NotNil predicate on the "uuid" field. +func UUIDNotNil() predicate.Extension { + return predicate.Extension(sql.FieldNotNull(FieldUUID)) +} + +// UUIDEqualFold applies the EqualFold predicate on the "uuid" field. +func UUIDEqualFold(v string) predicate.Extension { + return predicate.Extension(sql.FieldEqualFold(FieldUUID, v)) +} + +// UUIDContainsFold applies the ContainsFold predicate on the "uuid" field. +func UUIDContainsFold(v string) predicate.Extension { + return predicate.Extension(sql.FieldContainsFold(FieldUUID, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v string) predicate.Extension { return predicate.Extension(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/internal/models/ent/extension_create.go b/internal/models/ent/extension_create.go index bf9e5e9..f71562d 100644 --- a/internal/models/ent/extension_create.go +++ b/internal/models/ent/extension_create.go @@ -19,6 +19,20 @@ type ExtensionCreate struct { hooks []Hook } +// SetUUID sets the "uuid" field. +func (_c *ExtensionCreate) SetUUID(v string) *ExtensionCreate { + _c.mutation.SetUUID(v) + return _c +} + +// SetNillableUUID sets the "uuid" field if the given value is not nil. +func (_c *ExtensionCreate) SetNillableUUID(v *string) *ExtensionCreate { + if v != nil { + _c.SetUUID(*v) + } + return _c +} + // SetCreatedAt sets the "created_at" field. func (_c *ExtensionCreate) SetCreatedAt(v string) *ExtensionCreate { _c.mutation.SetCreatedAt(v) @@ -124,6 +138,13 @@ func (_c *ExtensionCreate) ExecX(ctx context.Context) { // defaults sets the default values of the builder before save. func (_c *ExtensionCreate) defaults() error { + if _, ok := _c.mutation.UUID(); !ok { + if extension.DefaultUUID == nil { + return fmt.Errorf("ent: uninitialized extension.DefaultUUID (forgotten import ent/runtime?)") + } + v := extension.DefaultUUID() + _c.mutation.SetUUID(v) + } if _, ok := _c.mutation.CreatedAt(); !ok { if extension.DefaultCreatedAt == nil { return fmt.Errorf("ent: uninitialized extension.DefaultCreatedAt (forgotten import ent/runtime?)") @@ -190,6 +211,10 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) { _node = &Extension{config: _c.config} _spec = sqlgraph.NewCreateSpec(extension.Table, sqlgraph.NewFieldSpec(extension.FieldID, field.TypeInt)) ) + if value, ok := _c.mutation.UUID(); ok { + _spec.SetField(extension.FieldUUID, field.TypeString, value) + _node.UUID = value + } if value, ok := _c.mutation.CreatedAt(); ok { _spec.SetField(extension.FieldCreatedAt, field.TypeString, value) _node.CreatedAt = value diff --git a/internal/models/ent/extension_query.go b/internal/models/ent/extension_query.go index 2aa0341..0a83a59 100644 --- a/internal/models/ent/extension_query.go +++ b/internal/models/ent/extension_query.go @@ -265,12 +265,12 @@ func (_q *ExtensionQuery) Clone() *ExtensionQuery { // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // Count int `json:"count,omitempty"` // } // // client.Extension.Query(). -// GroupBy(extension.FieldCreatedAt). +// GroupBy(extension.FieldUUID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (_q *ExtensionQuery) GroupBy(field string, fields ...string) *ExtensionGroupBy { @@ -288,11 +288,11 @@ func (_q *ExtensionQuery) GroupBy(field string, fields ...string) *ExtensionGrou // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // } // // client.Extension.Query(). -// Select(extension.FieldCreatedAt). +// Select(extension.FieldUUID). // Scan(ctx, &v) func (_q *ExtensionQuery) Select(fields ...string) *ExtensionSelect { _q.ctx.Fields = append(_q.ctx.Fields, fields...) diff --git a/internal/models/ent/extension_update.go b/internal/models/ent/extension_update.go index 5185c09..896b889 100644 --- a/internal/models/ent/extension_update.go +++ b/internal/models/ent/extension_update.go @@ -162,6 +162,9 @@ func (_u *ExtensionUpdate) sqlSave(ctx context.Context) (_node int, err error) { } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(extension.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(extension.FieldUpdatedAt, field.TypeString, value) } @@ -369,6 +372,9 @@ func (_u *ExtensionUpdateOne) sqlSave(ctx context.Context) (_node *Extension, er } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(extension.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(extension.FieldUpdatedAt, field.TypeString, value) } diff --git a/internal/models/ent/keybinding.go b/internal/models/ent/keybinding.go index b053131..37479a7 100644 --- a/internal/models/ent/keybinding.go +++ b/internal/models/ent/keybinding.go @@ -16,19 +16,21 @@ type KeyBinding struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // 创建时间 + // UUID for cross-device sync (UUIDv7) + UUID string `json:"uuid"` + // creation time CreatedAt string `json:"created_at"` - // 最后更新时间 + // update time UpdatedAt string `json:"updated_at"` - // 删除时间,NULL表示未删除 + // deleted at DeletedAt *string `json:"deleted_at,omitempty"` - // 快捷键标识符 + // key binding key Key string `json:"key"` - // 快捷键命令 + // key binding command Command string `json:"command"` - // 所属扩展标识符 + // key binding extension Extension string `json:"extension,omitempty"` - // 是否启用 + // key binding enabled Enabled bool `json:"enabled"` selectValues sql.SelectValues } @@ -42,7 +44,7 @@ func (*KeyBinding) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case keybinding.FieldID: values[i] = new(sql.NullInt64) - case keybinding.FieldCreatedAt, keybinding.FieldUpdatedAt, keybinding.FieldDeletedAt, keybinding.FieldKey, keybinding.FieldCommand, keybinding.FieldExtension: + case keybinding.FieldUUID, keybinding.FieldCreatedAt, keybinding.FieldUpdatedAt, keybinding.FieldDeletedAt, keybinding.FieldKey, keybinding.FieldCommand, keybinding.FieldExtension: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -65,6 +67,12 @@ func (_m *KeyBinding) assignValues(columns []string, values []any) error { return fmt.Errorf("unexpected type %T for field id", value) } _m.ID = int(value.Int64) + case keybinding.FieldUUID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field uuid", values[i]) + } else if value.Valid { + _m.UUID = value.String + } case keybinding.FieldCreatedAt: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -144,6 +152,9 @@ func (_m *KeyBinding) String() string { var builder strings.Builder builder.WriteString("KeyBinding(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("uuid=") + builder.WriteString(_m.UUID) + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(_m.CreatedAt) builder.WriteString(", ") diff --git a/internal/models/ent/keybinding/keybinding.go b/internal/models/ent/keybinding/keybinding.go index f7b198a..926d148 100644 --- a/internal/models/ent/keybinding/keybinding.go +++ b/internal/models/ent/keybinding/keybinding.go @@ -12,6 +12,8 @@ const ( Label = "key_binding" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldUUID holds the string denoting the uuid field in the database. + FieldUUID = "uuid" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. @@ -33,6 +35,7 @@ const ( // Columns holds all SQL columns for keybinding fields. var Columns = []string{ FieldID, + FieldUUID, FieldCreatedAt, FieldUpdatedAt, FieldDeletedAt, @@ -60,6 +63,8 @@ func ValidColumn(column string) bool { var ( Hooks [2]ent.Hook Interceptors [1]ent.Interceptor + // DefaultUUID holds the default value on creation for the "uuid" field. + DefaultUUID func() string // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() string // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -82,6 +87,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// ByUUID orders the results by the uuid field. +func ByUUID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUUID, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/internal/models/ent/keybinding/where.go b/internal/models/ent/keybinding/where.go index 1e28be4..3a671e2 100644 --- a/internal/models/ent/keybinding/where.go +++ b/internal/models/ent/keybinding/where.go @@ -53,6 +53,11 @@ func IDLTE(id int) predicate.KeyBinding { return predicate.KeyBinding(sql.FieldLTE(FieldID, id)) } +// UUID applies equality check predicate on the "uuid" field. It's identical to UUIDEQ. +func UUID(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldEQ(FieldUUID, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v string) predicate.KeyBinding { return predicate.KeyBinding(sql.FieldEQ(FieldCreatedAt, v)) @@ -88,6 +93,81 @@ func Enabled(v bool) predicate.KeyBinding { return predicate.KeyBinding(sql.FieldEQ(FieldEnabled, v)) } +// UUIDEQ applies the EQ predicate on the "uuid" field. +func UUIDEQ(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldEQ(FieldUUID, v)) +} + +// UUIDNEQ applies the NEQ predicate on the "uuid" field. +func UUIDNEQ(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldNEQ(FieldUUID, v)) +} + +// UUIDIn applies the In predicate on the "uuid" field. +func UUIDIn(vs ...string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldIn(FieldUUID, vs...)) +} + +// UUIDNotIn applies the NotIn predicate on the "uuid" field. +func UUIDNotIn(vs ...string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldNotIn(FieldUUID, vs...)) +} + +// UUIDGT applies the GT predicate on the "uuid" field. +func UUIDGT(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldGT(FieldUUID, v)) +} + +// UUIDGTE applies the GTE predicate on the "uuid" field. +func UUIDGTE(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldGTE(FieldUUID, v)) +} + +// UUIDLT applies the LT predicate on the "uuid" field. +func UUIDLT(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldLT(FieldUUID, v)) +} + +// UUIDLTE applies the LTE predicate on the "uuid" field. +func UUIDLTE(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldLTE(FieldUUID, v)) +} + +// UUIDContains applies the Contains predicate on the "uuid" field. +func UUIDContains(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldContains(FieldUUID, v)) +} + +// UUIDHasPrefix applies the HasPrefix predicate on the "uuid" field. +func UUIDHasPrefix(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldHasPrefix(FieldUUID, v)) +} + +// UUIDHasSuffix applies the HasSuffix predicate on the "uuid" field. +func UUIDHasSuffix(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldHasSuffix(FieldUUID, v)) +} + +// UUIDIsNil applies the IsNil predicate on the "uuid" field. +func UUIDIsNil() predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldIsNull(FieldUUID)) +} + +// UUIDNotNil applies the NotNil predicate on the "uuid" field. +func UUIDNotNil() predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldNotNull(FieldUUID)) +} + +// UUIDEqualFold applies the EqualFold predicate on the "uuid" field. +func UUIDEqualFold(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldEqualFold(FieldUUID, v)) +} + +// UUIDContainsFold applies the ContainsFold predicate on the "uuid" field. +func UUIDContainsFold(v string) predicate.KeyBinding { + return predicate.KeyBinding(sql.FieldContainsFold(FieldUUID, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v string) predicate.KeyBinding { return predicate.KeyBinding(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/internal/models/ent/keybinding_create.go b/internal/models/ent/keybinding_create.go index 40727b0..f649080 100644 --- a/internal/models/ent/keybinding_create.go +++ b/internal/models/ent/keybinding_create.go @@ -19,6 +19,20 @@ type KeyBindingCreate struct { hooks []Hook } +// SetUUID sets the "uuid" field. +func (_c *KeyBindingCreate) SetUUID(v string) *KeyBindingCreate { + _c.mutation.SetUUID(v) + return _c +} + +// SetNillableUUID sets the "uuid" field if the given value is not nil. +func (_c *KeyBindingCreate) SetNillableUUID(v *string) *KeyBindingCreate { + if v != nil { + _c.SetUUID(*v) + } + return _c +} + // SetCreatedAt sets the "created_at" field. func (_c *KeyBindingCreate) SetCreatedAt(v string) *KeyBindingCreate { _c.mutation.SetCreatedAt(v) @@ -138,6 +152,13 @@ func (_c *KeyBindingCreate) ExecX(ctx context.Context) { // defaults sets the default values of the builder before save. func (_c *KeyBindingCreate) defaults() error { + if _, ok := _c.mutation.UUID(); !ok { + if keybinding.DefaultUUID == nil { + return fmt.Errorf("ent: uninitialized keybinding.DefaultUUID (forgotten import ent/runtime?)") + } + v := keybinding.DefaultUUID() + _c.mutation.SetUUID(v) + } if _, ok := _c.mutation.CreatedAt(); !ok { if keybinding.DefaultCreatedAt == nil { return fmt.Errorf("ent: uninitialized keybinding.DefaultCreatedAt (forgotten import ent/runtime?)") @@ -217,6 +238,10 @@ func (_c *KeyBindingCreate) createSpec() (*KeyBinding, *sqlgraph.CreateSpec) { _node = &KeyBinding{config: _c.config} _spec = sqlgraph.NewCreateSpec(keybinding.Table, sqlgraph.NewFieldSpec(keybinding.FieldID, field.TypeInt)) ) + if value, ok := _c.mutation.UUID(); ok { + _spec.SetField(keybinding.FieldUUID, field.TypeString, value) + _node.UUID = value + } if value, ok := _c.mutation.CreatedAt(); ok { _spec.SetField(keybinding.FieldCreatedAt, field.TypeString, value) _node.CreatedAt = value diff --git a/internal/models/ent/keybinding_query.go b/internal/models/ent/keybinding_query.go index 95e0a59..7069a88 100644 --- a/internal/models/ent/keybinding_query.go +++ b/internal/models/ent/keybinding_query.go @@ -265,12 +265,12 @@ func (_q *KeyBindingQuery) Clone() *KeyBindingQuery { // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // Count int `json:"count,omitempty"` // } // // client.KeyBinding.Query(). -// GroupBy(keybinding.FieldCreatedAt). +// GroupBy(keybinding.FieldUUID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (_q *KeyBindingQuery) GroupBy(field string, fields ...string) *KeyBindingGroupBy { @@ -288,11 +288,11 @@ func (_q *KeyBindingQuery) GroupBy(field string, fields ...string) *KeyBindingGr // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // } // // client.KeyBinding.Query(). -// Select(keybinding.FieldCreatedAt). +// Select(keybinding.FieldUUID). // Scan(ctx, &v) func (_q *KeyBindingQuery) Select(fields ...string) *KeyBindingSelect { _q.ctx.Fields = append(_q.ctx.Fields, fields...) diff --git a/internal/models/ent/keybinding_update.go b/internal/models/ent/keybinding_update.go index 22da443..af6e934 100644 --- a/internal/models/ent/keybinding_update.go +++ b/internal/models/ent/keybinding_update.go @@ -194,6 +194,9 @@ func (_u *KeyBindingUpdate) sqlSave(ctx context.Context) (_node int, err error) } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(keybinding.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(keybinding.FieldUpdatedAt, field.TypeString, value) } @@ -436,6 +439,9 @@ func (_u *KeyBindingUpdateOne) sqlSave(ctx context.Context) (_node *KeyBinding, } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(keybinding.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(keybinding.FieldUpdatedAt, field.TypeString, value) } diff --git a/internal/models/ent/migrate/schema.go b/internal/models/ent/migrate/schema.go index 3b7b506..60307aa 100644 --- a/internal/models/ent/migrate/schema.go +++ b/internal/models/ent/migrate/schema.go @@ -12,6 +12,7 @@ var ( // DocumentsColumns holds the columns for the "documents" table. DocumentsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, + {Name: "uuid", Type: field.TypeString, Unique: true, Nullable: true}, {Name: "created_at", Type: field.TypeString}, {Name: "updated_at", Type: field.TypeString}, {Name: "deleted_at", Type: field.TypeString, Nullable: true}, @@ -26,30 +27,36 @@ var ( PrimaryKey: []*schema.Column{DocumentsColumns[0]}, Indexes: []*schema.Index{ { - Name: "document_deleted_at", - Unique: false, - Columns: []*schema.Column{DocumentsColumns[3]}, - }, - { - Name: "document_title", - Unique: false, - Columns: []*schema.Column{DocumentsColumns[4]}, - }, - { - Name: "document_created_at", + Name: "document_uuid", Unique: false, Columns: []*schema.Column{DocumentsColumns[1]}, }, { - Name: "document_updated_at", + Name: "document_deleted_at", + Unique: false, + Columns: []*schema.Column{DocumentsColumns[4]}, + }, + { + Name: "document_title", + Unique: false, + Columns: []*schema.Column{DocumentsColumns[5]}, + }, + { + Name: "document_created_at", Unique: false, Columns: []*schema.Column{DocumentsColumns[2]}, }, + { + Name: "document_updated_at", + Unique: false, + Columns: []*schema.Column{DocumentsColumns[3]}, + }, }, } // ExtensionsColumns holds the columns for the "extensions" table. ExtensionsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, + {Name: "uuid", Type: field.TypeString, Unique: true, Nullable: true}, {Name: "created_at", Type: field.TypeString}, {Name: "updated_at", Type: field.TypeString}, {Name: "deleted_at", Type: field.TypeString, Nullable: true}, @@ -63,21 +70,27 @@ var ( Columns: ExtensionsColumns, PrimaryKey: []*schema.Column{ExtensionsColumns[0]}, Indexes: []*schema.Index{ + { + Name: "extension_uuid", + Unique: false, + Columns: []*schema.Column{ExtensionsColumns[1]}, + }, { Name: "extension_deleted_at", Unique: false, - Columns: []*schema.Column{ExtensionsColumns[3]}, + Columns: []*schema.Column{ExtensionsColumns[4]}, }, { Name: "extension_enabled", Unique: false, - Columns: []*schema.Column{ExtensionsColumns[5]}, + Columns: []*schema.Column{ExtensionsColumns[6]}, }, }, } // KeyBindingsColumns holds the columns for the "key_bindings" table. KeyBindingsColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, + {Name: "uuid", Type: field.TypeString, Unique: true, Nullable: true}, {Name: "created_at", Type: field.TypeString}, {Name: "updated_at", Type: field.TypeString}, {Name: "deleted_at", Type: field.TypeString, Nullable: true}, @@ -92,26 +105,32 @@ var ( Columns: KeyBindingsColumns, PrimaryKey: []*schema.Column{KeyBindingsColumns[0]}, Indexes: []*schema.Index{ + { + Name: "keybinding_uuid", + Unique: false, + Columns: []*schema.Column{KeyBindingsColumns[1]}, + }, { Name: "keybinding_deleted_at", Unique: false, - Columns: []*schema.Column{KeyBindingsColumns[3]}, + Columns: []*schema.Column{KeyBindingsColumns[4]}, }, { Name: "keybinding_extension", Unique: false, - Columns: []*schema.Column{KeyBindingsColumns[6]}, + Columns: []*schema.Column{KeyBindingsColumns[7]}, }, { Name: "keybinding_enabled", Unique: false, - Columns: []*schema.Column{KeyBindingsColumns[7]}, + Columns: []*schema.Column{KeyBindingsColumns[8]}, }, }, } // ThemesColumns holds the columns for the "themes" table. ThemesColumns = []*schema.Column{ {Name: "id", Type: field.TypeInt, Increment: true}, + {Name: "uuid", Type: field.TypeString, Unique: true, Nullable: true}, {Name: "created_at", Type: field.TypeString}, {Name: "updated_at", Type: field.TypeString}, {Name: "deleted_at", Type: field.TypeString, Nullable: true}, @@ -125,10 +144,15 @@ var ( Columns: ThemesColumns, PrimaryKey: []*schema.Column{ThemesColumns[0]}, Indexes: []*schema.Index{ + { + Name: "theme_uuid", + Unique: false, + Columns: []*schema.Column{ThemesColumns[1]}, + }, { Name: "theme_deleted_at", Unique: false, - Columns: []*schema.Column{ThemesColumns[3]}, + Columns: []*schema.Column{ThemesColumns[4]}, }, }, } diff --git a/internal/models/ent/mutation.go b/internal/models/ent/mutation.go index a9dc87f..dc807c2 100644 --- a/internal/models/ent/mutation.go +++ b/internal/models/ent/mutation.go @@ -38,6 +38,7 @@ type DocumentMutation struct { op Op typ string id *int + uuid *string created_at *string updated_at *string deleted_at *string @@ -148,6 +149,55 @@ func (m *DocumentMutation) IDs(ctx context.Context) ([]int, error) { } } +// SetUUID sets the "uuid" field. +func (m *DocumentMutation) SetUUID(s string) { + m.uuid = &s +} + +// UUID returns the value of the "uuid" field in the mutation. +func (m *DocumentMutation) UUID() (r string, exists bool) { + v := m.uuid + if v == nil { + return + } + return *v, true +} + +// OldUUID returns the old "uuid" field's value of the Document entity. +// If the Document object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *DocumentMutation) OldUUID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUUID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUUID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUUID: %w", err) + } + return oldValue.UUID, nil +} + +// ClearUUID clears the value of the "uuid" field. +func (m *DocumentMutation) ClearUUID() { + m.uuid = nil + m.clearedFields[document.FieldUUID] = struct{}{} +} + +// UUIDCleared returns if the "uuid" field was cleared in this mutation. +func (m *DocumentMutation) UUIDCleared() bool { + _, ok := m.clearedFields[document.FieldUUID] + return ok +} + +// ResetUUID resets all changes to the "uuid" field. +func (m *DocumentMutation) ResetUUID() { + m.uuid = nil + delete(m.clearedFields, document.FieldUUID) +} + // SetCreatedAt sets the "created_at" field. func (m *DocumentMutation) SetCreatedAt(s string) { m.created_at = &s @@ -424,7 +474,10 @@ func (m *DocumentMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *DocumentMutation) Fields() []string { - fields := make([]string, 0, 6) + fields := make([]string, 0, 7) + if m.uuid != nil { + fields = append(fields, document.FieldUUID) + } if m.created_at != nil { fields = append(fields, document.FieldCreatedAt) } @@ -451,6 +504,8 @@ func (m *DocumentMutation) Fields() []string { // schema. func (m *DocumentMutation) Field(name string) (ent.Value, bool) { switch name { + case document.FieldUUID: + return m.UUID() case document.FieldCreatedAt: return m.CreatedAt() case document.FieldUpdatedAt: @@ -472,6 +527,8 @@ func (m *DocumentMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *DocumentMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case document.FieldUUID: + return m.OldUUID(ctx) case document.FieldCreatedAt: return m.OldCreatedAt(ctx) case document.FieldUpdatedAt: @@ -493,6 +550,13 @@ func (m *DocumentMutation) OldField(ctx context.Context, name string) (ent.Value // type. func (m *DocumentMutation) SetField(name string, value ent.Value) error { switch name { + case document.FieldUUID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUUID(v) + return nil case document.FieldCreatedAt: v, ok := value.(string) if !ok { @@ -565,6 +629,9 @@ func (m *DocumentMutation) AddField(name string, value ent.Value) error { // mutation. func (m *DocumentMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(document.FieldUUID) { + fields = append(fields, document.FieldUUID) + } if m.FieldCleared(document.FieldDeletedAt) { fields = append(fields, document.FieldDeletedAt) } @@ -585,6 +652,9 @@ func (m *DocumentMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *DocumentMutation) ClearField(name string) error { switch name { + case document.FieldUUID: + m.ClearUUID() + return nil case document.FieldDeletedAt: m.ClearDeletedAt() return nil @@ -599,6 +669,9 @@ func (m *DocumentMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *DocumentMutation) ResetField(name string) error { switch name { + case document.FieldUUID: + m.ResetUUID() + return nil case document.FieldCreatedAt: m.ResetCreatedAt() return nil @@ -675,6 +748,7 @@ type ExtensionMutation struct { op Op typ string id *int + uuid *string created_at *string updated_at *string deleted_at *string @@ -785,6 +859,55 @@ func (m *ExtensionMutation) IDs(ctx context.Context) ([]int, error) { } } +// SetUUID sets the "uuid" field. +func (m *ExtensionMutation) SetUUID(s string) { + m.uuid = &s +} + +// UUID returns the value of the "uuid" field in the mutation. +func (m *ExtensionMutation) UUID() (r string, exists bool) { + v := m.uuid + if v == nil { + return + } + return *v, true +} + +// OldUUID returns the old "uuid" field's value of the Extension entity. +// If the Extension object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ExtensionMutation) OldUUID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUUID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUUID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUUID: %w", err) + } + return oldValue.UUID, nil +} + +// ClearUUID clears the value of the "uuid" field. +func (m *ExtensionMutation) ClearUUID() { + m.uuid = nil + m.clearedFields[extension.FieldUUID] = struct{}{} +} + +// UUIDCleared returns if the "uuid" field was cleared in this mutation. +func (m *ExtensionMutation) UUIDCleared() bool { + _, ok := m.clearedFields[extension.FieldUUID] + return ok +} + +// ResetUUID resets all changes to the "uuid" field. +func (m *ExtensionMutation) ResetUUID() { + m.uuid = nil + delete(m.clearedFields, extension.FieldUUID) +} + // SetCreatedAt sets the "created_at" field. func (m *ExtensionMutation) SetCreatedAt(s string) { m.created_at = &s @@ -1061,7 +1184,10 @@ func (m *ExtensionMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ExtensionMutation) Fields() []string { - fields := make([]string, 0, 6) + fields := make([]string, 0, 7) + if m.uuid != nil { + fields = append(fields, extension.FieldUUID) + } if m.created_at != nil { fields = append(fields, extension.FieldCreatedAt) } @@ -1088,6 +1214,8 @@ func (m *ExtensionMutation) Fields() []string { // schema. func (m *ExtensionMutation) Field(name string) (ent.Value, bool) { switch name { + case extension.FieldUUID: + return m.UUID() case extension.FieldCreatedAt: return m.CreatedAt() case extension.FieldUpdatedAt: @@ -1109,6 +1237,8 @@ func (m *ExtensionMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *ExtensionMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case extension.FieldUUID: + return m.OldUUID(ctx) case extension.FieldCreatedAt: return m.OldCreatedAt(ctx) case extension.FieldUpdatedAt: @@ -1130,6 +1260,13 @@ func (m *ExtensionMutation) OldField(ctx context.Context, name string) (ent.Valu // type. func (m *ExtensionMutation) SetField(name string, value ent.Value) error { switch name { + case extension.FieldUUID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUUID(v) + return nil case extension.FieldCreatedAt: v, ok := value.(string) if !ok { @@ -1202,6 +1339,9 @@ func (m *ExtensionMutation) AddField(name string, value ent.Value) error { // mutation. func (m *ExtensionMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(extension.FieldUUID) { + fields = append(fields, extension.FieldUUID) + } if m.FieldCleared(extension.FieldDeletedAt) { fields = append(fields, extension.FieldDeletedAt) } @@ -1222,6 +1362,9 @@ func (m *ExtensionMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *ExtensionMutation) ClearField(name string) error { switch name { + case extension.FieldUUID: + m.ClearUUID() + return nil case extension.FieldDeletedAt: m.ClearDeletedAt() return nil @@ -1236,6 +1379,9 @@ func (m *ExtensionMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *ExtensionMutation) ResetField(name string) error { switch name { + case extension.FieldUUID: + m.ResetUUID() + return nil case extension.FieldCreatedAt: m.ResetCreatedAt() return nil @@ -1312,6 +1458,7 @@ type KeyBindingMutation struct { op Op typ string id *int + uuid *string created_at *string updated_at *string deleted_at *string @@ -1423,6 +1570,55 @@ func (m *KeyBindingMutation) IDs(ctx context.Context) ([]int, error) { } } +// SetUUID sets the "uuid" field. +func (m *KeyBindingMutation) SetUUID(s string) { + m.uuid = &s +} + +// UUID returns the value of the "uuid" field in the mutation. +func (m *KeyBindingMutation) UUID() (r string, exists bool) { + v := m.uuid + if v == nil { + return + } + return *v, true +} + +// OldUUID returns the old "uuid" field's value of the KeyBinding entity. +// If the KeyBinding object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *KeyBindingMutation) OldUUID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUUID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUUID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUUID: %w", err) + } + return oldValue.UUID, nil +} + +// ClearUUID clears the value of the "uuid" field. +func (m *KeyBindingMutation) ClearUUID() { + m.uuid = nil + m.clearedFields[keybinding.FieldUUID] = struct{}{} +} + +// UUIDCleared returns if the "uuid" field was cleared in this mutation. +func (m *KeyBindingMutation) UUIDCleared() bool { + _, ok := m.clearedFields[keybinding.FieldUUID] + return ok +} + +// ResetUUID resets all changes to the "uuid" field. +func (m *KeyBindingMutation) ResetUUID() { + m.uuid = nil + delete(m.clearedFields, keybinding.FieldUUID) +} + // SetCreatedAt sets the "created_at" field. func (m *KeyBindingMutation) SetCreatedAt(s string) { m.created_at = &s @@ -1735,7 +1931,10 @@ func (m *KeyBindingMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *KeyBindingMutation) Fields() []string { - fields := make([]string, 0, 7) + fields := make([]string, 0, 8) + if m.uuid != nil { + fields = append(fields, keybinding.FieldUUID) + } if m.created_at != nil { fields = append(fields, keybinding.FieldCreatedAt) } @@ -1765,6 +1964,8 @@ func (m *KeyBindingMutation) Fields() []string { // schema. func (m *KeyBindingMutation) Field(name string) (ent.Value, bool) { switch name { + case keybinding.FieldUUID: + return m.UUID() case keybinding.FieldCreatedAt: return m.CreatedAt() case keybinding.FieldUpdatedAt: @@ -1788,6 +1989,8 @@ func (m *KeyBindingMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *KeyBindingMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case keybinding.FieldUUID: + return m.OldUUID(ctx) case keybinding.FieldCreatedAt: return m.OldCreatedAt(ctx) case keybinding.FieldUpdatedAt: @@ -1811,6 +2014,13 @@ func (m *KeyBindingMutation) OldField(ctx context.Context, name string) (ent.Val // type. func (m *KeyBindingMutation) SetField(name string, value ent.Value) error { switch name { + case keybinding.FieldUUID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUUID(v) + return nil case keybinding.FieldCreatedAt: v, ok := value.(string) if !ok { @@ -1890,6 +2100,9 @@ func (m *KeyBindingMutation) AddField(name string, value ent.Value) error { // mutation. func (m *KeyBindingMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(keybinding.FieldUUID) { + fields = append(fields, keybinding.FieldUUID) + } if m.FieldCleared(keybinding.FieldDeletedAt) { fields = append(fields, keybinding.FieldDeletedAt) } @@ -1910,6 +2123,9 @@ func (m *KeyBindingMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *KeyBindingMutation) ClearField(name string) error { switch name { + case keybinding.FieldUUID: + m.ClearUUID() + return nil case keybinding.FieldDeletedAt: m.ClearDeletedAt() return nil @@ -1924,6 +2140,9 @@ func (m *KeyBindingMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *KeyBindingMutation) ResetField(name string) error { switch name { + case keybinding.FieldUUID: + m.ResetUUID() + return nil case keybinding.FieldCreatedAt: m.ResetCreatedAt() return nil @@ -2003,6 +2222,7 @@ type ThemeMutation struct { op Op typ string id *int + uuid *string created_at *string updated_at *string deleted_at *string @@ -2113,6 +2333,55 @@ func (m *ThemeMutation) IDs(ctx context.Context) ([]int, error) { } } +// SetUUID sets the "uuid" field. +func (m *ThemeMutation) SetUUID(s string) { + m.uuid = &s +} + +// UUID returns the value of the "uuid" field in the mutation. +func (m *ThemeMutation) UUID() (r string, exists bool) { + v := m.uuid + if v == nil { + return + } + return *v, true +} + +// OldUUID returns the old "uuid" field's value of the Theme entity. +// If the Theme object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ThemeMutation) OldUUID(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUUID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUUID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUUID: %w", err) + } + return oldValue.UUID, nil +} + +// ClearUUID clears the value of the "uuid" field. +func (m *ThemeMutation) ClearUUID() { + m.uuid = nil + m.clearedFields[theme.FieldUUID] = struct{}{} +} + +// UUIDCleared returns if the "uuid" field was cleared in this mutation. +func (m *ThemeMutation) UUIDCleared() bool { + _, ok := m.clearedFields[theme.FieldUUID] + return ok +} + +// ResetUUID resets all changes to the "uuid" field. +func (m *ThemeMutation) ResetUUID() { + m.uuid = nil + delete(m.clearedFields, theme.FieldUUID) +} + // SetCreatedAt sets the "created_at" field. func (m *ThemeMutation) SetCreatedAt(s string) { m.created_at = &s @@ -2389,7 +2658,10 @@ func (m *ThemeMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ThemeMutation) Fields() []string { - fields := make([]string, 0, 6) + fields := make([]string, 0, 7) + if m.uuid != nil { + fields = append(fields, theme.FieldUUID) + } if m.created_at != nil { fields = append(fields, theme.FieldCreatedAt) } @@ -2416,6 +2688,8 @@ func (m *ThemeMutation) Fields() []string { // schema. func (m *ThemeMutation) Field(name string) (ent.Value, bool) { switch name { + case theme.FieldUUID: + return m.UUID() case theme.FieldCreatedAt: return m.CreatedAt() case theme.FieldUpdatedAt: @@ -2437,6 +2711,8 @@ func (m *ThemeMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *ThemeMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case theme.FieldUUID: + return m.OldUUID(ctx) case theme.FieldCreatedAt: return m.OldCreatedAt(ctx) case theme.FieldUpdatedAt: @@ -2458,6 +2734,13 @@ func (m *ThemeMutation) OldField(ctx context.Context, name string) (ent.Value, e // type. func (m *ThemeMutation) SetField(name string, value ent.Value) error { switch name { + case theme.FieldUUID: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUUID(v) + return nil case theme.FieldCreatedAt: v, ok := value.(string) if !ok { @@ -2530,6 +2813,9 @@ func (m *ThemeMutation) AddField(name string, value ent.Value) error { // mutation. func (m *ThemeMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(theme.FieldUUID) { + fields = append(fields, theme.FieldUUID) + } if m.FieldCleared(theme.FieldDeletedAt) { fields = append(fields, theme.FieldDeletedAt) } @@ -2550,6 +2836,9 @@ func (m *ThemeMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *ThemeMutation) ClearField(name string) error { switch name { + case theme.FieldUUID: + m.ClearUUID() + return nil case theme.FieldDeletedAt: m.ClearDeletedAt() return nil @@ -2564,6 +2853,9 @@ func (m *ThemeMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *ThemeMutation) ResetField(name string) error { switch name { + case theme.FieldUUID: + m.ResetUUID() + return nil case theme.FieldCreatedAt: m.ResetCreatedAt() return nil diff --git a/internal/models/ent/runtime/runtime.go b/internal/models/ent/runtime/runtime.go index 8adff2a..7403048 100644 --- a/internal/models/ent/runtime/runtime.go +++ b/internal/models/ent/runtime/runtime.go @@ -15,22 +15,28 @@ import ( // to their package variables. func init() { documentMixin := schema.Document{}.Mixin() - documentMixinHooks0 := documentMixin[0].Hooks() documentMixinHooks1 := documentMixin[1].Hooks() - document.Hooks[0] = documentMixinHooks0[0] - document.Hooks[1] = documentMixinHooks1[0] - documentMixinInters1 := documentMixin[1].Interceptors() - document.Interceptors[0] = documentMixinInters1[0] + documentMixinHooks2 := documentMixin[2].Hooks() + document.Hooks[0] = documentMixinHooks1[0] + document.Hooks[1] = documentMixinHooks2[0] + documentMixinInters2 := documentMixin[2].Interceptors() + document.Interceptors[0] = documentMixinInters2[0] documentMixinFields0 := documentMixin[0].Fields() _ = documentMixinFields0 + documentMixinFields1 := documentMixin[1].Fields() + _ = documentMixinFields1 documentFields := schema.Document{}.Fields() _ = documentFields + // documentDescUUID is the schema descriptor for uuid field. + documentDescUUID := documentMixinFields0[0].Descriptor() + // document.DefaultUUID holds the default value on creation for the uuid field. + document.DefaultUUID = documentDescUUID.Default.(func() string) // documentDescCreatedAt is the schema descriptor for created_at field. - documentDescCreatedAt := documentMixinFields0[0].Descriptor() + documentDescCreatedAt := documentMixinFields1[0].Descriptor() // document.DefaultCreatedAt holds the default value on creation for the created_at field. document.DefaultCreatedAt = documentDescCreatedAt.Default.(func() string) // documentDescUpdatedAt is the schema descriptor for updated_at field. - documentDescUpdatedAt := documentMixinFields0[1].Descriptor() + documentDescUpdatedAt := documentMixinFields1[1].Descriptor() // document.DefaultUpdatedAt holds the default value on creation for the updated_at field. document.DefaultUpdatedAt = documentDescUpdatedAt.Default.(func() string) // documentDescTitle is the schema descriptor for title field. @@ -60,22 +66,28 @@ func init() { // document.DefaultLocked holds the default value on creation for the locked field. document.DefaultLocked = documentDescLocked.Default.(bool) extensionMixin := schema.Extension{}.Mixin() - extensionMixinHooks0 := extensionMixin[0].Hooks() extensionMixinHooks1 := extensionMixin[1].Hooks() - extension.Hooks[0] = extensionMixinHooks0[0] - extension.Hooks[1] = extensionMixinHooks1[0] - extensionMixinInters1 := extensionMixin[1].Interceptors() - extension.Interceptors[0] = extensionMixinInters1[0] + extensionMixinHooks2 := extensionMixin[2].Hooks() + extension.Hooks[0] = extensionMixinHooks1[0] + extension.Hooks[1] = extensionMixinHooks2[0] + extensionMixinInters2 := extensionMixin[2].Interceptors() + extension.Interceptors[0] = extensionMixinInters2[0] extensionMixinFields0 := extensionMixin[0].Fields() _ = extensionMixinFields0 + extensionMixinFields1 := extensionMixin[1].Fields() + _ = extensionMixinFields1 extensionFields := schema.Extension{}.Fields() _ = extensionFields + // extensionDescUUID is the schema descriptor for uuid field. + extensionDescUUID := extensionMixinFields0[0].Descriptor() + // extension.DefaultUUID holds the default value on creation for the uuid field. + extension.DefaultUUID = extensionDescUUID.Default.(func() string) // extensionDescCreatedAt is the schema descriptor for created_at field. - extensionDescCreatedAt := extensionMixinFields0[0].Descriptor() + extensionDescCreatedAt := extensionMixinFields1[0].Descriptor() // extension.DefaultCreatedAt holds the default value on creation for the created_at field. extension.DefaultCreatedAt = extensionDescCreatedAt.Default.(func() string) // extensionDescUpdatedAt is the schema descriptor for updated_at field. - extensionDescUpdatedAt := extensionMixinFields0[1].Descriptor() + extensionDescUpdatedAt := extensionMixinFields1[1].Descriptor() // extension.DefaultUpdatedAt holds the default value on creation for the updated_at field. extension.DefaultUpdatedAt = extensionDescUpdatedAt.Default.(func() string) // extensionDescKey is the schema descriptor for key field. @@ -101,22 +113,28 @@ func init() { // extension.DefaultEnabled holds the default value on creation for the enabled field. extension.DefaultEnabled = extensionDescEnabled.Default.(bool) keybindingMixin := schema.KeyBinding{}.Mixin() - keybindingMixinHooks0 := keybindingMixin[0].Hooks() keybindingMixinHooks1 := keybindingMixin[1].Hooks() - keybinding.Hooks[0] = keybindingMixinHooks0[0] - keybinding.Hooks[1] = keybindingMixinHooks1[0] - keybindingMixinInters1 := keybindingMixin[1].Interceptors() - keybinding.Interceptors[0] = keybindingMixinInters1[0] + keybindingMixinHooks2 := keybindingMixin[2].Hooks() + keybinding.Hooks[0] = keybindingMixinHooks1[0] + keybinding.Hooks[1] = keybindingMixinHooks2[0] + keybindingMixinInters2 := keybindingMixin[2].Interceptors() + keybinding.Interceptors[0] = keybindingMixinInters2[0] keybindingMixinFields0 := keybindingMixin[0].Fields() _ = keybindingMixinFields0 + keybindingMixinFields1 := keybindingMixin[1].Fields() + _ = keybindingMixinFields1 keybindingFields := schema.KeyBinding{}.Fields() _ = keybindingFields + // keybindingDescUUID is the schema descriptor for uuid field. + keybindingDescUUID := keybindingMixinFields0[0].Descriptor() + // keybinding.DefaultUUID holds the default value on creation for the uuid field. + keybinding.DefaultUUID = keybindingDescUUID.Default.(func() string) // keybindingDescCreatedAt is the schema descriptor for created_at field. - keybindingDescCreatedAt := keybindingMixinFields0[0].Descriptor() + keybindingDescCreatedAt := keybindingMixinFields1[0].Descriptor() // keybinding.DefaultCreatedAt holds the default value on creation for the created_at field. keybinding.DefaultCreatedAt = keybindingDescCreatedAt.Default.(func() string) // keybindingDescUpdatedAt is the schema descriptor for updated_at field. - keybindingDescUpdatedAt := keybindingMixinFields0[1].Descriptor() + keybindingDescUpdatedAt := keybindingMixinFields1[1].Descriptor() // keybinding.DefaultUpdatedAt holds the default value on creation for the updated_at field. keybinding.DefaultUpdatedAt = keybindingDescUpdatedAt.Default.(func() string) // keybindingDescKey is the schema descriptor for key field. @@ -164,22 +182,28 @@ func init() { // keybinding.DefaultEnabled holds the default value on creation for the enabled field. keybinding.DefaultEnabled = keybindingDescEnabled.Default.(bool) themeMixin := schema.Theme{}.Mixin() - themeMixinHooks0 := themeMixin[0].Hooks() themeMixinHooks1 := themeMixin[1].Hooks() - theme.Hooks[0] = themeMixinHooks0[0] - theme.Hooks[1] = themeMixinHooks1[0] - themeMixinInters1 := themeMixin[1].Interceptors() - theme.Interceptors[0] = themeMixinInters1[0] + themeMixinHooks2 := themeMixin[2].Hooks() + theme.Hooks[0] = themeMixinHooks1[0] + theme.Hooks[1] = themeMixinHooks2[0] + themeMixinInters2 := themeMixin[2].Interceptors() + theme.Interceptors[0] = themeMixinInters2[0] themeMixinFields0 := themeMixin[0].Fields() _ = themeMixinFields0 + themeMixinFields1 := themeMixin[1].Fields() + _ = themeMixinFields1 themeFields := schema.Theme{}.Fields() _ = themeFields + // themeDescUUID is the schema descriptor for uuid field. + themeDescUUID := themeMixinFields0[0].Descriptor() + // theme.DefaultUUID holds the default value on creation for the uuid field. + theme.DefaultUUID = themeDescUUID.Default.(func() string) // themeDescCreatedAt is the schema descriptor for created_at field. - themeDescCreatedAt := themeMixinFields0[0].Descriptor() + themeDescCreatedAt := themeMixinFields1[0].Descriptor() // theme.DefaultCreatedAt holds the default value on creation for the created_at field. theme.DefaultCreatedAt = themeDescCreatedAt.Default.(func() string) // themeDescUpdatedAt is the schema descriptor for updated_at field. - themeDescUpdatedAt := themeMixinFields0[1].Descriptor() + themeDescUpdatedAt := themeMixinFields1[1].Descriptor() // theme.DefaultUpdatedAt holds the default value on creation for the updated_at field. theme.DefaultUpdatedAt = themeDescUpdatedAt.Default.(func() string) // themeDescKey is the schema descriptor for key field. diff --git a/internal/models/ent/theme.go b/internal/models/ent/theme.go index 9dae16d..97942fa 100644 --- a/internal/models/ent/theme.go +++ b/internal/models/ent/theme.go @@ -17,17 +17,19 @@ type Theme struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // 创建时间 + // UUID for cross-device sync (UUIDv7) + UUID string `json:"uuid"` + // creation time CreatedAt string `json:"created_at"` - // 最后更新时间 + // update time UpdatedAt string `json:"updated_at"` - // 删除时间,NULL表示未删除 + // deleted at DeletedAt *string `json:"deleted_at,omitempty"` - // 主题标识符 + // theme key Key string `json:"key"` - // 主题类型 + // theme type Type theme.Type `json:"type"` - // 主题颜色配置 + // theme colors Colors map[string]interface{} `json:"colors"` selectValues sql.SelectValues } @@ -41,7 +43,7 @@ func (*Theme) scanValues(columns []string) ([]any, error) { values[i] = new([]byte) case theme.FieldID: values[i] = new(sql.NullInt64) - case theme.FieldCreatedAt, theme.FieldUpdatedAt, theme.FieldDeletedAt, theme.FieldKey, theme.FieldType: + case theme.FieldUUID, theme.FieldCreatedAt, theme.FieldUpdatedAt, theme.FieldDeletedAt, theme.FieldKey, theme.FieldType: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -64,6 +66,12 @@ func (_m *Theme) assignValues(columns []string, values []any) error { return fmt.Errorf("unexpected type %T for field id", value) } _m.ID = int(value.Int64) + case theme.FieldUUID: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field uuid", values[i]) + } else if value.Valid { + _m.UUID = value.String + } case theme.FieldCreatedAt: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -139,6 +147,9 @@ func (_m *Theme) String() string { var builder strings.Builder builder.WriteString("Theme(") builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID)) + builder.WriteString("uuid=") + builder.WriteString(_m.UUID) + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(_m.CreatedAt) builder.WriteString(", ") diff --git a/internal/models/ent/theme/theme.go b/internal/models/ent/theme/theme.go index 51d0310..0627661 100644 --- a/internal/models/ent/theme/theme.go +++ b/internal/models/ent/theme/theme.go @@ -14,6 +14,8 @@ const ( Label = "theme" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldUUID holds the string denoting the uuid field in the database. + FieldUUID = "uuid" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. @@ -33,6 +35,7 @@ const ( // Columns holds all SQL columns for theme fields. var Columns = []string{ FieldID, + FieldUUID, FieldCreatedAt, FieldUpdatedAt, FieldDeletedAt, @@ -59,6 +62,8 @@ func ValidColumn(column string) bool { var ( Hooks [2]ent.Hook Interceptors [1]ent.Interceptor + // DefaultUUID holds the default value on creation for the "uuid" field. + DefaultUUID func() string // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() string // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. @@ -98,6 +103,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// ByUUID orders the results by the uuid field. +func ByUUID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUUID, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/internal/models/ent/theme/where.go b/internal/models/ent/theme/where.go index 22a2b21..06ae4c6 100644 --- a/internal/models/ent/theme/where.go +++ b/internal/models/ent/theme/where.go @@ -53,6 +53,11 @@ func IDLTE(id int) predicate.Theme { return predicate.Theme(sql.FieldLTE(FieldID, id)) } +// UUID applies equality check predicate on the "uuid" field. It's identical to UUIDEQ. +func UUID(v string) predicate.Theme { + return predicate.Theme(sql.FieldEQ(FieldUUID, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v string) predicate.Theme { return predicate.Theme(sql.FieldEQ(FieldCreatedAt, v)) @@ -73,6 +78,81 @@ func Key(v string) predicate.Theme { return predicate.Theme(sql.FieldEQ(FieldKey, v)) } +// UUIDEQ applies the EQ predicate on the "uuid" field. +func UUIDEQ(v string) predicate.Theme { + return predicate.Theme(sql.FieldEQ(FieldUUID, v)) +} + +// UUIDNEQ applies the NEQ predicate on the "uuid" field. +func UUIDNEQ(v string) predicate.Theme { + return predicate.Theme(sql.FieldNEQ(FieldUUID, v)) +} + +// UUIDIn applies the In predicate on the "uuid" field. +func UUIDIn(vs ...string) predicate.Theme { + return predicate.Theme(sql.FieldIn(FieldUUID, vs...)) +} + +// UUIDNotIn applies the NotIn predicate on the "uuid" field. +func UUIDNotIn(vs ...string) predicate.Theme { + return predicate.Theme(sql.FieldNotIn(FieldUUID, vs...)) +} + +// UUIDGT applies the GT predicate on the "uuid" field. +func UUIDGT(v string) predicate.Theme { + return predicate.Theme(sql.FieldGT(FieldUUID, v)) +} + +// UUIDGTE applies the GTE predicate on the "uuid" field. +func UUIDGTE(v string) predicate.Theme { + return predicate.Theme(sql.FieldGTE(FieldUUID, v)) +} + +// UUIDLT applies the LT predicate on the "uuid" field. +func UUIDLT(v string) predicate.Theme { + return predicate.Theme(sql.FieldLT(FieldUUID, v)) +} + +// UUIDLTE applies the LTE predicate on the "uuid" field. +func UUIDLTE(v string) predicate.Theme { + return predicate.Theme(sql.FieldLTE(FieldUUID, v)) +} + +// UUIDContains applies the Contains predicate on the "uuid" field. +func UUIDContains(v string) predicate.Theme { + return predicate.Theme(sql.FieldContains(FieldUUID, v)) +} + +// UUIDHasPrefix applies the HasPrefix predicate on the "uuid" field. +func UUIDHasPrefix(v string) predicate.Theme { + return predicate.Theme(sql.FieldHasPrefix(FieldUUID, v)) +} + +// UUIDHasSuffix applies the HasSuffix predicate on the "uuid" field. +func UUIDHasSuffix(v string) predicate.Theme { + return predicate.Theme(sql.FieldHasSuffix(FieldUUID, v)) +} + +// UUIDIsNil applies the IsNil predicate on the "uuid" field. +func UUIDIsNil() predicate.Theme { + return predicate.Theme(sql.FieldIsNull(FieldUUID)) +} + +// UUIDNotNil applies the NotNil predicate on the "uuid" field. +func UUIDNotNil() predicate.Theme { + return predicate.Theme(sql.FieldNotNull(FieldUUID)) +} + +// UUIDEqualFold applies the EqualFold predicate on the "uuid" field. +func UUIDEqualFold(v string) predicate.Theme { + return predicate.Theme(sql.FieldEqualFold(FieldUUID, v)) +} + +// UUIDContainsFold applies the ContainsFold predicate on the "uuid" field. +func UUIDContainsFold(v string) predicate.Theme { + return predicate.Theme(sql.FieldContainsFold(FieldUUID, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v string) predicate.Theme { return predicate.Theme(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/internal/models/ent/theme_create.go b/internal/models/ent/theme_create.go index 5811dc7..96da598 100644 --- a/internal/models/ent/theme_create.go +++ b/internal/models/ent/theme_create.go @@ -19,6 +19,20 @@ type ThemeCreate struct { hooks []Hook } +// SetUUID sets the "uuid" field. +func (_c *ThemeCreate) SetUUID(v string) *ThemeCreate { + _c.mutation.SetUUID(v) + return _c +} + +// SetNillableUUID sets the "uuid" field if the given value is not nil. +func (_c *ThemeCreate) SetNillableUUID(v *string) *ThemeCreate { + if v != nil { + _c.SetUUID(*v) + } + return _c +} + // SetCreatedAt sets the "created_at" field. func (_c *ThemeCreate) SetCreatedAt(v string) *ThemeCreate { _c.mutation.SetCreatedAt(v) @@ -116,6 +130,13 @@ func (_c *ThemeCreate) ExecX(ctx context.Context) { // defaults sets the default values of the builder before save. func (_c *ThemeCreate) defaults() error { + if _, ok := _c.mutation.UUID(); !ok { + if theme.DefaultUUID == nil { + return fmt.Errorf("ent: uninitialized theme.DefaultUUID (forgotten import ent/runtime?)") + } + v := theme.DefaultUUID() + _c.mutation.SetUUID(v) + } if _, ok := _c.mutation.CreatedAt(); !ok { if theme.DefaultCreatedAt == nil { return fmt.Errorf("ent: uninitialized theme.DefaultCreatedAt (forgotten import ent/runtime?)") @@ -183,6 +204,10 @@ func (_c *ThemeCreate) createSpec() (*Theme, *sqlgraph.CreateSpec) { _node = &Theme{config: _c.config} _spec = sqlgraph.NewCreateSpec(theme.Table, sqlgraph.NewFieldSpec(theme.FieldID, field.TypeInt)) ) + if value, ok := _c.mutation.UUID(); ok { + _spec.SetField(theme.FieldUUID, field.TypeString, value) + _node.UUID = value + } if value, ok := _c.mutation.CreatedAt(); ok { _spec.SetField(theme.FieldCreatedAt, field.TypeString, value) _node.CreatedAt = value diff --git a/internal/models/ent/theme_query.go b/internal/models/ent/theme_query.go index 6ffe8b6..2bd2428 100644 --- a/internal/models/ent/theme_query.go +++ b/internal/models/ent/theme_query.go @@ -265,12 +265,12 @@ func (_q *ThemeQuery) Clone() *ThemeQuery { // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // Count int `json:"count,omitempty"` // } // // client.Theme.Query(). -// GroupBy(theme.FieldCreatedAt). +// GroupBy(theme.FieldUUID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (_q *ThemeQuery) GroupBy(field string, fields ...string) *ThemeGroupBy { @@ -288,11 +288,11 @@ func (_q *ThemeQuery) GroupBy(field string, fields ...string) *ThemeGroupBy { // Example: // // var v []struct { -// CreatedAt string `json:"created_at"` +// UUID string `json:"uuid"` // } // // client.Theme.Query(). -// Select(theme.FieldCreatedAt). +// Select(theme.FieldUUID). // Scan(ctx, &v) func (_q *ThemeQuery) Select(fields ...string) *ThemeSelect { _q.ctx.Fields = append(_q.ctx.Fields, fields...) diff --git a/internal/models/ent/theme_update.go b/internal/models/ent/theme_update.go index d7b1dfc..1ae3683 100644 --- a/internal/models/ent/theme_update.go +++ b/internal/models/ent/theme_update.go @@ -167,6 +167,9 @@ func (_u *ThemeUpdate) sqlSave(ctx context.Context) (_node int, err error) { } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(theme.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(theme.FieldUpdatedAt, field.TypeString, value) } @@ -379,6 +382,9 @@ func (_u *ThemeUpdateOne) sqlSave(ctx context.Context) (_node *Theme, err error) } } } + if _u.mutation.UUIDCleared() { + _spec.ClearField(theme.FieldUUID, field.TypeString) + } if value, ok := _u.mutation.UpdatedAt(); ok { _spec.SetField(theme.FieldUpdatedAt, field.TypeString, value) } diff --git a/internal/models/schema/document.go b/internal/models/schema/document.go index e7b104d..e1d5900 100644 --- a/internal/models/schema/document.go +++ b/internal/models/schema/document.go @@ -25,6 +25,7 @@ func (Document) Annotations() []schema.Annotation { // Mixin of the Document. func (Document) Mixin() []ent.Mixin { return []ent.Mixin{ + mixin.UUIDMixin{}, mixin.TimeMixin{}, mixin.SoftDeleteMixin{}, } diff --git a/internal/models/schema/extension.go b/internal/models/schema/extension.go index 4dd9cbe..46ae918 100644 --- a/internal/models/schema/extension.go +++ b/internal/models/schema/extension.go @@ -25,6 +25,7 @@ func (Extension) Annotations() []schema.Annotation { // Mixin of the Extension. func (Extension) Mixin() []ent.Mixin { return []ent.Mixin{ + mixin.UUIDMixin{}, mixin.TimeMixin{}, mixin.SoftDeleteMixin{}, } diff --git a/internal/models/schema/keybinding.go b/internal/models/schema/keybinding.go index 31d3859..7c4fd4e 100644 --- a/internal/models/schema/keybinding.go +++ b/internal/models/schema/keybinding.go @@ -25,6 +25,7 @@ func (KeyBinding) Annotations() []schema.Annotation { // Mixin of the KeyBinding. func (KeyBinding) Mixin() []ent.Mixin { return []ent.Mixin{ + mixin.UUIDMixin{}, mixin.TimeMixin{}, mixin.SoftDeleteMixin{}, } diff --git a/internal/models/schema/mixin/time.go b/internal/models/schema/mixin/time.go index 0740352..ef80999 100644 --- a/internal/models/schema/mixin/time.go +++ b/internal/models/schema/mixin/time.go @@ -17,6 +17,14 @@ func NowString() string { return time.Now().Format(TimeFormat) } +// skipAutoUpdateKey context key for skipping auto update +type skipAutoUpdateKey struct{} + +// SkipAutoUpdate 返回跳过自动更新 updated_at 的 context +func SkipAutoUpdate(ctx context.Context) context.Context { + return context.WithValue(ctx, skipAutoUpdateKey{}, true) +} + // TimeMixin 时间字段混入 // created_at: 创建时间 // updated_at: 更新时间(自动更新) @@ -44,6 +52,10 @@ func (TimeMixin) Hooks() []ent.Hook { return []ent.Hook{ func(next ent.Mutator) ent.Mutator { return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + // 跳过自动更新(用于同步导入场景) + if ctx.Value(skipAutoUpdateKey{}) != nil { + return next.Mutate(ctx, m) + } // 只在更新操作时设置 updated_at if m.Op().Is(ent.OpUpdate | ent.OpUpdateOne) { if setter, ok := m.(interface{ SetUpdatedAt(string) }); ok { diff --git a/internal/models/schema/mixin/uuid.go b/internal/models/schema/mixin/uuid.go new file mode 100644 index 0000000..b76ecb9 --- /dev/null +++ b/internal/models/schema/mixin/uuid.go @@ -0,0 +1,36 @@ +package mixin + +import ( + "entgo.io/ent" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" + "entgo.io/ent/schema/mixin" + "github.com/google/uuid" +) + +// UUIDMixin 添加 UUID 字段用于跨设备同步 +// 使用 UUIDv7,具有时间有序性,索引性能更好 +type UUIDMixin struct { + mixin.Schema +} + +// Fields of the UUIDMixin. +func (UUIDMixin) Fields() []ent.Field { + return []ent.Field{ + field.String("uuid"). + DefaultFunc(func() string { + return uuid.Must(uuid.NewV7()).String() + }). + Unique(). + Immutable(). + StructTag(`json:"uuid"`). + Comment("UUID for cross-device sync (UUIDv7)"), + } +} + +// Indexes of the UUIDMixin. +func (UUIDMixin) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("uuid"), + } +} diff --git a/internal/models/schema/theme.go b/internal/models/schema/theme.go index a3ae2d7..018d9f9 100644 --- a/internal/models/schema/theme.go +++ b/internal/models/schema/theme.go @@ -24,6 +24,7 @@ func (Theme) Annotations() []schema.Annotation { // Mixin of the Theme. func (Theme) Mixin() []ent.Mixin { return []ent.Mixin{ + mixin.UUIDMixin{}, mixin.TimeMixin{}, mixin.SoftDeleteMixin{}, } diff --git a/internal/services/backup_service.go b/internal/services/backup_service.go index f2fa794..51577df 100644 --- a/internal/services/backup_service.go +++ b/internal/services/backup_service.go @@ -1,10 +1,11 @@ package services import ( + "bufio" "context" + "encoding/json" "errors" "fmt" - "github.com/wailsapp/wails/v3/pkg/application" "os" "path/filepath" "strings" @@ -13,20 +14,43 @@ import ( "github.com/go-git/go-git/v5" gitConfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/log" "voidraft/internal/models" + "voidraft/internal/models/ent" + "voidraft/internal/models/ent/document" + "voidraft/internal/models/ent/extension" + "voidraft/internal/models/ent/keybinding" + "voidraft/internal/models/ent/theme" + "voidraft/internal/models/schema/mixin" ) const ( - dbSerializeFile = "voidraft_data.bin" + backupDir = "backup" // Git 仓库目录,JSONL 文件直接放这里 + remoteName = "origin" + branchName = "master" + maxRetries = 3 + jsonlSuffix = ".jsonl" + + // 通用字段名 + fieldUUID = "uuid" + fieldUpdatedAt = "updated_at" ) -// BackupService 提供基于Git的备份功能 +// 定义错误 +var ( + ErrNotInitialized = errors.New("backup service not initialized") + ErrDisabled = errors.New("backup is disabled") + ErrPushFailed = errors.New("push failed after max retries") +) + +// BackupService 提供基于Git的备份同步功能 type BackupService struct { configService *ConfigService dbService *DatabaseService @@ -35,11 +59,9 @@ type BackupService struct { isInitialized bool autoBackupTicker *time.Ticker autoBackupStop chan bool - autoBackupWg sync.WaitGroup // 等待自动备份goroutine完成 - mu sync.Mutex // 推送操作互斥锁 - - // 配置观察者取消函数 - cancelObserver CancelFunc + autoBackupWg sync.WaitGroup + mu sync.Mutex + cancelObserver CancelFunc } // NewBackupService 创建新的备份服务实例 @@ -51,24 +73,20 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService, } } -func (ds *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - ds.cancelObserver = ds.configService.Watch("backup", ds.onBackupConfigChange) - - if err := ds.Initialize(); err != nil { - return fmt.Errorf("initializing backup service: %w", err) +func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + s.cancelObserver = s.configService.Watch("backup.enabled", s.onBackupConfigChange) + if err := s.Initialize(); err != nil { + s.logger.Error("initializing backup service: %v", err) } return nil } -// onBackupConfigChange 备份配置变更回调 -func (ds *BackupService) onBackupConfigChange(oldValue, newValue interface{}) { - // 重新加载配置 - config, err := ds.configService.GetConfig() +func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) { + config, err := s.configService.GetConfig() if err != nil { return } - // 处理配置变更 - _ = ds.HandleConfigChange(&config.Backup) + _ = s.HandleConfigChange(&config.Backup) } // Initialize 初始化备份服务 @@ -82,302 +100,1018 @@ func (s *BackupService) Initialize() error { return nil } - // 初始化仓库 + // 仓库地址为空时不初始化(等待用户配置) + if strings.TrimSpace(config.RepoURL) == "" { + return nil + } + if err := s.initializeRepository(config, repoPath); err != nil { return fmt.Errorf("initializing repository: %w", err) } - // 验证远程仓库连接 if err := s.verifyRemoteConnection(config); err != nil { return fmt.Errorf("verifying remote connection: %w", err) } - // 启动自动备份 if config.AutoBackup && config.BackupInterval > 0 { - s.StartAutoBackup() + _ = s.StartAutoBackup() } + s.mu.Lock() s.isInitialized = true + s.mu.Unlock() + return nil } -// getConfigAndPath 获取备份配置和仓库路径 func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) { appConfig, err := s.configService.GetConfig() if err != nil { return nil, "", fmt.Errorf("getting app config: %w", err) } - return &appConfig.Backup, appConfig.General.DataPath, nil + // 返回 backup 目录作为 Git 仓库路径 + repoPath := filepath.Join(appConfig.General.DataPath, backupDir) + return &appConfig.Backup, repoPath, nil } -// initializeRepository 初始化或打开Git仓库并设置远程 func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error { + // 确保父目录存在 + if err := os.MkdirAll(repoPath, 0755); err != nil { + return fmt.Errorf("creating backup directory: %w", err) + } - // 检查本地仓库是否存在 - _, err := os.Stat(filepath.Join(repoPath, ".git")) - if os.IsNotExist(err) { - // 仓库不存在,初始化新仓库 + gitPath := filepath.Join(repoPath, ".git") + if _, err := os.Stat(gitPath); os.IsNotExist(err) { repo, err := git.PlainInit(repoPath, false) if err != nil { - return fmt.Errorf("error initializing repository: %w", err) + return fmt.Errorf("initializing repository: %w", err) } s.repository = repo + + // 创建 .gitignore + gitignorePath := filepath.Join(repoPath, ".gitignore") + if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { + _ = os.WriteFile(gitignorePath, []byte("*.tmp\n*.log\n"), 0644) + } } else if err != nil { - return fmt.Errorf("error checking repository path: %w", err) + return fmt.Errorf("checking repository path: %w", err) } else { - // 仓库已存在,打开现有仓库 repo, err := git.PlainOpen(repoPath) if err != nil { - return fmt.Errorf("error opening local repository: %w", err) + return fmt.Errorf("opening repository: %w", err) } s.repository = repo } - // 设置或更新远程仓库 - remote, err := s.repository.Remote("origin") + return s.setupRemote(config.RepoURL) +} + +func (s *BackupService) setupRemote(repoURL string) error { + remote, err := s.repository.Remote(remoteName) + if errors.Is(err, git.ErrRemoteNotFound) { + _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ + Name: remoteName, + URLs: []string{repoURL}, + }) + return err + } if err != nil { - if errors.Is(err, git.ErrRemoteNotFound) { - // 远程不存在,添加远程 - _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ - Name: "origin", - URLs: []string{config.RepoURL}, - }) - if err != nil { - return fmt.Errorf("error creating remote: %w", err) - } - } else { - return fmt.Errorf("error getting remote: %w", err) - } - } else { - // 检查远程URL是否一致,如果不一致则更新 - if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != config.RepoURL { - if err := s.repository.DeleteRemote("origin"); err != nil { - return fmt.Errorf("error deleting remote: %w", err) - } - _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ - Name: "origin", - URLs: []string{config.RepoURL}, - }) - if err != nil { - return fmt.Errorf("error creating new remote: %w", err) - } - } + return err } + if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != repoURL { + if err := s.repository.DeleteRemote(remoteName); err != nil { + return err + } + _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ + Name: remoteName, + URLs: []string{repoURL}, + }) + return err + } return nil } -// verifyRemoteConnection 验证远程仓库连接 func (s *BackupService) verifyRemoteConnection(config *models.GitBackupConfig) error { auth, err := s.getAuthMethod(config) if err != nil { return err } - remote, err := s.repository.Remote("origin") + remote, err := s.repository.Remote(remoteName) if err != nil { return err } + // 验证能否连接远程仓库,空仓库返回空列表是正常的 _, err = remote.List(&git.ListOptions{Auth: auth}) - return err + if err != nil { + // 空仓库或无引用是允许的(第一次同步场景) + if strings.Contains(err.Error(), "empty") || strings.Contains(err.Error(), "no reference") { + return nil + } + return err + } + return nil } -// getAuthMethod 根据配置获取认证方法 func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) { switch config.AuthMethod { case models.Token: if config.Token == "" { - return nil, errors.New("token authentication requires a valid token") + return nil, errors.New("token required") } - return &http.BasicAuth{ - Username: "git", // 使用token时,用户名可以是任意值 - Password: config.Token, - }, nil + return &http.BasicAuth{Username: "git", Password: config.Token}, nil case models.UserPass: if config.Username == "" || config.Password == "" { - return nil, errors.New("username/password authentication requires both username and password") + return nil, errors.New("username and password required") } - return &http.BasicAuth{ - Username: config.Username, - Password: config.Password, - }, nil + return &http.BasicAuth{Username: config.Username, Password: config.Password}, nil case models.SSHKey: if config.SSHKeyPath == "" { - return nil, errors.New("SSH key authentication requires a valid SSH key path") + return nil, errors.New("SSH key path required") } - publicKeys, err := ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass) - if err != nil { - return nil, fmt.Errorf("error creating SSH public keys: %w", err) - } - return publicKeys, nil + return ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass) default: - return nil, fmt.Errorf("unsupported authentication method: %s", config.AuthMethod) + return nil, fmt.Errorf("unsupported auth method: %s", config.AuthMethod) } } -// serializeDatabase 序列化数据库到文件 -func (s *BackupService) serializeDatabase(repoPath string) error { - //if s.dbService == nil || s.dbService.Engine == nil { - // return errors.New("database service not available") - //} - // - //binFilePath := filepath.Join(repoPath, dbSerializeFile) - // - //// 使用 VACUUM INTO 创建数据库副本,不影响现有连接 - //s.dbService.mu.RLock() - //_, err := s.dbService.db.Exec(fmt.Sprintf("VACUUM INTO '%s'", binFilePath)) - //s.dbService.mu.RUnlock() - // - //if err != nil { - // return fmt.Errorf("creating database backup: %w", err) - //} - - return nil -} - -// PushToRemote 推送本地更改到远程仓库 -func (s *BackupService) PushToRemote() error { - // 互斥锁防止并发推送 - s.mu.Lock() - defer s.mu.Unlock() - - if !s.isInitialized { - return errors.New("backup service not initialized") - } - +// Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入 +func (s *BackupService) Sync() error { config, repoPath, err := s.getConfigAndPath() if err != nil { - return fmt.Errorf("getting backup config: %w", err) - } - - if !config.Enabled { - return errors.New("backup is disabled") - } - - // 检查是否有未推送的commit - hasUnpushed, err := s.hasUnpushedCommits() - if err != nil { - return fmt.Errorf("checking unpushed commits: %w", err) - } - - binFilePath := filepath.Join(repoPath, dbSerializeFile) - - // 只有在没有未推送commit时才创建新commit - if !hasUnpushed { - // 序列化数据库 - if err := s.serializeDatabase(repoPath); err != nil { - return fmt.Errorf("serializing database: %w", err) - } - - // 获取工作树 - w, err := s.repository.Worktree() - if err != nil { - os.Remove(binFilePath) - return fmt.Errorf("getting worktree: %w", err) - } - - // 添加序列化的数据库文件 - if _, err := w.Add(dbSerializeFile); err != nil { - os.Remove(binFilePath) - return fmt.Errorf("adding serialized database file: %w", err) - } - - // 检查是否有变化需要提交 - status, err := w.Status() - if err != nil { - os.Remove(binFilePath) - return fmt.Errorf("getting worktree status: %w", err) - } - - // 如果没有变化,删除文件并返回 - if status.IsClean() { - os.Remove(binFilePath) - return errors.New("no changes to backup") - } - - // 创建提交 - _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ - Author: &object.Signature{ - Name: "voidraft", - Email: "backup@voidraft.app", - When: time.Now(), - }, - }) - if err != nil { - os.Remove(binFilePath) - if strings.Contains(err.Error(), "cannot create empty commit") { - return errors.New("no changes to backup") - } - return fmt.Errorf("creating commit: %w", err) - } - } - - // 获取认证方法并推送到远程 - auth, err := s.getAuthMethod(config) - if err != nil { - return fmt.Errorf("getting auth method: %w", err) - } - - // 推送到远程仓库(包括之前失败的commit) - if err := s.repository.Push(&git.PushOptions{ - RemoteName: "origin", - Auth: auth, - }); err != nil { return err } - // 只在推送成功后删除临时文件 - os.Remove(binFilePath) - return nil -} - -// hasUnpushedCommits 检查是否有未推送的commit -func (s *BackupService) hasUnpushedCommits() (bool, error) { - localRef, err := s.repository.Head() - if err != nil { - return false, nil + if !config.Enabled { + return ErrDisabled } - config, _, err := s.getConfigAndPath() - if err != nil { - return false, err + // 检查仓库地址是否配置 + if strings.TrimSpace(config.RepoURL) == "" { + return errors.New("repository URL is not configured") } + // 如果未初始化,尝试初始化 + s.mu.Lock() + initialized := s.isInitialized + s.mu.Unlock() + + if !initialized { + if err := s.Initialize(); err != nil { + return fmt.Errorf("initializing backup service: %w", err) + } + s.mu.Lock() + initialized = s.isInitialized + s.mu.Unlock() + if !initialized { + return ErrNotInitialized + } + } + + s.mu.Lock() + defer s.mu.Unlock() + + ctx := context.Background() + auth, err := s.getAuthMethod(config) if err != nil { - return false, err + return err } - remote, err := s.repository.Remote("origin") - if err != nil { - return false, err + // 1. 拉取远程更新到本地工作区 + if err := s.fetchAndMergeRemote(auth, repoPath); err != nil { + s.logger.Warning("fetch remote: %v", err) } - refs, err := remote.List(&git.ListOptions{Auth: auth}) - if err != nil { - return false, err + // 2. 先将远程 JSONL 导入本地数据库(用 updated_at 解决记录级冲突) + if err := s.importAll(ctx, repoPath); err != nil { + s.logger.Warning("importing remote data: %v", err) } - localHash := localRef.Hash() + // 3. 导出合并后的本地数据库到 JSONL + if err := s.exportAll(ctx, repoPath); err != nil { + return fmt.Errorf("exporting data: %w", err) + } - for _, ref := range refs { - if ref.Name() == localRef.Name() { - return localHash != ref.Hash(), nil + // 4. 提交更改 + if _, err := s.commitChanges(); err != nil { + return fmt.Errorf("committing changes: %w", err) + } + + // 5. 推送到远程(带重试) + if err := s.pushWithRetry(auth, repoPath); err != nil { + return fmt.Errorf("pushing: %w", err) + } + + return nil +} + +// exportAll 导出所有表到 JSONL 文件 +func (s *BackupService) exportAll(ctx context.Context, dataPath string) error { + // 使用 SkipSoftDelete 获取所有数据(包括已删除的) + ctx = mixin.SkipSoftDelete(ctx) + client := s.dbService.Client + + // 定义导出任务 + exports := []struct { + name string + fn func() error + }{ + {"documents", func() error { + docs, err := client.Document.Query().Order(document.ByUUID()).All(ctx) + if err != nil { + return err + } + return writeJSONLFile(filepath.Join(dataPath, "documents"+jsonlSuffix), docs) + }}, + {"extensions", func() error { + items, err := client.Extension.Query().Order(extension.ByUUID()).All(ctx) + if err != nil { + return err + } + return writeJSONLFile(filepath.Join(dataPath, "extensions"+jsonlSuffix), items) + }}, + {"keybindings", func() error { + items, err := client.KeyBinding.Query().Order(keybinding.ByUUID()).All(ctx) + if err != nil { + return err + } + return writeJSONLFile(filepath.Join(dataPath, "keybindings"+jsonlSuffix), items) + }}, + {"themes", func() error { + items, err := client.Theme.Query().Order(theme.ByUUID()).All(ctx) + if err != nil { + return err + } + return writeJSONLFile(filepath.Join(dataPath, "themes"+jsonlSuffix), items) + }}, + } + + for _, export := range exports { + if err := export.fn(); err != nil { + return fmt.Errorf("exporting %s: %w", export.name, err) } } + return nil +} + +// writeJSONLFile 使用泛型写入 JSONL 文件 +func writeJSONLFile[T any](filePath string, items []T) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + defer writer.Flush() + + for _, item := range items { + data, err := json.Marshal(item) + if err != nil { + return err + } + if _, err := writer.Write(data); err != nil { + return err + } + if err := writer.WriteByte('\n'); err != nil { + return err + } + } + + return nil +} + +func (s *BackupService) commitChanges() (bool, error) { + w, err := s.repository.Worktree() + if err != nil { + return false, err + } + + // 添加所有变更 + if err := w.AddGlob("*.jsonl"); err != nil { + // 如果没有文件匹配,不是错误 + if !strings.Contains(err.Error(), "no matches found") { + return false, err + } + } + + status, err := w.Status() + if err != nil { + return false, err + } + + if status.IsClean() { + return false, nil + } + + _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ + Author: &object.Signature{ + Name: "voidraft", + Email: "backup@voidraft.app", + When: time.Now(), + }, + }) + if err != nil { + return false, err + } + return true, nil } -// StartAutoBackup 启动自动备份定时器 +// fetchAndMergeRemote 拉取远程更新并合并 +func (s *BackupService) fetchAndMergeRemote(auth transport.AuthMethod, dataPath string) error { + // 检查本地是否有 HEAD(是否有任何 commit) + head, err := s.repository.Head() + hasLocalCommits := err == nil && head != nil + + // 先 fetch 远程 + err = s.repository.Fetch(&git.FetchOptions{ + RemoteName: remoteName, + Auth: auth, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + // 远程分支不存在是正常的(首次推送) + if strings.Contains(err.Error(), "couldn't find remote ref") { + return nil + } + return fmt.Errorf("fetching: %w", err) + } + + // 获取远程分支引用 + remoteRef, err := s.repository.Reference(plumbing.NewRemoteReferenceName(remoteName, branchName), true) + if err != nil { + // 远程分支不存在,正常情况 + return nil + } + + // 如果本地没有 commit,直接 checkout 远程分支 + if !hasLocalCommits { + w, err := s.repository.Worktree() + if err != nil { + return err + } + + // 创建本地分支指向远程 + err = w.Checkout(&git.CheckoutOptions{ + Hash: remoteRef.Hash(), + Branch: plumbing.NewBranchReferenceName(branchName), + Create: true, + Force: true, + }) + if err != nil { + return fmt.Errorf("checkout remote: %w", err) + } + return nil + } + + // 本地有 commit,尝试 pull 合并 + w, err := s.repository.Worktree() + if err != nil { + return err + } + + err = w.Pull(&git.PullOptions{ + RemoteName: remoteName, + ReferenceName: plumbing.NewBranchReferenceName(branchName), + Auth: auth, + }) + + if err == nil || errors.Is(err, git.NoErrAlreadyUpToDate) { + return nil + } + + // 处理合并冲突 + if errors.Is(err, git.ErrNonFastForwardUpdate) || + strings.Contains(err.Error(), "conflict") || + strings.Contains(err.Error(), "merge") { + return s.resolveConflicts(dataPath) + } + + // 远程分支不存在(首次推送) + if strings.Contains(err.Error(), "reference not found") || + strings.Contains(err.Error(), "couldn't find remote ref") { + return nil + } + + return err +} + +// pushWithRetry 推送到远程,带重试逻辑 +func (s *BackupService) pushWithRetry(auth transport.AuthMethod, dataPath string) error { + for i := 0; i < maxRetries; i++ { + err := s.repository.Push(&git.PushOptions{ + RemoteName: remoteName, + Auth: auth, + }) + + switch { + case err == nil, errors.Is(err, git.NoErrAlreadyUpToDate): + return nil + + case errors.Is(err, git.ErrNonFastForwardUpdate): + // 非快进更新,需要先拉取合并 + if mergeErr := s.fetchAndMergeRemote(auth, dataPath); mergeErr != nil { + return fmt.Errorf("merge before push: %w", mergeErr) + } + _, _ = s.commitChanges() + continue + + default: + return err + } + } + + return ErrPushFailed +} + +// resolveConflicts 解决 JSONL 文件中的冲突(Last Write Wins) +func (s *BackupService) resolveConflicts(dataPath string) error { + files, err := filepath.Glob(filepath.Join(dataPath, "*.jsonl")) + if err != nil { + return err + } + + for _, file := range files { + content, err := os.ReadFile(file) + if err != nil { + continue + } + + // 检查是否有冲突标记 + if !strings.Contains(string(content), "<<<<<<<") { + continue + } + + resolved, err := s.resolveJSONLConflict(string(content)) + if err != nil { + return fmt.Errorf("resolving conflict in %s: %w", file, err) + } + + if err := os.WriteFile(file, []byte(resolved), 0644); err != nil { + return err + } + } + + // 提交解决后的冲突 + w, err := s.repository.Worktree() + if err != nil { + return err + } + + if err := w.AddGlob("*.jsonl"); err != nil { + return err + } + + _, err = w.Commit("Auto-resolve sync conflicts", &git.CommitOptions{ + Author: &object.Signature{ + Name: "voidraft", + Email: "backup@voidraft.app", + When: time.Now(), + }, + }) + + return err +} + +// resolveJSONLConflict 解析并解决 JSONL 文件中的 Git 冲突 +func (s *BackupService) resolveJSONLConflict(content string) (string, error) { + lines := strings.Split(content, "\n") + var result []string + + var localLines, remoteLines []string + inConflict := false + isLocal := true + + for _, line := range lines { + if strings.HasPrefix(line, "<<<<<<<") { + inConflict = true + isLocal = true + localLines = nil + remoteLines = nil + continue + } + if strings.HasPrefix(line, "=======") { + isLocal = false + continue + } + if strings.HasPrefix(line, ">>>>>>>") { + // 解决这个冲突块 + resolved := s.mergeConflictBlock(localLines, remoteLines) + result = append(result, resolved...) + inConflict = false + continue + } + + if inConflict { + if isLocal { + if line != "" { + localLines = append(localLines, line) + } + } else { + if line != "" { + remoteLines = append(remoteLines, line) + } + } + } else { + result = append(result, line) + } + } + + return strings.Join(result, "\n"), nil +} + +// mergeConflictBlock 合并冲突块,使用 Last Write Wins 策略 +func (s *BackupService) mergeConflictBlock(localLines, remoteLines []string) []string { + // 解析本地和远程的记录 + localRecords := s.parseRecords(localLines) + remoteRecords := s.parseRecords(remoteLines) + + // 合并:按 UUID 索引,updated_at 更新的记录获胜 + merged := make(map[string]map[string]interface{}) + mergedOrder := []string{} + + // 先添加本地记录 + for _, record := range localRecords { + uuid, ok := record[fieldUUID].(string) + if !ok { + continue + } + merged[uuid] = record + mergedOrder = append(mergedOrder, uuid) + } + + // 合并远程记录 + for _, record := range remoteRecords { + uuid, ok := record[fieldUUID].(string) + if !ok { + continue + } + + existing, exists := merged[uuid] + if !exists { + merged[uuid] = record + mergedOrder = append(mergedOrder, uuid) + } else { + // 比较 updated_at,更新的获胜 + localTime := s.parseTime(existing[fieldUpdatedAt]) + remoteTime := s.parseTime(record[fieldUpdatedAt]) + if remoteTime.After(localTime) { + merged[uuid] = record + } + } + } + + // 转回 JSON 行 + var result []string + for _, uuid := range mergedOrder { + if record, ok := merged[uuid]; ok { + data, _ := json.Marshal(record) + result = append(result, string(data)) + delete(merged, uuid) // 避免重复 + } + } + + return result +} + +func (s *BackupService) parseRecords(lines []string) []map[string]interface{} { + var records []map[string]interface{} + for _, line := range lines { + var record map[string]interface{} + if err := json.Unmarshal([]byte(line), &record); err == nil { + records = append(records, record) + } + } + return records +} + +func (s *BackupService) parseTime(v interface{}) time.Time { + if str, ok := v.(string); ok { + t, _ := time.Parse(time.RFC3339, str) + return t + } + return time.Time{} +} + +// importAll 从 JSONL 文件导入数据到数据库 +func (s *BackupService) importAll(ctx context.Context, dataPath string) error { + client := s.dbService.Client + + // 定义导入任务 + imports := []struct { + name string + fn func() error + }{ + {"documents", func() error { return s.importDocuments(ctx, client, dataPath) }}, + {"extensions", func() error { return s.importExtensions(ctx, client, dataPath) }}, + {"keybindings", func() error { return s.importKeyBindings(ctx, client, dataPath) }}, + {"themes", func() error { return s.importThemes(ctx, client, dataPath) }}, + } + + for _, imp := range imports { + if err := imp.fn(); err != nil { + s.logger.Error("importing %s: %v", imp.name, err) + } + } + + return nil +} + +func (s *BackupService) importDocuments(ctx context.Context, client *ent.Client, dataPath string) error { + filePath := filepath.Join(dataPath, "documents.jsonl") + records, err := s.readJSONL(filePath) + if err != nil { + return err + } + + // 跳过软删除过滤和自动更新时间 + importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) + + for _, record := range records { + uuid, _ := record[document.FieldUUID].(string) + if uuid == "" { + continue + } + + // 查找现有记录 + found, err := client.Document.Query(). + Where(document.UUIDEQ(uuid)). + First(importCtx) + + remoteTime := s.parseTime(record[document.FieldUpdatedAt]) + + if err != nil || found == nil { + // 新记录,创建 + if err := s.createDocument(importCtx, client, record); err != nil { + s.logger.Error("creating document: %v", err) + } + } else { + // 比较时间,更新的获胜 + localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) + if remoteTime.After(localTime) { + if err := s.updateDocument(importCtx, client, found.ID, record); err != nil { + s.logger.Error("updating document: %v", err) + } + } + } + } + + return nil +} + +func (s *BackupService) createDocument(ctx context.Context, client *ent.Client, record map[string]interface{}) error { + builder := client.Document.Create() + if v, ok := record[document.FieldUUID].(string); ok { + builder.SetUUID(v) + } + if v, ok := record[document.FieldTitle].(string); ok { + builder.SetTitle(v) + } + if v, ok := record[document.FieldContent].(string); ok { + builder.SetContent(v) + } + if v, ok := record[document.FieldLocked].(bool); ok { + builder.SetLocked(v) + } + if v, ok := record[document.FieldCreatedAt].(string); ok { + builder.SetCreatedAt(v) + } + if v, ok := record[document.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[document.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } + return builder.Exec(ctx) +} + +func (s *BackupService) updateDocument(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { + builder := client.Document.UpdateOneID(id) + if v, ok := record[document.FieldTitle].(string); ok { + builder.SetTitle(v) + } + if v, ok := record[document.FieldContent].(string); ok { + builder.SetContent(v) + } + if v, ok := record[document.FieldLocked].(bool); ok { + builder.SetLocked(v) + } + if v, ok := record[document.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[document.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } else { + builder.ClearDeletedAt() + } + return builder.Exec(ctx) +} + +func (s *BackupService) importExtensions(ctx context.Context, client *ent.Client, dataPath string) error { + filePath := filepath.Join(dataPath, "extensions.jsonl") + records, err := s.readJSONL(filePath) + if err != nil { + return err + } + + importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) + + for _, record := range records { + uuid, _ := record[extension.FieldUUID].(string) + if uuid == "" { + continue + } + + found, err := client.Extension.Query(). + Where(extension.UUIDEQ(uuid)). + First(importCtx) + + remoteTime := s.parseTime(record[extension.FieldUpdatedAt]) + + if err != nil || found == nil { + if err := s.createExtension(importCtx, client, record); err != nil { + s.logger.Error("creating extension: %v", err) + } + } else { + localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) + if remoteTime.After(localTime) { + if err := s.updateExtension(importCtx, client, found.ID, record); err != nil { + s.logger.Error("updating extension: %v", err) + } + } + } + } + + return nil +} + +func (s *BackupService) createExtension(ctx context.Context, client *ent.Client, record map[string]interface{}) error { + builder := client.Extension.Create() + if v, ok := record[extension.FieldUUID].(string); ok { + builder.SetUUID(v) + } + if v, ok := record[extension.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[extension.FieldEnabled].(bool); ok { + builder.SetEnabled(v) + } + if v, ok := record[extension.FieldConfig].(map[string]interface{}); ok { + builder.SetConfig(v) + } + if v, ok := record[extension.FieldCreatedAt].(string); ok { + builder.SetCreatedAt(v) + } + if v, ok := record[extension.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[extension.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } + return builder.Exec(ctx) +} + +func (s *BackupService) updateExtension(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { + builder := client.Extension.UpdateOneID(id) + if v, ok := record[extension.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[extension.FieldEnabled].(bool); ok { + builder.SetEnabled(v) + } + if v, ok := record[extension.FieldConfig].(map[string]interface{}); ok { + builder.SetConfig(v) + } + if v, ok := record[extension.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[extension.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } else { + builder.ClearDeletedAt() + } + return builder.Exec(ctx) +} + +func (s *BackupService) importKeyBindings(ctx context.Context, client *ent.Client, dataPath string) error { + filePath := filepath.Join(dataPath, "keybindings.jsonl") + records, err := s.readJSONL(filePath) + if err != nil { + return err + } + + importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) + + for _, record := range records { + uuid, _ := record[keybinding.FieldUUID].(string) + if uuid == "" { + continue + } + + found, err := client.KeyBinding.Query(). + Where(keybinding.UUIDEQ(uuid)). + First(importCtx) + + remoteTime := s.parseTime(record[keybinding.FieldUpdatedAt]) + + if err != nil || found == nil { + if err := s.createKeyBinding(importCtx, client, record); err != nil { + s.logger.Error("creating keybinding: %v", err) + } + } else { + localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) + if remoteTime.After(localTime) { + if err := s.updateKeyBinding(importCtx, client, found.ID, record); err != nil { + s.logger.Error("updating keybinding: %v", err) + } + } + } + } + + return nil +} + +func (s *BackupService) createKeyBinding(ctx context.Context, client *ent.Client, record map[string]interface{}) error { + builder := client.KeyBinding.Create() + if v, ok := record[keybinding.FieldUUID].(string); ok { + builder.SetUUID(v) + } + if v, ok := record[keybinding.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[keybinding.FieldCommand].(string); ok { + builder.SetCommand(v) + } + if v, ok := record[keybinding.FieldExtension].(string); ok { + builder.SetExtension(v) + } + if v, ok := record[keybinding.FieldEnabled].(bool); ok { + builder.SetEnabled(v) + } + if v, ok := record[keybinding.FieldCreatedAt].(string); ok { + builder.SetCreatedAt(v) + } + if v, ok := record[keybinding.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[keybinding.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } + return builder.Exec(ctx) +} + +func (s *BackupService) updateKeyBinding(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { + builder := client.KeyBinding.UpdateOneID(id) + if v, ok := record[keybinding.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[keybinding.FieldCommand].(string); ok { + builder.SetCommand(v) + } + if v, ok := record[keybinding.FieldExtension].(string); ok { + builder.SetExtension(v) + } + if v, ok := record[keybinding.FieldEnabled].(bool); ok { + builder.SetEnabled(v) + } + if v, ok := record[keybinding.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[keybinding.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } else { + builder.ClearDeletedAt() + } + return builder.Exec(ctx) +} + +func (s *BackupService) importThemes(ctx context.Context, client *ent.Client, dataPath string) error { + filePath := filepath.Join(dataPath, "themes.jsonl") + records, err := s.readJSONL(filePath) + if err != nil { + return err + } + + importCtx := mixin.SkipAutoUpdate(mixin.SkipSoftDelete(ctx)) + + for _, record := range records { + uuid, _ := record[theme.FieldUUID].(string) + if uuid == "" { + continue + } + + found, err := client.Theme.Query(). + Where(theme.UUIDEQ(uuid)). + First(importCtx) + + remoteTime := s.parseTime(record[theme.FieldUpdatedAt]) + + if err != nil || found == nil { + if err := s.createTheme(importCtx, client, record); err != nil { + s.logger.Error("creating theme: %v", err) + } + } else { + localTime, _ := time.Parse(time.RFC3339, found.UpdatedAt) + if remoteTime.After(localTime) { + if err := s.updateTheme(importCtx, client, found.ID, record); err != nil { + s.logger.Error("updating theme: %v", err) + } + } + } + } + + return nil +} + +func (s *BackupService) createTheme(ctx context.Context, client *ent.Client, record map[string]interface{}) error { + builder := client.Theme.Create() + if v, ok := record[theme.FieldUUID].(string); ok { + builder.SetUUID(v) + } + if v, ok := record[theme.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[theme.FieldType].(string); ok { + builder.SetType(theme.Type(v)) + } + if v, ok := record[theme.FieldColors].(map[string]interface{}); ok { + builder.SetColors(v) + } + if v, ok := record[theme.FieldCreatedAt].(string); ok { + builder.SetCreatedAt(v) + } + if v, ok := record[theme.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[theme.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } + return builder.Exec(ctx) +} + +func (s *BackupService) updateTheme(ctx context.Context, client *ent.Client, id int, record map[string]interface{}) error { + builder := client.Theme.UpdateOneID(id) + if v, ok := record[theme.FieldKey].(string); ok { + builder.SetKey(v) + } + if v, ok := record[theme.FieldType].(string); ok { + builder.SetType(theme.Type(v)) + } + if v, ok := record[theme.FieldColors].(map[string]interface{}); ok { + builder.SetColors(v) + } + if v, ok := record[theme.FieldUpdatedAt].(string); ok { + builder.SetUpdatedAt(v) + } + if v, ok := record[theme.FieldDeletedAt].(string); ok { + builder.SetDeletedAt(v) + } else { + builder.ClearDeletedAt() + } + return builder.Exec(ctx) +} + +func (s *BackupService) readJSONL(filePath string) ([]map[string]interface{}, error) { + file, err := os.Open(filePath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + defer file.Close() + + var records []map[string]interface{} + scanner := bufio.NewScanner(file) + // 增加 buffer 大小以处理大行 + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + var record map[string]interface{} + if err := json.Unmarshal([]byte(line), &record); err == nil { + records = append(records, record) + } + } + + return records, scanner.Err() +} + +// StartAutoBackup 启动自动备份 func (s *BackupService) StartAutoBackup() error { config, _, err := s.getConfigAndPath() if err != nil { - return fmt.Errorf("getting backup config: %w", err) + return err } if !config.AutoBackup || config.BackupInterval <= 0 { @@ -386,7 +1120,6 @@ func (s *BackupService) StartAutoBackup() error { s.StopAutoBackup() - // 将秒转换为分钟 s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute) s.autoBackupStop = make(chan bool) @@ -396,7 +1129,9 @@ func (s *BackupService) StartAutoBackup() error { for { select { case <-s.autoBackupTicker.C: - s.PushToRemote() + if err := s.Sync(); err != nil { + s.logger.Error("auto backup failed: %v", err) + } case <-s.autoBackupStop: return } @@ -408,34 +1143,42 @@ func (s *BackupService) StartAutoBackup() error { // StopAutoBackup 停止自动备份 func (s *BackupService) StopAutoBackup() { + // 先停止 ticker if s.autoBackupTicker != nil { s.autoBackupTicker.Stop() s.autoBackupTicker = nil } + // 安全关闭 channel(只关闭一次) if s.autoBackupStop != nil { - close(s.autoBackupStop) - s.autoBackupStop = nil + select { + case <-s.autoBackupStop: + // channel 已关闭,不做任何事 + default: + close(s.autoBackupStop) + } s.autoBackupWg.Wait() + s.autoBackupStop = nil } } -// Reinitialize 重新初始化备份服务,用于响应配置变更 +// Reinitialize 重新初始化 func (s *BackupService) Reinitialize() error { - // 先停止自动备份,等待goroutine完成 s.StopAutoBackup() s.mu.Lock() s.isInitialized = false s.mu.Unlock() - // 重新初始化 return s.Initialize() } -// HandleConfigChange 处理备份配置变更 +// HandleConfigChange 处理配置变更 func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error { - // 如果备份功能禁用,只需停止自动备份 + s.mu.Lock() + initialized := s.isInitialized + s.mu.Unlock() + if !config.Enabled { s.StopAutoBackup() s.mu.Lock() @@ -444,22 +1187,15 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error return nil } - // 如果服务已初始化,重新初始化以应用新配置 - if s.isInitialized { + if initialized { return s.Reinitialize() } - // 如果服务未初始化但已启用,则初始化 - if config.Enabled && !s.isInitialized { - return s.Initialize() - } - - return nil + return s.Initialize() } -// ServiceShutdown 服务关闭时的清理工作 +// ServiceShutdown 服务关闭 func (s *BackupService) ServiceShutdown() { - // 取消配置观察者 if s.cancelObserver != nil { s.cancelObserver() } diff --git a/internal/services/extension_service.go b/internal/services/extension_service.go index da87727..70ace9d 100644 --- a/internal/services/extension_service.go +++ b/internal/services/extension_service.go @@ -7,6 +7,7 @@ import ( "voidraft/internal/models/ent" "voidraft/internal/models/ent/extension" + "voidraft/internal/models/ent/keybinding" "voidraft/internal/models/schema/mixin" "github.com/wailsapp/wails/v3/pkg/application" @@ -113,9 +114,23 @@ func (s *ExtensionService) UpdateExtensionEnabled(ctx context.Context, key strin if ext == nil { return fmt.Errorf("extension not found: %s", key) } - return s.db.Client.Extension.UpdateOneID(ext.ID). + + // 更新扩展状态 + if err := s.db.Client.Extension.UpdateOneID(ext.ID). SetEnabled(enabled). - Exec(ctx) + Exec(ctx); err != nil { + return err + } + + // 同步更新该扩展关联的快捷键启用状态 + if _, err := s.db.Client.KeyBinding.Update(). + Where(keybinding.Extension(key)). + SetEnabled(enabled). + Save(ctx); err != nil { + return fmt.Errorf("update keybindings for extension %s error: %w", key, err) + } + + return nil } // UpdateExtensionConfig 更新扩展配置